Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #42 from tsirysndr/feat/search

setup media library search engine

authored by

Tsiry Sandratraina and committed by
GitHub
6d68d302 6ae6fda1

+3661 -176
+1 -1
.devcontainer/Dockerfile
··· 34 34 35 35 ENV PATH=/root/.bun/bin:$PATH 36 36 37 - RUN pkgx install node 37 + RUN pkgx install node protoc buf 38 38 39 39 ENV LANG=en_US.UTF-8 40 40
+429 -80
Cargo.lock
··· 138 138 checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 139 139 dependencies = [ 140 140 "quote", 141 - "syn 2.0.77", 141 + "syn 2.0.82", 142 142 ] 143 143 144 144 [[package]] ··· 273 273 "actix-router", 274 274 "proc-macro2", 275 275 "quote", 276 - "syn 2.0.77", 276 + "syn 2.0.82", 277 277 ] 278 278 279 279 [[package]] ··· 284 284 dependencies = [ 285 285 "proc-macro2", 286 286 "quote", 287 - "syn 2.0.77", 287 + "syn 2.0.82", 288 288 ] 289 289 290 290 [[package]] ··· 480 480 checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" 481 481 482 482 [[package]] 483 + name = "arc-swap" 484 + version = "1.7.1" 485 + source = "registry+https://github.com/rust-lang/crates.io-index" 486 + checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 487 + 488 + [[package]] 483 489 name = "arrayvec" 484 490 version = "0.7.6" 485 491 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 551 557 "proc-macro2", 552 558 "quote", 553 559 "swc_macros_common", 554 - "syn 2.0.77", 560 + "syn 2.0.82", 555 561 ] 556 562 557 563 [[package]] ··· 646 652 "proc-macro2", 647 653 "quote", 648 654 "strum 0.26.3", 649 - "syn 2.0.77", 655 + "syn 2.0.82", 650 656 "thiserror", 651 657 ] 652 658 ··· 693 699 dependencies = [ 694 700 "proc-macro2", 695 701 "quote", 696 - "syn 2.0.77", 702 + "syn 2.0.82", 697 703 ] 698 704 699 705 [[package]] ··· 704 710 dependencies = [ 705 711 "proc-macro2", 706 712 "quote", 707 - "syn 2.0.77", 713 + "syn 2.0.82", 708 714 ] 709 715 710 716 [[package]] ··· 892 898 "regex", 893 899 "rustc-hash 1.1.0", 894 900 "shlex", 895 - "syn 2.0.77", 901 + "syn 2.0.82", 896 902 "which 4.4.2", 897 903 ] 898 904 ··· 927 933 ] 928 934 929 935 [[package]] 936 + name = "bitpacking" 937 + version = "0.9.2" 938 + source = "registry+https://github.com/rust-lang/crates.io-index" 939 + checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" 940 + dependencies = [ 941 + "crunchy", 942 + ] 943 + 944 + [[package]] 930 945 name = "bitvec" 931 946 version = "1.0.1" 932 947 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1058 1073 ] 1059 1074 1060 1075 [[package]] 1076 + name = "census" 1077 + version = "0.4.2" 1078 + source = "registry+https://github.com/rust-lang/crates.io-index" 1079 + checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" 1080 + 1081 + [[package]] 1061 1082 name = "cexpr" 1062 1083 version = "0.6.0" 1063 1084 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1314 1335 checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 1315 1336 1316 1337 [[package]] 1338 + name = "crunchy" 1339 + version = "0.2.2" 1340 + source = "registry+https://github.com/rust-lang/crates.io-index" 1341 + checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 1342 + 1343 + [[package]] 1317 1344 name = "crypto-bigint" 1318 1345 version = "0.5.5" 1319 1346 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1404 1431 dependencies = [ 1405 1432 "proc-macro2", 1406 1433 "quote", 1407 - "syn 2.0.77", 1434 + "syn 2.0.82", 1408 1435 ] 1409 1436 1410 1437 [[package]] ··· 1439 1466 "proc-macro2", 1440 1467 "quote", 1441 1468 "strsim", 1442 - "syn 2.0.77", 1469 + "syn 2.0.82", 1443 1470 ] 1444 1471 1445 1472 [[package]] ··· 1450 1477 dependencies = [ 1451 1478 "darling_core", 1452 1479 "quote", 1453 - "syn 2.0.77", 1480 + "syn 2.0.82", 1454 1481 ] 1455 1482 1456 1483 [[package]] ··· 2025 2052 "quote", 2026 2053 "strum 0.25.0", 2027 2054 "strum_macros 0.25.3", 2028 - "syn 2.0.77", 2055 + "syn 2.0.82", 2029 2056 "thiserror", 2030 2057 ] 2031 2058 ··· 2414 2441 dependencies = [ 2415 2442 "proc-macro2", 2416 2443 "quote", 2417 - "syn 2.0.77", 2444 + "syn 2.0.82", 2418 2445 ] 2419 2446 2420 2447 [[package]] ··· 2424 2451 checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 2425 2452 dependencies = [ 2426 2453 "powerfmt", 2454 + "serde", 2427 2455 ] 2428 2456 2429 2457 [[package]] ··· 2436 2464 "proc-macro2", 2437 2465 "quote", 2438 2466 "rustc_version 0.4.1", 2439 - "syn 2.0.77", 2467 + "syn 2.0.82", 2440 2468 ] 2441 2469 2442 2470 [[package]] ··· 2459 2487 dependencies = [ 2460 2488 "proc-macro2", 2461 2489 "quote", 2462 - "syn 2.0.77", 2490 + "syn 2.0.82", 2463 2491 ] 2464 2492 2465 2493 [[package]] ··· 2494 2522 dependencies = [ 2495 2523 "proc-macro2", 2496 2524 "quote", 2497 - "syn 2.0.77", 2525 + "syn 2.0.82", 2498 2526 ] 2499 2527 2500 2528 [[package]] ··· 2513 2541 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 2514 2542 2515 2543 [[package]] 2544 + name = "downcast-rs" 2545 + version = "1.2.1" 2546 + source = "registry+https://github.com/rust-lang/crates.io-index" 2547 + checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 2548 + 2549 + [[package]] 2516 2550 name = "dprint-swc-ext" 2517 2551 version = "0.18.0" 2518 2552 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2572 2606 dependencies = [ 2573 2607 "byteorder", 2574 2608 "dynasm", 2575 - "memmap2", 2609 + "memmap2 0.5.10", 2576 2610 ] 2577 2611 2578 2612 [[package]] ··· 2681 2715 "heck 0.5.0", 2682 2716 "proc-macro2", 2683 2717 "quote", 2684 - "syn 2.0.77", 2718 + "syn 2.0.82", 2685 2719 ] 2686 2720 2687 2721 [[package]] ··· 2809 2843 ] 2810 2844 2811 2845 [[package]] 2846 + name = "fastdivide" 2847 + version = "0.4.1" 2848 + source = "registry+https://github.com/rust-lang/crates.io-index" 2849 + checksum = "59668941c55e5c186b8b58c391629af56774ec768f73c08bbcd56f09348eb00b" 2850 + 2851 + [[package]] 2812 2852 name = "faster-hex" 2813 2853 version = "0.9.0" 2814 2854 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2934 2974 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 2935 2975 2936 2976 [[package]] 2977 + name = "foldhash" 2978 + version = "0.1.3" 2979 + source = "registry+https://github.com/rust-lang/crates.io-index" 2980 + checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" 2981 + 2982 + [[package]] 2937 2983 name = "foreign-types" 2938 2984 version = "0.5.0" 2939 2985 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2951 2997 dependencies = [ 2952 2998 "proc-macro2", 2953 2999 "quote", 2954 - "syn 2.0.77", 3000 + "syn 2.0.82", 2955 3001 ] 2956 3002 2957 3003 [[package]] ··· 2983 3029 dependencies = [ 2984 3030 "proc-macro2", 2985 3031 "swc_macros_common", 2986 - "syn 2.0.77", 3032 + "syn 2.0.82", 2987 3033 ] 2988 3034 2989 3035 [[package]] ··· 2995 3041 "libc", 2996 3042 "rustc_version 0.2.3", 2997 3043 "winapi", 3044 + ] 3045 + 3046 + [[package]] 3047 + name = "fs4" 3048 + version = "0.8.4" 3049 + source = "registry+https://github.com/rust-lang/crates.io-index" 3050 + checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" 3051 + dependencies = [ 3052 + "rustix", 3053 + "windows-sys 0.52.0", 2998 3054 ] 2999 3055 3000 3056 [[package]] ··· 3089 3145 dependencies = [ 3090 3146 "proc-macro2", 3091 3147 "quote", 3092 - "syn 2.0.77", 3148 + "syn 2.0.82", 3093 3149 ] 3094 3150 3095 3151 [[package]] ··· 3344 3400 ] 3345 3401 3346 3402 [[package]] 3403 + name = "hashbrown" 3404 + version = "0.15.0" 3405 + source = "registry+https://github.com/rust-lang/crates.io-index" 3406 + checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 3407 + dependencies = [ 3408 + "allocator-api2", 3409 + "equivalent", 3410 + "foldhash", 3411 + ] 3412 + 3413 + [[package]] 3347 3414 name = "hashlink" 3348 3415 version = "0.9.1" 3349 3416 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3446 3513 ] 3447 3514 3448 3515 [[package]] 3516 + name = "htmlescape" 3517 + version = "0.3.1" 3518 + source = "registry+https://github.com/rust-lang/crates.io-index" 3519 + checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" 3520 + 3521 + [[package]] 3449 3522 name = "http" 3450 3523 version = "0.2.12" 3451 3524 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3741 3814 ] 3742 3815 3743 3816 [[package]] 3817 + name = "instant" 3818 + version = "0.1.13" 3819 + source = "registry+https://github.com/rust-lang/crates.io-index" 3820 + checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 3821 + dependencies = [ 3822 + "cfg-if", 3823 + "js-sys", 3824 + "wasm-bindgen", 3825 + "web-sys", 3826 + ] 3827 + 3828 + [[package]] 3744 3829 name = "ipconfig" 3745 3830 version = "0.3.2" 3746 3831 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3776 3861 "Inflector", 3777 3862 "proc-macro2", 3778 3863 "quote", 3779 - "syn 2.0.77", 3864 + "syn 2.0.82", 3780 3865 ] 3781 3866 3782 3867 [[package]] ··· 3938 4023 "proc-macro2", 3939 4024 "quote", 3940 4025 "regex", 3941 - "syn 2.0.77", 4026 + "syn 2.0.82", 3942 4027 ] 3943 4028 3944 4029 [[package]] ··· 3955 4040 version = "1.3.0" 3956 4041 source = "registry+https://github.com/rust-lang/crates.io-index" 3957 4042 checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 4043 + 4044 + [[package]] 4045 + name = "levenshtein_automata" 4046 + version = "0.2.1" 4047 + source = "registry+https://github.com/rust-lang/crates.io-index" 4048 + checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" 3958 4049 3959 4050 [[package]] 3960 4051 name = "lexical-core" ··· 4172 4263 dependencies = [ 4173 4264 "proc-macro2", 4174 4265 "quote", 4175 - "syn 2.0.77", 4266 + "syn 2.0.82", 4176 4267 ] 4177 4268 4178 4269 [[package]] ··· 4182 4273 checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 4183 4274 4184 4275 [[package]] 4276 + name = "lru" 4277 + version = "0.12.5" 4278 + source = "registry+https://github.com/rust-lang/crates.io-index" 4279 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 4280 + dependencies = [ 4281 + "hashbrown 0.15.0", 4282 + ] 4283 + 4284 + [[package]] 4185 4285 name = "lru-cache" 4186 4286 version = "0.1.2" 4187 4287 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4189 4289 dependencies = [ 4190 4290 "linked-hash-map", 4191 4291 ] 4292 + 4293 + [[package]] 4294 + name = "lz4_flex" 4295 + version = "0.11.3" 4296 + source = "registry+https://github.com/rust-lang/crates.io-index" 4297 + checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" 4192 4298 4193 4299 [[package]] 4194 4300 name = "malloc_buf" ··· 4237 4343 checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 4238 4344 4239 4345 [[package]] 4346 + name = "measure_time" 4347 + version = "0.8.3" 4348 + source = "registry+https://github.com/rust-lang/crates.io-index" 4349 + checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc" 4350 + dependencies = [ 4351 + "instant", 4352 + "log", 4353 + ] 4354 + 4355 + [[package]] 4240 4356 name = "memchr" 4241 4357 version = "2.7.4" 4242 4358 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4247 4363 version = "0.5.10" 4248 4364 source = "registry+https://github.com/rust-lang/crates.io-index" 4249 4365 checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" 4366 + dependencies = [ 4367 + "libc", 4368 + ] 4369 + 4370 + [[package]] 4371 + name = "memmap2" 4372 + version = "0.9.5" 4373 + source = "registry+https://github.com/rust-lang/crates.io-index" 4374 + checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 4250 4375 dependencies = [ 4251 4376 "libc", 4252 4377 ] ··· 4396 4521 version = "0.10.0" 4397 4522 source = "registry+https://github.com/rust-lang/crates.io-index" 4398 4523 checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" 4524 + 4525 + [[package]] 4526 + name = "murmurhash32" 4527 + version = "0.3.1" 4528 + source = "registry+https://github.com/rust-lang/crates.io-index" 4529 + checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" 4399 4530 4400 4531 [[package]] 4401 4532 name = "naga" ··· 4700 4831 checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 4701 4832 4702 4833 [[package]] 4834 + name = "oneshot" 4835 + version = "0.1.8" 4836 + source = "registry+https://github.com/rust-lang/crates.io-index" 4837 + checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29" 4838 + 4839 + [[package]] 4703 4840 name = "opaque-debug" 4704 4841 version = "0.3.1" 4705 4842 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4743 4880 checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" 4744 4881 4745 4882 [[package]] 4883 + name = "ownedbytes" 4884 + version = "0.7.0" 4885 + source = "registry+https://github.com/rust-lang/crates.io-index" 4886 + checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" 4887 + dependencies = [ 4888 + "stable_deref_trait", 4889 + ] 4890 + 4891 + [[package]] 4746 4892 name = "owo-colors" 4747 4893 version = "4.1.0" 4748 4894 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4918 5064 "pest_meta", 4919 5065 "proc-macro2", 4920 5066 "quote", 4921 - "syn 2.0.77", 5067 + "syn 2.0.82", 4922 5068 ] 4923 5069 4924 5070 [[package]] ··· 4972 5118 "phf_shared", 4973 5119 "proc-macro2", 4974 5120 "quote", 4975 - "syn 2.0.77", 5121 + "syn 2.0.82", 4976 5122 ] 4977 5123 4978 5124 [[package]] ··· 5001 5147 dependencies = [ 5002 5148 "proc-macro2", 5003 5149 "quote", 5004 - "syn 2.0.77", 5150 + "syn 2.0.82", 5005 5151 ] 5006 5152 5007 5153 [[package]] ··· 5117 5263 checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" 5118 5264 dependencies = [ 5119 5265 "proc-macro2", 5120 - "syn 2.0.77", 5266 + "syn 2.0.82", 5121 5267 ] 5122 5268 5123 5269 [[package]] ··· 5170 5316 dependencies = [ 5171 5317 "proc-macro-rules-macros", 5172 5318 "proc-macro2", 5173 - "syn 2.0.77", 5319 + "syn 2.0.82", 5174 5320 ] 5175 5321 5176 5322 [[package]] ··· 5182 5328 "once_cell", 5183 5329 "proc-macro2", 5184 5330 "quote", 5185 - "syn 2.0.77", 5331 + "syn 2.0.82", 5186 5332 ] 5187 5333 5188 5334 [[package]] ··· 5212 5358 5213 5359 [[package]] 5214 5360 name = "prost" 5215 - version = "0.13.2" 5361 + version = "0.13.3" 5216 5362 source = "registry+https://github.com/rust-lang/crates.io-index" 5217 - checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" 5363 + checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" 5218 5364 dependencies = [ 5219 5365 "bytes", 5220 - "prost-derive 0.13.2", 5366 + "prost-derive 0.13.3", 5221 5367 ] 5222 5368 5223 5369 [[package]] ··· 5244 5390 5245 5391 [[package]] 5246 5392 name = "prost-build" 5247 - version = "0.13.2" 5393 + version = "0.13.3" 5248 5394 source = "registry+https://github.com/rust-lang/crates.io-index" 5249 - checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" 5395 + checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" 5250 5396 dependencies = [ 5251 5397 "bytes", 5252 5398 "heck 0.5.0", ··· 5256 5402 "once_cell", 5257 5403 "petgraph", 5258 5404 "prettyplease 0.2.22", 5259 - "prost 0.13.2", 5260 - "prost-types 0.13.2", 5405 + "prost 0.13.3", 5406 + "prost-types 0.13.3", 5261 5407 "regex", 5262 - "syn 2.0.77", 5408 + "syn 2.0.82", 5263 5409 "tempfile", 5264 5410 ] 5265 5411 ··· 5278 5424 5279 5425 [[package]] 5280 5426 name = "prost-derive" 5281 - version = "0.13.2" 5427 + version = "0.13.3" 5282 5428 source = "registry+https://github.com/rust-lang/crates.io-index" 5283 - checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" 5429 + checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" 5284 5430 dependencies = [ 5285 5431 "anyhow", 5286 5432 "itertools 0.13.0", 5287 5433 "proc-macro2", 5288 5434 "quote", 5289 - "syn 2.0.77", 5435 + "syn 2.0.82", 5290 5436 ] 5291 5437 5292 5438 [[package]] ··· 5300 5446 5301 5447 [[package]] 5302 5448 name = "prost-types" 5303 - version = "0.13.2" 5449 + version = "0.13.3" 5304 5450 source = "registry+https://github.com/rust-lang/crates.io-index" 5305 - checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" 5451 + checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" 5306 5452 dependencies = [ 5307 - "prost 0.13.2", 5453 + "prost 0.13.3", 5308 5454 ] 5309 5455 5310 5456 [[package]] ··· 5459 5605 ] 5460 5606 5461 5607 [[package]] 5608 + name = "rand_distr" 5609 + version = "0.4.3" 5610 + source = "registry+https://github.com/rust-lang/crates.io-index" 5611 + checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" 5612 + dependencies = [ 5613 + "num-traits", 5614 + "rand", 5615 + ] 5616 + 5617 + [[package]] 5462 5618 name = "range-alloc" 5463 5619 version = "0.1.3" 5464 5620 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5516 5672 dependencies = [ 5517 5673 "proc-macro2", 5518 5674 "quote", 5519 - "syn 2.0.77", 5675 + "syn 2.0.82", 5520 5676 ] 5521 5677 5522 5678 [[package]] ··· 5649 5805 "clap", 5650 5806 "owo-colors 4.1.0", 5651 5807 "rockbox-library", 5808 + "rockbox-search", 5652 5809 "tokio", 5653 5810 ] 5654 5811 ··· 5697 5854 "owo-colors 4.1.0", 5698 5855 "reqwest", 5699 5856 "rockbox-library", 5857 + "rockbox-search", 5700 5858 "rockbox-sys", 5859 + "rockbox-types", 5701 5860 "rockbox-webui", 5702 5861 "serde", 5703 5862 "serde_json", 5704 5863 "slab", 5705 5864 "sqlx", 5865 + "tantivy", 5706 5866 "tokio", 5707 5867 ] 5708 5868 ··· 5732 5892 "cuid", 5733 5893 "futures", 5734 5894 "owo-colors 5.0.0", 5735 - "prost 0.13.2", 5895 + "prost 0.13.3", 5736 5896 "reqwest", 5737 5897 "rockbox-library", 5898 + "rockbox-search", 5738 5899 "rockbox-sys", 5900 + "rockbox-types", 5739 5901 "serde", 5740 5902 "serde_json", 5741 5903 "sqlx", 5904 + "tantivy", 5742 5905 "tokio", 5743 5906 "tonic", 5744 5907 "tonic-build", ··· 5747 5910 ] 5748 5911 5749 5912 [[package]] 5913 + name = "rockbox-search" 5914 + version = "0.1.0" 5915 + dependencies = [ 5916 + "anyhow", 5917 + "rockbox-library", 5918 + "serde", 5919 + "tantivy", 5920 + ] 5921 + 5922 + [[package]] 5750 5923 name = "rockbox-server" 5751 5924 version = "0.1.0" 5752 5925 dependencies = [ ··· 5759 5932 "rockbox-graphql", 5760 5933 "rockbox-library", 5761 5934 "rockbox-rpc", 5935 + "rockbox-search", 5762 5936 "rockbox-sys", 5937 + "rockbox-types", 5763 5938 "serde", 5764 5939 "serde_json", 5765 5940 "sqlx", ··· 5772 5947 version = "0.1.0" 5773 5948 dependencies = [ 5774 5949 "anyhow", 5950 + "serde", 5951 + ] 5952 + 5953 + [[package]] 5954 + name = "rockbox-types" 5955 + version = "0.1.0" 5956 + dependencies = [ 5957 + "rockbox-search", 5775 5958 "serde", 5776 5959 ] 5777 5960 ··· 5853 6036 "proc-macro2", 5854 6037 "quote", 5855 6038 "rust-embed-utils", 5856 - "syn 2.0.77", 6039 + "syn 2.0.82", 5857 6040 "walkdir", 5858 6041 ] 5859 6042 ··· 5865 6048 dependencies = [ 5866 6049 "sha2", 5867 6050 "walkdir", 6051 + ] 6052 + 6053 + [[package]] 6054 + name = "rust-stemmers" 6055 + version = "1.2.0" 6056 + source = "registry+https://github.com/rust-lang/crates.io-index" 6057 + checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" 6058 + dependencies = [ 6059 + "serde", 6060 + "serde_derive", 5868 6061 ] 5869 6062 5870 6063 [[package]] ··· 6154 6347 6155 6348 [[package]] 6156 6349 name = "serde" 6157 - version = "1.0.210" 6350 + version = "1.0.213" 6158 6351 source = "registry+https://github.com/rust-lang/crates.io-index" 6159 - checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 6352 + checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" 6160 6353 dependencies = [ 6161 6354 "serde_derive", 6162 6355 ] ··· 6182 6375 6183 6376 [[package]] 6184 6377 name = "serde_derive" 6185 - version = "1.0.210" 6378 + version = "1.0.213" 6186 6379 source = "registry+https://github.com/rust-lang/crates.io-index" 6187 - checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 6380 + checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" 6188 6381 dependencies = [ 6189 6382 "proc-macro2", 6190 6383 "quote", 6191 - "syn 2.0.77", 6384 + "syn 2.0.82", 6192 6385 ] 6193 6386 6194 6387 [[package]] ··· 6350 6543 checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 6351 6544 6352 6545 [[package]] 6546 + name = "sketches-ddsketch" 6547 + version = "0.2.2" 6548 + source = "registry+https://github.com/rust-lang/crates.io-index" 6549 + checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" 6550 + dependencies = [ 6551 + "serde", 6552 + ] 6553 + 6554 + [[package]] 6353 6555 name = "slab" 6354 6556 version = "0.4.9" 6355 6557 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6529 6731 "quote", 6530 6732 "sqlx-core", 6531 6733 "sqlx-macros-core", 6532 - "syn 2.0.77", 6734 + "syn 2.0.82", 6533 6735 ] 6534 6736 6535 6737 [[package]] ··· 6552 6754 "sqlx-mysql", 6553 6755 "sqlx-postgres", 6554 6756 "sqlx-sqlite", 6555 - "syn 2.0.77", 6757 + "syn 2.0.82", 6556 6758 "tempfile", 6557 6759 "tokio", 6558 6760 "url", ··· 6704 6906 "proc-macro2", 6705 6907 "quote", 6706 6908 "swc_macros_common", 6707 - "syn 2.0.77", 6909 + "syn 2.0.82", 6708 6910 ] 6709 6911 6710 6912 [[package]] ··· 6752 6954 "proc-macro2", 6753 6955 "quote", 6754 6956 "rustversion", 6755 - "syn 2.0.77", 6957 + "syn 2.0.82", 6756 6958 ] 6757 6959 6758 6960 [[package]] ··· 6765 6967 "proc-macro2", 6766 6968 "quote", 6767 6969 "rustversion", 6768 - "syn 2.0.77", 6970 + "syn 2.0.82", 6769 6971 ] 6770 6972 6771 6973 [[package]] ··· 6863 7065 "proc-macro2", 6864 7066 "quote", 6865 7067 "swc_macros_common", 6866 - "syn 2.0.77", 7068 + "syn 2.0.82", 6867 7069 ] 6868 7070 6869 7071 [[package]] ··· 6912 7114 "proc-macro2", 6913 7115 "quote", 6914 7116 "swc_macros_common", 6915 - "syn 2.0.77", 7117 + "syn 2.0.82", 6916 7118 ] 6917 7119 6918 7120 [[package]] ··· 6997 7199 "proc-macro2", 6998 7200 "quote", 6999 7201 "swc_macros_common", 7000 - "syn 2.0.77", 7202 + "syn 2.0.82", 7001 7203 ] 7002 7204 7003 7205 [[package]] ··· 7104 7306 dependencies = [ 7105 7307 "proc-macro2", 7106 7308 "quote", 7107 - "syn 2.0.77", 7309 + "syn 2.0.82", 7108 7310 ] 7109 7311 7110 7312 [[package]] ··· 7115 7317 dependencies = [ 7116 7318 "proc-macro2", 7117 7319 "quote", 7118 - "syn 2.0.77", 7320 + "syn 2.0.82", 7119 7321 ] 7120 7322 7121 7323 [[package]] ··· 7137 7339 "proc-macro2", 7138 7340 "quote", 7139 7341 "swc_macros_common", 7140 - "syn 2.0.77", 7342 + "syn 2.0.82", 7141 7343 ] 7142 7344 7143 7345 [[package]] ··· 7153 7355 7154 7356 [[package]] 7155 7357 name = "syn" 7156 - version = "2.0.77" 7358 + version = "2.0.82" 7157 7359 source = "registry+https://github.com/rust-lang/crates.io-index" 7158 - checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 7360 + checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" 7159 7361 dependencies = [ 7160 7362 "proc-macro2", 7161 7363 "quote", ··· 7197 7399 dependencies = [ 7198 7400 "proc-macro2", 7199 7401 "quote", 7200 - "syn 2.0.77", 7402 + "syn 2.0.82", 7403 + ] 7404 + 7405 + [[package]] 7406 + name = "tantivy" 7407 + version = "0.22.0" 7408 + source = "registry+https://github.com/rust-lang/crates.io-index" 7409 + checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" 7410 + dependencies = [ 7411 + "aho-corasick", 7412 + "arc-swap", 7413 + "base64 0.22.1", 7414 + "bitpacking", 7415 + "byteorder", 7416 + "census", 7417 + "crc32fast", 7418 + "crossbeam-channel", 7419 + "downcast-rs", 7420 + "fastdivide", 7421 + "fnv", 7422 + "fs4", 7423 + "htmlescape", 7424 + "itertools 0.12.1", 7425 + "levenshtein_automata", 7426 + "log", 7427 + "lru", 7428 + "lz4_flex", 7429 + "measure_time", 7430 + "memmap2 0.9.5", 7431 + "num_cpus", 7432 + "once_cell", 7433 + "oneshot", 7434 + "rayon", 7435 + "regex", 7436 + "rust-stemmers", 7437 + "rustc-hash 1.1.0", 7438 + "serde", 7439 + "serde_json", 7440 + "sketches-ddsketch", 7441 + "smallvec", 7442 + "tantivy-bitpacker", 7443 + "tantivy-columnar", 7444 + "tantivy-common", 7445 + "tantivy-fst", 7446 + "tantivy-query-grammar", 7447 + "tantivy-stacker", 7448 + "tantivy-tokenizer-api", 7449 + "tempfile", 7450 + "thiserror", 7451 + "time", 7452 + "uuid", 7453 + "winapi", 7454 + ] 7455 + 7456 + [[package]] 7457 + name = "tantivy-bitpacker" 7458 + version = "0.6.0" 7459 + source = "registry+https://github.com/rust-lang/crates.io-index" 7460 + checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" 7461 + dependencies = [ 7462 + "bitpacking", 7463 + ] 7464 + 7465 + [[package]] 7466 + name = "tantivy-columnar" 7467 + version = "0.3.0" 7468 + source = "registry+https://github.com/rust-lang/crates.io-index" 7469 + checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" 7470 + dependencies = [ 7471 + "downcast-rs", 7472 + "fastdivide", 7473 + "itertools 0.12.1", 7474 + "serde", 7475 + "tantivy-bitpacker", 7476 + "tantivy-common", 7477 + "tantivy-sstable", 7478 + "tantivy-stacker", 7479 + ] 7480 + 7481 + [[package]] 7482 + name = "tantivy-common" 7483 + version = "0.7.0" 7484 + source = "registry+https://github.com/rust-lang/crates.io-index" 7485 + checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" 7486 + dependencies = [ 7487 + "async-trait", 7488 + "byteorder", 7489 + "ownedbytes", 7490 + "serde", 7491 + "time", 7492 + ] 7493 + 7494 + [[package]] 7495 + name = "tantivy-fst" 7496 + version = "0.5.0" 7497 + source = "registry+https://github.com/rust-lang/crates.io-index" 7498 + checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" 7499 + dependencies = [ 7500 + "byteorder", 7501 + "regex-syntax", 7502 + "utf8-ranges", 7503 + ] 7504 + 7505 + [[package]] 7506 + name = "tantivy-query-grammar" 7507 + version = "0.22.0" 7508 + source = "registry+https://github.com/rust-lang/crates.io-index" 7509 + checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" 7510 + dependencies = [ 7511 + "nom 7.1.3", 7512 + ] 7513 + 7514 + [[package]] 7515 + name = "tantivy-sstable" 7516 + version = "0.3.0" 7517 + source = "registry+https://github.com/rust-lang/crates.io-index" 7518 + checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" 7519 + dependencies = [ 7520 + "tantivy-bitpacker", 7521 + "tantivy-common", 7522 + "tantivy-fst", 7523 + "zstd", 7524 + ] 7525 + 7526 + [[package]] 7527 + name = "tantivy-stacker" 7528 + version = "0.3.0" 7529 + source = "registry+https://github.com/rust-lang/crates.io-index" 7530 + checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" 7531 + dependencies = [ 7532 + "murmurhash32", 7533 + "rand_distr", 7534 + "tantivy-common", 7535 + ] 7536 + 7537 + [[package]] 7538 + name = "tantivy-tokenizer-api" 7539 + version = "0.3.0" 7540 + source = "registry+https://github.com/rust-lang/crates.io-index" 7541 + checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" 7542 + dependencies = [ 7543 + "serde", 7201 7544 ] 7202 7545 7203 7546 [[package]] ··· 7254 7597 dependencies = [ 7255 7598 "proc-macro2", 7256 7599 "quote", 7257 - "syn 2.0.77", 7600 + "syn 2.0.82", 7258 7601 ] 7259 7602 7260 7603 [[package]] ··· 7339 7682 dependencies = [ 7340 7683 "proc-macro2", 7341 7684 "quote", 7342 - "syn 2.0.77", 7685 + "syn 2.0.82", 7343 7686 ] 7344 7687 7345 7688 [[package]] ··· 7442 7785 "hyper-util", 7443 7786 "percent-encoding", 7444 7787 "pin-project", 7445 - "prost 0.13.2", 7788 + "prost 0.13.3", 7446 7789 "socket2", 7447 7790 "tokio", 7448 7791 "tokio-stream", ··· 7460 7803 dependencies = [ 7461 7804 "prettyplease 0.2.22", 7462 7805 "proc-macro2", 7463 - "prost-build 0.13.2", 7806 + "prost-build 0.13.3", 7464 7807 "quote", 7465 - "syn 2.0.77", 7808 + "syn 2.0.82", 7466 7809 ] 7467 7810 7468 7811 [[package]] ··· 7471 7814 source = "registry+https://github.com/rust-lang/crates.io-index" 7472 7815 checksum = "7b56b874eedb04f89907573b408eab1e87c1c1dce43aac6ad63742f57faa99ff" 7473 7816 dependencies = [ 7474 - "prost 0.13.2", 7475 - "prost-types 0.13.2", 7817 + "prost 0.13.3", 7818 + "prost-types 0.13.3", 7476 7819 "tokio", 7477 7820 "tokio-stream", 7478 7821 "tonic", ··· 7570 7913 dependencies = [ 7571 7914 "proc-macro2", 7572 7915 "quote", 7573 - "syn 2.0.77", 7916 + "syn 2.0.82", 7574 7917 ] 7575 7918 7576 7919 [[package]] ··· 7835 8178 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 7836 8179 7837 8180 [[package]] 8181 + name = "utf8-ranges" 8182 + version = "1.0.5" 8183 + source = "registry+https://github.com/rust-lang/crates.io-index" 8184 + checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" 8185 + 8186 + [[package]] 7838 8187 name = "utf8parse" 7839 8188 version = "0.2.2" 7840 8189 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7971 8320 "once_cell", 7972 8321 "proc-macro2", 7973 8322 "quote", 7974 - "syn 2.0.77", 8323 + "syn 2.0.82", 7975 8324 "wasm-bindgen-shared", 7976 8325 ] 7977 8326 ··· 8005 8354 dependencies = [ 8006 8355 "proc-macro2", 8007 8356 "quote", 8008 - "syn 2.0.77", 8357 + "syn 2.0.82", 8009 8358 "wasm-bindgen-backend", 8010 8359 "wasm-bindgen-shared", 8011 8360 ] ··· 8482 8831 dependencies = [ 8483 8832 "proc-macro2", 8484 8833 "quote", 8485 - "syn 2.0.77", 8834 + "syn 2.0.82", 8486 8835 "synstructure 0.13.1", 8487 8836 ] 8488 8837 ··· 8504 8853 dependencies = [ 8505 8854 "proc-macro2", 8506 8855 "quote", 8507 - "syn 2.0.77", 8856 + "syn 2.0.82", 8508 8857 ] 8509 8858 8510 8859 [[package]] ··· 8524 8873 dependencies = [ 8525 8874 "proc-macro2", 8526 8875 "quote", 8527 - "syn 2.0.77", 8876 + "syn 2.0.82", 8528 8877 "synstructure 0.13.1", 8529 8878 ] 8530 8879 ··· 8545 8894 dependencies = [ 8546 8895 "proc-macro2", 8547 8896 "quote", 8548 - "syn 2.0.77", 8897 + "syn 2.0.82", 8549 8898 ] 8550 8899 8551 8900 [[package]]
+1 -1
Dockerfile
··· 12 12 13 13 RUN curl -Ssf https://pkgx.sh | sh 14 14 15 - RUN pkgx install zig@0.13.0 node bun@1.1.30 15 + RUN pkgx install zig@0.13.0 node bun@1.1.30 protoc buf 16 16 17 17 COPY . /app 18 18
+1
crates/cli/Cargo.toml
··· 11 11 clap = "4.5.17" 12 12 owo-colors = "4.1.0" 13 13 rockbox-library = {path = "../library"} 14 + rockbox-search = {path = "../search"} 14 15 tokio = {version = "1.36.0", features = ["full"]}
+42 -1
crates/cli/src/lib.rs
··· 3 3 use owo_colors::OwoColorize; 4 4 use rockbox_library::audio_scan::scan_audio_files; 5 5 use rockbox_library::{create_connection_pool, repo}; 6 + use rockbox_search::album::Album; 7 + use rockbox_search::artist::Artist; 8 + use rockbox_search::track::Track; 9 + use rockbox_search::{create_indexes, delete_all_documents, index_entity}; 6 10 use std::{env, ffi::CStr}; 7 11 use std::{fs, thread}; 8 12 ··· 76 80 let pool = create_connection_pool().await?; 77 81 let tracks = repo::track::all(pool.clone()).await?; 78 82 if tracks.is_empty() || update_library { 79 - scan_audio_files(pool, path.into()).await?; 83 + scan_audio_files(pool.clone(), path.into()).await?; 84 + let tracks = repo::track::all(pool.clone()).await?; 85 + let albums = repo::album::all(pool.clone()).await?; 86 + let artists = repo::artist::all(pool.clone()).await?; 87 + let indexes = create_indexes()?; 88 + let tracks_index = indexes.tracks.clone(); 89 + let albums_index = indexes.albums.clone(); 90 + let artists_index = indexes.artists.clone(); 91 + 92 + thread::spawn(move || { 93 + match delete_all_documents(&tracks_index) { 94 + Ok(_) => {} 95 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 96 + } 97 + for track in tracks { 98 + index_entity::<Track>(&tracks_index, &track.into()).unwrap(); 99 + } 100 + }); 101 + 102 + thread::spawn(move || { 103 + match delete_all_documents(&albums_index) { 104 + Ok(_) => {} 105 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 106 + } 107 + for album in albums { 108 + index_entity::<Album>(&albums_index, &album.into()).unwrap(); 109 + } 110 + }); 111 + 112 + thread::spawn(move || { 113 + match delete_all_documents(&artists_index) { 114 + Ok(_) => {} 115 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 116 + } 117 + for artist in artists { 118 + index_entity::<Artist>(&artists_index, &artist.into()).unwrap(); 119 + } 120 + }); 80 121 } 81 122 Ok::<(), Error>(()) 82 123 })
+3
crates/graphql/Cargo.toml
··· 20 20 owo-colors = "4.1.0" 21 21 reqwest = {version = "0.12.7", features = ["rustls-tls", "json"], default-features = false} 22 22 rockbox-library = {path = "../library"} 23 + rockbox-search = {path = "../search"} 23 24 rockbox-sys = {path = "../sys"} 25 + rockbox-types = {path = "../types"} 24 26 rockbox-webui = {path = "../../webui"} 25 27 serde = "1.0.210" 26 28 serde_json = "1.0.128" 27 29 slab = "0.4.9" 28 30 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 31 + tantivy = "0.22.0" 29 32 tokio = {version = "1.36.0", features = ["full"]}
+44 -9
crates/graphql/src/schema/library.rs
··· 1 - use std::env; 2 - 3 1 use async_graphql::*; 4 - use rockbox_library::{audio_scan::scan_audio_files, entity::favourites::Favourites, repo}; 2 + use rockbox_library::{entity::favourites::Favourites, repo}; 3 + use rockbox_search::{search_entities, Indexes}; 5 4 use sqlx::{Pool, Sqlite}; 6 5 7 - use crate::schema::objects::track::Track; 6 + use crate::{rockbox_url, schema::objects::track::Track}; 8 7 9 - use super::objects::{album::Album, artist::Artist}; 8 + use super::objects::{album::Album, artist::Artist, search::SearchResults}; 10 9 11 10 #[derive(Default)] 12 11 pub struct LibraryQuery; ··· 74 73 let results = repo::favourites::all_albums(pool.clone()).await?; 75 74 Ok(results.into_iter().map(Into::into).collect()) 76 75 } 76 + 77 + async fn search(&self, ctx: &Context<'_>, term: String) -> Result<SearchResults, Error> { 78 + let indexes = ctx.data::<Indexes>()?; 79 + let albums = search_entities( 80 + &indexes.albums, 81 + &term, 82 + &rockbox_search::album::Album::default(), 83 + )?; 84 + let artists = search_entities( 85 + &indexes.artists, 86 + &term, 87 + &rockbox_search::artist::Artist::default(), 88 + )?; 89 + let tracks = search_entities( 90 + &indexes.tracks, 91 + &term, 92 + &rockbox_search::track::Track::default(), 93 + )?; 94 + let liked_tracks = search_entities( 95 + &indexes.liked_tracks, 96 + &term, 97 + &rockbox_search::liked_track::LikedTrack::default(), 98 + )?; 99 + let liked_albums = search_entities( 100 + &indexes.liked_albums, 101 + &term, 102 + &rockbox_search::liked_album::LikedAlbum::default(), 103 + )?; 104 + 105 + Ok(SearchResults { 106 + albums: albums.into_iter().map(|(_, x)| x.into()).collect(), 107 + artists: artists.into_iter().map(|(_, x)| x.into()).collect(), 108 + tracks: tracks.into_iter().map(|(_, x)| x.into()).collect(), 109 + liked_tracks: liked_tracks.into_iter().map(|(_, x)| x.into()).collect(), 110 + liked_albums: liked_albums.into_iter().map(|(_, x)| x.into()).collect(), 111 + }) 112 + } 77 113 } 78 114 79 115 #[derive(Default)] ··· 124 160 } 125 161 126 162 async fn scan_library(&self, ctx: &Context<'_>) -> Result<i32, Error> { 127 - let pool = ctx.data::<Pool<Sqlite>>()?; 128 - let home = env::var("HOME")?; 129 - let path = env::var("ROCKBOX_LIBRARY").unwrap_or(format!("{}/Music", home)); 130 - scan_audio_files(pool.clone(), path.into()).await?; 163 + let client = ctx.data::<reqwest::Client>().unwrap(); 164 + let url = format!("{}/scan-library", rockbox_url()); 165 + client.put(&url).send().await?; 131 166 Ok(0) 132 167 } 133 168 }
+109
crates/graphql/src/schema/objects/album.rs
··· 1 1 use async_graphql::*; 2 2 use serde::{Deserialize, Serialize}; 3 + use tantivy::schema::Schema; 4 + use tantivy::schema::SchemaBuilder; 5 + use tantivy::schema::Value; 6 + use tantivy::schema::*; 7 + use tantivy::TantivyDocument; 3 8 4 9 use super::track::Track; 5 10 ··· 70 75 } 71 76 } 72 77 } 78 + 79 + impl From<rockbox_search::album::Album> for Album { 80 + fn from(album: rockbox_search::album::Album) -> Self { 81 + Self { 82 + id: album.id, 83 + title: album.title, 84 + artist: album.artist, 85 + year: album.year as u32, 86 + year_string: album.year_string, 87 + album_art: album.album_art, 88 + md5: album.md5, 89 + artist_id: album.artist_id, 90 + tracks: vec![], 91 + } 92 + } 93 + } 94 + 95 + impl From<rockbox_search::liked_album::LikedAlbum> for Album { 96 + fn from(album: rockbox_search::liked_album::LikedAlbum) -> Self { 97 + Self { 98 + id: album.id, 99 + title: album.title, 100 + artist: album.artist, 101 + year: album.year as u32, 102 + year_string: album.year_string, 103 + album_art: album.album_art, 104 + md5: album.md5, 105 + artist_id: album.artist_id, 106 + tracks: vec![], 107 + } 108 + } 109 + } 110 + 111 + impl From<TantivyDocument> for Album { 112 + fn from(document: TantivyDocument) -> Self { 113 + let mut schema_builder: SchemaBuilder = Schema::builder(); 114 + 115 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 116 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 117 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 118 + let year_field = schema_builder.add_i64_field("year", STORED); 119 + let year_string_field = schema_builder.add_text_field("year_string", STRING | STORED); 120 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 121 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 122 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 123 + 124 + let id = document 125 + .get_first(id_field) 126 + .unwrap() 127 + .as_str() 128 + .unwrap() 129 + .to_string(); 130 + let title = document 131 + .get_first(title_field) 132 + .unwrap() 133 + .as_str() 134 + .unwrap() 135 + .to_string(); 136 + let artist = document 137 + .get_first(artist_field) 138 + .unwrap() 139 + .as_str() 140 + .unwrap() 141 + .to_string(); 142 + let year = document.get_first(year_field).unwrap().as_i64().unwrap() as u32; 143 + let year_string = document 144 + .get_first(year_string_field) 145 + .unwrap() 146 + .as_str() 147 + .unwrap() 148 + .to_string(); 149 + let album_art = match document.get_first(album_art_field) { 150 + Some(album_art) => album_art.as_str(), 151 + None => None, 152 + }; 153 + let album_art = match album_art { 154 + Some("") => None, 155 + Some(album_art) => Some(album_art.to_string()), 156 + None => None, 157 + }; 158 + let md5 = document 159 + .get_first(md5_field) 160 + .unwrap() 161 + .as_str() 162 + .unwrap() 163 + .to_string(); 164 + let artist_id = match document.get_first(artist_id_field) { 165 + Some(artist_id) => artist_id.as_str().unwrap().to_string(), 166 + None => "".to_string(), 167 + }; 168 + 169 + Self { 170 + id, 171 + title, 172 + artist, 173 + year, 174 + year_string, 175 + album_art, 176 + md5, 177 + artist_id, 178 + ..Default::default() 179 + } 180 + } 181 + }
+57 -2
crates/graphql/src/schema/objects/artist.rs
··· 1 + use super::{album::Album, track::Track}; 1 2 use async_graphql::*; 2 3 use serde::{Deserialize, Serialize}; 3 - 4 - use super::{album::Album, track::Track}; 4 + use tantivy::schema::Schema; 5 + use tantivy::schema::SchemaBuilder; 6 + use tantivy::schema::Value; 7 + use tantivy::schema::*; 8 + use tantivy::TantivyDocument; 5 9 6 10 #[derive(Default, Clone, Serialize, Deserialize)] 7 11 pub struct Artist { ··· 52 56 } 53 57 } 54 58 } 59 + 60 + impl From<rockbox_search::artist::Artist> for Artist { 61 + fn from(artist: rockbox_search::artist::Artist) -> Self { 62 + Self { 63 + id: artist.id, 64 + name: artist.name, 65 + bio: artist.bio, 66 + image: artist.image, 67 + tracks: vec![], 68 + albums: vec![], 69 + } 70 + } 71 + } 72 + 73 + impl From<TantivyDocument> for Artist { 74 + fn from(document: TantivyDocument) -> Self { 75 + let mut schema_builder: SchemaBuilder = Schema::builder(); 76 + 77 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 78 + let name_field = schema_builder.add_text_field("name", TEXT | STORED); 79 + let bio_field = schema_builder.add_text_field("bio", TEXT | STORED); 80 + let image_field = schema_builder.add_text_field("image", STRING | STORED); 81 + 82 + let id = document 83 + .get_first(id_field) 84 + .unwrap() 85 + .as_str() 86 + .unwrap() 87 + .to_string(); 88 + let name = document 89 + .get_first(name_field) 90 + .unwrap() 91 + .as_str() 92 + .unwrap() 93 + .to_string(); 94 + let bio = document 95 + .get_first(bio_field) 96 + .map(|value| value.as_str().unwrap().to_string()); 97 + let image = document 98 + .get_first(image_field) 99 + .map(|value| value.as_str().unwrap().to_string()); 100 + 101 + Self { 102 + id, 103 + name, 104 + bio, 105 + image, 106 + ..Default::default() 107 + } 108 + } 109 + }
+1
crates/graphql/src/schema/objects/mod.rs
··· 6 6 pub mod eq_band_setting; 7 7 pub mod playlist; 8 8 pub mod replaygain_settings; 9 + pub mod search; 9 10 pub mod settings_list; 10 11 pub mod system_status; 11 12 pub mod track;
+48
crates/graphql/src/schema/objects/search.rs
··· 1 + use async_graphql::*; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + use super::{album::Album, artist::Artist, track::Track}; 5 + 6 + #[derive(Default, Clone, Serialize, Deserialize)] 7 + pub struct SearchResults { 8 + pub artists: Vec<Artist>, 9 + pub albums: Vec<Album>, 10 + pub tracks: Vec<Track>, 11 + pub liked_tracks: Vec<Track>, 12 + pub liked_albums: Vec<Album>, 13 + } 14 + 15 + #[Object] 16 + impl SearchResults { 17 + async fn artists(&self) -> Vec<Artist> { 18 + self.artists.clone() 19 + } 20 + 21 + async fn albums(&self) -> Vec<Album> { 22 + self.albums.clone() 23 + } 24 + 25 + async fn tracks(&self) -> Vec<Track> { 26 + self.tracks.clone() 27 + } 28 + 29 + async fn liked_tracks(&self) -> Vec<Track> { 30 + self.liked_tracks.clone() 31 + } 32 + 33 + async fn liked_albums(&self) -> Vec<Album> { 34 + self.liked_albums.clone() 35 + } 36 + } 37 + 38 + impl From<rockbox_types::SearchResults> for SearchResults { 39 + fn from(results: rockbox_types::SearchResults) -> Self { 40 + SearchResults { 41 + artists: results.artists.into_iter().map(Into::into).collect(), 42 + albums: results.albums.into_iter().map(Into::into).collect(), 43 + tracks: results.tracks.into_iter().map(Into::into).collect(), 44 + liked_tracks: results.liked_tracks.into_iter().map(Into::into).collect(), 45 + liked_albums: results.liked_albums.into_iter().map(Into::into).collect(), 46 + } 47 + } 48 + }
+224
crates/graphql/src/schema/objects/track.rs
··· 1 1 use async_graphql::*; 2 2 use rockbox_sys::types::mp3_entry::Mp3Entry; 3 3 use serde::{Deserialize, Serialize}; 4 + use tantivy::schema::Schema; 5 + use tantivy::schema::SchemaBuilder; 6 + use tantivy::schema::Value; 7 + use tantivy::schema::*; 8 + use tantivy::TantivyDocument; 4 9 5 10 #[derive(Default, Clone, Serialize, Deserialize)] 6 11 pub struct Track { ··· 225 230 } 226 231 } 227 232 } 233 + 234 + impl From<rockbox_search::track::Track> for Track { 235 + fn from(track: rockbox_search::track::Track) -> Self { 236 + Self { 237 + id: Some(track.id), 238 + title: track.title, 239 + artist: track.artist, 240 + album: track.album, 241 + genre: track.genre, 242 + year_string: track.year_string, 243 + composer: track.composer, 244 + album_artist: track.album_artist, 245 + discnum: track.disc_number as i32, 246 + tracknum: track.track_number as i32, 247 + year: track.year as i32, 248 + bitrate: track.bitrate as u32, 249 + frequency: track.frequency as u64, 250 + filesize: track.filesize as u64, 251 + length: track.length as u64, 252 + artist_id: track.artist_id, 253 + album_id: track.album_id, 254 + genre_id: track.genre_id, 255 + path: track.path, 256 + album_art: track.album_art, 257 + ..Default::default() 258 + } 259 + } 260 + } 261 + 262 + impl From<rockbox_search::liked_track::LikedTrack> for Track { 263 + fn from(track: rockbox_search::liked_track::LikedTrack) -> Self { 264 + Self { 265 + id: Some(track.id), 266 + title: track.title, 267 + artist: track.artist, 268 + album: track.album, 269 + genre: track.genre, 270 + year_string: track.year_string, 271 + composer: track.composer, 272 + album_artist: track.album_artist, 273 + discnum: track.disc_number as i32, 274 + tracknum: track.track_number as i32, 275 + year: track.year as i32, 276 + bitrate: track.bitrate as u32, 277 + frequency: track.frequency as u64, 278 + filesize: track.filesize as u64, 279 + length: track.length as u64, 280 + artist_id: track.artist_id, 281 + album_id: track.album_id, 282 + genre_id: track.genre_id, 283 + path: track.path, 284 + album_art: track.album_art, 285 + ..Default::default() 286 + } 287 + } 288 + } 289 + 290 + impl From<TantivyDocument> for Track { 291 + fn from(document: TantivyDocument) -> Self { 292 + let mut schema_builder: SchemaBuilder = Schema::builder(); 293 + 294 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 295 + let path_field = schema_builder.add_text_field("path", TEXT | STORED); 296 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 297 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 298 + let album_field = schema_builder.add_text_field("album", TEXT | STORED); 299 + let album_artist_field = schema_builder.add_text_field("album_artist", TEXT | STORED); 300 + let bitrate_field = schema_builder.add_i64_field("bitrate", STORED); 301 + let composer_field = schema_builder.add_text_field("composer", TEXT | STORED); 302 + let disc_number_field = schema_builder.add_i64_field("disc_number", STORED); 303 + let filesize_field = schema_builder.add_i64_field("filesize", STORED); 304 + let frequency_field = schema_builder.add_i64_field("frequency", STORED); 305 + let length_field = schema_builder.add_i64_field("length", STORED); 306 + let track_number_field = schema_builder.add_i64_field("track_number", STORED); 307 + let year_field = schema_builder.add_i64_field("year", STORED); 308 + let year_string_field = schema_builder.add_text_field("year_string", STRING | STORED); 309 + let genre_field = schema_builder.add_text_field("genre", TEXT | STORED); 310 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 311 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 312 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 313 + let album_id_field = schema_builder.add_text_field("album_id", STRING | STORED); 314 + let genre_id_field = schema_builder.add_text_field("genre_id", STRING | STORED); 315 + let created_at_field = schema_builder.add_text_field("created_at", STRING | STORED); 316 + let updated_at_field = schema_builder.add_text_field("updated_at", STRING | STORED); 317 + 318 + let id = document 319 + .get_first(id_field) 320 + .unwrap() 321 + .as_str() 322 + .unwrap() 323 + .to_string(); 324 + let path = document 325 + .get_first(path_field) 326 + .unwrap() 327 + .as_str() 328 + .unwrap() 329 + .to_string(); 330 + let title = document 331 + .get_first(title_field) 332 + .unwrap() 333 + .as_str() 334 + .unwrap() 335 + .to_string(); 336 + let artist = document 337 + .get_first(artist_field) 338 + .unwrap() 339 + .as_str() 340 + .unwrap() 341 + .to_string(); 342 + let album = document 343 + .get_first(album_field) 344 + .unwrap() 345 + .as_str() 346 + .unwrap() 347 + .to_string(); 348 + let album_artist = document 349 + .get_first(album_artist_field) 350 + .unwrap() 351 + .as_str() 352 + .unwrap() 353 + .to_string(); 354 + let bitrate = document.get_first(bitrate_field).unwrap().as_i64().unwrap() as u32; 355 + let composer = document 356 + .get_first(composer_field) 357 + .unwrap() 358 + .as_str() 359 + .unwrap() 360 + .to_string(); 361 + let disc_number = document 362 + .get_first(disc_number_field) 363 + .unwrap() 364 + .as_i64() 365 + .unwrap() as u64; 366 + let filesize = document 367 + .get_first(filesize_field) 368 + .unwrap() 369 + .as_i64() 370 + .unwrap() as u64; 371 + let frequency = document 372 + .get_first(frequency_field) 373 + .unwrap() 374 + .as_i64() 375 + .unwrap() as u64; 376 + let length = document.get_first(length_field).unwrap().as_i64().unwrap() as u64; 377 + let track_number = document 378 + .get_first(track_number_field) 379 + .unwrap() 380 + .as_i64() 381 + .unwrap() as u64; 382 + let year = document.get_first(year_field).unwrap().as_i64().unwrap() as i32; 383 + let year_string = document 384 + .get_first(year_string_field) 385 + .unwrap() 386 + .as_str() 387 + .unwrap() 388 + .to_string(); 389 + let genre = document 390 + .get_first(genre_field) 391 + .unwrap() 392 + .as_str() 393 + .unwrap() 394 + .to_string(); 395 + let md5 = document 396 + .get_first(md5_field) 397 + .unwrap() 398 + .as_str() 399 + .unwrap() 400 + .to_string(); 401 + let album_art = match document.get_first(album_art_field) { 402 + Some(album_art) => album_art.as_str(), 403 + None => None, 404 + }; 405 + let album_art = match album_art { 406 + Some("") => None, 407 + Some(album_art) => Some(album_art.to_string()), 408 + None => None, 409 + }; 410 + let artist_id = match document.get_first(artist_id_field) { 411 + Some(artist_id) => Some(artist_id.as_str().unwrap().to_string()), 412 + None => None, 413 + }; 414 + let album_id = match document.get_first(album_id_field) { 415 + Some(album_id) => Some(album_id.as_str().unwrap().to_string()), 416 + None => None, 417 + }; 418 + let album_id = match album_id { 419 + Some(album_id) => Some(album_id.to_string()), 420 + None => None, 421 + }; 422 + let genre_id = match document.get_first(genre_id_field) { 423 + Some(genre_id) => Some(genre_id.as_str().unwrap().to_string()), 424 + None => None, 425 + }; 426 + 427 + Self { 428 + id: Some(id), 429 + path, 430 + title, 431 + artist, 432 + album, 433 + album_artist, 434 + bitrate, 435 + composer, 436 + discnum: disc_number as i32, 437 + filesize, 438 + frequency, 439 + length, 440 + tracknum: track_number as i32, 441 + year, 442 + year_string, 443 + genre, 444 + album_art, 445 + artist_id, 446 + album_id, 447 + genre_id, 448 + ..Default::default() 449 + } 450 + } 451 + }
+3
crates/graphql/src/server.rs
··· 16 16 use async_graphql::{http::GraphiQLSource, Schema}; 17 17 use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; 18 18 use rockbox_library::{create_connection_pool, repo}; 19 + use rockbox_search::create_indexes; 19 20 use rockbox_sys::events::RockboxCommand; 20 21 use rockbox_webui::{dist, index, index_spa}; 21 22 use sqlx::{Pool, Sqlite}; ··· 89 90 pub async fn start(cmd_tx: Arc<Mutex<Sender<RockboxCommand>>>) -> Result<(), Error> { 90 91 let client = reqwest::Client::new(); 91 92 let pool = create_connection_pool().await?; 93 + let indexes = create_indexes()?; 92 94 let schema = Schema::build( 93 95 Query::default(), 94 96 Mutation::default(), ··· 97 99 .data(cmd_tx) 98 100 .data(client) 99 101 .data(pool.clone()) 102 + .data(indexes) 100 103 .finish(); 101 104 102 105 let graphql_port = std::env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or("6062".to_string());
+1 -1
crates/library/src/entity/album.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 - #[derive(sqlx::FromRow, Default, Serialize, Deserialize)] 3 + #[derive(sqlx::FromRow, Default, Serialize, Deserialize, Clone)] 4 4 pub struct Album { 5 5 pub id: String, 6 6 pub title: String,
+1 -1
crates/library/src/entity/artist.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 - #[derive(sqlx::FromRow, Default, Serialize, Deserialize)] 3 + #[derive(sqlx::FromRow, Default, Serialize, Deserialize, Clone)] 4 4 pub struct Artist { 5 5 pub id: String, 6 6 pub name: String,
+3
crates/rpc/Cargo.toml
··· 12 12 prost = "0.13.2" 13 13 reqwest = {version = "0.12.7", features = ["rustls-tls", "json"], default-features = false} 14 14 rockbox-library = {path = "../library"} 15 + rockbox-search = {path = "../search"} 15 16 rockbox-sys = {path = "../sys"} 17 + rockbox-types = {path = "../types"} 16 18 serde = {version = "1.0.210", features = ["derive"]} 17 19 serde_json = "1.0.128" 18 20 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]} 21 + tantivy = "0.22.0" 19 22 tokio = {version = "1.36.0", features = ["full"]} 20 23 tonic = "0.12.2" 21 24 tonic-reflection = "0.12.2"
+11
crates/rpc/proto/rockbox/v1alpha1/library.proto
··· 131 131 132 132 message ScanLibraryResponse {} 133 133 134 + message SearchRequest { 135 + string term = 1; 136 + } 137 + 138 + message SearchResponse { 139 + repeated Track tracks = 1; 140 + repeated Album albums = 2; 141 + repeated Artist artists = 3; 142 + } 143 + 134 144 service LibraryService { 135 145 rpc GetAlbums(GetAlbumsRequest) returns (GetAlbumsResponse); 136 146 rpc GetArtists(GetArtistsRequest) returns (GetArtistsResponse); ··· 145 155 rpc GetLikedTracks(GetLikedTracksRequest) returns (GetLikedTracksResponse); 146 156 rpc GetLikedAlbums(GetLikedAlbumsRequest) returns (GetLikedAlbumsResponse); 147 157 rpc ScanLibrary(ScanLibraryRequest) returns (ScanLibraryResponse); 158 + rpc Search(SearchRequest) returns (SearchResponse); 148 159 }
+85
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 505 505 pub struct ScanLibraryRequest {} 506 506 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 507 507 pub struct ScanLibraryResponse {} 508 + #[derive(Clone, PartialEq, ::prost::Message)] 509 + pub struct SearchRequest { 510 + #[prost(string, tag = "1")] 511 + pub term: ::prost::alloc::string::String, 512 + } 513 + #[derive(Clone, PartialEq, ::prost::Message)] 514 + pub struct SearchResponse { 515 + #[prost(message, repeated, tag = "1")] 516 + pub tracks: ::prost::alloc::vec::Vec<Track>, 517 + #[prost(message, repeated, tag = "2")] 518 + pub albums: ::prost::alloc::vec::Vec<Album>, 519 + #[prost(message, repeated, tag = "3")] 520 + pub artists: ::prost::alloc::vec::Vec<Artist>, 521 + } 508 522 /// Generated client implementations. 509 523 pub mod library_service_client { 510 524 #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] ··· 927 941 ); 928 942 self.inner.unary(req, path, codec).await 929 943 } 944 + pub async fn search( 945 + &mut self, 946 + request: impl tonic::IntoRequest<super::SearchRequest>, 947 + ) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> { 948 + self.inner 949 + .ready() 950 + .await 951 + .map_err(|e| { 952 + tonic::Status::new( 953 + tonic::Code::Unknown, 954 + format!("Service was not ready: {}", e.into()), 955 + ) 956 + })?; 957 + let codec = tonic::codec::ProstCodec::default(); 958 + let path = http::uri::PathAndQuery::from_static( 959 + "/rockbox.v1alpha1.LibraryService/Search", 960 + ); 961 + let mut req = request.into_request(); 962 + req.extensions_mut() 963 + .insert(GrpcMethod::new("rockbox.v1alpha1.LibraryService", "Search")); 964 + self.inner.unary(req, path, codec).await 965 + } 930 966 } 931 967 } 932 968 /// Generated server implementations. ··· 1027 1063 tonic::Response<super::ScanLibraryResponse>, 1028 1064 tonic::Status, 1029 1065 >; 1066 + async fn search( 1067 + &self, 1068 + request: tonic::Request<super::SearchRequest>, 1069 + ) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>; 1030 1070 } 1031 1071 #[derive(Debug)] 1032 1072 pub struct LibraryServiceServer<T> { ··· 1676 1716 let inner = self.inner.clone(); 1677 1717 let fut = async move { 1678 1718 let method = ScanLibrarySvc(inner); 1719 + let codec = tonic::codec::ProstCodec::default(); 1720 + let mut grpc = tonic::server::Grpc::new(codec) 1721 + .apply_compression_config( 1722 + accept_compression_encodings, 1723 + send_compression_encodings, 1724 + ) 1725 + .apply_max_message_size_config( 1726 + max_decoding_message_size, 1727 + max_encoding_message_size, 1728 + ); 1729 + let res = grpc.unary(method, req).await; 1730 + Ok(res) 1731 + }; 1732 + Box::pin(fut) 1733 + } 1734 + "/rockbox.v1alpha1.LibraryService/Search" => { 1735 + #[allow(non_camel_case_types)] 1736 + struct SearchSvc<T: LibraryService>(pub Arc<T>); 1737 + impl< 1738 + T: LibraryService, 1739 + > tonic::server::UnaryService<super::SearchRequest> 1740 + for SearchSvc<T> { 1741 + type Response = super::SearchResponse; 1742 + type Future = BoxFuture< 1743 + tonic::Response<Self::Response>, 1744 + tonic::Status, 1745 + >; 1746 + fn call( 1747 + &mut self, 1748 + request: tonic::Request<super::SearchRequest>, 1749 + ) -> Self::Future { 1750 + let inner = Arc::clone(&self.0); 1751 + let fut = async move { 1752 + <T as LibraryService>::search(&inner, request).await 1753 + }; 1754 + Box::pin(fut) 1755 + } 1756 + } 1757 + let accept_compression_encodings = self.accept_compression_encodings; 1758 + let send_compression_encodings = self.send_compression_encodings; 1759 + let max_decoding_message_size = self.max_decoding_message_size; 1760 + let max_encoding_message_size = self.max_encoding_message_size; 1761 + let inner = self.inner.clone(); 1762 + let fut = async move { 1763 + let method = SearchSvc(inner); 1679 1764 let codec = tonic::codec::ProstCodec::default(); 1680 1765 let mut grpc = tonic::server::Grpc::new(codec) 1681 1766 .apply_compression_config(
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+378 -1
crates/rpc/src/lib.rs
··· 26 26 system_status::SystemStatus, 27 27 user_settings::{CompressorSettings, EqBandSetting, ReplaygainSettings, UserSettings}, 28 28 }; 29 + use tantivy::schema::Schema; 30 + use tantivy::schema::SchemaBuilder; 31 + use tantivy::schema::*; 32 + use tantivy::TantivyDocument; 29 33 use v1alpha1::{ 30 34 Album, Artist, CurrentTrackResponse, Entry, GetGlobalSettingsResponse, 31 - GetGlobalStatusResponse, NextTrackResponse, Track, 35 + GetGlobalStatusResponse, NextTrackResponse, SearchResponse, Track, 32 36 }; 33 37 34 38 #[path = "rockbox.v1alpha1.rs"] ··· 719 723 genre_id: Some(track.genre_id), 720 724 created_at: track.created_at.to_rfc3339(), 721 725 updated_at: track.updated_at.to_rfc3339(), 726 + } 727 + } 728 + } 729 + 730 + impl From<rockbox_search::album::Album> for Album { 731 + fn from(album: rockbox_search::album::Album) -> Self { 732 + Self { 733 + id: album.id, 734 + title: album.title, 735 + artist: album.artist, 736 + year: album.year as u32, 737 + year_string: album.year_string, 738 + album_art: album.album_art, 739 + md5: album.md5, 740 + artist_id: album.artist_id, 741 + tracks: vec![], 742 + } 743 + } 744 + } 745 + 746 + impl From<rockbox_search::artist::Artist> for Artist { 747 + fn from(artist: rockbox_search::artist::Artist) -> Self { 748 + Self { 749 + id: artist.id, 750 + name: artist.name, 751 + bio: artist.bio, 752 + image: artist.image, 753 + albums: vec![], 754 + tracks: vec![], 755 + } 756 + } 757 + } 758 + 759 + impl From<rockbox_search::track::Track> for Track { 760 + fn from(track: rockbox_search::track::Track) -> Self { 761 + Self { 762 + id: track.id, 763 + path: track.path, 764 + title: track.title, 765 + artist: track.artist, 766 + album: track.album, 767 + album_artist: track.album_artist, 768 + bitrate: track.bitrate as u32, 769 + composer: track.composer, 770 + disc_number: track.disc_number as u32, 771 + filesize: track.filesize as u32, 772 + frequency: track.frequency as u32, 773 + length: track.length as u32, 774 + track_number: track.track_number as u32, 775 + year: track.year as u32, 776 + year_string: track.year_string, 777 + genre: track.genre, 778 + md5: track.md5, 779 + album_art: track.album_art, 780 + artist_id: track.artist_id, 781 + album_id: track.album_id, 782 + genre_id: track.genre_id, 783 + created_at: track.created_at, 784 + updated_at: track.updated_at, 785 + } 786 + } 787 + } 788 + 789 + impl From<rockbox_types::SearchResults> for SearchResponse { 790 + fn from(results: rockbox_types::SearchResults) -> Self { 791 + let artists = results 792 + .artists 793 + .into_iter() 794 + .map(|artist| artist.into()) 795 + .collect(); 796 + let albums = results 797 + .albums 798 + .into_iter() 799 + .map(|album| album.into()) 800 + .collect(); 801 + let tracks = results 802 + .tracks 803 + .into_iter() 804 + .map(|track| track.into()) 805 + .collect(); 806 + 807 + Self { 808 + artists, 809 + albums, 810 + tracks, 811 + } 812 + } 813 + } 814 + 815 + impl From<TantivyDocument> for Album { 816 + fn from(document: TantivyDocument) -> Self { 817 + let mut schema_builder: SchemaBuilder = Schema::builder(); 818 + 819 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 820 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 821 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 822 + let year_field = schema_builder.add_i64_field("year", STORED); 823 + let year_string_field = 824 + schema_builder.add_text_field("year_string", STRING | STORED); 825 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 826 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 827 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 828 + 829 + let id = document 830 + .get_first(id_field) 831 + .unwrap() 832 + .as_str() 833 + .unwrap() 834 + .to_string(); 835 + let title = document 836 + .get_first(title_field) 837 + .unwrap() 838 + .as_str() 839 + .unwrap() 840 + .to_string(); 841 + let artist = document 842 + .get_first(artist_field) 843 + .unwrap() 844 + .as_str() 845 + .unwrap() 846 + .to_string(); 847 + let year = document.get_first(year_field).unwrap().as_i64().unwrap() as u32; 848 + let year_string = document 849 + .get_first(year_string_field) 850 + .unwrap() 851 + .as_str() 852 + .unwrap() 853 + .to_string(); 854 + let album_art = match document.get_first(album_art_field) { 855 + Some(album_art) => album_art.as_str(), 856 + None => None, 857 + }; 858 + let album_art = match album_art { 859 + Some("") => None, 860 + Some(album_art) => Some(album_art.to_string()), 861 + None => None, 862 + }; 863 + let md5 = document 864 + .get_first(md5_field) 865 + .unwrap() 866 + .as_str() 867 + .unwrap() 868 + .to_string(); 869 + let artist_id = match document.get_first(artist_id_field) { 870 + Some(artist_id) => artist_id.as_str().unwrap().to_string(), 871 + None => "".to_string(), 872 + }; 873 + 874 + Self { 875 + id, 876 + title, 877 + artist, 878 + year, 879 + year_string, 880 + album_art, 881 + md5, 882 + artist_id, 883 + ..Default::default() 884 + } 885 + } 886 + } 887 + 888 + impl From<TantivyDocument> for Artist { 889 + fn from(document: TantivyDocument) -> Self { 890 + let mut schema_builder: SchemaBuilder = Schema::builder(); 891 + 892 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 893 + let name_field = schema_builder.add_text_field("name", TEXT | STORED); 894 + let bio_field = schema_builder.add_text_field("bio", TEXT | STORED); 895 + let image_field = schema_builder.add_text_field("image", STRING | STORED); 896 + 897 + let id = document 898 + .get_first(id_field) 899 + .unwrap() 900 + .as_str() 901 + .unwrap() 902 + .to_string(); 903 + let name = document 904 + .get_first(name_field) 905 + .unwrap() 906 + .as_str() 907 + .unwrap() 908 + .to_string(); 909 + let bio = document 910 + .get_first(bio_field) 911 + .map(|value| value.as_str().unwrap().to_string()); 912 + let image = document 913 + .get_first(image_field) 914 + .map(|value| value.as_str().unwrap().to_string()); 915 + 916 + Self { 917 + id, 918 + name, 919 + bio, 920 + image, 921 + ..Default::default() 922 + } 923 + } 924 + } 925 + 926 + impl From<TantivyDocument> for Track { 927 + fn from(document: TantivyDocument) -> Self { 928 + let mut schema_builder: SchemaBuilder = Schema::builder(); 929 + 930 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 931 + let path_field = schema_builder.add_text_field("path", TEXT | STORED); 932 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 933 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 934 + let album_field = schema_builder.add_text_field("album", TEXT | STORED); 935 + let album_artist_field = 936 + schema_builder.add_text_field("album_artist", TEXT | STORED); 937 + let bitrate_field = schema_builder.add_i64_field("bitrate", STORED); 938 + let composer_field = schema_builder.add_text_field("composer", TEXT | STORED); 939 + let disc_number_field = schema_builder.add_i64_field("disc_number", STORED); 940 + let filesize_field = schema_builder.add_i64_field("filesize", STORED); 941 + let frequency_field = schema_builder.add_i64_field("frequency", STORED); 942 + let length_field = schema_builder.add_i64_field("length", STORED); 943 + let track_number_field = schema_builder.add_i64_field("track_number", STORED); 944 + let year_field = schema_builder.add_i64_field("year", STORED); 945 + let year_string_field = 946 + schema_builder.add_text_field("year_string", STRING | STORED); 947 + let genre_field = schema_builder.add_text_field("genre", TEXT | STORED); 948 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 949 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 950 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 951 + let album_id_field = schema_builder.add_text_field("album_id", STRING | STORED); 952 + let genre_id_field = schema_builder.add_text_field("genre_id", STRING | STORED); 953 + let created_at_field = schema_builder.add_text_field("created_at", STRING | STORED); 954 + let updated_at_field = schema_builder.add_text_field("updated_at", STRING | STORED); 955 + 956 + let id = document 957 + .get_first(id_field) 958 + .unwrap() 959 + .as_str() 960 + .unwrap() 961 + .to_string(); 962 + let path = document 963 + .get_first(path_field) 964 + .unwrap() 965 + .as_str() 966 + .unwrap() 967 + .to_string(); 968 + let title = document 969 + .get_first(title_field) 970 + .unwrap() 971 + .as_str() 972 + .unwrap() 973 + .to_string(); 974 + let artist = document 975 + .get_first(artist_field) 976 + .unwrap() 977 + .as_str() 978 + .unwrap() 979 + .to_string(); 980 + let album = document 981 + .get_first(album_field) 982 + .unwrap() 983 + .as_str() 984 + .unwrap() 985 + .to_string(); 986 + let album_artist = document 987 + .get_first(album_artist_field) 988 + .unwrap() 989 + .as_str() 990 + .unwrap() 991 + .to_string(); 992 + let bitrate = document.get_first(bitrate_field).unwrap().as_i64().unwrap() as u32; 993 + let composer = document 994 + .get_first(composer_field) 995 + .unwrap() 996 + .as_str() 997 + .unwrap() 998 + .to_string(); 999 + let disc_number = document 1000 + .get_first(disc_number_field) 1001 + .unwrap() 1002 + .as_i64() 1003 + .unwrap() as u32; 1004 + let filesize = document 1005 + .get_first(filesize_field) 1006 + .unwrap() 1007 + .as_i64() 1008 + .unwrap() as u32; 1009 + let frequency = document 1010 + .get_first(frequency_field) 1011 + .unwrap() 1012 + .as_i64() 1013 + .unwrap() as u32; 1014 + let length = document.get_first(length_field).unwrap().as_i64().unwrap() as u32; 1015 + let track_number = document 1016 + .get_first(track_number_field) 1017 + .unwrap() 1018 + .as_i64() 1019 + .unwrap() as u32; 1020 + let year = document.get_first(year_field).unwrap().as_i64().unwrap() as u32; 1021 + let year_string = document 1022 + .get_first(year_string_field) 1023 + .unwrap() 1024 + .as_str() 1025 + .unwrap() 1026 + .to_string(); 1027 + let genre = document 1028 + .get_first(genre_field) 1029 + .unwrap() 1030 + .as_str() 1031 + .unwrap() 1032 + .to_string(); 1033 + let md5 = document 1034 + .get_first(md5_field) 1035 + .unwrap() 1036 + .as_str() 1037 + .unwrap() 1038 + .to_string(); 1039 + let album_art = match document.get_first(album_art_field) { 1040 + Some(album_art) => album_art.as_str(), 1041 + None => None, 1042 + }; 1043 + let album_art = match album_art { 1044 + Some("") => None, 1045 + Some(album_art) => Some(album_art.to_string()), 1046 + None => None, 1047 + }; 1048 + let artist_id = match document.get_first(artist_id_field) { 1049 + Some(artist_id) => Some(artist_id.as_str().unwrap().to_string()), 1050 + None => None, 1051 + }; 1052 + let album_id = match document.get_first(album_id_field) { 1053 + Some(album_id) => Some(album_id.as_str().unwrap().to_string()), 1054 + None => None, 1055 + }; 1056 + let album_id = match album_id { 1057 + Some(album_id) => Some(album_id.to_string()), 1058 + None => None, 1059 + }; 1060 + let genre_id = match document.get_first(genre_id_field) { 1061 + Some(genre_id) => Some(genre_id.as_str().unwrap().to_string()), 1062 + None => None, 1063 + }; 1064 + let created_at = document 1065 + .get_first(created_at_field) 1066 + .unwrap() 1067 + .as_str() 1068 + .unwrap() 1069 + .to_string(); 1070 + let updated_at = match document.get_first(updated_at_field) { 1071 + Some(updated_at) => updated_at.as_str().unwrap().to_string(), 1072 + None => "".to_string(), 1073 + }; 1074 + 1075 + Self { 1076 + id, 1077 + path, 1078 + title, 1079 + artist, 1080 + album, 1081 + album_artist, 1082 + bitrate, 1083 + composer, 1084 + disc_number, 1085 + filesize, 1086 + frequency, 1087 + length, 1088 + track_number, 1089 + year, 1090 + year_string, 1091 + genre, 1092 + md5, 1093 + album_art, 1094 + artist_id, 1095 + album_id, 1096 + genre_id, 1097 + created_at, 1098 + updated_at, 722 1099 } 723 1100 } 724 1101 }
+81 -18
crates/rpc/src/library.rs
··· 1 - use std::env; 2 - 3 - use rockbox_library::{audio_scan::scan_audio_files, entity::favourites::Favourites, repo}; 1 + use rockbox_library::{entity::favourites::Favourites, repo}; 2 + use rockbox_search::search_entities; 3 + use rockbox_types::SearchResults; 4 4 use sqlx::Sqlite; 5 5 6 - use crate::api::rockbox::v1alpha1::{ 7 - library_service_server::LibraryService, Album, Artist, GetAlbumRequest, GetAlbumResponse, 8 - GetAlbumsRequest, GetAlbumsResponse, GetArtistRequest, GetArtistResponse, GetArtistsRequest, 9 - GetArtistsResponse, GetLikedAlbumsRequest, GetLikedAlbumsResponse, GetLikedTracksRequest, 10 - GetLikedTracksResponse, GetTrackRequest, GetTrackResponse, GetTracksRequest, GetTracksResponse, 11 - LikeAlbumRequest, LikeAlbumResponse, LikeTrackRequest, LikeTrackResponse, ScanLibraryRequest, 12 - ScanLibraryResponse, UnlikeAlbumRequest, UnlikeAlbumResponse, UnlikeTrackRequest, 13 - UnlikeTrackResponse, 6 + use crate::{ 7 + api::rockbox::v1alpha1::{ 8 + library_service_server::LibraryService, Album, Artist, GetAlbumRequest, GetAlbumResponse, 9 + GetAlbumsRequest, GetAlbumsResponse, GetArtistRequest, GetArtistResponse, 10 + GetArtistsRequest, GetArtistsResponse, GetLikedAlbumsRequest, GetLikedAlbumsResponse, 11 + GetLikedTracksRequest, GetLikedTracksResponse, GetTrackRequest, GetTrackResponse, 12 + GetTracksRequest, GetTracksResponse, LikeAlbumRequest, LikeAlbumResponse, LikeTrackRequest, 13 + LikeTrackResponse, ScanLibraryRequest, ScanLibraryResponse, SearchRequest, SearchResponse, 14 + UnlikeAlbumRequest, UnlikeAlbumResponse, UnlikeTrackRequest, UnlikeTrackResponse, 15 + }, 16 + rockbox_url, 14 17 }; 15 18 16 19 pub struct Library { 17 20 pool: sqlx::Pool<Sqlite>, 21 + client: reqwest::Client, 22 + indexes: rockbox_search::Indexes, 18 23 } 19 24 20 25 impl Library { 21 - pub fn new(pool: sqlx::Pool<Sqlite>) -> Self { 22 - Self { pool } 26 + pub fn new( 27 + pool: sqlx::Pool<Sqlite>, 28 + client: reqwest::Client, 29 + indexes: rockbox_search::Indexes, 30 + ) -> Self { 31 + Self { 32 + pool, 33 + client, 34 + indexes, 35 + } 23 36 } 24 37 } 25 38 ··· 207 220 &self, 208 221 _request: tonic::Request<ScanLibraryRequest>, 209 222 ) -> Result<tonic::Response<ScanLibraryResponse>, tonic::Status> { 210 - let home = env::var("HOME").map_err(|e| tonic::Status::internal(e.to_string()))?; 211 - let path = env::var("ROCKBOX_LIBRARY").unwrap_or(format!("{}/Music", home)); 212 - 213 - scan_audio_files(self.pool.clone(), path.into()) 223 + let url = format!("{}/scan-library", rockbox_url()); 224 + self.client 225 + .put(&url) 226 + .send() 214 227 .await 215 228 .map_err(|e| tonic::Status::internal(e.to_string()))?; 229 + Ok(tonic::Response::new(ScanLibraryResponse {})) 230 + } 216 231 217 - Ok(tonic::Response::new(ScanLibraryResponse {})) 232 + async fn search( 233 + &self, 234 + request: tonic::Request<SearchRequest>, 235 + ) -> Result<tonic::Response<SearchResponse>, tonic::Status> { 236 + let request = request.into_inner(); 237 + let term = request.term; 238 + 239 + let albums = search_entities( 240 + &self.indexes.albums, 241 + &term, 242 + &rockbox_search::album::Album::default(), 243 + ) 244 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 245 + let artists = search_entities( 246 + &self.indexes.artists, 247 + &term, 248 + &rockbox_search::artist::Artist::default(), 249 + ) 250 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 251 + let tracks = search_entities( 252 + &self.indexes.tracks, 253 + &term, 254 + &rockbox_search::track::Track::default(), 255 + ) 256 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 257 + let files = search_entities( 258 + &self.indexes.files, 259 + &term, 260 + &rockbox_search::file::File::default(), 261 + ) 262 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 263 + let liked_tracks = search_entities( 264 + &self.indexes.liked_tracks, 265 + &term, 266 + &rockbox_search::liked_track::LikedTrack::default(), 267 + ) 268 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 269 + let liked_albums = search_entities( 270 + &self.indexes.liked_albums, 271 + &term, 272 + &rockbox_search::liked_album::LikedAlbum::default(), 273 + ) 274 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 275 + 276 + Ok(tonic::Response::new(SearchResponse { 277 + albums: albums.into_iter().map(|(_, x)| x.into()).collect(), 278 + artists: artists.into_iter().map(|(_, x)| x.into()).collect(), 279 + tracks: tracks.into_iter().map(|(_, x)| x.into()).collect(), 280 + })) 218 281 } 219 282 }
+4
crates/rpc/src/server.rs
··· 17 17 use crate::sound::Sound; 18 18 use crate::system::System; 19 19 use rockbox_library::create_connection_pool; 20 + use rockbox_search::create_indexes; 20 21 use rockbox_sys::events::RockboxCommand; 21 22 use tonic::transport::Server; 22 23 ··· 32 33 33 34 let client = reqwest::Client::new(); 34 35 let pool = create_connection_pool().await?; 36 + let indexes = create_indexes()?; 35 37 36 38 Server::builder() 37 39 .accept_http1(true) ··· 42 44 ) 43 45 .add_service(tonic_web::enable(LibraryServiceServer::new(Library::new( 44 46 pool.clone(), 47 + client.clone(), 48 + indexes, 45 49 )))) 46 50 .add_service(tonic_web::enable(PlaylistServiceServer::new( 47 51 Playlist::new(cmd_tx.clone(), client.clone(), pool.clone()),
+10
crates/search/Cargo.toml
··· 1 + [package] 2 + edition = "2021" 3 + name = "rockbox-search" 4 + version = "0.1.0" 5 + 6 + [dependencies] 7 + anyhow = "1.0.90" 8 + rockbox-library = {path = "../library"} 9 + serde = {version = "1.0.213", features = ["derive"]} 10 + tantivy = "0.22.0"
+159
crates/search/src/album.rs
··· 1 + use crate::{Indexable, Searchable}; 2 + use rockbox_library::entity; 3 + use serde::{Deserialize, Serialize}; 4 + use tantivy::{doc, schema::*, TantivyDocument}; 5 + 6 + #[derive(Debug, Default, Serialize, Deserialize)] 7 + pub struct Album { 8 + pub id: String, 9 + pub title: String, 10 + pub artist: String, 11 + pub year: i64, 12 + pub year_string: String, 13 + pub album_art: Option<String>, 14 + pub md5: String, 15 + pub artist_id: String, 16 + } 17 + 18 + impl Indexable for Album { 19 + fn to_document(&self) -> TantivyDocument { 20 + let schema: Schema = self.build_schema(); 21 + 22 + let id = schema.get_field("id").unwrap(); 23 + let title = schema.get_field("title").unwrap(); 24 + let artist = schema.get_field("artist").unwrap(); 25 + let year = schema.get_field("year").unwrap(); 26 + let year_string = schema.get_field("year_string").unwrap(); 27 + let album_art = schema.get_field("album_art").unwrap(); 28 + let md5 = schema.get_field("md5").unwrap(); 29 + let artist_id = schema.get_field("artist_id").unwrap(); 30 + 31 + let mut document = doc!( 32 + id => self.id.to_owned(), 33 + title => self.title.to_owned(), 34 + artist => self.artist.to_owned(), 35 + year => self.year, 36 + year_string => self.year_string.to_owned(), 37 + ); 38 + 39 + if let Some(value) = &self.album_art { 40 + document.add_text(album_art, value); 41 + } 42 + 43 + document.add_text(md5, &self.md5); 44 + document.add_text(artist_id, &self.artist_id); 45 + 46 + document 47 + } 48 + 49 + fn build_schema(&self) -> Schema { 50 + let mut schema_builder: SchemaBuilder = Schema::builder(); 51 + 52 + schema_builder.add_text_field("id", STRING | STORED); 53 + schema_builder.add_text_field("title", TEXT | STORED); 54 + schema_builder.add_text_field("artist", TEXT | STORED); 55 + schema_builder.add_i64_field("year", STORED); 56 + schema_builder.add_text_field("year_string", STRING | STORED); 57 + schema_builder.add_text_field("album_art", STRING | STORED); 58 + schema_builder.add_text_field("md5", STRING | STORED); 59 + schema_builder.add_text_field("artist_id", STRING | STORED); 60 + 61 + schema_builder.build() 62 + } 63 + } 64 + 65 + impl Searchable for Album { 66 + fn schema(&self) -> Schema { 67 + self.build_schema() 68 + } 69 + 70 + fn default_fields(&self) -> Vec<String> { 71 + vec!["title".to_string(), "artist".to_string()] 72 + } 73 + } 74 + 75 + impl From<entity::album::Album> for Album { 76 + fn from(album: entity::album::Album) -> Self { 77 + Self { 78 + id: album.id, 79 + title: album.title, 80 + artist: album.artist, 81 + year: album.year as i64, 82 + year_string: album.year_string, 83 + album_art: album.album_art, 84 + md5: album.md5, 85 + artist_id: album.artist_id, 86 + } 87 + } 88 + } 89 + 90 + impl From<TantivyDocument> for Album { 91 + fn from(document: TantivyDocument) -> Self { 92 + let mut schema_builder: SchemaBuilder = Schema::builder(); 93 + 94 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 95 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 96 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 97 + let year_field = schema_builder.add_i64_field("year", STORED); 98 + let year_string_field = schema_builder.add_text_field("year_string", STRING | STORED); 99 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 100 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 101 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 102 + 103 + let id = document 104 + .get_first(id_field) 105 + .unwrap() 106 + .as_str() 107 + .unwrap() 108 + .to_string(); 109 + let title = document 110 + .get_first(title_field) 111 + .unwrap() 112 + .as_str() 113 + .unwrap() 114 + .to_string(); 115 + let artist = document 116 + .get_first(artist_field) 117 + .unwrap() 118 + .as_str() 119 + .unwrap() 120 + .to_string(); 121 + let year = document.get_first(year_field).unwrap().as_i64().unwrap(); 122 + let year_string = document 123 + .get_first(year_string_field) 124 + .unwrap() 125 + .as_str() 126 + .unwrap() 127 + .to_string(); 128 + let album_art = match document.get_first(album_art_field) { 129 + Some(album_art) => album_art.as_str(), 130 + None => None, 131 + }; 132 + let album_art = match album_art { 133 + Some("") => None, 134 + Some(album_art) => Some(album_art.to_string()), 135 + None => None, 136 + }; 137 + let md5 = document 138 + .get_first(md5_field) 139 + .unwrap() 140 + .as_str() 141 + .unwrap() 142 + .to_string(); 143 + let artist_id = match document.get_first(artist_id_field) { 144 + Some(artist_id) => artist_id.as_str().unwrap().to_string(), 145 + None => "".to_string(), 146 + }; 147 + 148 + Self { 149 + id, 150 + title, 151 + artist, 152 + year, 153 + year_string, 154 + album_art, 155 + md5, 156 + artist_id, 157 + } 158 + } 159 + }
+107
crates/search/src/artist.rs
··· 1 + use crate::{Indexable, Searchable}; 2 + use rockbox_library::entity; 3 + use serde::{Deserialize, Serialize}; 4 + use tantivy::{doc, schema::*, TantivyDocument}; 5 + 6 + #[derive(Debug, Default, Serialize, Deserialize)] 7 + pub struct Artist { 8 + pub id: String, 9 + pub name: String, 10 + pub bio: Option<String>, 11 + pub image: Option<String>, 12 + } 13 + 14 + impl Indexable for Artist { 15 + fn to_document(&self) -> TantivyDocument { 16 + let schema: Schema = self.build_schema(); 17 + 18 + let id = schema.get_field("id").unwrap(); 19 + let name = schema.get_field("name").unwrap(); 20 + let bio = schema.get_field("bio").unwrap(); 21 + let image = schema.get_field("image").unwrap(); 22 + 23 + let mut document = doc!( 24 + id => self.id.to_owned(), 25 + name => self.name.to_owned(), 26 + ); 27 + 28 + if let Some(value) = &self.bio { 29 + document.add_text(bio, value); 30 + } 31 + 32 + if let Some(value) = &self.image { 33 + document.add_text(image, value); 34 + } 35 + 36 + document 37 + } 38 + 39 + fn build_schema(&self) -> Schema { 40 + let mut schema_builder: SchemaBuilder = Schema::builder(); 41 + 42 + schema_builder.add_text_field("id", STRING | STORED); 43 + schema_builder.add_text_field("name", TEXT | STORED); 44 + schema_builder.add_text_field("bio", TEXT | STORED); 45 + schema_builder.add_text_field("image", STRING | STORED); 46 + 47 + schema_builder.build() 48 + } 49 + } 50 + 51 + impl Searchable for Artist { 52 + fn schema(&self) -> Schema { 53 + self.build_schema() 54 + } 55 + 56 + fn default_fields(&self) -> Vec<String> { 57 + vec!["name".to_string(), "bio".to_string()] 58 + } 59 + } 60 + 61 + impl From<entity::artist::Artist> for Artist { 62 + fn from(artist: entity::artist::Artist) -> Self { 63 + Self { 64 + id: artist.id, 65 + name: artist.name, 66 + bio: artist.bio, 67 + image: artist.image, 68 + } 69 + } 70 + } 71 + 72 + impl From<TantivyDocument> for Artist { 73 + fn from(document: TantivyDocument) -> Self { 74 + let mut schema_builder: SchemaBuilder = Schema::builder(); 75 + 76 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 77 + let name_field = schema_builder.add_text_field("name", TEXT | STORED); 78 + let bio_field = schema_builder.add_text_field("bio", TEXT | STORED); 79 + let image_field = schema_builder.add_text_field("image", STRING | STORED); 80 + 81 + let id = document 82 + .get_first(id_field) 83 + .unwrap() 84 + .as_str() 85 + .unwrap() 86 + .to_string(); 87 + let name = document 88 + .get_first(name_field) 89 + .unwrap() 90 + .as_str() 91 + .unwrap() 92 + .to_string(); 93 + let bio = document 94 + .get_first(bio_field) 95 + .map(|value| value.as_str().unwrap().to_string()); 96 + let image = document 97 + .get_first(image_field) 98 + .map(|value| value.as_str().unwrap().to_string()); 99 + 100 + Self { 101 + id, 102 + name, 103 + bio, 104 + image, 105 + } 106 + } 107 + }
+76
crates/search/src/file.rs
··· 1 + use crate::{Indexable, Searchable}; 2 + use serde::{Deserialize, Serialize}; 3 + use tantivy::{doc, schema::*, TantivyDocument}; 4 + 5 + #[derive(Debug, Default, Serialize, Deserialize)] 6 + pub struct File { 7 + pub name: String, 8 + pub time_write: i64, 9 + pub is_directory: bool, 10 + } 11 + 12 + impl Indexable for File { 13 + fn to_document(&self) -> TantivyDocument { 14 + let schema: Schema = self.build_schema(); 15 + let name = schema.get_field("name").unwrap(); 16 + let time_write = schema.get_field("time_write").unwrap(); 17 + let is_directory = schema.get_field("is_directory").unwrap(); 18 + 19 + let document = doc!( 20 + name => self.name.to_owned(), 21 + time_write => self.time_write, 22 + is_directory => self.is_directory, 23 + ); 24 + 25 + document 26 + } 27 + 28 + fn build_schema(&self) -> Schema { 29 + let mut schema_builder: SchemaBuilder = Schema::builder(); 30 + 31 + schema_builder.add_text_field("name", TEXT | STORED); 32 + schema_builder.add_i64_field("time_write", STORED); 33 + schema_builder.add_bool_field("is_directory", STORED); 34 + 35 + schema_builder.build() 36 + } 37 + } 38 + 39 + impl Searchable for File { 40 + fn schema(&self) -> Schema { 41 + self.build_schema() 42 + } 43 + 44 + fn default_fields(&self) -> Vec<String> { 45 + vec!["name".to_string()] 46 + } 47 + } 48 + 49 + impl From<TantivyDocument> for File { 50 + fn from(doc: TantivyDocument) -> Self { 51 + let mut schema_builder: SchemaBuilder = Schema::builder(); 52 + 53 + let name_field = schema_builder.add_text_field("name", TEXT | STORED); 54 + let time_write_field = schema_builder.add_i64_field("time_write", STORED); 55 + let is_directory_field = schema_builder.add_bool_field("is_directory", STORED); 56 + 57 + let name = doc 58 + .get_first(name_field) 59 + .unwrap() 60 + .as_str() 61 + .unwrap() 62 + .to_string(); 63 + let time_write = doc.get_first(time_write_field).unwrap().as_i64().unwrap(); 64 + let is_directory = doc 65 + .get_first(is_directory_field) 66 + .unwrap() 67 + .as_bool() 68 + .unwrap(); 69 + 70 + Self { 71 + name, 72 + time_write, 73 + is_directory, 74 + } 75 + } 76 + }
+145
crates/search/src/lib.rs
··· 1 + use std::env; 2 + 3 + use album::Album; 4 + use anyhow::Error; 5 + use artist::Artist; 6 + use file::File; 7 + use liked_album::LikedAlbum; 8 + use liked_track::LikedTrack; 9 + use tantivy::collector::TopDocs; 10 + use tantivy::directory::MmapDirectory; 11 + use tantivy::query::{FuzzyTermQuery, QueryParser}; 12 + use tantivy::{schema::Schema, Index, TantivyDocument}; 13 + use tantivy::{ReloadPolicy, Term}; 14 + use track::Track; 15 + 16 + pub mod album; 17 + pub mod artist; 18 + pub mod file; 19 + pub mod liked_album; 20 + pub mod liked_track; 21 + pub mod track; 22 + 23 + #[derive(Clone)] 24 + pub struct Indexes { 25 + pub albums: Index, 26 + pub artists: Index, 27 + pub tracks: Index, 28 + pub liked_albums: Index, 29 + pub liked_tracks: Index, 30 + pub files: Index, 31 + } 32 + 33 + pub fn create_indexes() -> Result<Indexes, Error> { 34 + let home = env::var("HOME")?; 35 + let rockbox_dir = format!("{}/.config/rockbox.org", home); 36 + let index_dir = format!("{}/indexes", rockbox_dir); 37 + 38 + let albums = create_index(Album::default().schema(), &format!("{}/albums", index_dir))?; 39 + let artists = create_index( 40 + Artist::default().schema(), 41 + &format!("{}/artists", index_dir), 42 + )?; 43 + let tracks = create_index(Track::default().schema(), &format!("{}/tracks", index_dir))?; 44 + let liked_albums = create_index( 45 + LikedAlbum::default().schema(), 46 + &format!("{}/liked_albums", index_dir), 47 + )?; 48 + let liked_tracks = create_index( 49 + LikedTrack::default().schema(), 50 + &format!("{}/liked_tracks", index_dir), 51 + )?; 52 + let files = create_index(File::default().schema(), &format!("{}/files", index_dir))?; 53 + 54 + Ok(Indexes { 55 + albums, 56 + artists, 57 + tracks, 58 + liked_albums, 59 + liked_tracks, 60 + files, 61 + }) 62 + } 63 + 64 + fn create_index(schema: Schema, index_path: &str) -> Result<Index, Error> { 65 + std::fs::create_dir_all(index_path)?; 66 + let dir = MmapDirectory::open(index_path)?; 67 + let index: Index = Index::open_or_create(dir, schema.clone())?; 68 + Ok(index) 69 + } 70 + 71 + pub fn delete_all_documents(index: &Index) -> Result<(), Error> { 72 + let mut index_writer = index.writer::<TantivyDocument>(50_000_000)?; 73 + index_writer.delete_all_documents()?; 74 + index_writer.commit()?; 75 + Ok(()) 76 + } 77 + 78 + pub trait Indexable { 79 + fn to_document(&self) -> TantivyDocument; 80 + fn build_schema(&self) -> Schema; 81 + } 82 + 83 + pub trait Searchable { 84 + fn schema(&self) -> Schema; 85 + fn default_fields(&self) -> Vec<String>; // Default fields to search in 86 + } 87 + 88 + pub fn index_entity<T: Indexable>(index: &Index, entity: &T) -> Result<(), Error> { 89 + let mut index_writer = index.writer(50_000_000)?; 90 + let doc = entity.to_document(); 91 + index_writer.add_document(doc)?; 92 + index_writer.commit()?; 93 + Ok(()) 94 + } 95 + 96 + pub fn search_entities<T: Searchable>( 97 + index: &Index, 98 + query_string: &str, 99 + entity: &T, // The entity type to search 100 + ) -> tantivy::Result<Vec<(f32, TantivyDocument)>> { 101 + let reader = index 102 + .reader_builder() 103 + .reload_policy(ReloadPolicy::OnCommitWithDelay) 104 + .try_into()?; 105 + let searcher = reader.searcher(); 106 + 107 + // Get the schema and fields for the entity 108 + let schema = entity.schema(); 109 + let default_fields: Vec<String> = entity.default_fields(); 110 + 111 + // Convert field names to Field objects 112 + let fields: Vec<tantivy::schema::Field> = default_fields 113 + .iter() 114 + .filter_map(|field_name| Some(schema.get_field(field_name).unwrap())) 115 + .collect(); 116 + 117 + // Parse the query 118 + let query_parser = QueryParser::for_index(index, fields.clone()); 119 + let query = query_parser.parse_query(query_string)?; 120 + 121 + // Execute the search and collect the top 10 results 122 + let top_docs = searcher.search(&query, &TopDocs::with_limit(10))?; 123 + 124 + // Return the documents and their scores 125 + let mut results: Vec<(f32, TantivyDocument)> = top_docs 126 + .into_iter() 127 + .map(|(score, doc_address)| (score, searcher.doc(doc_address).unwrap())) 128 + .collect(); 129 + 130 + if results.is_empty() { 131 + // loop through the fields and search for fuzzy matches 132 + for field in fields { 133 + let term = Term::from_field_text(field, query_string); 134 + let query = FuzzyTermQuery::new(term, 1, true); 135 + let fuzzy_results: Vec<(f32, TantivyDocument)> = searcher 136 + .search(&query, &TopDocs::with_limit(10))? 137 + .into_iter() 138 + .map(|(score, doc_address)| (score, searcher.doc(doc_address).unwrap())) 139 + .collect(); 140 + results.extend(fuzzy_results); 141 + } 142 + } 143 + 144 + Ok(results) 145 + }
+159
crates/search/src/liked_album.rs
··· 1 + use crate::{Indexable, Searchable}; 2 + use rockbox_library::entity; 3 + use serde::{Deserialize, Serialize}; 4 + use tantivy::{doc, schema::*, TantivyDocument}; 5 + 6 + #[derive(Debug, Default, Serialize, Deserialize)] 7 + pub struct LikedAlbum { 8 + pub id: String, 9 + pub title: String, 10 + pub artist: String, 11 + pub year: i64, 12 + pub year_string: String, 13 + pub album_art: Option<String>, 14 + pub md5: String, 15 + pub artist_id: String, 16 + } 17 + 18 + impl Indexable for LikedAlbum { 19 + fn to_document(&self) -> TantivyDocument { 20 + let schema: Schema = self.build_schema(); 21 + 22 + let id = schema.get_field("id").unwrap(); 23 + let title = schema.get_field("title").unwrap(); 24 + let artist = schema.get_field("artist").unwrap(); 25 + let year = schema.get_field("year").unwrap(); 26 + let year_string = schema.get_field("year_string").unwrap(); 27 + let album_art = schema.get_field("album_art").unwrap(); 28 + let md5 = schema.get_field("md5").unwrap(); 29 + let artist_id = schema.get_field("artist_id").unwrap(); 30 + 31 + let mut document = doc!( 32 + id => self.id.to_owned(), 33 + title => self.title.to_owned(), 34 + artist => self.artist.to_owned(), 35 + year => self.year, 36 + year_string => self.year_string.to_owned(), 37 + ); 38 + 39 + if let Some(value) = &self.album_art { 40 + document.add_text(album_art, value); 41 + } 42 + 43 + document.add_text(md5, &self.md5); 44 + document.add_text(artist_id, &self.artist_id); 45 + 46 + document 47 + } 48 + 49 + fn build_schema(&self) -> Schema { 50 + let mut schema_builder: SchemaBuilder = Schema::builder(); 51 + 52 + schema_builder.add_text_field("id", STRING | STORED); 53 + schema_builder.add_text_field("title", TEXT | STORED); 54 + schema_builder.add_text_field("artist", TEXT | STORED); 55 + schema_builder.add_i64_field("year", STORED); 56 + schema_builder.add_text_field("year_string", STRING | STORED); 57 + schema_builder.add_text_field("album_art", STRING | STORED); 58 + schema_builder.add_text_field("md5", STRING | STORED); 59 + schema_builder.add_text_field("artist_id", STRING | STORED); 60 + 61 + schema_builder.build() 62 + } 63 + } 64 + 65 + impl Searchable for LikedAlbum { 66 + fn schema(&self) -> Schema { 67 + self.build_schema() 68 + } 69 + 70 + fn default_fields(&self) -> Vec<String> { 71 + vec!["title".to_string(), "artist".to_string()] 72 + } 73 + } 74 + 75 + impl From<entity::album::Album> for LikedAlbum { 76 + fn from(album: entity::album::Album) -> Self { 77 + Self { 78 + id: album.id, 79 + title: album.title, 80 + artist: album.artist, 81 + year: album.year as i64, 82 + year_string: album.year_string, 83 + album_art: album.album_art, 84 + md5: album.md5, 85 + artist_id: album.artist_id, 86 + } 87 + } 88 + } 89 + 90 + impl From<TantivyDocument> for LikedAlbum { 91 + fn from(document: TantivyDocument) -> Self { 92 + let mut schema_builder: SchemaBuilder = Schema::builder(); 93 + 94 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 95 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 96 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 97 + let year_field = schema_builder.add_i64_field("year", STORED); 98 + let year_string_field = schema_builder.add_text_field("year_string", STRING | STORED); 99 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 100 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 101 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 102 + 103 + let id = document 104 + .get_first(id_field) 105 + .unwrap() 106 + .as_str() 107 + .unwrap() 108 + .to_string(); 109 + let title = document 110 + .get_first(title_field) 111 + .unwrap() 112 + .as_str() 113 + .unwrap() 114 + .to_string(); 115 + let artist = document 116 + .get_first(artist_field) 117 + .unwrap() 118 + .as_str() 119 + .unwrap() 120 + .to_string(); 121 + let year = document.get_first(year_field).unwrap().as_i64().unwrap(); 122 + let year_string = document 123 + .get_first(year_string_field) 124 + .unwrap() 125 + .as_str() 126 + .unwrap() 127 + .to_string(); 128 + let album_art = match document.get_first(album_art_field) { 129 + Some(album_art) => album_art.as_str(), 130 + None => None, 131 + }; 132 + let album_art = match album_art { 133 + Some("") => None, 134 + Some(album_art) => Some(album_art.to_string()), 135 + None => None, 136 + }; 137 + let md5 = document 138 + .get_first(md5_field) 139 + .unwrap() 140 + .as_str() 141 + .unwrap() 142 + .to_string(); 143 + let artist_id = match document.get_first(artist_id_field) { 144 + Some(artist_id) => artist_id.as_str().unwrap().to_string(), 145 + None => "".to_string(), 146 + }; 147 + 148 + Self { 149 + id, 150 + title, 151 + artist, 152 + year, 153 + year_string, 154 + album_art, 155 + md5, 156 + artist_id, 157 + } 158 + } 159 + }
+353
crates/search/src/liked_track.rs
··· 1 + use crate::{Indexable, Searchable}; 2 + use rockbox_library::entity; 3 + use serde::{Deserialize, Serialize}; 4 + use tantivy::{doc, schema::*, TantivyDocument}; 5 + 6 + #[derive(Debug, Default, Serialize, Deserialize)] 7 + pub struct LikedTrack { 8 + pub id: String, 9 + pub path: String, 10 + pub title: String, 11 + pub artist: String, 12 + pub album: String, 13 + pub album_artist: String, 14 + pub bitrate: i64, 15 + pub composer: String, 16 + pub disc_number: i64, 17 + pub filesize: i64, 18 + pub frequency: i64, 19 + pub length: i64, 20 + pub track_number: i64, 21 + pub year: i64, 22 + pub year_string: String, 23 + pub genre: String, 24 + pub md5: String, 25 + pub album_art: Option<String>, 26 + pub artist_id: Option<String>, 27 + pub album_id: Option<String>, 28 + pub genre_id: Option<String>, 29 + pub created_at: String, 30 + pub updated_at: String, 31 + } 32 + 33 + impl Indexable for LikedTrack { 34 + fn to_document(&self) -> TantivyDocument { 35 + let schema: Schema = self.build_schema(); 36 + 37 + let id = schema.get_field("id").unwrap(); 38 + let path = schema.get_field("path").unwrap(); 39 + let title = schema.get_field("title").unwrap(); 40 + let artist = schema.get_field("artist").unwrap(); 41 + let album = schema.get_field("album").unwrap(); 42 + let album_artist = schema.get_field("album_artist").unwrap(); 43 + let bitrate = schema.get_field("bitrate").unwrap(); 44 + let composer = schema.get_field("composer").unwrap(); 45 + let disc_number = schema.get_field("disc_number").unwrap(); 46 + let filesize = schema.get_field("filesize").unwrap(); 47 + let frequency = schema.get_field("frequency").unwrap(); 48 + let length = schema.get_field("length").unwrap(); 49 + let track_number = schema.get_field("track_number").unwrap(); 50 + let year = schema.get_field("year").unwrap(); 51 + let year_string = schema.get_field("year_string").unwrap(); 52 + let genre = schema.get_field("genre").unwrap(); 53 + let md5 = schema.get_field("md5").unwrap(); 54 + let album_art = schema.get_field("album_art").unwrap(); 55 + let artist_id = schema.get_field("artist_id").unwrap(); 56 + let album_id = schema.get_field("album_id").unwrap(); 57 + let genre_id = schema.get_field("genre_id").unwrap(); 58 + let created_at = schema.get_field("created_at").unwrap(); 59 + let updated_at = schema.get_field("updated_at").unwrap(); 60 + 61 + let mut document = doc!( 62 + id => self.id.to_owned(), 63 + path => self.path.to_owned(), 64 + title => self.title.to_owned(), 65 + artist => self.artist.to_owned(), 66 + album => self.album.to_owned(), 67 + album_artist => self.album_artist.to_owned(), 68 + bitrate => self.bitrate, 69 + composer => self.composer.to_owned(), 70 + disc_number => self.disc_number, 71 + filesize => self.filesize, 72 + frequency => self.frequency, 73 + length => self.length, 74 + track_number => self.track_number, 75 + year => self.year, 76 + year_string => self.year_string.to_owned(), 77 + genre => self.genre.to_owned(), 78 + md5 => self.md5.to_owned(), 79 + ); 80 + 81 + if let Some(value) = &self.album_art { 82 + document.add_text(album_art, value); 83 + } 84 + 85 + if let Some(value) = &self.artist_id { 86 + document.add_text(artist_id, value); 87 + } 88 + 89 + if let Some(value) = &self.album_id { 90 + document.add_text(album_id, value); 91 + } 92 + 93 + if let Some(value) = &self.genre_id { 94 + document.add_text(genre_id, value); 95 + } 96 + 97 + document.add_text(created_at, &self.created_at); 98 + document.add_text(updated_at, &self.updated_at); 99 + 100 + document 101 + } 102 + 103 + fn build_schema(&self) -> Schema { 104 + let mut schema_builder: SchemaBuilder = Schema::builder(); 105 + 106 + schema_builder.add_text_field("id", STRING | STORED); 107 + schema_builder.add_text_field("path", TEXT | STORED); 108 + schema_builder.add_text_field("title", TEXT | STORED); 109 + schema_builder.add_text_field("artist", TEXT | STORED); 110 + schema_builder.add_text_field("album", TEXT | STORED); 111 + schema_builder.add_text_field("album_artist", TEXT | STORED); 112 + schema_builder.add_i64_field("bitrate", STORED); 113 + schema_builder.add_text_field("composer", TEXT | STORED); 114 + schema_builder.add_i64_field("disc_number", STORED); 115 + schema_builder.add_i64_field("filesize", STORED); 116 + schema_builder.add_i64_field("frequency", STORED); 117 + schema_builder.add_i64_field("length", STORED); 118 + schema_builder.add_i64_field("track_number", STORED); 119 + schema_builder.add_i64_field("year", STORED); 120 + schema_builder.add_text_field("year_string", STRING | STORED); 121 + schema_builder.add_text_field("genre", TEXT | STORED); 122 + schema_builder.add_text_field("md5", STRING | STORED); 123 + schema_builder.add_text_field("album_art", STRING | STORED); 124 + schema_builder.add_text_field("artist_id", STRING | STORED); 125 + schema_builder.add_text_field("album_id", STRING | STORED); 126 + schema_builder.add_text_field("genre_id", STRING | STORED); 127 + schema_builder.add_text_field("created_at", STRING | STORED); 128 + schema_builder.add_text_field("updated_at", STRING | STORED); 129 + 130 + schema_builder.build() 131 + } 132 + } 133 + 134 + impl Searchable for LikedTrack { 135 + fn schema(&self) -> Schema { 136 + self.build_schema() 137 + } 138 + 139 + fn default_fields(&self) -> Vec<String> { 140 + vec![ 141 + "title".to_string(), 142 + "artist".to_string(), 143 + "album".to_string(), 144 + "album_artist".to_string(), 145 + "composer".to_string(), 146 + ] 147 + } 148 + } 149 + 150 + impl From<entity::track::Track> for LikedTrack { 151 + fn from(track: entity::track::Track) -> Self { 152 + Self { 153 + id: track.id, 154 + path: track.path, 155 + title: track.title, 156 + artist: track.artist, 157 + album: track.album, 158 + album_artist: track.album_artist, 159 + bitrate: track.bitrate as i64, 160 + composer: track.composer, 161 + disc_number: track.disc_number as i64, 162 + filesize: track.filesize as i64, 163 + frequency: track.frequency as i64, 164 + length: track.length as i64, 165 + track_number: track.track_number.unwrap_or_default() as i64, 166 + year: track.year.unwrap_or_default() as i64, 167 + year_string: track.year_string.unwrap_or_default(), 168 + genre: track.genre.unwrap_or_default(), 169 + md5: track.md5, 170 + album_art: track.album_art, 171 + artist_id: Some(track.artist_id), 172 + album_id: Some(track.album_id), 173 + genre_id: Some(track.genre_id), 174 + created_at: track.created_at.to_rfc3339(), 175 + updated_at: track.updated_at.to_rfc3339(), 176 + } 177 + } 178 + } 179 + 180 + impl From<TantivyDocument> for LikedTrack { 181 + fn from(document: TantivyDocument) -> Self { 182 + let mut schema_builder: SchemaBuilder = Schema::builder(); 183 + 184 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 185 + let path_field = schema_builder.add_text_field("path", TEXT | STORED); 186 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 187 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 188 + let album_field = schema_builder.add_text_field("album", TEXT | STORED); 189 + let album_artist_field = schema_builder.add_text_field("album_artist", TEXT | STORED); 190 + let bitrate_field = schema_builder.add_i64_field("bitrate", STORED); 191 + let composer_field = schema_builder.add_text_field("composer", TEXT | STORED); 192 + let disc_number_field = schema_builder.add_i64_field("disc_number", STORED); 193 + let filesize_field = schema_builder.add_i64_field("filesize", STORED); 194 + let frequency_field = schema_builder.add_i64_field("frequency", STORED); 195 + let length_field = schema_builder.add_i64_field("length", STORED); 196 + let track_number_field = schema_builder.add_i64_field("track_number", STORED); 197 + let year_field = schema_builder.add_i64_field("year", STORED); 198 + let year_string_field = schema_builder.add_text_field("year_string", STRING | STORED); 199 + let genre_field = schema_builder.add_text_field("genre", TEXT | STORED); 200 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 201 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 202 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 203 + let album_id_field = schema_builder.add_text_field("album_id", STRING | STORED); 204 + let genre_id_field = schema_builder.add_text_field("genre_id", STRING | STORED); 205 + let created_at_field = schema_builder.add_text_field("created_at", STRING | STORED); 206 + let updated_at_field = schema_builder.add_text_field("updated_at", STRING | STORED); 207 + 208 + let id = document 209 + .get_first(id_field) 210 + .unwrap() 211 + .as_str() 212 + .unwrap() 213 + .to_string(); 214 + let path = document 215 + .get_first(path_field) 216 + .unwrap() 217 + .as_str() 218 + .unwrap() 219 + .to_string(); 220 + let title = document 221 + .get_first(title_field) 222 + .unwrap() 223 + .as_str() 224 + .unwrap() 225 + .to_string(); 226 + let artist = document 227 + .get_first(artist_field) 228 + .unwrap() 229 + .as_str() 230 + .unwrap() 231 + .to_string(); 232 + let album = document 233 + .get_first(album_field) 234 + .unwrap() 235 + .as_str() 236 + .unwrap() 237 + .to_string(); 238 + let album_artist = document 239 + .get_first(album_artist_field) 240 + .unwrap() 241 + .as_str() 242 + .unwrap() 243 + .to_string(); 244 + let bitrate = document.get_first(bitrate_field).unwrap().as_i64().unwrap(); 245 + let composer = document 246 + .get_first(composer_field) 247 + .unwrap() 248 + .as_str() 249 + .unwrap() 250 + .to_string(); 251 + let disc_number = document 252 + .get_first(disc_number_field) 253 + .unwrap() 254 + .as_i64() 255 + .unwrap(); 256 + let filesize = document 257 + .get_first(filesize_field) 258 + .unwrap() 259 + .as_i64() 260 + .unwrap(); 261 + let frequency = document 262 + .get_first(frequency_field) 263 + .unwrap() 264 + .as_i64() 265 + .unwrap(); 266 + let length = document.get_first(length_field).unwrap().as_i64().unwrap(); 267 + let track_number = document 268 + .get_first(track_number_field) 269 + .unwrap() 270 + .as_i64() 271 + .unwrap(); 272 + let year = document.get_first(year_field).unwrap().as_i64().unwrap(); 273 + let year_string = document 274 + .get_first(year_string_field) 275 + .unwrap() 276 + .as_str() 277 + .unwrap() 278 + .to_string(); 279 + let genre = document 280 + .get_first(genre_field) 281 + .unwrap() 282 + .as_str() 283 + .unwrap() 284 + .to_string(); 285 + let md5 = document 286 + .get_first(md5_field) 287 + .unwrap() 288 + .as_str() 289 + .unwrap() 290 + .to_string(); 291 + let album_art = match document.get_first(album_art_field) { 292 + Some(album_art) => album_art.as_str(), 293 + None => None, 294 + }; 295 + let album_art = match album_art { 296 + Some("") => None, 297 + Some(album_art) => Some(album_art.to_string()), 298 + None => None, 299 + }; 300 + let artist_id = match document.get_first(artist_id_field) { 301 + Some(artist_id) => Some(artist_id.as_str().unwrap().to_string()), 302 + None => None, 303 + }; 304 + let album_id = match document.get_first(album_id_field) { 305 + Some(album_id) => Some(album_id.as_str().unwrap().to_string()), 306 + None => None, 307 + }; 308 + let album_id = match album_id { 309 + Some(album_id) => Some(album_id.to_string()), 310 + None => None, 311 + }; 312 + let genre_id = match document.get_first(genre_id_field) { 313 + Some(genre_id) => Some(genre_id.as_str().unwrap().to_string()), 314 + None => None, 315 + }; 316 + let created_at = document 317 + .get_first(created_at_field) 318 + .unwrap() 319 + .as_str() 320 + .unwrap() 321 + .to_string(); 322 + let updated_at = match document.get_first(updated_at_field) { 323 + Some(updated_at) => updated_at.as_str().unwrap().to_string(), 324 + None => "".to_string(), 325 + }; 326 + 327 + Self { 328 + id, 329 + path, 330 + title, 331 + artist, 332 + album, 333 + album_artist, 334 + bitrate, 335 + composer, 336 + disc_number, 337 + filesize, 338 + frequency, 339 + length, 340 + track_number, 341 + year, 342 + year_string, 343 + genre, 344 + md5, 345 + album_art, 346 + artist_id, 347 + album_id, 348 + genre_id, 349 + created_at, 350 + updated_at, 351 + } 352 + } 353 + }
+353
crates/search/src/track.rs
··· 1 + use crate::{Indexable, Searchable}; 2 + use rockbox_library::entity; 3 + use serde::{Deserialize, Serialize}; 4 + use tantivy::{doc, schema::*, TantivyDocument}; 5 + 6 + #[derive(Debug, Default, Serialize, Deserialize)] 7 + pub struct Track { 8 + pub id: String, 9 + pub path: String, 10 + pub title: String, 11 + pub artist: String, 12 + pub album: String, 13 + pub album_artist: String, 14 + pub bitrate: i64, 15 + pub composer: String, 16 + pub disc_number: i64, 17 + pub filesize: i64, 18 + pub frequency: i64, 19 + pub length: i64, 20 + pub track_number: i64, 21 + pub year: i64, 22 + pub year_string: String, 23 + pub genre: String, 24 + pub md5: String, 25 + pub album_art: Option<String>, 26 + pub artist_id: Option<String>, 27 + pub album_id: Option<String>, 28 + pub genre_id: Option<String>, 29 + pub created_at: String, 30 + pub updated_at: String, 31 + } 32 + 33 + impl Indexable for Track { 34 + fn to_document(&self) -> TantivyDocument { 35 + let schema: Schema = self.build_schema(); 36 + 37 + let id = schema.get_field("id").unwrap(); 38 + let path = schema.get_field("path").unwrap(); 39 + let title = schema.get_field("title").unwrap(); 40 + let artist = schema.get_field("artist").unwrap(); 41 + let album = schema.get_field("album").unwrap(); 42 + let album_artist = schema.get_field("album_artist").unwrap(); 43 + let bitrate = schema.get_field("bitrate").unwrap(); 44 + let composer = schema.get_field("composer").unwrap(); 45 + let disc_number = schema.get_field("disc_number").unwrap(); 46 + let filesize = schema.get_field("filesize").unwrap(); 47 + let frequency = schema.get_field("frequency").unwrap(); 48 + let length = schema.get_field("length").unwrap(); 49 + let track_number = schema.get_field("track_number").unwrap(); 50 + let year = schema.get_field("year").unwrap(); 51 + let year_string = schema.get_field("year_string").unwrap(); 52 + let genre = schema.get_field("genre").unwrap(); 53 + let md5 = schema.get_field("md5").unwrap(); 54 + let album_art = schema.get_field("album_art").unwrap(); 55 + let artist_id = schema.get_field("artist_id").unwrap(); 56 + let album_id = schema.get_field("album_id").unwrap(); 57 + let genre_id = schema.get_field("genre_id").unwrap(); 58 + let created_at = schema.get_field("created_at").unwrap(); 59 + let updated_at = schema.get_field("updated_at").unwrap(); 60 + 61 + let mut document = doc!( 62 + id => self.id.to_owned(), 63 + path => self.path.to_owned(), 64 + title => self.title.to_owned(), 65 + artist => self.artist.to_owned(), 66 + album => self.album.to_owned(), 67 + album_artist => self.album_artist.to_owned(), 68 + bitrate => self.bitrate, 69 + composer => self.composer.to_owned(), 70 + disc_number => self.disc_number, 71 + filesize => self.filesize, 72 + frequency => self.frequency, 73 + length => self.length, 74 + track_number => self.track_number, 75 + year => self.year, 76 + year_string => self.year_string.to_owned(), 77 + genre => self.genre.to_owned(), 78 + md5 => self.md5.to_owned(), 79 + ); 80 + 81 + if let Some(value) = &self.album_art { 82 + document.add_text(album_art, value); 83 + } 84 + 85 + if let Some(value) = &self.artist_id { 86 + document.add_text(artist_id, value); 87 + } 88 + 89 + if let Some(value) = &self.album_id { 90 + document.add_text(album_id, value); 91 + } 92 + 93 + if let Some(value) = &self.genre_id { 94 + document.add_text(genre_id, value); 95 + } 96 + 97 + document.add_text(created_at, &self.created_at); 98 + document.add_text(updated_at, &self.updated_at); 99 + 100 + document 101 + } 102 + 103 + fn build_schema(&self) -> Schema { 104 + let mut schema_builder: SchemaBuilder = Schema::builder(); 105 + 106 + schema_builder.add_text_field("id", STRING | STORED); 107 + schema_builder.add_text_field("path", TEXT | STORED); 108 + schema_builder.add_text_field("title", TEXT | STORED); 109 + schema_builder.add_text_field("artist", TEXT | STORED); 110 + schema_builder.add_text_field("album", TEXT | STORED); 111 + schema_builder.add_text_field("album_artist", TEXT | STORED); 112 + schema_builder.add_i64_field("bitrate", STORED); 113 + schema_builder.add_text_field("composer", TEXT | STORED); 114 + schema_builder.add_i64_field("disc_number", STORED); 115 + schema_builder.add_i64_field("filesize", STORED); 116 + schema_builder.add_i64_field("frequency", STORED); 117 + schema_builder.add_i64_field("length", STORED); 118 + schema_builder.add_i64_field("track_number", STORED); 119 + schema_builder.add_i64_field("year", STORED); 120 + schema_builder.add_text_field("year_string", STRING | STORED); 121 + schema_builder.add_text_field("genre", TEXT | STORED); 122 + schema_builder.add_text_field("md5", STRING | STORED); 123 + schema_builder.add_text_field("album_art", STRING | STORED); 124 + schema_builder.add_text_field("artist_id", STRING | STORED); 125 + schema_builder.add_text_field("album_id", STRING | STORED); 126 + schema_builder.add_text_field("genre_id", STRING | STORED); 127 + schema_builder.add_text_field("created_at", STRING | STORED); 128 + schema_builder.add_text_field("updated_at", STRING | STORED); 129 + 130 + schema_builder.build() 131 + } 132 + } 133 + 134 + impl Searchable for Track { 135 + fn schema(&self) -> Schema { 136 + self.build_schema() 137 + } 138 + 139 + fn default_fields(&self) -> Vec<String> { 140 + vec![ 141 + "title".to_string(), 142 + "artist".to_string(), 143 + "album".to_string(), 144 + "album_artist".to_string(), 145 + "composer".to_string(), 146 + ] 147 + } 148 + } 149 + 150 + impl From<entity::track::Track> for Track { 151 + fn from(track: entity::track::Track) -> Self { 152 + Self { 153 + id: track.id, 154 + path: track.path, 155 + title: track.title, 156 + artist: track.artist, 157 + album: track.album, 158 + album_artist: track.album_artist, 159 + bitrate: track.bitrate as i64, 160 + composer: track.composer, 161 + disc_number: track.disc_number as i64, 162 + filesize: track.filesize as i64, 163 + frequency: track.frequency as i64, 164 + length: track.length as i64, 165 + track_number: track.track_number.unwrap_or_default() as i64, 166 + year: track.year.unwrap_or_default() as i64, 167 + year_string: track.year_string.unwrap_or_default(), 168 + genre: track.genre.unwrap_or_default(), 169 + md5: track.md5, 170 + album_art: track.album_art, 171 + artist_id: Some(track.artist_id), 172 + album_id: Some(track.album_id), 173 + genre_id: Some(track.genre_id), 174 + created_at: track.created_at.to_rfc3339(), 175 + updated_at: track.updated_at.to_rfc3339(), 176 + } 177 + } 178 + } 179 + 180 + impl From<TantivyDocument> for Track { 181 + fn from(document: TantivyDocument) -> Self { 182 + let mut schema_builder: SchemaBuilder = Schema::builder(); 183 + 184 + let id_field = schema_builder.add_text_field("id", STRING | STORED); 185 + let path_field = schema_builder.add_text_field("path", TEXT | STORED); 186 + let title_field = schema_builder.add_text_field("title", TEXT | STORED); 187 + let artist_field = schema_builder.add_text_field("artist", TEXT | STORED); 188 + let album_field = schema_builder.add_text_field("album", TEXT | STORED); 189 + let album_artist_field = schema_builder.add_text_field("album_artist", TEXT | STORED); 190 + let bitrate_field = schema_builder.add_i64_field("bitrate", STORED); 191 + let composer_field = schema_builder.add_text_field("composer", TEXT | STORED); 192 + let disc_number_field = schema_builder.add_i64_field("disc_number", STORED); 193 + let filesize_field = schema_builder.add_i64_field("filesize", STORED); 194 + let frequency_field = schema_builder.add_i64_field("frequency", STORED); 195 + let length_field = schema_builder.add_i64_field("length", STORED); 196 + let track_number_field = schema_builder.add_i64_field("track_number", STORED); 197 + let year_field = schema_builder.add_i64_field("year", STORED); 198 + let year_string_field = schema_builder.add_text_field("year_string", STRING | STORED); 199 + let genre_field = schema_builder.add_text_field("genre", TEXT | STORED); 200 + let md5_field = schema_builder.add_text_field("md5", STRING | STORED); 201 + let album_art_field = schema_builder.add_text_field("album_art", STRING | STORED); 202 + let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED); 203 + let album_id_field = schema_builder.add_text_field("album_id", STRING | STORED); 204 + let genre_id_field = schema_builder.add_text_field("genre_id", STRING | STORED); 205 + let created_at_field = schema_builder.add_text_field("created_at", STRING | STORED); 206 + let updated_at_field = schema_builder.add_text_field("updated_at", STRING | STORED); 207 + 208 + let id = document 209 + .get_first(id_field) 210 + .unwrap() 211 + .as_str() 212 + .unwrap() 213 + .to_string(); 214 + let path = document 215 + .get_first(path_field) 216 + .unwrap() 217 + .as_str() 218 + .unwrap() 219 + .to_string(); 220 + let title = document 221 + .get_first(title_field) 222 + .unwrap() 223 + .as_str() 224 + .unwrap() 225 + .to_string(); 226 + let artist = document 227 + .get_first(artist_field) 228 + .unwrap() 229 + .as_str() 230 + .unwrap() 231 + .to_string(); 232 + let album = document 233 + .get_first(album_field) 234 + .unwrap() 235 + .as_str() 236 + .unwrap() 237 + .to_string(); 238 + let album_artist = document 239 + .get_first(album_artist_field) 240 + .unwrap() 241 + .as_str() 242 + .unwrap() 243 + .to_string(); 244 + let bitrate = document.get_first(bitrate_field).unwrap().as_i64().unwrap(); 245 + let composer = document 246 + .get_first(composer_field) 247 + .unwrap() 248 + .as_str() 249 + .unwrap() 250 + .to_string(); 251 + let disc_number = document 252 + .get_first(disc_number_field) 253 + .unwrap() 254 + .as_i64() 255 + .unwrap(); 256 + let filesize = document 257 + .get_first(filesize_field) 258 + .unwrap() 259 + .as_i64() 260 + .unwrap(); 261 + let frequency = document 262 + .get_first(frequency_field) 263 + .unwrap() 264 + .as_i64() 265 + .unwrap(); 266 + let length = document.get_first(length_field).unwrap().as_i64().unwrap(); 267 + let track_number = document 268 + .get_first(track_number_field) 269 + .unwrap() 270 + .as_i64() 271 + .unwrap(); 272 + let year = document.get_first(year_field).unwrap().as_i64().unwrap(); 273 + let year_string = document 274 + .get_first(year_string_field) 275 + .unwrap() 276 + .as_str() 277 + .unwrap() 278 + .to_string(); 279 + let genre = document 280 + .get_first(genre_field) 281 + .unwrap() 282 + .as_str() 283 + .unwrap() 284 + .to_string(); 285 + let md5 = document 286 + .get_first(md5_field) 287 + .unwrap() 288 + .as_str() 289 + .unwrap() 290 + .to_string(); 291 + let album_art = match document.get_first(album_art_field) { 292 + Some(album_art) => album_art.as_str(), 293 + None => None, 294 + }; 295 + let album_art = match album_art { 296 + Some("") => None, 297 + Some(album_art) => Some(album_art.to_string()), 298 + None => None, 299 + }; 300 + let artist_id = match document.get_first(artist_id_field) { 301 + Some(artist_id) => Some(artist_id.as_str().unwrap().to_string()), 302 + None => None, 303 + }; 304 + let album_id = match document.get_first(album_id_field) { 305 + Some(album_id) => Some(album_id.as_str().unwrap().to_string()), 306 + None => None, 307 + }; 308 + let album_id = match album_id { 309 + Some(album_id) => Some(album_id.to_string()), 310 + None => None, 311 + }; 312 + let genre_id = match document.get_first(genre_id_field) { 313 + Some(genre_id) => Some(genre_id.as_str().unwrap().to_string()), 314 + None => None, 315 + }; 316 + let created_at = document 317 + .get_first(created_at_field) 318 + .unwrap() 319 + .as_str() 320 + .unwrap() 321 + .to_string(); 322 + let updated_at = match document.get_first(updated_at_field) { 323 + Some(updated_at) => updated_at.as_str().unwrap().to_string(), 324 + None => "".to_string(), 325 + }; 326 + 327 + Self { 328 + id, 329 + path, 330 + title, 331 + artist, 332 + album, 333 + album_artist, 334 + bitrate, 335 + composer, 336 + disc_number, 337 + filesize, 338 + frequency, 339 + length, 340 + track_number, 341 + year, 342 + year_string, 343 + genre, 344 + md5, 345 + album_art, 346 + artist_id, 347 + album_id, 348 + genre_id, 349 + created_at, 350 + updated_at, 351 + } 352 + } 353 + }
+2
crates/server/Cargo.toml
··· 16 16 rockbox-graphql = {path = "../graphql"} 17 17 rockbox-library = {path = "../library"} 18 18 rockbox-rpc = {path = "../rpc"} 19 + rockbox-search = {path = "../search"} 19 20 rockbox-sys = {path = "../sys"} 21 + rockbox-types = {path = "../types"} 20 22 serde = {version = "1.0.210", features = ["derive"]} 21 23 serde_json = "1.0.128" 22 24 sqlx = {version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite", "chrono", "derive", "macros"]}
+2
crates/server/src/handlers/mod.rs
··· 4 4 pub mod docs; 5 5 pub mod player; 6 6 pub mod playlists; 7 + pub mod search; 7 8 pub mod settings; 8 9 pub mod system; 9 10 pub mod tracks; ··· 64 65 async_handler!(settings, get_global_settings); 65 66 async_handler!(docs, get_openapi); 66 67 async_handler!(docs, index); 68 + async_handler!(search, search);
+2 -4
crates/server/src/handlers/player.rs
··· 1 - use crate::{ 2 - http::{Context, Request, Response}, 3 - types::NewVolume, 4 - }; 1 + use crate::http::{Context, Request, Response}; 5 2 use anyhow::Error; 6 3 use rockbox_sys as rb; 4 + use rockbox_types::NewVolume; 7 5 8 6 pub async fn play(_ctx: &Context, req: &Request, _res: &mut Response) -> Result<(), Error> { 9 7 let elapsed = match req.query_params.get("elapsed") {
+2 -4
crates/server/src/handlers/playlists.rs
··· 1 - use crate::{ 2 - http::{Context, Request, Response}, 3 - types::{DeleteTracks, InsertTracks, NewPlaylist, StatusCode}, 4 - }; 1 + use crate::http::{Context, Request, Response}; 5 2 use anyhow::Error; 6 3 use rand::seq::SliceRandom; 7 4 use rockbox_graphql::read_files; ··· 10 7 self as rb, types::playlist_amount::PlaylistAmount, PLAYLIST_INSERT_LAST, 11 8 PLAYLIST_INSERT_LAST_SHUFFLED, 12 9 }; 10 + use rockbox_types::{DeleteTracks, InsertTracks, NewPlaylist, StatusCode}; 13 11 14 12 pub async fn create_playlist( 15 13 _ctx: &Context,
+62
crates/server/src/handlers/search.rs
··· 1 + use crate::http::{Context, Request, Response}; 2 + use anyhow::Error; 3 + use rockbox_search::search_entities; 4 + use rockbox_types::SearchResults; 5 + 6 + pub async fn search(ctx: &Context, req: &Request, res: &mut Response) -> Result<(), Error> { 7 + let term = req 8 + .query_params 9 + .get("q") 10 + .map(|t| t.as_str()) 11 + .unwrap_or_default(); 12 + 13 + match term { 14 + None => { 15 + res.json(&SearchResults::default()); 16 + } 17 + Some(term) => { 18 + let albums = search_entities( 19 + &ctx.indexes.albums, 20 + term, 21 + &rockbox_search::album::Album::default(), 22 + )?; 23 + let artists = search_entities( 24 + &ctx.indexes.artists, 25 + term, 26 + &rockbox_search::artist::Artist::default(), 27 + )?; 28 + let tracks = search_entities( 29 + &ctx.indexes.tracks, 30 + term, 31 + &rockbox_search::track::Track::default(), 32 + )?; 33 + let files = search_entities( 34 + &ctx.indexes.files, 35 + term, 36 + &rockbox_search::file::File::default(), 37 + )?; 38 + let liked_tracks = search_entities( 39 + &ctx.indexes.liked_tracks, 40 + term, 41 + &rockbox_search::liked_track::LikedTrack::default(), 42 + )?; 43 + let liked_albums = search_entities( 44 + &ctx.indexes.liked_albums, 45 + term, 46 + &rockbox_search::liked_album::LikedAlbum::default(), 47 + )?; 48 + 49 + let results = SearchResults { 50 + albums: albums.into_iter().map(|(_, x)| x.into()).collect(), 51 + artists: artists.into_iter().map(|(_, x)| x.into()).collect(), 52 + tracks: tracks.into_iter().map(|(_, x)| x.into()).collect(), 53 + files: files.into_iter().map(|(_, x)| x.into()).collect(), 54 + liked_tracks: liked_tracks.into_iter().map(|(_, x)| x.into()).collect(), 55 + liked_albums: liked_albums.into_iter().map(|(_, x)| x.into()).collect(), 56 + }; 57 + 58 + res.json(&results); 59 + } 60 + } 61 + Ok(()) 62 + }
+68 -2
crates/server/src/handlers/system.rs
··· 1 - use std::env; 1 + use std::{env, thread}; 2 2 3 3 use crate::http::{Context, Request, Response}; 4 4 use anyhow::Error; 5 - use rockbox_library::audio_scan::scan_audio_files; 5 + use rockbox_library::{audio_scan::scan_audio_files, repo}; 6 + use rockbox_search::{ 7 + album::Album, artist::Artist, delete_all_documents, index_entity, liked_album::LikedAlbum, 8 + liked_track::LikedTrack, track::Track, 9 + }; 6 10 use rockbox_sys as rb; 7 11 8 12 pub async fn get_status(_ctx: &Context, _req: &Request, res: &mut Response) -> Result<(), Error> { ··· 25 29 let home = env::var("HOME")?; 26 30 let path = env::var("ROCKBOX_LIBRARY").unwrap_or(format!("{}/Music", home)); 27 31 scan_audio_files(ctx.pool.clone(), path.into()).await?; 32 + let tracks = repo::track::all(ctx.pool.clone()).await?; 33 + let albums = repo::album::all(ctx.pool.clone()).await?; 34 + let artists = repo::artist::all(ctx.pool.clone()).await?; 35 + let liked_albums = repo::favourites::all_albums(ctx.pool.clone()).await?; 36 + let liked_tracks = repo::favourites::all_tracks(ctx.pool.clone()).await?; 37 + 38 + let tracks_index = ctx.indexes.tracks.clone(); 39 + let albums_index = ctx.indexes.albums.clone(); 40 + let artists_index = ctx.indexes.artists.clone(); 41 + let liked_albums_index = ctx.indexes.liked_albums.clone(); 42 + let liked_tracks_index = ctx.indexes.liked_tracks.clone(); 43 + 44 + thread::spawn(move || { 45 + match delete_all_documents(&tracks_index) { 46 + Ok(_) => {} 47 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 48 + } 49 + for track in tracks { 50 + index_entity::<Track>(&tracks_index, &track.into()).unwrap(); 51 + } 52 + }); 53 + 54 + thread::spawn(move || { 55 + match delete_all_documents(&albums_index) { 56 + Ok(_) => {} 57 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 58 + } 59 + for album in albums { 60 + index_entity::<Album>(&albums_index, &album.into()).unwrap(); 61 + } 62 + }); 63 + 64 + thread::spawn(move || { 65 + match delete_all_documents(&artists_index) { 66 + Ok(_) => {} 67 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 68 + } 69 + for artist in artists { 70 + index_entity::<Artist>(&artists_index, &artist.into()).unwrap(); 71 + } 72 + }); 73 + 74 + thread::spawn(move || { 75 + match delete_all_documents(&liked_albums_index) { 76 + Ok(_) => {} 77 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 78 + } 79 + for liked_album in liked_albums { 80 + index_entity::<LikedAlbum>(&liked_albums_index, &liked_album.into()).unwrap(); 81 + } 82 + }); 83 + 84 + thread::spawn(move || { 85 + match delete_all_documents(&liked_tracks_index) { 86 + Ok(_) => {} 87 + Err(e) => eprintln!("Error deleting all documents: {:?}", e), 88 + } 89 + for liked_track in liked_tracks { 90 + index_entity::<LikedTrack>(&liked_tracks_index, &liked_track.into()).unwrap(); 91 + } 92 + }); 93 + 28 94 res.text("0"); 29 95 Ok(()) 30 96 }
+8
crates/server/src/http.rs
··· 1 1 use anyhow::Error; 2 2 use owo_colors::OwoColorize; 3 + use rockbox_search::{create_indexes, Indexes}; 3 4 use rockbox_sys::{ 4 5 self as rb, 5 6 types::{mp3_entry::Mp3Entry, tree::Entry}, ··· 23 24 pub pool: sqlx::Pool<Sqlite>, 24 25 pub fs_cache: Arc<tokio::sync::Mutex<HashMap<String, Vec<Entry>>>>, 25 26 pub metadata_cache: Arc<tokio::sync::Mutex<HashMap<String, Mp3Entry>>>, 27 + pub indexes: Indexes, 26 28 } 27 29 28 30 #[derive(Debug)] ··· 237 239 let fs_cache = Arc::new(tokio::sync::Mutex::new(HashMap::new())); 238 240 let metadata_cache = Arc::new(tokio::sync::Mutex::new(HashMap::new())); 239 241 242 + let indexes = create_indexes()?; 243 + 240 244 loop { 241 245 match listener.accept() { 242 246 Ok((stream, _)) => { ··· 249 253 let mut cloned_self = self.clone(); 250 254 let cloned_fs_cache = fs_cache.clone(); 251 255 let cloned_metadata_cache = metadata_cache.clone(); 256 + let cloned_indexes = indexes.clone(); 252 257 pool.execute(move || { 253 258 let mut buf_reader = BufReader::new(&stream); 254 259 let mut request = String::new(); ··· 314 319 req_body, 315 320 cloned_fs_cache, 316 321 cloned_metadata_cache, 322 + cloned_indexes, 317 323 ); 318 324 } 319 325 ··· 357 363 body: Option<String>, 358 364 fs_cache: Arc<tokio::sync::Mutex<HashMap<String, Vec<Entry>>>>, 359 365 metadata_cache: Arc<tokio::sync::Mutex<HashMap<String, Mp3Entry>>>, 366 + indexes: Indexes, 360 367 ) { 361 368 println!("{} {}", method.bright_cyan(), path); 362 369 match self.router.route(method, path) { ··· 366 373 pool, 367 374 fs_cache, 368 375 metadata_cache, 376 + indexes, 369 377 }; 370 378 let request = Request { 371 379 method: method.to_string(),
+1 -1
crates/server/src/lib.rs
··· 18 18 pub mod cache; 19 19 pub mod handlers; 20 20 pub mod http; 21 - pub mod types; 22 21 23 22 pub const AUDIO_EXTENSIONS: [&str; 17] = [ 24 23 "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", ··· 79 78 app.get("/status", get_status); 80 79 app.get("/settings", get_global_settings); 81 80 app.put("/scan-library", scan_library); 81 + app.get("/search", search); 82 82 83 83 app.get("/", index); 84 84 app.get("/operations/:id", index);
-30
crates/server/src/types.rs
··· 1 - use serde::{Deserialize, Serialize}; 2 - 3 - #[derive(Debug, Serialize, Deserialize)] 4 - pub struct NewPlaylist { 5 - pub name: Option<String>, 6 - pub tracks: Vec<String>, 7 - } 8 - 9 - #[derive(Debug, Serialize, Deserialize)] 10 - pub struct InsertTracks { 11 - pub position: i32, 12 - pub tracks: Vec<String>, 13 - pub directory: Option<String>, 14 - pub shuffle: Option<bool>, 15 - } 16 - 17 - #[derive(Debug, Serialize, Deserialize)] 18 - pub struct NewVolume { 19 - pub steps: i32, 20 - } 21 - 22 - #[derive(Debug, Serialize, Deserialize)] 23 - pub struct DeleteTracks { 24 - pub positions: Vec<i32>, 25 - } 26 - 27 - #[derive(Debug, Serialize, Deserialize)] 28 - pub struct StatusCode { 29 - pub code: i32, 30 - }
+9
crates/types/Cargo.toml
··· 1 + [package] 2 + edition = "2021" 3 + name = "rockbox-types" 4 + version = "0.1.0" 5 + 6 + [dependencies] 7 + rockbox-search = {path = "../search"} 8 + serde = {version = "1.0.213", features = ["derive"]} 9 +
+45
crates/types/src/lib.rs
··· 1 + use rockbox_search::artist::Artist; 2 + use rockbox_search::file::File; 3 + use rockbox_search::liked_album::LikedAlbum; 4 + use rockbox_search::liked_track::LikedTrack; 5 + use rockbox_search::{album::Album, track::Track}; 6 + use serde::{Deserialize, Serialize}; 7 + 8 + #[derive(Debug, Serialize, Deserialize)] 9 + pub struct NewPlaylist { 10 + pub name: Option<String>, 11 + pub tracks: Vec<String>, 12 + } 13 + 14 + #[derive(Debug, Serialize, Deserialize)] 15 + pub struct InsertTracks { 16 + pub position: i32, 17 + pub tracks: Vec<String>, 18 + pub directory: Option<String>, 19 + pub shuffle: Option<bool>, 20 + } 21 + 22 + #[derive(Debug, Serialize, Deserialize)] 23 + pub struct NewVolume { 24 + pub steps: i32, 25 + } 26 + 27 + #[derive(Debug, Serialize, Deserialize)] 28 + pub struct DeleteTracks { 29 + pub positions: Vec<i32>, 30 + } 31 + 32 + #[derive(Debug, Serialize, Deserialize)] 33 + pub struct StatusCode { 34 + pub code: i32, 35 + } 36 + 37 + #[derive(Default, Serialize, Deserialize)] 38 + pub struct SearchResults { 39 + pub artists: Vec<Artist>, 40 + pub albums: Vec<Album>, 41 + pub tracks: Vec<Track>, 42 + pub liked_tracks: Vec<LikedTrack>, 43 + pub liked_albums: Vec<LikedAlbum>, 44 + pub files: Vec<File>, 45 + }
+9 -2
tools/root.make
··· 412 412 413 413 ziginstall: zig 414 414 @echo "Installing your build in your '$(RBPREFIX)' dir" 415 - cd .. && zig build install-rockbox && mkdir -p $(RBPREFIX)/bin $(RBPREFIX)/share/rockbox && cp zig-out/bin/rockbox $(RBPREFIX)/bin && cp -r assets/* $(RBPREFIX)/share/rockbox 415 + cd .. \ 416 + && zig build install-rockbox \ 417 + && mkdir -p $(RBPREFIX)/bin $(RBPREFIX)/share/rockbox \ 418 + && cp zig-out/bin/rockbox $(RBPREFIX)/bin \ 419 + && cp -r assets/* $(RBPREFIX)/share/rockbox 416 420 417 421 symlinkinstall: simext1 418 422 @echo "Installing a full setup with links in your '$(RBPREFIX)' dir" ··· 424 428 $(SILENT)$(CC) $(CFLAGS) -c -o $(BUILDDIR)/apps/recorder/jpeg_load.o $(ROOTDIR)/apps/recorder/jpeg_load.c 425 429 426 430 zig: $(BUILDDIR)/apps/recorder/jpeg_load.o $(BUILDDIR)/lang/lang.h $(BUILDDIR)/lang_enum.h $(BUILDDIR)/lang/lang_core.c $(BUILDDIR)/lang/max_language_size.h $(BUILDDIR)/sysfont.o $(BUILDDIR)/rbversion.h $(PBMPHFILES) $(LUA_BUILDDIR)/actions.lua $(LUA_BUILDDIR)/settings.lua $(LUA_BUILDDIR)/buttons.lua $(LUA_BUILDDIR)/rb_defines.lua $(LUA_BUILDDIR)/sound_defines.lua $(LUA_BUILDDIR)/rocklib_aux.c $(BUILDDIR)/credits.raw credits.raw $(DEPFILE) $(TOOLS) $(CODECS) 427 - cd .. && cargo build -p rockbox-cli --release && cargo build -p rockbox-server --release && zig build all 431 + cd .. \ 432 + && cargo build -p rockbox-cli --release \ 433 + && cargo build -p rockbox-server --release \ 434 + && cd .. && zig build all 428 435 help: 429 436 @echo "A few helpful make targets" 430 437 @echo ""
+180
webui/rockbox/graphql.schema.json
··· 1785 1785 "deprecationReason": null 1786 1786 }, 1787 1787 { 1788 + "name": "scanLibrary", 1789 + "description": null, 1790 + "args": [], 1791 + "type": { 1792 + "kind": "NON_NULL", 1793 + "name": null, 1794 + "ofType": { 1795 + "kind": "SCALAR", 1796 + "name": "Int", 1797 + "ofType": null 1798 + } 1799 + }, 1800 + "isDeprecated": false, 1801 + "deprecationReason": null 1802 + }, 1803 + { 1788 1804 "name": "setPitch", 1789 1805 "description": null, 1790 1806 "args": [], ··· 2475 2491 "deprecationReason": null 2476 2492 }, 2477 2493 { 2494 + "name": "search", 2495 + "description": null, 2496 + "args": [ 2497 + { 2498 + "name": "term", 2499 + "description": null, 2500 + "type": { 2501 + "kind": "NON_NULL", 2502 + "name": null, 2503 + "ofType": { 2504 + "kind": "SCALAR", 2505 + "name": "String", 2506 + "ofType": null 2507 + } 2508 + }, 2509 + "defaultValue": null, 2510 + "isDeprecated": false, 2511 + "deprecationReason": null 2512 + } 2513 + ], 2514 + "type": { 2515 + "kind": "NON_NULL", 2516 + "name": null, 2517 + "ofType": { 2518 + "kind": "OBJECT", 2519 + "name": "SearchResults", 2520 + "ofType": null 2521 + } 2522 + }, 2523 + "isDeprecated": false, 2524 + "deprecationReason": null 2525 + }, 2526 + { 2478 2527 "name": "soundCurrent", 2479 2528 "description": null, 2480 2529 "args": [], ··· 2682 2731 "kind": "SCALAR", 2683 2732 "name": "Int", 2684 2733 "ofType": null 2734 + } 2735 + }, 2736 + "isDeprecated": false, 2737 + "deprecationReason": null 2738 + } 2739 + ], 2740 + "inputFields": null, 2741 + "interfaces": [], 2742 + "enumValues": null, 2743 + "possibleTypes": null 2744 + }, 2745 + { 2746 + "kind": "OBJECT", 2747 + "name": "SearchResults", 2748 + "description": null, 2749 + "fields": [ 2750 + { 2751 + "name": "albums", 2752 + "description": null, 2753 + "args": [], 2754 + "type": { 2755 + "kind": "NON_NULL", 2756 + "name": null, 2757 + "ofType": { 2758 + "kind": "LIST", 2759 + "name": null, 2760 + "ofType": { 2761 + "kind": "NON_NULL", 2762 + "name": null, 2763 + "ofType": { 2764 + "kind": "OBJECT", 2765 + "name": "Album", 2766 + "ofType": null 2767 + } 2768 + } 2769 + } 2770 + }, 2771 + "isDeprecated": false, 2772 + "deprecationReason": null 2773 + }, 2774 + { 2775 + "name": "artists", 2776 + "description": null, 2777 + "args": [], 2778 + "type": { 2779 + "kind": "NON_NULL", 2780 + "name": null, 2781 + "ofType": { 2782 + "kind": "LIST", 2783 + "name": null, 2784 + "ofType": { 2785 + "kind": "NON_NULL", 2786 + "name": null, 2787 + "ofType": { 2788 + "kind": "OBJECT", 2789 + "name": "Artist", 2790 + "ofType": null 2791 + } 2792 + } 2793 + } 2794 + }, 2795 + "isDeprecated": false, 2796 + "deprecationReason": null 2797 + }, 2798 + { 2799 + "name": "likedAlbums", 2800 + "description": null, 2801 + "args": [], 2802 + "type": { 2803 + "kind": "NON_NULL", 2804 + "name": null, 2805 + "ofType": { 2806 + "kind": "LIST", 2807 + "name": null, 2808 + "ofType": { 2809 + "kind": "NON_NULL", 2810 + "name": null, 2811 + "ofType": { 2812 + "kind": "OBJECT", 2813 + "name": "Album", 2814 + "ofType": null 2815 + } 2816 + } 2817 + } 2818 + }, 2819 + "isDeprecated": false, 2820 + "deprecationReason": null 2821 + }, 2822 + { 2823 + "name": "likedTracks", 2824 + "description": null, 2825 + "args": [], 2826 + "type": { 2827 + "kind": "NON_NULL", 2828 + "name": null, 2829 + "ofType": { 2830 + "kind": "LIST", 2831 + "name": null, 2832 + "ofType": { 2833 + "kind": "NON_NULL", 2834 + "name": null, 2835 + "ofType": { 2836 + "kind": "OBJECT", 2837 + "name": "Track", 2838 + "ofType": null 2839 + } 2840 + } 2841 + } 2842 + }, 2843 + "isDeprecated": false, 2844 + "deprecationReason": null 2845 + }, 2846 + { 2847 + "name": "tracks", 2848 + "description": null, 2849 + "args": [], 2850 + "type": { 2851 + "kind": "NON_NULL", 2852 + "name": null, 2853 + "ofType": { 2854 + "kind": "LIST", 2855 + "name": null, 2856 + "ofType": { 2857 + "kind": "NON_NULL", 2858 + "name": null, 2859 + "ofType": { 2860 + "kind": "OBJECT", 2861 + "name": "Track", 2862 + "ofType": null 2863 + } 2864 + } 2685 2865 } 2686 2866 }, 2687 2867 "isDeprecated": false,
+4 -2
webui/rockbox/src/Components/Albums/Albums.tsx
··· 13 13 onFilter: (filter: string) => void; 14 14 onLike: (album: any) => void; 15 15 onUnLike: (album: any) => void; 16 + keyword?: string; 17 + loading?: boolean; 16 18 }; 17 19 18 20 const Albums: FC<AlbumsProps> = (props) => { ··· 24 26 <ControlBar /> 25 27 <Scrollable> 26 28 <Title>Albums</Title> 27 - {props.albums.length > 0 && ( 29 + {(props.albums.length > 0 || props.keyword) && !props.loading && ( 28 30 <> 29 31 <FilterContainer> 30 - <Filter placeholder="Search albums" onChange={() => {}} /> 32 + <Filter placeholder="Search albums" /> 31 33 </FilterContainer> 32 34 <div style={{ marginBottom: 100 }}> 33 35 <Grid
+34 -1
webui/rockbox/src/Components/Albums/AlbumsWithData.tsx
··· 1 1 import { FC, useEffect, useState } from "react"; 2 2 import Albums from "./Albums"; 3 3 import { useGetAlbumsQuery } from "../../Hooks/GraphQL"; 4 + import { useRecoilValue } from "recoil"; 5 + import { filterState } from "../Filter/FilterState"; 4 6 5 7 const AlbumsWithData: FC = () => { 8 + const filter = useRecoilValue(filterState); 6 9 // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 10 const [albums, setAlbums] = useState<any[]>([]); 8 - const { data } = useGetAlbumsQuery(); 11 + const { data, loading } = useGetAlbumsQuery(); 12 + 13 + useEffect(() => { 14 + if (filter.term.length > 0 && filter.results) { 15 + setAlbums( 16 + filter.results.albums.map((x) => ({ 17 + id: x.id, 18 + title: x.title, 19 + artist: x.artist, 20 + cover: x.albumArt ? `http://localhost:6062/covers/${x.albumArt}` : "", 21 + year: x.year, 22 + artistId: x.artistId, 23 + })) 24 + ); 25 + return; 26 + } 27 + if (data) { 28 + setAlbums( 29 + data.albums.map((x) => ({ 30 + id: x.id, 31 + title: x.title, 32 + artist: x.artist, 33 + cover: x.albumArt ? `http://localhost:6062/covers/${x.albumArt}` : "", 34 + year: x.year, 35 + artistId: x.artistId, 36 + })) 37 + ); 38 + } 39 + }, [filter, data]); 9 40 10 41 useEffect(() => { 11 42 if (data) { ··· 28 59 albums={albums} 29 60 onLike={() => {}} 30 61 onUnLike={() => {}} 62 + keyword={filter.term} 63 + loading={loading} 31 64 /> 32 65 ); 33 66 };
+4 -2
webui/rockbox/src/Components/Artists/Artists.tsx
··· 21 21 artists: any[]; 22 22 onClickArtist: (artist: any) => void; 23 23 onFilter: (filter: string) => void; 24 + keyword?: string; 25 + loading?: boolean; 24 26 }; 25 27 26 28 const Artists: FC<ArtistsProps> = (props) => { ··· 32 34 <ControlBar /> 33 35 <Scrollable> 34 36 <Title>Artists</Title> 35 - {props.artists.length > 0 && ( 37 + {(props.artists.length > 0 || props.keyword) && !props.loading && ( 36 38 <> 37 39 <FilterContainer> 38 - <Filter placeholder="Search artists" onChange={() => {}} /> 40 + <Filter placeholder="Search artists" /> 39 41 </FilterContainer> 40 42 <div style={{ marginBottom: 100 }}> 41 43 <Grid
+18 -3
webui/rockbox/src/Components/Artists/ArtistsWithData.tsx
··· 1 1 import { FC, useMemo } from "react"; 2 2 import Artists from "./Artists"; 3 3 import { useGetArtistsQuery } from "../../Hooks/GraphQL"; 4 + import { useRecoilValue } from "recoil"; 5 + import { filterState } from "../Filter/FilterState"; 4 6 5 7 const ArtistsWithData: FC = () => { 6 - const { data } = useGetArtistsQuery(); 8 + const filter = useRecoilValue(filterState); 9 + const { data, loading } = useGetArtistsQuery(); 7 10 const artists = useMemo(() => { 11 + if (filter.term.length > 0 && filter.results) { 12 + return (filter.results?.artists || []).map((x) => ({ 13 + id: x.id, 14 + name: x.name, 15 + })); 16 + } 8 17 return (data?.artists || []).map((x) => ({ 9 18 id: x.id, 10 19 name: x.name, 11 20 })); 12 - }, [data]); 21 + }, [data, filter]); 13 22 14 23 return ( 15 - <Artists onFilter={() => {}} onClickArtist={() => {}} artists={artists} /> 24 + <Artists 25 + onFilter={() => {}} 26 + onClickArtist={() => {}} 27 + artists={artists} 28 + keyword={filter.term} 29 + loading={loading} 30 + /> 16 31 ); 17 32 }; 18 33
+1 -1
webui/rockbox/src/Components/ControlBar/PlayQueue/PlayQueue.tsx
··· 113 113 position: "absolute", 114 114 top: 0, 115 115 left: 0, 116 - width: "calc(100% - 34px)", 116 + width: "calc(100% - 12px)", 117 117 transform: `translateY(${virtualItem.start}px)`, 118 118 }} 119 119 >
+1 -1
webui/rockbox/src/Components/ControlBar/PlayQueue/__snapshots__/PlayQueue.test.tsx.snap
··· 31 31 </div> 32 32 </div> 33 33 <div 34 - class="css-1wmblyr" 34 + class="css-1nzjdw3" 35 35 > 36 36 <div 37 37 style="height: 192px; width: 100%; position: relative;"
+2
webui/rockbox/src/Components/ControlBar/PlayQueue/styles.tsx
··· 33 33 export const List = styled.div` 34 34 height: calc(100% - 59.5px); 35 35 overflow-y: auto; 36 + overflow-x: hidden; 36 37 `; 37 38 38 39 export const ListItem = styled.div` ··· 95 96 background-color: transparent; 96 97 cursor: pointer; 97 98 border: none; 99 + margin-right: 10px; 98 100 `; 99 101 100 102 export const Placeholder = styled.div`
+1
webui/rockbox/src/Components/Files/FilesWithData.tsx
··· 28 28 playDirectory({ 29 29 variables: { 30 30 path, 31 + recurse: true, 31 32 }, 32 33 }); 33 34 };
+1 -1
webui/rockbox/src/Components/Filter/Filter.tsx
··· 7 7 8 8 export type FilterProps = { 9 9 placeholder?: string; 10 - onChange: (value: string) => void; 10 + onChange: (value: string) => Promise<void>; 11 11 }; 12 12 13 13 const Filter: FC<FilterProps> = ({ placeholder = "Filter", onChange }) => {
+13
webui/rockbox/src/Components/Filter/FilterState.tsx
··· 1 + import { atom } from "recoil"; 2 + import { SearchResults } from "../../Hooks/GraphQL"; 3 + 4 + export const filterState = atom<{ 5 + term: string; 6 + results?: SearchResults; 7 + }>({ 8 + key: "filterState", 9 + default: { 10 + term: "", 11 + results: undefined, 12 + }, 13 + });
+27
webui/rockbox/src/Components/Filter/FilterWithData.tsx
··· 1 + import { FC } from "react"; 2 + import Filter from "./Filter"; 3 + import { useSearchLazyQuery } from "../../Hooks/GraphQL"; 4 + import { useRecoilState } from "recoil"; 5 + import { filterState } from "./FilterState"; 6 + import _ from "lodash"; 7 + 8 + const FilterWithData: FC<{ placeholder?: string }> = (props) => { 9 + const [, setFilterState] = useRecoilState(filterState); 10 + const [search] = useSearchLazyQuery(); 11 + const onSearch = async (term: string) => { 12 + setFilterState((state) => ({ ...state, term })); 13 + if (term.length > 2) { 14 + _.debounce(async () => { 15 + const { data } = await search({ variables: { term } }); 16 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 + setFilterState((state) => ({ ...state, results: data?.search as any })); 18 + }, 500)(); 19 + } 20 + if (term.length === 0) { 21 + setFilterState({ term: "", results: undefined }); 22 + } 23 + }; 24 + return <Filter onChange={onSearch} {...props} />; 25 + }; 26 + 27 + export default FilterWithData;
+1 -1
webui/rockbox/src/Components/Filter/index.tsx
··· 1 - import Filter from "./Filter"; 1 + import Filter from "./FilterWithData"; 2 2 3 3 export default Filter;
+4 -2
webui/rockbox/src/Components/Likes/Likes.tsx
··· 34 34 onPlayTrack: (index: number) => void; 35 35 onPlayAll: () => void; 36 36 onShuffleAll: () => void; 37 + keyword?: string; 38 + loading?: boolean; 37 39 }; 38 40 39 41 const Likes: FC<TracksProps> = (props) => { ··· 188 190 <ControlBar /> 189 191 <ContentWrapper ref={containerRef}> 190 192 <Title>Likes</Title> 191 - {props.tracks.length > 0 && ( 193 + {(props.tracks.length > 0 || props.keyword) && !props.loading && ( 192 194 <> 193 195 <HeaderWrapper> 194 196 <ButtonGroup> ··· 207 209 </Button> 208 210 </ButtonGroup> 209 211 <FilterContainer> 210 - <Filter placeholder="Search song" onChange={() => {}} /> 212 + <Filter placeholder="Search song" /> 211 213 </FilterContainer> 212 214 </HeaderWrapper> 213 215 <div style={{ marginBottom: 60 }}>
+46 -1
webui/rockbox/src/Components/Likes/LikesWithData.tsx
··· 5 5 usePlayLikedTracksMutation, 6 6 } from "../../Hooks/GraphQL"; 7 7 import { useTimeFormat } from "../../Hooks/useFormat"; 8 - import { useRecoilState } from "recoil"; 8 + import { useRecoilState, useRecoilValue } from "recoil"; 9 9 import { likedTracks, likesState } from "./LikesState"; 10 + import { filterState } from "../Filter/FilterState"; 10 11 11 12 const LikesWithData: FC = () => { 13 + const filter = useRecoilValue(filterState); 12 14 const [likes, setLikes] = useRecoilState(likesState); 13 15 const { data, loading } = useGetLikedTracksQuery({ 14 16 fetchPolicy: "network-only", ··· 16 18 const [tracks, setTracks] = useRecoilState(likedTracks); 17 19 const [playLikedTracks] = usePlayLikedTracksMutation(); 18 20 const { formatTime } = useTimeFormat(); 21 + 22 + useEffect(() => { 23 + if (filter.term.length > 0 && filter.results) { 24 + setTracks( 25 + filter.results.tracks.map((x, i) => ({ 26 + id: x.id!, 27 + trackNumber: i + 1, 28 + title: x.title, 29 + artist: x.artist, 30 + album: x.album, 31 + time: formatTime(x.length), 32 + albumArt: x.albumArt 33 + ? `http://localhost:6062/covers/${x.albumArt}` 34 + : undefined, 35 + albumId: x.albumId, 36 + artistId: x.artistId, 37 + path: x.path, 38 + })) 39 + ); 40 + return; 41 + } 42 + if (data) { 43 + setTracks( 44 + data.likedTracks.map((x, i) => ({ 45 + id: x.id!, 46 + trackNumber: i + 1, 47 + title: x.title, 48 + artist: x.artist, 49 + album: x.album, 50 + time: formatTime(x.length), 51 + albumArt: x.albumArt 52 + ? `http://localhost:6062/covers/${x.albumArt}` 53 + : undefined, 54 + albumId: x.albumId, 55 + artistId: x.artistId, 56 + path: x.path, 57 + })) 58 + ); 59 + } 60 + // eslint-disable-next-line react-hooks/exhaustive-deps 61 + }, [filter, data]); 19 62 20 63 useEffect(() => { 21 64 if (!data || loading) { ··· 75 118 onPlayTrack={onPlayTrack} 76 119 onPlayAll={onPlayAll} 77 120 onShuffleAll={onShuffleAll} 121 + keyword={filter.term} 122 + loading={loading} 78 123 /> 79 124 ); 80 125 };
+4 -2
webui/rockbox/src/Components/Tracks/Tracks.tsx
··· 27 27 export type TracksProps = { 28 28 tracks: Track[]; 29 29 onPlayTrack: (index: number) => void; 30 + keyword?: string; 31 + loading?: boolean; 30 32 }; 31 33 32 34 const Tracks: FC<TracksProps> = (props) => { ··· 180 182 <ControlBar /> 181 183 <ContentWrapper ref={containerRef}> 182 184 <Title>Songs</Title> 183 - {props.tracks.length > 0 && ( 185 + {(props.tracks.length > 0 || props.keyword) && !props.loading && ( 184 186 <> 185 187 <FilterContainer> 186 - <Filter placeholder="Search song" onChange={() => {}} /> 188 + <Filter placeholder="Search song" /> 187 189 </FilterContainer> 188 190 <div style={{ marginBottom: 60 }}> 189 191 {props.tracks.length > 0 && (
+52 -1
webui/rockbox/src/Components/Tracks/TracksWithData.tsx
··· 3 3 import { usePlayAllTracksMutation, useTracksQuery } from "../../Hooks/GraphQL"; 4 4 import { useTimeFormat } from "../../Hooks/useFormat"; 5 5 import { Track } from "../../Types/track"; 6 + import { useRecoilValue } from "recoil"; 7 + import { filterState } from "../Filter/FilterState"; 6 8 7 9 const TracksWithData: FC = () => { 10 + const filter = useRecoilValue(filterState); 8 11 const { data, loading } = useTracksQuery(); 9 12 const [tracks, setTracks] = useState<Track[]>([]); 10 13 const { formatTime } = useTimeFormat(); 11 14 const [playAllTracks] = usePlayAllTracksMutation(); 12 15 13 16 useEffect(() => { 17 + if (filter.term.length > 0 && filter.results) { 18 + setTracks( 19 + filter.results.tracks.map((x, i) => ({ 20 + id: x.id!, 21 + trackNumber: i + 1, 22 + title: x.title, 23 + artist: x.artist, 24 + album: x.album, 25 + time: formatTime(x.length), 26 + albumArt: x.albumArt 27 + ? `http://localhost:6062/covers/${x.albumArt}` 28 + : undefined, 29 + albumId: x.albumId, 30 + artistId: x.artistId, 31 + path: x.path, 32 + })) 33 + ); 34 + return; 35 + } 36 + if (data) { 37 + setTracks( 38 + data.tracks.map((x, i) => ({ 39 + id: x.id!, 40 + trackNumber: i + 1, 41 + title: x.title, 42 + artist: x.artist, 43 + album: x.album, 44 + time: formatTime(x.length), 45 + albumArt: x.albumArt 46 + ? `http://localhost:6062/covers/${x.albumArt}` 47 + : undefined, 48 + albumId: x.albumId, 49 + artistId: x.artistId, 50 + path: x.path, 51 + })) 52 + ); 53 + } 54 + // eslint-disable-next-line react-hooks/exhaustive-deps 55 + }, [filter, data]); 56 + 57 + useEffect(() => { 14 58 if (!data || loading) { 15 59 return; 16 60 } ··· 42 86 }); 43 87 }; 44 88 45 - return <Tracks tracks={tracks} onPlayTrack={onPlayTrack} />; 89 + return ( 90 + <Tracks 91 + tracks={tracks} 92 + onPlayTrack={onPlayTrack} 93 + keyword={filter.term} 94 + loading={loading} 95 + /> 96 + ); 46 97 }; 47 98 48 99 export default TracksWithData;
+57
webui/rockbox/src/GraphQL/Library/Query.ts
··· 157 157 } 158 158 } 159 159 `; 160 + 161 + export const SEARCH = gql` 162 + query Search($term: String!) { 163 + search(term: $term) { 164 + tracks { 165 + id 166 + title 167 + artist 168 + album 169 + albumArtist 170 + path 171 + albumArt 172 + length 173 + composer 174 + comment 175 + albumId 176 + artistId 177 + } 178 + albums { 179 + id 180 + title 181 + year 182 + yearString 183 + albumArt 184 + artist 185 + artistId 186 + } 187 + artists { 188 + id 189 + name 190 + image 191 + } 192 + likedTracks { 193 + id 194 + title 195 + artist 196 + album 197 + albumArtist 198 + path 199 + albumArt 200 + length 201 + composer 202 + comment 203 + albumId 204 + artistId 205 + } 206 + likedAlbums { 207 + id 208 + title 209 + albumArt 210 + artist 211 + artistId 212 + year 213 + } 214 + } 215 + } 216 + `;
+112
webui/rockbox/src/Hooks/GraphQL.tsx
··· 107 107 previous: Scalars['Int']['output']; 108 108 resume: Scalars['Int']['output']; 109 109 resumeTrack: Scalars['String']['output']; 110 + scanLibrary: Scalars['Int']['output']; 110 111 setPitch: Scalars['String']['output']; 111 112 shufflePlaylist: Scalars['Int']['output']; 112 113 soundMax: Scalars['String']['output']; ··· 279 280 playlistAmount: Scalars['Int']['output']; 280 281 playlistGetCurrent: Playlist; 281 282 rockboxVersion: Scalars['String']['output']; 283 + search: SearchResults; 282 284 soundCurrent: Scalars['String']['output']; 283 285 soundDefault: Scalars['String']['output']; 284 286 soundVal2Phys: Scalars['String']['output']; ··· 299 301 }; 300 302 301 303 304 + export type QuerySearchArgs = { 305 + term: Scalars['String']['input']; 306 + }; 307 + 308 + 302 309 export type QueryTrackArgs = { 303 310 id: Scalars['String']['input']; 304 311 }; ··· 313 320 noclip: Scalars['Boolean']['output']; 314 321 preamp: Scalars['Int']['output']; 315 322 type: Scalars['Int']['output']; 323 + }; 324 + 325 + export type SearchResults = { 326 + __typename?: 'SearchResults'; 327 + albums: Array<Album>; 328 + artists: Array<Artist>; 329 + likedAlbums: Array<Album>; 330 + likedTracks: Array<Track>; 331 + tracks: Array<Track>; 316 332 }; 317 333 318 334 export type Subscription = { ··· 640 656 641 657 642 658 export type GetLikedAlbumsQuery = { __typename?: 'Query', likedAlbums: Array<{ __typename?: 'Album', id: string, title: string, artist: string, albumArt?: string | null, year: number, yearString: string, artistId: string, md5: string, tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, album: string, albumArtist: string, artistId?: string | null, albumId?: string | null, path: string, length: number }> }> }; 659 + 660 + export type SearchQueryVariables = Exact<{ 661 + term: Scalars['String']['input']; 662 + }>; 663 + 664 + 665 + export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResults', tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, album: string, albumArtist: string, path: string, albumArt?: string | null, length: number, composer: string, comment: string, albumId?: string | null, artistId?: string | null }>, albums: Array<{ __typename?: 'Album', id: string, title: string, year: number, yearString: string, albumArt?: string | null, artist: string, artistId: string }>, artists: Array<{ __typename?: 'Artist', id: string, name: string, image?: string | null }>, likedTracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, album: string, albumArtist: string, path: string, albumArt?: string | null, length: number, composer: string, comment: string, albumId?: string | null, artistId?: string | null }>, likedAlbums: Array<{ __typename?: 'Album', id: string, title: string, albumArt?: string | null, artist: string, artistId: string, year: number }> } }; 643 666 644 667 export type PlayMutationVariables = Exact<{ 645 668 elapsed: Scalars['Int']['input']; ··· 1373 1396 export type GetLikedAlbumsLazyQueryHookResult = ReturnType<typeof useGetLikedAlbumsLazyQuery>; 1374 1397 export type GetLikedAlbumsSuspenseQueryHookResult = ReturnType<typeof useGetLikedAlbumsSuspenseQuery>; 1375 1398 export type GetLikedAlbumsQueryResult = Apollo.QueryResult<GetLikedAlbumsQuery, GetLikedAlbumsQueryVariables>; 1399 + export const SearchDocument = gql` 1400 + query Search($term: String!) { 1401 + search(term: $term) { 1402 + tracks { 1403 + id 1404 + title 1405 + artist 1406 + album 1407 + albumArtist 1408 + path 1409 + albumArt 1410 + length 1411 + composer 1412 + comment 1413 + albumId 1414 + artistId 1415 + } 1416 + albums { 1417 + id 1418 + title 1419 + year 1420 + yearString 1421 + albumArt 1422 + artist 1423 + artistId 1424 + } 1425 + artists { 1426 + id 1427 + name 1428 + image 1429 + } 1430 + likedTracks { 1431 + id 1432 + title 1433 + artist 1434 + album 1435 + albumArtist 1436 + path 1437 + albumArt 1438 + length 1439 + composer 1440 + comment 1441 + albumId 1442 + artistId 1443 + } 1444 + likedAlbums { 1445 + id 1446 + title 1447 + albumArt 1448 + artist 1449 + artistId 1450 + year 1451 + } 1452 + } 1453 + } 1454 + `; 1455 + 1456 + /** 1457 + * __useSearchQuery__ 1458 + * 1459 + * To run a query within a React component, call `useSearchQuery` and pass it any options that fit your needs. 1460 + * When your component renders, `useSearchQuery` returns an object from Apollo Client that contains loading, error, and data properties 1461 + * you can use to render your UI. 1462 + * 1463 + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 1464 + * 1465 + * @example 1466 + * const { data, loading, error } = useSearchQuery({ 1467 + * variables: { 1468 + * term: // value for 'term' 1469 + * }, 1470 + * }); 1471 + */ 1472 + export function useSearchQuery(baseOptions: Apollo.QueryHookOptions<SearchQuery, SearchQueryVariables> & ({ variables: SearchQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { 1473 + const options = {...defaultOptions, ...baseOptions} 1474 + return Apollo.useQuery<SearchQuery, SearchQueryVariables>(SearchDocument, options); 1475 + } 1476 + export function useSearchLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchQuery, SearchQueryVariables>) { 1477 + const options = {...defaultOptions, ...baseOptions} 1478 + return Apollo.useLazyQuery<SearchQuery, SearchQueryVariables>(SearchDocument, options); 1479 + } 1480 + export function useSearchSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<SearchQuery, SearchQueryVariables>) { 1481 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1482 + return Apollo.useSuspenseQuery<SearchQuery, SearchQueryVariables>(SearchDocument, options); 1483 + } 1484 + export type SearchQueryHookResult = ReturnType<typeof useSearchQuery>; 1485 + export type SearchLazyQueryHookResult = ReturnType<typeof useSearchLazyQuery>; 1486 + export type SearchSuspenseQueryHookResult = ReturnType<typeof useSearchSuspenseQuery>; 1487 + export type SearchQueryResult = Apollo.QueryResult<SearchQuery, SearchQueryVariables>; 1376 1488 export const PlayDocument = gql` 1377 1489 mutation Play($elapsed: Int!, $offset: Int!) { 1378 1490 play(elapsed: $elapsed, offset: $offset)