aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-05-05 21:12:32 +0200
committersalmonik <raisersoft@proton.me>2026-05-05 21:12:32 +0200
commitbd29978b2a0d0d6def486fcdaa63bc8934d68045 (patch)
tree5df0efb17d350ae7f095eb9c0940b1bbf568de96
downloadrsplayer-bd29978b2a0d0d6def486fcdaa63bc8934d68045.tar.gz
rsplayer-bd29978b2a0d0d6def486fcdaa63bc8934d68045.zip
fix(daemon): robust auto-advance; ensure next track starts even if playback fails
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--AGENTS.md55
-rw-r--r--Cargo.lock3135
-rw-r--r--Cargo.toml21
-rw-r--r--PLAN.md24
-rw-r--r--TODO.md51
-rw-r--r--src/audio.rs418
-rw-r--r--src/cli.rs186
-rw-r--r--src/config.rs180
-rw-r--r--src/daemon.rs574
-rw-r--r--src/database.rs259
-rw-r--r--src/main.rs18
-rw-r--r--src/playback.rs131
-rw-r--r--src/scrobbler.rs75
14 files changed, 5128 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
/target
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..31f1205
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,55 @@
1# Agent Instructions for rsplayer
2
3## Overview
4Rust CLI music player daemon for GNU/Linux with shell autocompletion, SQLite library database, Unix socket IPC, and Last.fm/ListenBrainz scrobbling.
5
6## Required Verification Commands
7Run these before completing any task:
8- Lint: `cargo clippy -- -D warnings`
9- Format check: `cargo fmt --check`
10- Test: `cargo test`
11- Build: `cargo build`
12
13## Development Workflow
14- Use `rsp restart` or `rsplayer restart` to kill any running daemon and start a new one (useful during development)
15- Daemon runs in background; restart command handles cleanup automatically
16- After making changes to audio/playback/daemon code, always restart the daemon to test changes
17
18## Code Conventions
19- Use `directories` crate to resolve XDG paths (respect `XDG_CONFIG_HOME` and `XDG_RUNTIME_DIR` environment variables)
20- Follow standard Rust style (enforced by `cargo fmt`)
21- Never commit changes unless explicitly requested by the user
22- Update shell completions when adding/modifying CLI subcommands
23- Use `rand` crate for shuffle/randomization features (added in v0.9)
24
25## Task-Specific Guidelines
26- **CLI changes**: Update clap subcommands (including `rsp scan` and `rsp settings`) and regenerate shell completion scripts
27- **Database changes**: Update SQLite schema and `rsp scan` logic
28- **Playback changes**: Modify daemon audio engine or queue management code
29 - Audio engine uses cpal for output and symphonia for decoding
30 - Queue system implemented in daemon.rs with `Queue` struct
31 - Sample buffering implemented to prevent buffer underruns - uses device default sample rate, not file's sample rate
32 - Pause/resume uses cpal stream `pause()`/`play()` methods
33- **Scrobbling changes**: Update Last.fm/ListenBrainz client implementation
34- **Config/Settings changes**: Update config schema, `rsp settings` subcommands, and scan interval/auto-scan logic
35
36## Completed Features (Phase 2)
37- [x] Unix socket daemon IPC
38- [x] Audio engine with play/pause/stop/volume
39- [x] Queue system with in-memory tracking and auto-advance
40- [x] Shuffle mode: `none`, `all`, and `album` (structure ready) implemented
41- [x] Next/previous track commands with queue navigation
42- [x] Resume CLI subcommand
43- [x] Directory playback - queues all audio files in folder recursively
44- [x] Fixed audio buffer underrun issues by using device default sample rate
45- [x] Queue display with pager support (`rsplayer queue`)
46- [x] Auto-play next track when current track ends
47
48## Remaining Work
49- Album shuffle mode (`ShuffleMode::Album`)
50- Database integration for queue persistence
51- `rsplayer queue` command for queue management
52- Scrobbling (Last.fm/ListenBrainz)
53- Database scanning and schema
54- Shell completion scripts
55- Systemd service unit
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..1baedf3
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,3135 @@
1# This file is automatically @generated by Cargo.
2# It is not intended for manual editing.
3version = 4
4
5[[package]]
6name = "alsa"
7version = "0.11.0"
8source = "registry+https://github.com/rust-lang/crates.io-index"
9checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
10dependencies = [
11 "alsa-sys",
12 "bitflags 2.11.1",
13 "cfg-if",
14 "libc",
15]
16
17[[package]]
18name = "alsa-sys"
19version = "0.4.0"
20source = "registry+https://github.com/rust-lang/crates.io-index"
21checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
22dependencies = [
23 "libc",
24 "pkg-config",
25]
26
27[[package]]
28name = "android_system_properties"
29version = "0.1.5"
30source = "registry+https://github.com/rust-lang/crates.io-index"
31checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
32dependencies = [
33 "libc",
34]
35
36[[package]]
37name = "anstream"
38version = "1.0.0"
39source = "registry+https://github.com/rust-lang/crates.io-index"
40checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
41dependencies = [
42 "anstyle",
43 "anstyle-parse",
44 "anstyle-query",
45 "anstyle-wincon",
46 "colorchoice",
47 "is_terminal_polyfill",
48 "utf8parse",
49]
50
51[[package]]
52name = "anstyle"
53version = "1.0.14"
54source = "registry+https://github.com/rust-lang/crates.io-index"
55checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
56
57[[package]]
58name = "anstyle-parse"
59version = "1.0.0"
60source = "registry+https://github.com/rust-lang/crates.io-index"
61checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
62dependencies = [
63 "utf8parse",
64]
65
66[[package]]
67name = "anstyle-query"
68version = "1.1.5"
69source = "registry+https://github.com/rust-lang/crates.io-index"
70checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
71dependencies = [
72 "windows-sys 0.61.2",
73]
74
75[[package]]
76name = "anstyle-wincon"
77version = "3.0.11"
78source = "registry+https://github.com/rust-lang/crates.io-index"
79checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
80dependencies = [
81 "anstyle",
82 "once_cell_polyfill",
83 "windows-sys 0.61.2",
84]
85
86[[package]]
87name = "anyhow"
88version = "1.0.102"
89source = "registry+https://github.com/rust-lang/crates.io-index"
90checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
91
92[[package]]
93name = "arrayvec"
94version = "0.7.6"
95source = "registry+https://github.com/rust-lang/crates.io-index"
96checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
97
98[[package]]
99name = "atomic-waker"
100version = "1.1.2"
101source = "registry+https://github.com/rust-lang/crates.io-index"
102checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
103
104[[package]]
105name = "autocfg"
106version = "1.5.0"
107source = "registry+https://github.com/rust-lang/crates.io-index"
108checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
109
110[[package]]
111name = "aws-lc-rs"
112version = "1.16.3"
113source = "registry+https://github.com/rust-lang/crates.io-index"
114checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
115dependencies = [
116 "aws-lc-sys",
117 "zeroize",
118]
119
120[[package]]
121name = "aws-lc-sys"
122version = "0.40.0"
123source = "registry+https://github.com/rust-lang/crates.io-index"
124checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
125dependencies = [
126 "cc",
127 "cmake",
128 "dunce",
129 "fs_extra",
130]
131
132[[package]]
133name = "base64"
134version = "0.22.1"
135source = "registry+https://github.com/rust-lang/crates.io-index"
136checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
137
138[[package]]
139name = "bitflags"
140version = "1.3.2"
141source = "registry+https://github.com/rust-lang/crates.io-index"
142checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
143
144[[package]]
145name = "bitflags"
146version = "2.11.1"
147source = "registry+https://github.com/rust-lang/crates.io-index"
148checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
149
150[[package]]
151name = "block2"
152version = "0.6.2"
153source = "registry+https://github.com/rust-lang/crates.io-index"
154checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
155dependencies = [
156 "objc2",
157]
158
159[[package]]
160name = "bumpalo"
161version = "3.20.2"
162source = "registry+https://github.com/rust-lang/crates.io-index"
163checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
164
165[[package]]
166name = "bytemuck"
167version = "1.25.0"
168source = "registry+https://github.com/rust-lang/crates.io-index"
169checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
170
171[[package]]
172name = "bytes"
173version = "1.11.1"
174source = "registry+https://github.com/rust-lang/crates.io-index"
175checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
176
177[[package]]
178name = "cc"
179version = "1.2.61"
180source = "registry+https://github.com/rust-lang/crates.io-index"
181checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
182dependencies = [
183 "find-msvc-tools",
184 "jobserver",
185 "libc",
186 "shlex",
187]
188
189[[package]]
190name = "cesu8"
191version = "1.1.0"
192source = "registry+https://github.com/rust-lang/crates.io-index"
193checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
194
195[[package]]
196name = "cfg-if"
197version = "1.0.4"
198source = "registry+https://github.com/rust-lang/crates.io-index"
199checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
200
201[[package]]
202name = "cfg_aliases"
203version = "0.2.1"
204source = "registry+https://github.com/rust-lang/crates.io-index"
205checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
206
207[[package]]
208name = "chacha20"
209version = "0.10.0"
210source = "registry+https://github.com/rust-lang/crates.io-index"
211checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
212dependencies = [
213 "cfg-if",
214 "cpufeatures",
215 "rand_core 0.10.1",
216]
217
218[[package]]
219name = "chrono"
220version = "0.4.44"
221source = "registry+https://github.com/rust-lang/crates.io-index"
222checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
223dependencies = [
224 "iana-time-zone",
225 "js-sys",
226 "num-traits",
227 "wasm-bindgen",
228 "windows-link",
229]
230
231[[package]]
232name = "clap"
233version = "4.6.1"
234source = "registry+https://github.com/rust-lang/crates.io-index"
235checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
236dependencies = [
237 "clap_builder",
238 "clap_derive",
239]
240
241[[package]]
242name = "clap_builder"
243version = "4.6.0"
244source = "registry+https://github.com/rust-lang/crates.io-index"
245checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
246dependencies = [
247 "anstream",
248 "anstyle",
249 "clap_lex",
250 "strsim",
251]
252
253[[package]]
254name = "clap_derive"
255version = "4.6.1"
256source = "registry+https://github.com/rust-lang/crates.io-index"
257checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
258dependencies = [
259 "heck",
260 "proc-macro2",
261 "quote",
262 "syn",
263]
264
265[[package]]
266name = "clap_lex"
267version = "1.1.0"
268source = "registry+https://github.com/rust-lang/crates.io-index"
269checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
270
271[[package]]
272name = "cmake"
273version = "0.1.58"
274source = "registry+https://github.com/rust-lang/crates.io-index"
275checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
276dependencies = [
277 "cc",
278]
279
280[[package]]
281name = "colorchoice"
282version = "1.0.5"
283source = "registry+https://github.com/rust-lang/crates.io-index"
284checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
285
286[[package]]
287name = "combine"
288version = "4.6.7"
289source = "registry+https://github.com/rust-lang/crates.io-index"
290checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
291dependencies = [
292 "bytes",
293 "memchr",
294]
295
296[[package]]
297name = "core-foundation"
298version = "0.9.4"
299source = "registry+https://github.com/rust-lang/crates.io-index"
300checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
301dependencies = [
302 "core-foundation-sys",
303 "libc",
304]
305
306[[package]]
307name = "core-foundation"
308version = "0.10.1"
309source = "registry+https://github.com/rust-lang/crates.io-index"
310checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
311dependencies = [
312 "core-foundation-sys",
313 "libc",
314]
315
316[[package]]
317name = "core-foundation-sys"
318version = "0.8.7"
319source = "registry+https://github.com/rust-lang/crates.io-index"
320checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
321
322[[package]]
323name = "coreaudio-rs"
324version = "0.14.2"
325source = "registry+https://github.com/rust-lang/crates.io-index"
326checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
327dependencies = [
328 "bitflags 2.11.1",
329 "libc",
330 "objc2-audio-toolbox",
331 "objc2-core-audio",
332 "objc2-core-audio-types",
333 "objc2-core-foundation",
334]
335
336[[package]]
337name = "cpal"
338version = "0.17.3"
339source = "registry+https://github.com/rust-lang/crates.io-index"
340checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
341dependencies = [
342 "alsa",
343 "coreaudio-rs",
344 "dasp_sample",
345 "jni 0.21.1",
346 "js-sys",
347 "libc",
348 "mach2",
349 "ndk",
350 "ndk-context",
351 "num-derive",
352 "num-traits",
353 "objc2",
354 "objc2-audio-toolbox",
355 "objc2-avf-audio",
356 "objc2-core-audio",
357 "objc2-core-audio-types",
358 "objc2-core-foundation",
359 "objc2-foundation",
360 "wasm-bindgen",
361 "wasm-bindgen-futures",
362 "web-sys",
363 "windows",
364]
365
366[[package]]
367name = "cpufeatures"
368version = "0.3.0"
369source = "registry+https://github.com/rust-lang/crates.io-index"
370checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
371dependencies = [
372 "libc",
373]
374
375[[package]]
376name = "ctrlc"
377version = "3.5.2"
378source = "registry+https://github.com/rust-lang/crates.io-index"
379checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
380dependencies = [
381 "dispatch2",
382 "nix",
383 "windows-sys 0.61.2",
384]
385
386[[package]]
387name = "dasp_sample"
388version = "0.11.0"
389source = "registry+https://github.com/rust-lang/crates.io-index"
390checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
391
392[[package]]
393name = "directories"
394version = "6.0.0"
395source = "registry+https://github.com/rust-lang/crates.io-index"
396checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
397dependencies = [
398 "dirs-sys",
399]
400
401[[package]]
402name = "dirs"
403version = "6.0.0"
404source = "registry+https://github.com/rust-lang/crates.io-index"
405checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
406dependencies = [
407 "dirs-sys",
408]
409
410[[package]]
411name = "dirs-sys"
412version = "0.5.0"
413source = "registry+https://github.com/rust-lang/crates.io-index"
414checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
415dependencies = [
416 "libc",
417 "option-ext",
418 "redox_users",
419 "windows-sys 0.61.2",
420]
421
422[[package]]
423name = "dispatch2"
424version = "0.3.1"
425source = "registry+https://github.com/rust-lang/crates.io-index"
426checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
427dependencies = [
428 "bitflags 2.11.1",
429 "block2",
430 "libc",
431 "objc2",
432]
433
434[[package]]
435name = "displaydoc"
436version = "0.2.5"
437source = "registry+https://github.com/rust-lang/crates.io-index"
438checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
439dependencies = [
440 "proc-macro2",
441 "quote",
442 "syn",
443]
444
445[[package]]
446name = "dunce"
447version = "1.0.5"
448source = "registry+https://github.com/rust-lang/crates.io-index"
449checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
450
451[[package]]
452name = "encoding_rs"
453version = "0.8.35"
454source = "registry+https://github.com/rust-lang/crates.io-index"
455checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
456dependencies = [
457 "cfg-if",
458]
459
460[[package]]
461name = "equivalent"
462version = "1.0.2"
463source = "registry+https://github.com/rust-lang/crates.io-index"
464checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
465
466[[package]]
467name = "errno"
468version = "0.3.14"
469source = "registry+https://github.com/rust-lang/crates.io-index"
470checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
471dependencies = [
472 "libc",
473 "windows-sys 0.61.2",
474]
475
476[[package]]
477name = "extended"
478version = "0.1.0"
479source = "registry+https://github.com/rust-lang/crates.io-index"
480checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
481
482[[package]]
483name = "fallible-iterator"
484version = "0.3.0"
485source = "registry+https://github.com/rust-lang/crates.io-index"
486checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
487
488[[package]]
489name = "fallible-streaming-iterator"
490version = "0.1.9"
491source = "registry+https://github.com/rust-lang/crates.io-index"
492checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
493
494[[package]]
495name = "find-msvc-tools"
496version = "0.1.9"
497source = "registry+https://github.com/rust-lang/crates.io-index"
498checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
499
500[[package]]
501name = "fnv"
502version = "1.0.7"
503source = "registry+https://github.com/rust-lang/crates.io-index"
504checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
505
506[[package]]
507name = "foldhash"
508version = "0.1.5"
509source = "registry+https://github.com/rust-lang/crates.io-index"
510checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
511
512[[package]]
513name = "foldhash"
514version = "0.2.0"
515source = "registry+https://github.com/rust-lang/crates.io-index"
516checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
517
518[[package]]
519name = "form_urlencoded"
520version = "1.2.2"
521source = "registry+https://github.com/rust-lang/crates.io-index"
522checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
523dependencies = [
524 "percent-encoding",
525]
526
527[[package]]
528name = "fs_extra"
529version = "1.3.0"
530source = "registry+https://github.com/rust-lang/crates.io-index"
531checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
532
533[[package]]
534name = "futures-channel"
535version = "0.3.32"
536source = "registry+https://github.com/rust-lang/crates.io-index"
537checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
538dependencies = [
539 "futures-core",
540]
541
542[[package]]
543name = "futures-core"
544version = "0.3.32"
545source = "registry+https://github.com/rust-lang/crates.io-index"
546checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
547
548[[package]]
549name = "futures-sink"
550version = "0.3.32"
551source = "registry+https://github.com/rust-lang/crates.io-index"
552checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
553
554[[package]]
555name = "futures-task"
556version = "0.3.32"
557source = "registry+https://github.com/rust-lang/crates.io-index"
558checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
559
560[[package]]
561name = "futures-util"
562version = "0.3.32"
563source = "registry+https://github.com/rust-lang/crates.io-index"
564checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
565dependencies = [
566 "futures-core",
567 "futures-task",
568 "pin-project-lite",
569 "slab",
570]
571
572[[package]]
573name = "getrandom"
574version = "0.2.17"
575source = "registry+https://github.com/rust-lang/crates.io-index"
576checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
577dependencies = [
578 "cfg-if",
579 "js-sys",
580 "libc",
581 "wasi",
582 "wasm-bindgen",
583]
584
585[[package]]
586name = "getrandom"
587version = "0.3.4"
588source = "registry+https://github.com/rust-lang/crates.io-index"
589checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
590dependencies = [
591 "cfg-if",
592 "js-sys",
593 "libc",
594 "r-efi 5.3.0",
595 "wasip2",
596 "wasm-bindgen",
597]
598
599[[package]]
600name = "getrandom"
601version = "0.4.2"
602source = "registry+https://github.com/rust-lang/crates.io-index"
603checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
604dependencies = [
605 "cfg-if",
606 "libc",
607 "r-efi 6.0.0",
608 "rand_core 0.10.1",
609 "wasip2",
610 "wasip3",
611]
612
613[[package]]
614name = "h2"
615version = "0.4.14"
616source = "registry+https://github.com/rust-lang/crates.io-index"
617checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
618dependencies = [
619 "atomic-waker",
620 "bytes",
621 "fnv",
622 "futures-core",
623 "futures-sink",
624 "http",
625 "indexmap",
626 "slab",
627 "tokio",
628 "tokio-util",
629 "tracing",
630]
631
632[[package]]
633name = "hashbrown"
634version = "0.15.5"
635source = "registry+https://github.com/rust-lang/crates.io-index"
636checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
637dependencies = [
638 "foldhash 0.1.5",
639]
640
641[[package]]
642name = "hashbrown"
643version = "0.16.1"
644source = "registry+https://github.com/rust-lang/crates.io-index"
645checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
646dependencies = [
647 "foldhash 0.2.0",
648]
649
650[[package]]
651name = "hashbrown"
652version = "0.17.0"
653source = "registry+https://github.com/rust-lang/crates.io-index"
654checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
655
656[[package]]
657name = "hashlink"
658version = "0.11.0"
659source = "registry+https://github.com/rust-lang/crates.io-index"
660checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230"
661dependencies = [
662 "hashbrown 0.16.1",
663]
664
665[[package]]
666name = "heck"
667version = "0.5.0"
668source = "registry+https://github.com/rust-lang/crates.io-index"
669checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
670
671[[package]]
672name = "http"
673version = "1.4.0"
674source = "registry+https://github.com/rust-lang/crates.io-index"
675checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
676dependencies = [
677 "bytes",
678 "itoa",
679]
680
681[[package]]
682name = "http-body"
683version = "1.0.1"
684source = "registry+https://github.com/rust-lang/crates.io-index"
685checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
686dependencies = [
687 "bytes",
688 "http",
689]
690
691[[package]]
692name = "http-body-util"
693version = "0.1.3"
694source = "registry+https://github.com/rust-lang/crates.io-index"
695checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
696dependencies = [
697 "bytes",
698 "futures-core",
699 "http",
700 "http-body",
701 "pin-project-lite",
702]
703
704[[package]]
705name = "httparse"
706version = "1.10.1"
707source = "registry+https://github.com/rust-lang/crates.io-index"
708checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
709
710[[package]]
711name = "hyper"
712version = "1.9.0"
713source = "registry+https://github.com/rust-lang/crates.io-index"
714checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
715dependencies = [
716 "atomic-waker",
717 "bytes",
718 "futures-channel",
719 "futures-core",
720 "h2",
721 "http",
722 "http-body",
723 "httparse",
724 "itoa",
725 "pin-project-lite",
726 "smallvec",
727 "tokio",
728 "want",
729]
730
731[[package]]
732name = "hyper-rustls"
733version = "0.27.9"
734source = "registry+https://github.com/rust-lang/crates.io-index"
735checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
736dependencies = [
737 "http",
738 "hyper",
739 "hyper-util",
740 "rustls",
741 "tokio",
742 "tokio-rustls",
743 "tower-service",
744]
745
746[[package]]
747name = "hyper-util"
748version = "0.1.20"
749source = "registry+https://github.com/rust-lang/crates.io-index"
750checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
751dependencies = [
752 "base64",
753 "bytes",
754 "futures-channel",
755 "futures-util",
756 "http",
757 "http-body",
758 "hyper",
759 "ipnet",
760 "libc",
761 "percent-encoding",
762 "pin-project-lite",
763 "socket2",
764 "system-configuration",
765 "tokio",
766 "tower-service",
767 "tracing",
768 "windows-registry",
769]
770
771[[package]]
772name = "iana-time-zone"
773version = "0.1.65"
774source = "registry+https://github.com/rust-lang/crates.io-index"
775checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
776dependencies = [
777 "android_system_properties",
778 "core-foundation-sys",
779 "iana-time-zone-haiku",
780 "js-sys",
781 "log",
782 "wasm-bindgen",
783 "windows-core",
784]
785
786[[package]]
787name = "iana-time-zone-haiku"
788version = "0.1.2"
789source = "registry+https://github.com/rust-lang/crates.io-index"
790checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
791dependencies = [
792 "cc",
793]
794
795[[package]]
796name = "icu_collections"
797version = "2.2.0"
798source = "registry+https://github.com/rust-lang/crates.io-index"
799checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
800dependencies = [
801 "displaydoc",
802 "potential_utf",
803 "utf8_iter",
804 "yoke",
805 "zerofrom",
806 "zerovec",
807]
808
809[[package]]
810name = "icu_locale_core"
811version = "2.2.0"
812source = "registry+https://github.com/rust-lang/crates.io-index"
813checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
814dependencies = [
815 "displaydoc",
816 "litemap",
817 "tinystr",
818 "writeable",
819 "zerovec",
820]
821
822[[package]]
823name = "icu_normalizer"
824version = "2.2.0"
825source = "registry+https://github.com/rust-lang/crates.io-index"
826checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
827dependencies = [
828 "icu_collections",
829 "icu_normalizer_data",
830 "icu_properties",
831 "icu_provider",
832 "smallvec",
833 "zerovec",
834]
835
836[[package]]
837name = "icu_normalizer_data"
838version = "2.2.0"
839source = "registry+https://github.com/rust-lang/crates.io-index"
840checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
841
842[[package]]
843name = "icu_properties"
844version = "2.2.0"
845source = "registry+https://github.com/rust-lang/crates.io-index"
846checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
847dependencies = [
848 "icu_collections",
849 "icu_locale_core",
850 "icu_properties_data",
851 "icu_provider",
852 "zerotrie",
853 "zerovec",
854]
855
856[[package]]
857name = "icu_properties_data"
858version = "2.2.0"
859source = "registry+https://github.com/rust-lang/crates.io-index"
860checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
861
862[[package]]
863name = "icu_provider"
864version = "2.2.0"
865source = "registry+https://github.com/rust-lang/crates.io-index"
866checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
867dependencies = [
868 "displaydoc",
869 "icu_locale_core",
870 "writeable",
871 "yoke",
872 "zerofrom",
873 "zerotrie",
874 "zerovec",
875]
876
877[[package]]
878name = "id-arena"
879version = "2.3.0"
880source = "registry+https://github.com/rust-lang/crates.io-index"
881checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
882
883[[package]]
884name = "idna"
885version = "1.1.0"
886source = "registry+https://github.com/rust-lang/crates.io-index"
887checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
888dependencies = [
889 "idna_adapter",
890 "smallvec",
891 "utf8_iter",
892]
893
894[[package]]
895name = "idna_adapter"
896version = "1.2.2"
897source = "registry+https://github.com/rust-lang/crates.io-index"
898checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
899dependencies = [
900 "icu_normalizer",
901 "icu_properties",
902]
903
904[[package]]
905name = "indexmap"
906version = "2.14.0"
907source = "registry+https://github.com/rust-lang/crates.io-index"
908checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
909dependencies = [
910 "equivalent",
911 "hashbrown 0.17.0",
912 "serde",
913 "serde_core",
914]
915
916[[package]]
917name = "ipnet"
918version = "2.12.0"
919source = "registry+https://github.com/rust-lang/crates.io-index"
920checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
921
922[[package]]
923name = "iri-string"
924version = "0.7.12"
925source = "registry+https://github.com/rust-lang/crates.io-index"
926checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
927dependencies = [
928 "memchr",
929 "serde",
930]
931
932[[package]]
933name = "is_terminal_polyfill"
934version = "1.70.2"
935source = "registry+https://github.com/rust-lang/crates.io-index"
936checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
937
938[[package]]
939name = "itoa"
940version = "1.0.18"
941source = "registry+https://github.com/rust-lang/crates.io-index"
942checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
943
944[[package]]
945name = "jni"
946version = "0.21.1"
947source = "registry+https://github.com/rust-lang/crates.io-index"
948checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
949dependencies = [
950 "cesu8",
951 "cfg-if",
952 "combine",
953 "jni-sys 0.3.1",
954 "log",
955 "thiserror 1.0.69",
956 "walkdir",
957 "windows-sys 0.45.0",
958]
959
960[[package]]
961name = "jni"
962version = "0.22.4"
963source = "registry+https://github.com/rust-lang/crates.io-index"
964checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
965dependencies = [
966 "cfg-if",
967 "combine",
968 "jni-macros",
969 "jni-sys 0.4.1",
970 "log",
971 "simd_cesu8",
972 "thiserror 2.0.18",
973 "walkdir",
974 "windows-link",
975]
976
977[[package]]
978name = "jni-macros"
979version = "0.22.4"
980source = "registry+https://github.com/rust-lang/crates.io-index"
981checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
982dependencies = [
983 "proc-macro2",
984 "quote",
985 "rustc_version",
986 "simd_cesu8",
987 "syn",
988]
989
990[[package]]
991name = "jni-sys"
992version = "0.3.1"
993source = "registry+https://github.com/rust-lang/crates.io-index"
994checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
995dependencies = [
996 "jni-sys 0.4.1",
997]
998
999[[package]]
1000name = "jni-sys"
1001version = "0.4.1"
1002source = "registry+https://github.com/rust-lang/crates.io-index"
1003checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
1004dependencies = [
1005 "jni-sys-macros",
1006]
1007
1008[[package]]
1009name = "jni-sys-macros"
1010version = "0.4.1"
1011source = "registry+https://github.com/rust-lang/crates.io-index"
1012checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
1013dependencies = [
1014 "quote",
1015 "syn",
1016]
1017
1018[[package]]
1019name = "jobserver"
1020version = "0.1.34"
1021source = "registry+https://github.com/rust-lang/crates.io-index"
1022checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
1023dependencies = [
1024 "getrandom 0.3.4",
1025 "libc",
1026]
1027
1028[[package]]
1029name = "js-sys"
1030version = "0.3.97"
1031source = "registry+https://github.com/rust-lang/crates.io-index"
1032checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
1033dependencies = [
1034 "cfg-if",
1035 "futures-util",
1036 "once_cell",
1037 "wasm-bindgen",
1038]
1039
1040[[package]]
1041name = "lazy_static"
1042version = "1.5.0"
1043source = "registry+https://github.com/rust-lang/crates.io-index"
1044checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
1045
1046[[package]]
1047name = "leb128fmt"
1048version = "0.1.0"
1049source = "registry+https://github.com/rust-lang/crates.io-index"
1050checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
1051
1052[[package]]
1053name = "libc"
1054version = "0.2.186"
1055source = "registry+https://github.com/rust-lang/crates.io-index"
1056checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
1057
1058[[package]]
1059name = "libredox"
1060version = "0.1.16"
1061source = "registry+https://github.com/rust-lang/crates.io-index"
1062checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
1063dependencies = [
1064 "libc",
1065]
1066
1067[[package]]
1068name = "libsqlite3-sys"
1069version = "0.37.0"
1070source = "registry+https://github.com/rust-lang/crates.io-index"
1071checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
1072dependencies = [
1073 "cc",
1074 "pkg-config",
1075 "vcpkg",
1076]
1077
1078[[package]]
1079name = "litemap"
1080version = "0.8.2"
1081source = "registry+https://github.com/rust-lang/crates.io-index"
1082checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
1083
1084[[package]]
1085name = "lock_api"
1086version = "0.4.14"
1087source = "registry+https://github.com/rust-lang/crates.io-index"
1088checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
1089dependencies = [
1090 "scopeguard",
1091]
1092
1093[[package]]
1094name = "log"
1095version = "0.4.29"
1096source = "registry+https://github.com/rust-lang/crates.io-index"
1097checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
1098
1099[[package]]
1100name = "lru-slab"
1101version = "0.1.2"
1102source = "registry+https://github.com/rust-lang/crates.io-index"
1103checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
1104
1105[[package]]
1106name = "mach2"
1107version = "0.5.0"
1108source = "registry+https://github.com/rust-lang/crates.io-index"
1109checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
1110dependencies = [
1111 "libc",
1112]
1113
1114[[package]]
1115name = "memchr"
1116version = "2.8.0"
1117source = "registry+https://github.com/rust-lang/crates.io-index"
1118checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
1119
1120[[package]]
1121name = "mime"
1122version = "0.3.17"
1123source = "registry+https://github.com/rust-lang/crates.io-index"
1124checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
1125
1126[[package]]
1127name = "mio"
1128version = "1.2.0"
1129source = "registry+https://github.com/rust-lang/crates.io-index"
1130checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
1131dependencies = [
1132 "libc",
1133 "wasi",
1134 "windows-sys 0.61.2",
1135]
1136
1137[[package]]
1138name = "ndk"
1139version = "0.9.0"
1140source = "registry+https://github.com/rust-lang/crates.io-index"
1141checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
1142dependencies = [
1143 "bitflags 2.11.1",
1144 "jni-sys 0.3.1",
1145 "log",
1146 "ndk-sys",
1147 "num_enum",
1148 "thiserror 1.0.69",
1149]
1150
1151[[package]]
1152name = "ndk-context"
1153version = "0.1.1"
1154source = "registry+https://github.com/rust-lang/crates.io-index"
1155checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
1156
1157[[package]]
1158name = "ndk-sys"
1159version = "0.6.0+11769913"
1160source = "registry+https://github.com/rust-lang/crates.io-index"
1161checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
1162dependencies = [
1163 "jni-sys 0.3.1",
1164]
1165
1166[[package]]
1167name = "nix"
1168version = "0.31.2"
1169source = "registry+https://github.com/rust-lang/crates.io-index"
1170checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
1171dependencies = [
1172 "bitflags 2.11.1",
1173 "cfg-if",
1174 "cfg_aliases",
1175 "libc",
1176]
1177
1178[[package]]
1179name = "num-derive"
1180version = "0.4.2"
1181source = "registry+https://github.com/rust-lang/crates.io-index"
1182checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
1183dependencies = [
1184 "proc-macro2",
1185 "quote",
1186 "syn",
1187]
1188
1189[[package]]
1190name = "num-traits"
1191version = "0.2.19"
1192source = "registry+https://github.com/rust-lang/crates.io-index"
1193checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1194dependencies = [
1195 "autocfg",
1196]
1197
1198[[package]]
1199name = "num_enum"
1200version = "0.7.6"
1201source = "registry+https://github.com/rust-lang/crates.io-index"
1202checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
1203dependencies = [
1204 "num_enum_derive",
1205 "rustversion",
1206]
1207
1208[[package]]
1209name = "num_enum_derive"
1210version = "0.7.6"
1211source = "registry+https://github.com/rust-lang/crates.io-index"
1212checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
1213dependencies = [
1214 "proc-macro-crate",
1215 "proc-macro2",
1216 "quote",
1217 "syn",
1218]
1219
1220[[package]]
1221name = "objc2"
1222version = "0.6.4"
1223source = "registry+https://github.com/rust-lang/crates.io-index"
1224checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
1225dependencies = [
1226 "objc2-encode",
1227]
1228
1229[[package]]
1230name = "objc2-audio-toolbox"
1231version = "0.3.2"
1232source = "registry+https://github.com/rust-lang/crates.io-index"
1233checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
1234dependencies = [
1235 "bitflags 2.11.1",
1236 "libc",
1237 "objc2",
1238 "objc2-core-audio",
1239 "objc2-core-audio-types",
1240 "objc2-core-foundation",
1241 "objc2-foundation",
1242]
1243
1244[[package]]
1245name = "objc2-avf-audio"
1246version = "0.3.2"
1247source = "registry+https://github.com/rust-lang/crates.io-index"
1248checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
1249dependencies = [
1250 "objc2",
1251 "objc2-foundation",
1252]
1253
1254[[package]]
1255name = "objc2-core-audio"
1256version = "0.3.2"
1257source = "registry+https://github.com/rust-lang/crates.io-index"
1258checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
1259dependencies = [
1260 "dispatch2",
1261 "objc2",
1262 "objc2-core-audio-types",
1263 "objc2-core-foundation",
1264 "objc2-foundation",
1265]
1266
1267[[package]]
1268name = "objc2-core-audio-types"
1269version = "0.3.2"
1270source = "registry+https://github.com/rust-lang/crates.io-index"
1271checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
1272dependencies = [
1273 "bitflags 2.11.1",
1274 "objc2",
1275]
1276
1277[[package]]
1278name = "objc2-core-foundation"
1279version = "0.3.2"
1280source = "registry+https://github.com/rust-lang/crates.io-index"
1281checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
1282dependencies = [
1283 "bitflags 2.11.1",
1284 "block2",
1285 "dispatch2",
1286 "libc",
1287 "objc2",
1288]
1289
1290[[package]]
1291name = "objc2-encode"
1292version = "4.1.0"
1293source = "registry+https://github.com/rust-lang/crates.io-index"
1294checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
1295
1296[[package]]
1297name = "objc2-foundation"
1298version = "0.3.2"
1299source = "registry+https://github.com/rust-lang/crates.io-index"
1300checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
1301dependencies = [
1302 "bitflags 2.11.1",
1303 "block2",
1304 "libc",
1305 "objc2",
1306 "objc2-core-foundation",
1307]
1308
1309[[package]]
1310name = "once_cell"
1311version = "1.21.4"
1312source = "registry+https://github.com/rust-lang/crates.io-index"
1313checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
1314
1315[[package]]
1316name = "once_cell_polyfill"
1317version = "1.70.2"
1318source = "registry+https://github.com/rust-lang/crates.io-index"
1319checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
1320
1321[[package]]
1322name = "openssl-probe"
1323version = "0.2.1"
1324source = "registry+https://github.com/rust-lang/crates.io-index"
1325checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
1326
1327[[package]]
1328name = "option-ext"
1329version = "0.2.0"
1330source = "registry+https://github.com/rust-lang/crates.io-index"
1331checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
1332
1333[[package]]
1334name = "parking_lot"
1335version = "0.12.5"
1336source = "registry+https://github.com/rust-lang/crates.io-index"
1337checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
1338dependencies = [
1339 "lock_api",
1340 "parking_lot_core",
1341]
1342
1343[[package]]
1344name = "parking_lot_core"
1345version = "0.9.12"
1346source = "registry+https://github.com/rust-lang/crates.io-index"
1347checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
1348dependencies = [
1349 "cfg-if",
1350 "libc",
1351 "redox_syscall",
1352 "smallvec",
1353 "windows-link",
1354]
1355
1356[[package]]
1357name = "percent-encoding"
1358version = "2.3.2"
1359source = "registry+https://github.com/rust-lang/crates.io-index"
1360checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1361
1362[[package]]
1363name = "pin-project-lite"
1364version = "0.2.17"
1365source = "registry+https://github.com/rust-lang/crates.io-index"
1366checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1367
1368[[package]]
1369name = "pkg-config"
1370version = "0.3.33"
1371source = "registry+https://github.com/rust-lang/crates.io-index"
1372checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
1373
1374[[package]]
1375name = "potential_utf"
1376version = "0.1.5"
1377source = "registry+https://github.com/rust-lang/crates.io-index"
1378checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
1379dependencies = [
1380 "zerovec",
1381]
1382
1383[[package]]
1384name = "ppv-lite86"
1385version = "0.2.21"
1386source = "registry+https://github.com/rust-lang/crates.io-index"
1387checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
1388dependencies = [
1389 "zerocopy",
1390]
1391
1392[[package]]
1393name = "prettyplease"
1394version = "0.2.37"
1395source = "registry+https://github.com/rust-lang/crates.io-index"
1396checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
1397dependencies = [
1398 "proc-macro2",
1399 "syn",
1400]
1401
1402[[package]]
1403name = "proc-macro-crate"
1404version = "3.5.0"
1405source = "registry+https://github.com/rust-lang/crates.io-index"
1406checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
1407dependencies = [
1408 "toml_edit",
1409]
1410
1411[[package]]
1412name = "proc-macro2"
1413version = "1.0.106"
1414source = "registry+https://github.com/rust-lang/crates.io-index"
1415checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
1416dependencies = [
1417 "unicode-ident",
1418]
1419
1420[[package]]
1421name = "quinn"
1422version = "0.11.9"
1423source = "registry+https://github.com/rust-lang/crates.io-index"
1424checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
1425dependencies = [
1426 "bytes",
1427 "cfg_aliases",
1428 "pin-project-lite",
1429 "quinn-proto",
1430 "quinn-udp",
1431 "rustc-hash",
1432 "rustls",
1433 "socket2",
1434 "thiserror 2.0.18",
1435 "tokio",
1436 "tracing",
1437 "web-time",
1438]
1439
1440[[package]]
1441name = "quinn-proto"
1442version = "0.11.14"
1443source = "registry+https://github.com/rust-lang/crates.io-index"
1444checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
1445dependencies = [
1446 "aws-lc-rs",
1447 "bytes",
1448 "getrandom 0.3.4",
1449 "lru-slab",
1450 "rand 0.9.4",
1451 "ring",
1452 "rustc-hash",
1453 "rustls",
1454 "rustls-pki-types",
1455 "slab",
1456 "thiserror 2.0.18",
1457 "tinyvec",
1458 "tracing",
1459 "web-time",
1460]
1461
1462[[package]]
1463name = "quinn-udp"
1464version = "0.5.14"
1465source = "registry+https://github.com/rust-lang/crates.io-index"
1466checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
1467dependencies = [
1468 "cfg_aliases",
1469 "libc",
1470 "once_cell",
1471 "socket2",
1472 "tracing",
1473 "windows-sys 0.52.0",
1474]
1475
1476[[package]]
1477name = "quote"
1478version = "1.0.45"
1479source = "registry+https://github.com/rust-lang/crates.io-index"
1480checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
1481dependencies = [
1482 "proc-macro2",
1483]
1484
1485[[package]]
1486name = "r-efi"
1487version = "5.3.0"
1488source = "registry+https://github.com/rust-lang/crates.io-index"
1489checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1490
1491[[package]]
1492name = "r-efi"
1493version = "6.0.0"
1494source = "registry+https://github.com/rust-lang/crates.io-index"
1495checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1496
1497[[package]]
1498name = "rand"
1499version = "0.9.4"
1500source = "registry+https://github.com/rust-lang/crates.io-index"
1501checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
1502dependencies = [
1503 "rand_chacha",
1504 "rand_core 0.9.5",
1505]
1506
1507[[package]]
1508name = "rand"
1509version = "0.10.1"
1510source = "registry+https://github.com/rust-lang/crates.io-index"
1511checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
1512dependencies = [
1513 "chacha20",
1514 "getrandom 0.4.2",
1515 "rand_core 0.10.1",
1516]
1517
1518[[package]]
1519name = "rand_chacha"
1520version = "0.9.0"
1521source = "registry+https://github.com/rust-lang/crates.io-index"
1522checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1523dependencies = [
1524 "ppv-lite86",
1525 "rand_core 0.9.5",
1526]
1527
1528[[package]]
1529name = "rand_core"
1530version = "0.9.5"
1531source = "registry+https://github.com/rust-lang/crates.io-index"
1532checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
1533dependencies = [
1534 "getrandom 0.3.4",
1535]
1536
1537[[package]]
1538name = "rand_core"
1539version = "0.10.1"
1540source = "registry+https://github.com/rust-lang/crates.io-index"
1541checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
1542
1543[[package]]
1544name = "redox_syscall"
1545version = "0.5.18"
1546source = "registry+https://github.com/rust-lang/crates.io-index"
1547checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
1548dependencies = [
1549 "bitflags 2.11.1",
1550]
1551
1552[[package]]
1553name = "redox_users"
1554version = "0.5.2"
1555source = "registry+https://github.com/rust-lang/crates.io-index"
1556checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
1557dependencies = [
1558 "getrandom 0.2.17",
1559 "libredox",
1560 "thiserror 2.0.18",
1561]
1562
1563[[package]]
1564name = "reqwest"
1565version = "0.13.3"
1566source = "registry+https://github.com/rust-lang/crates.io-index"
1567checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
1568dependencies = [
1569 "base64",
1570 "bytes",
1571 "encoding_rs",
1572 "futures-core",
1573 "h2",
1574 "http",
1575 "http-body",
1576 "http-body-util",
1577 "hyper",
1578 "hyper-rustls",
1579 "hyper-util",
1580 "js-sys",
1581 "log",
1582 "mime",
1583 "percent-encoding",
1584 "pin-project-lite",
1585 "quinn",
1586 "rustls",
1587 "rustls-pki-types",
1588 "rustls-platform-verifier",
1589 "serde",
1590 "serde_json",
1591 "sync_wrapper",
1592 "tokio",
1593 "tokio-rustls",
1594 "tower",
1595 "tower-http",
1596 "tower-service",
1597 "url",
1598 "wasm-bindgen",
1599 "wasm-bindgen-futures",
1600 "web-sys",
1601]
1602
1603[[package]]
1604name = "ring"
1605version = "0.17.14"
1606source = "registry+https://github.com/rust-lang/crates.io-index"
1607checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
1608dependencies = [
1609 "cc",
1610 "cfg-if",
1611 "getrandom 0.2.17",
1612 "libc",
1613 "untrusted",
1614 "windows-sys 0.52.0",
1615]
1616
1617[[package]]
1618name = "rsp"
1619version = "0.1.0"
1620dependencies = [
1621 "chrono",
1622 "clap",
1623 "cpal",
1624 "ctrlc",
1625 "directories",
1626 "dirs",
1627 "libc",
1628 "rand 0.10.1",
1629 "reqwest",
1630 "rusqlite",
1631 "serde",
1632 "serde_json",
1633 "symphonia",
1634 "tokio",
1635 "toml",
1636]
1637
1638[[package]]
1639name = "rsqlite-vfs"
1640version = "0.1.0"
1641source = "registry+https://github.com/rust-lang/crates.io-index"
1642checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
1643dependencies = [
1644 "hashbrown 0.16.1",
1645 "thiserror 2.0.18",
1646]
1647
1648[[package]]
1649name = "rusqlite"
1650version = "0.39.0"
1651source = "registry+https://github.com/rust-lang/crates.io-index"
1652checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e"
1653dependencies = [
1654 "bitflags 2.11.1",
1655 "fallible-iterator",
1656 "fallible-streaming-iterator",
1657 "hashlink",
1658 "libsqlite3-sys",
1659 "smallvec",
1660 "sqlite-wasm-rs",
1661]
1662
1663[[package]]
1664name = "rustc-hash"
1665version = "2.1.2"
1666source = "registry+https://github.com/rust-lang/crates.io-index"
1667checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
1668
1669[[package]]
1670name = "rustc_version"
1671version = "0.4.1"
1672source = "registry+https://github.com/rust-lang/crates.io-index"
1673checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1674dependencies = [
1675 "semver",
1676]
1677
1678[[package]]
1679name = "rustls"
1680version = "0.23.40"
1681source = "registry+https://github.com/rust-lang/crates.io-index"
1682checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
1683dependencies = [
1684 "aws-lc-rs",
1685 "once_cell",
1686 "rustls-pki-types",
1687 "rustls-webpki",
1688 "subtle",
1689 "zeroize",
1690]
1691
1692[[package]]
1693name = "rustls-native-certs"
1694version = "0.8.3"
1695source = "registry+https://github.com/rust-lang/crates.io-index"
1696checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
1697dependencies = [
1698 "openssl-probe",
1699 "rustls-pki-types",
1700 "schannel",
1701 "security-framework",
1702]
1703
1704[[package]]
1705name = "rustls-pki-types"
1706version = "1.14.1"
1707source = "registry+https://github.com/rust-lang/crates.io-index"
1708checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
1709dependencies = [
1710 "web-time",
1711 "zeroize",
1712]
1713
1714[[package]]
1715name = "rustls-platform-verifier"
1716version = "0.7.0"
1717source = "registry+https://github.com/rust-lang/crates.io-index"
1718checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
1719dependencies = [
1720 "core-foundation 0.10.1",
1721 "core-foundation-sys",
1722 "jni 0.22.4",
1723 "log",
1724 "once_cell",
1725 "rustls",
1726 "rustls-native-certs",
1727 "rustls-platform-verifier-android",
1728 "rustls-webpki",
1729 "security-framework",
1730 "security-framework-sys",
1731 "webpki-root-certs",
1732 "windows-sys 0.61.2",
1733]
1734
1735[[package]]
1736name = "rustls-platform-verifier-android"
1737version = "0.1.1"
1738source = "registry+https://github.com/rust-lang/crates.io-index"
1739checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
1740
1741[[package]]
1742name = "rustls-webpki"
1743version = "0.103.13"
1744source = "registry+https://github.com/rust-lang/crates.io-index"
1745checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
1746dependencies = [
1747 "aws-lc-rs",
1748 "ring",
1749 "rustls-pki-types",
1750 "untrusted",
1751]
1752
1753[[package]]
1754name = "rustversion"
1755version = "1.0.22"
1756source = "registry+https://github.com/rust-lang/crates.io-index"
1757checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
1758
1759[[package]]
1760name = "same-file"
1761version = "1.0.6"
1762source = "registry+https://github.com/rust-lang/crates.io-index"
1763checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1764dependencies = [
1765 "winapi-util",
1766]
1767
1768[[package]]
1769name = "schannel"
1770version = "0.1.29"
1771source = "registry+https://github.com/rust-lang/crates.io-index"
1772checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
1773dependencies = [
1774 "windows-sys 0.61.2",
1775]
1776
1777[[package]]
1778name = "scopeguard"
1779version = "1.2.0"
1780source = "registry+https://github.com/rust-lang/crates.io-index"
1781checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1782
1783[[package]]
1784name = "security-framework"
1785version = "3.7.0"
1786source = "registry+https://github.com/rust-lang/crates.io-index"
1787checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
1788dependencies = [
1789 "bitflags 2.11.1",
1790 "core-foundation 0.10.1",
1791 "core-foundation-sys",
1792 "libc",
1793 "security-framework-sys",
1794]
1795
1796[[package]]
1797name = "security-framework-sys"
1798version = "2.17.0"
1799source = "registry+https://github.com/rust-lang/crates.io-index"
1800checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
1801dependencies = [
1802 "core-foundation-sys",
1803 "libc",
1804]
1805
1806[[package]]
1807name = "semver"
1808version = "1.0.28"
1809source = "registry+https://github.com/rust-lang/crates.io-index"
1810checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
1811
1812[[package]]
1813name = "serde"
1814version = "1.0.228"
1815source = "registry+https://github.com/rust-lang/crates.io-index"
1816checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
1817dependencies = [
1818 "serde_core",
1819 "serde_derive",
1820]
1821
1822[[package]]
1823name = "serde_core"
1824version = "1.0.228"
1825source = "registry+https://github.com/rust-lang/crates.io-index"
1826checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
1827dependencies = [
1828 "serde_derive",
1829]
1830
1831[[package]]
1832name = "serde_derive"
1833version = "1.0.228"
1834source = "registry+https://github.com/rust-lang/crates.io-index"
1835checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
1836dependencies = [
1837 "proc-macro2",
1838 "quote",
1839 "syn",
1840]
1841
1842[[package]]
1843name = "serde_json"
1844version = "1.0.149"
1845source = "registry+https://github.com/rust-lang/crates.io-index"
1846checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
1847dependencies = [
1848 "itoa",
1849 "memchr",
1850 "serde",
1851 "serde_core",
1852 "zmij",
1853]
1854
1855[[package]]
1856name = "serde_spanned"
1857version = "1.1.1"
1858source = "registry+https://github.com/rust-lang/crates.io-index"
1859checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
1860dependencies = [
1861 "serde_core",
1862]
1863
1864[[package]]
1865name = "shlex"
1866version = "1.3.0"
1867source = "registry+https://github.com/rust-lang/crates.io-index"
1868checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1869
1870[[package]]
1871name = "signal-hook-registry"
1872version = "1.4.8"
1873source = "registry+https://github.com/rust-lang/crates.io-index"
1874checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
1875dependencies = [
1876 "errno",
1877 "libc",
1878]
1879
1880[[package]]
1881name = "simd_cesu8"
1882version = "1.1.1"
1883source = "registry+https://github.com/rust-lang/crates.io-index"
1884checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
1885dependencies = [
1886 "rustc_version",
1887 "simdutf8",
1888]
1889
1890[[package]]
1891name = "simdutf8"
1892version = "0.1.5"
1893source = "registry+https://github.com/rust-lang/crates.io-index"
1894checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
1895
1896[[package]]
1897name = "slab"
1898version = "0.4.12"
1899source = "registry+https://github.com/rust-lang/crates.io-index"
1900checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1901
1902[[package]]
1903name = "smallvec"
1904version = "1.15.1"
1905source = "registry+https://github.com/rust-lang/crates.io-index"
1906checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
1907
1908[[package]]
1909name = "socket2"
1910version = "0.6.3"
1911source = "registry+https://github.com/rust-lang/crates.io-index"
1912checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
1913dependencies = [
1914 "libc",
1915 "windows-sys 0.61.2",
1916]
1917
1918[[package]]
1919name = "sqlite-wasm-rs"
1920version = "0.5.3"
1921source = "registry+https://github.com/rust-lang/crates.io-index"
1922checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36"
1923dependencies = [
1924 "cc",
1925 "js-sys",
1926 "rsqlite-vfs",
1927 "wasm-bindgen",
1928]
1929
1930[[package]]
1931name = "stable_deref_trait"
1932version = "1.2.1"
1933source = "registry+https://github.com/rust-lang/crates.io-index"
1934checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1935
1936[[package]]
1937name = "strsim"
1938version = "0.11.1"
1939source = "registry+https://github.com/rust-lang/crates.io-index"
1940checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1941
1942[[package]]
1943name = "subtle"
1944version = "2.6.1"
1945source = "registry+https://github.com/rust-lang/crates.io-index"
1946checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1947
1948[[package]]
1949name = "symphonia"
1950version = "0.5.5"
1951source = "registry+https://github.com/rust-lang/crates.io-index"
1952checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
1953dependencies = [
1954 "lazy_static",
1955 "symphonia-bundle-flac",
1956 "symphonia-bundle-mp3",
1957 "symphonia-codec-aac",
1958 "symphonia-codec-adpcm",
1959 "symphonia-codec-alac",
1960 "symphonia-codec-pcm",
1961 "symphonia-codec-vorbis",
1962 "symphonia-core",
1963 "symphonia-format-caf",
1964 "symphonia-format-isomp4",
1965 "symphonia-format-mkv",
1966 "symphonia-format-ogg",
1967 "symphonia-format-riff",
1968 "symphonia-metadata",
1969]
1970
1971[[package]]
1972name = "symphonia-bundle-flac"
1973version = "0.5.5"
1974source = "registry+https://github.com/rust-lang/crates.io-index"
1975checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976"
1976dependencies = [
1977 "log",
1978 "symphonia-core",
1979 "symphonia-metadata",
1980 "symphonia-utils-xiph",
1981]
1982
1983[[package]]
1984name = "symphonia-bundle-mp3"
1985version = "0.5.5"
1986source = "registry+https://github.com/rust-lang/crates.io-index"
1987checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
1988dependencies = [
1989 "lazy_static",
1990 "log",
1991 "symphonia-core",
1992 "symphonia-metadata",
1993]
1994
1995[[package]]
1996name = "symphonia-codec-aac"
1997version = "0.5.5"
1998source = "registry+https://github.com/rust-lang/crates.io-index"
1999checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790"
2000dependencies = [
2001 "lazy_static",
2002 "log",
2003 "symphonia-core",
2004]
2005
2006[[package]]
2007name = "symphonia-codec-adpcm"
2008version = "0.5.5"
2009source = "registry+https://github.com/rust-lang/crates.io-index"
2010checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f"
2011dependencies = [
2012 "log",
2013 "symphonia-core",
2014]
2015
2016[[package]]
2017name = "symphonia-codec-alac"
2018version = "0.5.5"
2019source = "registry+https://github.com/rust-lang/crates.io-index"
2020checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab"
2021dependencies = [
2022 "log",
2023 "symphonia-core",
2024]
2025
2026[[package]]
2027name = "symphonia-codec-pcm"
2028version = "0.5.5"
2029source = "registry+https://github.com/rust-lang/crates.io-index"
2030checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
2031dependencies = [
2032 "log",
2033 "symphonia-core",
2034]
2035
2036[[package]]
2037name = "symphonia-codec-vorbis"
2038version = "0.5.5"
2039source = "registry+https://github.com/rust-lang/crates.io-index"
2040checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73"
2041dependencies = [
2042 "log",
2043 "symphonia-core",
2044 "symphonia-utils-xiph",
2045]
2046
2047[[package]]
2048name = "symphonia-core"
2049version = "0.5.5"
2050source = "registry+https://github.com/rust-lang/crates.io-index"
2051checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
2052dependencies = [
2053 "arrayvec",
2054 "bitflags 1.3.2",
2055 "bytemuck",
2056 "lazy_static",
2057 "log",
2058]
2059
2060[[package]]
2061name = "symphonia-format-caf"
2062version = "0.5.5"
2063source = "registry+https://github.com/rust-lang/crates.io-index"
2064checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096"
2065dependencies = [
2066 "log",
2067 "symphonia-core",
2068 "symphonia-metadata",
2069]
2070
2071[[package]]
2072name = "symphonia-format-isomp4"
2073version = "0.5.5"
2074source = "registry+https://github.com/rust-lang/crates.io-index"
2075checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5"
2076dependencies = [
2077 "encoding_rs",
2078 "log",
2079 "symphonia-core",
2080 "symphonia-metadata",
2081 "symphonia-utils-xiph",
2082]
2083
2084[[package]]
2085name = "symphonia-format-mkv"
2086version = "0.5.5"
2087source = "registry+https://github.com/rust-lang/crates.io-index"
2088checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0"
2089dependencies = [
2090 "lazy_static",
2091 "log",
2092 "symphonia-core",
2093 "symphonia-metadata",
2094 "symphonia-utils-xiph",
2095]
2096
2097[[package]]
2098name = "symphonia-format-ogg"
2099version = "0.5.5"
2100source = "registry+https://github.com/rust-lang/crates.io-index"
2101checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb"
2102dependencies = [
2103 "log",
2104 "symphonia-core",
2105 "symphonia-metadata",
2106 "symphonia-utils-xiph",
2107]
2108
2109[[package]]
2110name = "symphonia-format-riff"
2111version = "0.5.5"
2112source = "registry+https://github.com/rust-lang/crates.io-index"
2113checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
2114dependencies = [
2115 "extended",
2116 "log",
2117 "symphonia-core",
2118 "symphonia-metadata",
2119]
2120
2121[[package]]
2122name = "symphonia-metadata"
2123version = "0.5.5"
2124source = "registry+https://github.com/rust-lang/crates.io-index"
2125checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
2126dependencies = [
2127 "encoding_rs",
2128 "lazy_static",
2129 "log",
2130 "symphonia-core",
2131]
2132
2133[[package]]
2134name = "symphonia-utils-xiph"
2135version = "0.5.5"
2136source = "registry+https://github.com/rust-lang/crates.io-index"
2137checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
2138dependencies = [
2139 "symphonia-core",
2140 "symphonia-metadata",
2141]
2142
2143[[package]]
2144name = "syn"
2145version = "2.0.117"
2146source = "registry+https://github.com/rust-lang/crates.io-index"
2147checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
2148dependencies = [
2149 "proc-macro2",
2150 "quote",
2151 "unicode-ident",
2152]
2153
2154[[package]]
2155name = "sync_wrapper"
2156version = "1.0.2"
2157source = "registry+https://github.com/rust-lang/crates.io-index"
2158checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
2159dependencies = [
2160 "futures-core",
2161]
2162
2163[[package]]
2164name = "synstructure"
2165version = "0.13.2"
2166source = "registry+https://github.com/rust-lang/crates.io-index"
2167checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
2168dependencies = [
2169 "proc-macro2",
2170 "quote",
2171 "syn",
2172]
2173
2174[[package]]
2175name = "system-configuration"
2176version = "0.7.0"
2177source = "registry+https://github.com/rust-lang/crates.io-index"
2178checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
2179dependencies = [
2180 "bitflags 2.11.1",
2181 "core-foundation 0.9.4",
2182 "system-configuration-sys",
2183]
2184
2185[[package]]
2186name = "system-configuration-sys"
2187version = "0.6.0"
2188source = "registry+https://github.com/rust-lang/crates.io-index"
2189checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
2190dependencies = [
2191 "core-foundation-sys",
2192 "libc",
2193]
2194
2195[[package]]
2196name = "thiserror"
2197version = "1.0.69"
2198source = "registry+https://github.com/rust-lang/crates.io-index"
2199checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
2200dependencies = [
2201 "thiserror-impl 1.0.69",
2202]
2203
2204[[package]]
2205name = "thiserror"
2206version = "2.0.18"
2207source = "registry+https://github.com/rust-lang/crates.io-index"
2208checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
2209dependencies = [
2210 "thiserror-impl 2.0.18",
2211]
2212
2213[[package]]
2214name = "thiserror-impl"
2215version = "1.0.69"
2216source = "registry+https://github.com/rust-lang/crates.io-index"
2217checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
2218dependencies = [
2219 "proc-macro2",
2220 "quote",
2221 "syn",
2222]
2223
2224[[package]]
2225name = "thiserror-impl"
2226version = "2.0.18"
2227source = "registry+https://github.com/rust-lang/crates.io-index"
2228checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
2229dependencies = [
2230 "proc-macro2",
2231 "quote",
2232 "syn",
2233]
2234
2235[[package]]
2236name = "tinystr"
2237version = "0.8.3"
2238source = "registry+https://github.com/rust-lang/crates.io-index"
2239checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
2240dependencies = [
2241 "displaydoc",
2242 "zerovec",
2243]
2244
2245[[package]]
2246name = "tinyvec"
2247version = "1.11.0"
2248source = "registry+https://github.com/rust-lang/crates.io-index"
2249checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
2250dependencies = [
2251 "tinyvec_macros",
2252]
2253
2254[[package]]
2255name = "tinyvec_macros"
2256version = "0.1.1"
2257source = "registry+https://github.com/rust-lang/crates.io-index"
2258checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2259
2260[[package]]
2261name = "tokio"
2262version = "1.52.2"
2263source = "registry+https://github.com/rust-lang/crates.io-index"
2264checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
2265dependencies = [
2266 "bytes",
2267 "libc",
2268 "mio",
2269 "parking_lot",
2270 "pin-project-lite",
2271 "signal-hook-registry",
2272 "socket2",
2273 "tokio-macros",
2274 "windows-sys 0.61.2",
2275]
2276
2277[[package]]
2278name = "tokio-macros"
2279version = "2.7.0"
2280source = "registry+https://github.com/rust-lang/crates.io-index"
2281checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
2282dependencies = [
2283 "proc-macro2",
2284 "quote",
2285 "syn",
2286]
2287
2288[[package]]
2289name = "tokio-rustls"
2290version = "0.26.4"
2291source = "registry+https://github.com/rust-lang/crates.io-index"
2292checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
2293dependencies = [
2294 "rustls",
2295 "tokio",
2296]
2297
2298[[package]]
2299name = "tokio-util"
2300version = "0.7.18"
2301source = "registry+https://github.com/rust-lang/crates.io-index"
2302checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
2303dependencies = [
2304 "bytes",
2305 "futures-core",
2306 "futures-sink",
2307 "pin-project-lite",
2308 "tokio",
2309]
2310
2311[[package]]
2312name = "toml"
2313version = "1.1.2+spec-1.1.0"
2314source = "registry+https://github.com/rust-lang/crates.io-index"
2315checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
2316dependencies = [
2317 "indexmap",
2318 "serde_core",
2319 "serde_spanned",
2320 "toml_datetime",
2321 "toml_parser",
2322 "toml_writer",
2323 "winnow",
2324]
2325
2326[[package]]
2327name = "toml_datetime"
2328version = "1.1.1+spec-1.1.0"
2329source = "registry+https://github.com/rust-lang/crates.io-index"
2330checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
2331dependencies = [
2332 "serde_core",
2333]
2334
2335[[package]]
2336name = "toml_edit"
2337version = "0.25.11+spec-1.1.0"
2338source = "registry+https://github.com/rust-lang/crates.io-index"
2339checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
2340dependencies = [
2341 "indexmap",
2342 "toml_datetime",
2343 "toml_parser",
2344 "winnow",
2345]
2346
2347[[package]]
2348name = "toml_parser"
2349version = "1.1.2+spec-1.1.0"
2350source = "registry+https://github.com/rust-lang/crates.io-index"
2351checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
2352dependencies = [
2353 "winnow",
2354]
2355
2356[[package]]
2357name = "toml_writer"
2358version = "1.1.1+spec-1.1.0"
2359source = "registry+https://github.com/rust-lang/crates.io-index"
2360checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
2361
2362[[package]]
2363name = "tower"
2364version = "0.5.3"
2365source = "registry+https://github.com/rust-lang/crates.io-index"
2366checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
2367dependencies = [
2368 "futures-core",
2369 "futures-util",
2370 "pin-project-lite",
2371 "sync_wrapper",
2372 "tokio",
2373 "tower-layer",
2374 "tower-service",
2375]
2376
2377[[package]]
2378name = "tower-http"
2379version = "0.6.8"
2380source = "registry+https://github.com/rust-lang/crates.io-index"
2381checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
2382dependencies = [
2383 "bitflags 2.11.1",
2384 "bytes",
2385 "futures-util",
2386 "http",
2387 "http-body",
2388 "iri-string",
2389 "pin-project-lite",
2390 "tower",
2391 "tower-layer",
2392 "tower-service",
2393]
2394
2395[[package]]
2396name = "tower-layer"
2397version = "0.3.3"
2398source = "registry+https://github.com/rust-lang/crates.io-index"
2399checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
2400
2401[[package]]
2402name = "tower-service"
2403version = "0.3.3"
2404source = "registry+https://github.com/rust-lang/crates.io-index"
2405checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
2406
2407[[package]]
2408name = "tracing"
2409version = "0.1.44"
2410source = "registry+https://github.com/rust-lang/crates.io-index"
2411checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
2412dependencies = [
2413 "pin-project-lite",
2414 "tracing-core",
2415]
2416
2417[[package]]
2418name = "tracing-core"
2419version = "0.1.36"
2420source = "registry+https://github.com/rust-lang/crates.io-index"
2421checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
2422dependencies = [
2423 "once_cell",
2424]
2425
2426[[package]]
2427name = "try-lock"
2428version = "0.2.5"
2429source = "registry+https://github.com/rust-lang/crates.io-index"
2430checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
2431
2432[[package]]
2433name = "unicode-ident"
2434version = "1.0.24"
2435source = "registry+https://github.com/rust-lang/crates.io-index"
2436checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
2437
2438[[package]]
2439name = "unicode-xid"
2440version = "0.2.6"
2441source = "registry+https://github.com/rust-lang/crates.io-index"
2442checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
2443
2444[[package]]
2445name = "untrusted"
2446version = "0.9.0"
2447source = "registry+https://github.com/rust-lang/crates.io-index"
2448checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
2449
2450[[package]]
2451name = "url"
2452version = "2.5.8"
2453source = "registry+https://github.com/rust-lang/crates.io-index"
2454checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
2455dependencies = [
2456 "form_urlencoded",
2457 "idna",
2458 "percent-encoding",
2459 "serde",
2460]
2461
2462[[package]]
2463name = "utf8_iter"
2464version = "1.0.4"
2465source = "registry+https://github.com/rust-lang/crates.io-index"
2466checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
2467
2468[[package]]
2469name = "utf8parse"
2470version = "0.2.2"
2471source = "registry+https://github.com/rust-lang/crates.io-index"
2472checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
2473
2474[[package]]
2475name = "vcpkg"
2476version = "0.2.15"
2477source = "registry+https://github.com/rust-lang/crates.io-index"
2478checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
2479
2480[[package]]
2481name = "walkdir"
2482version = "2.5.0"
2483source = "registry+https://github.com/rust-lang/crates.io-index"
2484checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
2485dependencies = [
2486 "same-file",
2487 "winapi-util",
2488]
2489
2490[[package]]
2491name = "want"
2492version = "0.3.1"
2493source = "registry+https://github.com/rust-lang/crates.io-index"
2494checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
2495dependencies = [
2496 "try-lock",
2497]
2498
2499[[package]]
2500name = "wasi"
2501version = "0.11.1+wasi-snapshot-preview1"
2502source = "registry+https://github.com/rust-lang/crates.io-index"
2503checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2504
2505[[package]]
2506name = "wasip2"
2507version = "1.0.3+wasi-0.2.9"
2508source = "registry+https://github.com/rust-lang/crates.io-index"
2509checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
2510dependencies = [
2511 "wit-bindgen 0.57.1",
2512]
2513
2514[[package]]
2515name = "wasip3"
2516version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
2517source = "registry+https://github.com/rust-lang/crates.io-index"
2518checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
2519dependencies = [
2520 "wit-bindgen 0.51.0",
2521]
2522
2523[[package]]
2524name = "wasm-bindgen"
2525version = "0.2.120"
2526source = "registry+https://github.com/rust-lang/crates.io-index"
2527checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
2528dependencies = [
2529 "cfg-if",
2530 "once_cell",
2531 "rustversion",
2532 "wasm-bindgen-macro",
2533 "wasm-bindgen-shared",
2534]
2535
2536[[package]]
2537name = "wasm-bindgen-futures"
2538version = "0.4.70"
2539source = "registry+https://github.com/rust-lang/crates.io-index"
2540checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084"
2541dependencies = [
2542 "js-sys",
2543 "wasm-bindgen",
2544]
2545
2546[[package]]
2547name = "wasm-bindgen-macro"
2548version = "0.2.120"
2549source = "registry+https://github.com/rust-lang/crates.io-index"
2550checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
2551dependencies = [
2552 "quote",
2553 "wasm-bindgen-macro-support",
2554]
2555
2556[[package]]
2557name = "wasm-bindgen-macro-support"
2558version = "0.2.120"
2559source = "registry+https://github.com/rust-lang/crates.io-index"
2560checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
2561dependencies = [
2562 "bumpalo",
2563 "proc-macro2",
2564 "quote",
2565 "syn",
2566 "wasm-bindgen-shared",
2567]
2568
2569[[package]]
2570name = "wasm-bindgen-shared"
2571version = "0.2.120"
2572source = "registry+https://github.com/rust-lang/crates.io-index"
2573checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
2574dependencies = [
2575 "unicode-ident",
2576]
2577
2578[[package]]
2579name = "wasm-encoder"
2580version = "0.244.0"
2581source = "registry+https://github.com/rust-lang/crates.io-index"
2582checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
2583dependencies = [
2584 "leb128fmt",
2585 "wasmparser",
2586]
2587
2588[[package]]
2589name = "wasm-metadata"
2590version = "0.244.0"
2591source = "registry+https://github.com/rust-lang/crates.io-index"
2592checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
2593dependencies = [
2594 "anyhow",
2595 "indexmap",
2596 "wasm-encoder",
2597 "wasmparser",
2598]
2599
2600[[package]]
2601name = "wasmparser"
2602version = "0.244.0"
2603source = "registry+https://github.com/rust-lang/crates.io-index"
2604checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
2605dependencies = [
2606 "bitflags 2.11.1",
2607 "hashbrown 0.15.5",
2608 "indexmap",
2609 "semver",
2610]
2611
2612[[package]]
2613name = "web-sys"
2614version = "0.3.97"
2615source = "registry+https://github.com/rust-lang/crates.io-index"
2616checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
2617dependencies = [
2618 "js-sys",
2619 "wasm-bindgen",
2620]
2621
2622[[package]]
2623name = "web-time"
2624version = "1.1.0"
2625source = "registry+https://github.com/rust-lang/crates.io-index"
2626checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
2627dependencies = [
2628 "js-sys",
2629 "wasm-bindgen",
2630]
2631
2632[[package]]
2633name = "webpki-root-certs"
2634version = "1.0.7"
2635source = "registry+https://github.com/rust-lang/crates.io-index"
2636checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
2637dependencies = [
2638 "rustls-pki-types",
2639]
2640
2641[[package]]
2642name = "winapi-util"
2643version = "0.1.11"
2644source = "registry+https://github.com/rust-lang/crates.io-index"
2645checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
2646dependencies = [
2647 "windows-sys 0.61.2",
2648]
2649
2650[[package]]
2651name = "windows"
2652version = "0.62.2"
2653source = "registry+https://github.com/rust-lang/crates.io-index"
2654checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
2655dependencies = [
2656 "windows-collections",
2657 "windows-core",
2658 "windows-future",
2659 "windows-numerics",
2660]
2661
2662[[package]]
2663name = "windows-collections"
2664version = "0.3.2"
2665source = "registry+https://github.com/rust-lang/crates.io-index"
2666checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
2667dependencies = [
2668 "windows-core",
2669]
2670
2671[[package]]
2672name = "windows-core"
2673version = "0.62.2"
2674source = "registry+https://github.com/rust-lang/crates.io-index"
2675checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
2676dependencies = [
2677 "windows-implement",
2678 "windows-interface",
2679 "windows-link",
2680 "windows-result",
2681 "windows-strings",
2682]
2683
2684[[package]]
2685name = "windows-future"
2686version = "0.3.2"
2687source = "registry+https://github.com/rust-lang/crates.io-index"
2688checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
2689dependencies = [
2690 "windows-core",
2691 "windows-link",
2692 "windows-threading",
2693]
2694
2695[[package]]
2696name = "windows-implement"
2697version = "0.60.2"
2698source = "registry+https://github.com/rust-lang/crates.io-index"
2699checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2700dependencies = [
2701 "proc-macro2",
2702 "quote",
2703 "syn",
2704]
2705
2706[[package]]
2707name = "windows-interface"
2708version = "0.59.3"
2709source = "registry+https://github.com/rust-lang/crates.io-index"
2710checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2711dependencies = [
2712 "proc-macro2",
2713 "quote",
2714 "syn",
2715]
2716
2717[[package]]
2718name = "windows-link"
2719version = "0.2.1"
2720source = "registry+https://github.com/rust-lang/crates.io-index"
2721checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2722
2723[[package]]
2724name = "windows-numerics"
2725version = "0.3.1"
2726source = "registry+https://github.com/rust-lang/crates.io-index"
2727checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
2728dependencies = [
2729 "windows-core",
2730 "windows-link",
2731]
2732
2733[[package]]
2734name = "windows-registry"
2735version = "0.6.1"
2736source = "registry+https://github.com/rust-lang/crates.io-index"
2737checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
2738dependencies = [
2739 "windows-link",
2740 "windows-result",
2741 "windows-strings",
2742]
2743
2744[[package]]
2745name = "windows-result"
2746version = "0.4.1"
2747source = "registry+https://github.com/rust-lang/crates.io-index"
2748checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
2749dependencies = [
2750 "windows-link",
2751]
2752
2753[[package]]
2754name = "windows-strings"
2755version = "0.5.1"
2756source = "registry+https://github.com/rust-lang/crates.io-index"
2757checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
2758dependencies = [
2759 "windows-link",
2760]
2761
2762[[package]]
2763name = "windows-sys"
2764version = "0.45.0"
2765source = "registry+https://github.com/rust-lang/crates.io-index"
2766checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
2767dependencies = [
2768 "windows-targets 0.42.2",
2769]
2770
2771[[package]]
2772name = "windows-sys"
2773version = "0.52.0"
2774source = "registry+https://github.com/rust-lang/crates.io-index"
2775checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
2776dependencies = [
2777 "windows-targets 0.52.6",
2778]
2779
2780[[package]]
2781name = "windows-sys"
2782version = "0.61.2"
2783source = "registry+https://github.com/rust-lang/crates.io-index"
2784checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
2785dependencies = [
2786 "windows-link",
2787]
2788
2789[[package]]
2790name = "windows-targets"
2791version = "0.42.2"
2792source = "registry+https://github.com/rust-lang/crates.io-index"
2793checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
2794dependencies = [
2795 "windows_aarch64_gnullvm 0.42.2",
2796 "windows_aarch64_msvc 0.42.2",
2797 "windows_i686_gnu 0.42.2",
2798 "windows_i686_msvc 0.42.2",
2799 "windows_x86_64_gnu 0.42.2",
2800 "windows_x86_64_gnullvm 0.42.2",
2801 "windows_x86_64_msvc 0.42.2",
2802]
2803
2804[[package]]
2805name = "windows-targets"
2806version = "0.52.6"
2807source = "registry+https://github.com/rust-lang/crates.io-index"
2808checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
2809dependencies = [
2810 "windows_aarch64_gnullvm 0.52.6",
2811 "windows_aarch64_msvc 0.52.6",
2812 "windows_i686_gnu 0.52.6",
2813 "windows_i686_gnullvm",
2814 "windows_i686_msvc 0.52.6",
2815 "windows_x86_64_gnu 0.52.6",
2816 "windows_x86_64_gnullvm 0.52.6",
2817 "windows_x86_64_msvc 0.52.6",
2818]
2819
2820[[package]]
2821name = "windows-threading"
2822version = "0.2.1"
2823source = "registry+https://github.com/rust-lang/crates.io-index"
2824checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
2825dependencies = [
2826 "windows-link",
2827]
2828
2829[[package]]
2830name = "windows_aarch64_gnullvm"
2831version = "0.42.2"
2832source = "registry+https://github.com/rust-lang/crates.io-index"
2833checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
2834
2835[[package]]
2836name = "windows_aarch64_gnullvm"
2837version = "0.52.6"
2838source = "registry+https://github.com/rust-lang/crates.io-index"
2839checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
2840
2841[[package]]
2842name = "windows_aarch64_msvc"
2843version = "0.42.2"
2844source = "registry+https://github.com/rust-lang/crates.io-index"
2845checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
2846
2847[[package]]
2848name = "windows_aarch64_msvc"
2849version = "0.52.6"
2850source = "registry+https://github.com/rust-lang/crates.io-index"
2851checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
2852
2853[[package]]
2854name = "windows_i686_gnu"
2855version = "0.42.2"
2856source = "registry+https://github.com/rust-lang/crates.io-index"
2857checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
2858
2859[[package]]
2860name = "windows_i686_gnu"
2861version = "0.52.6"
2862source = "registry+https://github.com/rust-lang/crates.io-index"
2863checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
2864
2865[[package]]
2866name = "windows_i686_gnullvm"
2867version = "0.52.6"
2868source = "registry+https://github.com/rust-lang/crates.io-index"
2869checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
2870
2871[[package]]
2872name = "windows_i686_msvc"
2873version = "0.42.2"
2874source = "registry+https://github.com/rust-lang/crates.io-index"
2875checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
2876
2877[[package]]
2878name = "windows_i686_msvc"
2879version = "0.52.6"
2880source = "registry+https://github.com/rust-lang/crates.io-index"
2881checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
2882
2883[[package]]
2884name = "windows_x86_64_gnu"
2885version = "0.42.2"
2886source = "registry+https://github.com/rust-lang/crates.io-index"
2887checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
2888
2889[[package]]
2890name = "windows_x86_64_gnu"
2891version = "0.52.6"
2892source = "registry+https://github.com/rust-lang/crates.io-index"
2893checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
2894
2895[[package]]
2896name = "windows_x86_64_gnullvm"
2897version = "0.42.2"
2898source = "registry+https://github.com/rust-lang/crates.io-index"
2899checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
2900
2901[[package]]
2902name = "windows_x86_64_gnullvm"
2903version = "0.52.6"
2904source = "registry+https://github.com/rust-lang/crates.io-index"
2905checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
2906
2907[[package]]
2908name = "windows_x86_64_msvc"
2909version = "0.42.2"
2910source = "registry+https://github.com/rust-lang/crates.io-index"
2911checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
2912
2913[[package]]
2914name = "windows_x86_64_msvc"
2915version = "0.52.6"
2916source = "registry+https://github.com/rust-lang/crates.io-index"
2917checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
2918
2919[[package]]
2920name = "winnow"
2921version = "1.0.2"
2922source = "registry+https://github.com/rust-lang/crates.io-index"
2923checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
2924dependencies = [
2925 "memchr",
2926]
2927
2928[[package]]
2929name = "wit-bindgen"
2930version = "0.51.0"
2931source = "registry+https://github.com/rust-lang/crates.io-index"
2932checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
2933dependencies = [
2934 "wit-bindgen-rust-macro",
2935]
2936
2937[[package]]
2938name = "wit-bindgen"
2939version = "0.57.1"
2940source = "registry+https://github.com/rust-lang/crates.io-index"
2941checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
2942
2943[[package]]
2944name = "wit-bindgen-core"
2945version = "0.51.0"
2946source = "registry+https://github.com/rust-lang/crates.io-index"
2947checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
2948dependencies = [
2949 "anyhow",
2950 "heck",
2951 "wit-parser",
2952]
2953
2954[[package]]
2955name = "wit-bindgen-rust"
2956version = "0.51.0"
2957source = "registry+https://github.com/rust-lang/crates.io-index"
2958checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
2959dependencies = [
2960 "anyhow",
2961 "heck",
2962 "indexmap",
2963 "prettyplease",
2964 "syn",
2965 "wasm-metadata",
2966 "wit-bindgen-core",
2967 "wit-component",
2968]
2969
2970[[package]]
2971name = "wit-bindgen-rust-macro"
2972version = "0.51.0"
2973source = "registry+https://github.com/rust-lang/crates.io-index"
2974checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
2975dependencies = [
2976 "anyhow",
2977 "prettyplease",
2978 "proc-macro2",
2979 "quote",
2980 "syn",
2981 "wit-bindgen-core",
2982 "wit-bindgen-rust",
2983]
2984
2985[[package]]
2986name = "wit-component"
2987version = "0.244.0"
2988source = "registry+https://github.com/rust-lang/crates.io-index"
2989checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
2990dependencies = [
2991 "anyhow",
2992 "bitflags 2.11.1",
2993 "indexmap",
2994 "log",
2995 "serde",
2996 "serde_derive",
2997 "serde_json",
2998 "wasm-encoder",
2999 "wasm-metadata",
3000 "wasmparser",
3001 "wit-parser",
3002]
3003
3004[[package]]
3005name = "wit-parser"
3006version = "0.244.0"
3007source = "registry+https://github.com/rust-lang/crates.io-index"
3008checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
3009dependencies = [
3010 "anyhow",
3011 "id-arena",
3012 "indexmap",
3013 "log",
3014 "semver",
3015 "serde",
3016 "serde_derive",
3017 "serde_json",
3018 "unicode-xid",
3019 "wasmparser",
3020]
3021
3022[[package]]
3023name = "writeable"
3024version = "0.6.3"
3025source = "registry+https://github.com/rust-lang/crates.io-index"
3026checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
3027
3028[[package]]
3029name = "yoke"
3030version = "0.8.2"
3031source = "registry+https://github.com/rust-lang/crates.io-index"
3032checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
3033dependencies = [
3034 "stable_deref_trait",
3035 "yoke-derive",
3036 "zerofrom",
3037]
3038
3039[[package]]
3040name = "yoke-derive"
3041version = "0.8.2"
3042source = "registry+https://github.com/rust-lang/crates.io-index"
3043checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
3044dependencies = [
3045 "proc-macro2",
3046 "quote",
3047 "syn",
3048 "synstructure",
3049]
3050
3051[[package]]
3052name = "zerocopy"
3053version = "0.8.48"
3054source = "registry+https://github.com/rust-lang/crates.io-index"
3055checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
3056dependencies = [
3057 "zerocopy-derive",
3058]
3059
3060[[package]]
3061name = "zerocopy-derive"
3062version = "0.8.48"
3063source = "registry+https://github.com/rust-lang/crates.io-index"
3064checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
3065dependencies = [
3066 "proc-macro2",
3067 "quote",
3068 "syn",
3069]
3070
3071[[package]]
3072name = "zerofrom"
3073version = "0.1.7"
3074source = "registry+https://github.com/rust-lang/crates.io-index"
3075checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
3076dependencies = [
3077 "zerofrom-derive",
3078]
3079
3080[[package]]
3081name = "zerofrom-derive"
3082version = "0.1.7"
3083source = "registry+https://github.com/rust-lang/crates.io-index"
3084checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
3085dependencies = [
3086 "proc-macro2",
3087 "quote",
3088 "syn",
3089 "synstructure",
3090]
3091
3092[[package]]
3093name = "zeroize"
3094version = "1.8.2"
3095source = "registry+https://github.com/rust-lang/crates.io-index"
3096checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
3097
3098[[package]]
3099name = "zerotrie"
3100version = "0.2.4"
3101source = "registry+https://github.com/rust-lang/crates.io-index"
3102checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
3103dependencies = [
3104 "displaydoc",
3105 "yoke",
3106 "zerofrom",
3107]
3108
3109[[package]]
3110name = "zerovec"
3111version = "0.11.6"
3112source = "registry+https://github.com/rust-lang/crates.io-index"
3113checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
3114dependencies = [
3115 "yoke",
3116 "zerofrom",
3117 "zerovec-derive",
3118]
3119
3120[[package]]
3121name = "zerovec-derive"
3122version = "0.11.3"
3123source = "registry+https://github.com/rust-lang/crates.io-index"
3124checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
3125dependencies = [
3126 "proc-macro2",
3127 "quote",
3128 "syn",
3129]
3130
3131[[package]]
3132name = "zmij"
3133version = "1.0.21"
3134source = "registry+https://github.com/rust-lang/crates.io-index"
3135checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..0ceee6f
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,21 @@
1[package]
2name = "rsp"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7clap = { version = "4.6.1", features = ["derive", "string"] }
8rusqlite = { version = "0.39", features = ["bundled"] }
9directories = "6.0"
10serde = { version = "1.0.228", features = ["derive"] }
11toml = "1.1.2"
12tokio = { version = "1.52.2", features = ["full"] }
13reqwest = { version = "0.13.3", features = ["json"] }
14symphonia = { version = "0.5.5", features = ["all"] }
15cpal = "0.17.3"
16chrono = "0.4.44"
17serde_json = "1.0.149"
18dirs = "6.0"
19libc = "0.2.186"
20ctrlc = "3.5.2"
21rand = "0.10.1"
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..98bd897
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,24 @@
1# rsplayer Project Plan
2
3## Phase 1: Setup & Core
41. Initialize Rust project, add dependencies: `clap` (CLI/autocompletion), `rusqlite` (SQLite), `symphonia`/`cpal` (audio), `directories` (XDG paths), `serde`/`toml` (config), `tokio` (daemon async), `reqwest` (scrobbling).
52. Config stored at `$XDG_CONFIG_HOME/rsplayer/config.toml`: `music_paths` (array of directories to scan), `scan_interval` (optional periodic rescan interval), scrobbling credentials, daemon socket path. Daemon auto-scans configured paths on first run if database is empty/missing.
63. SQLite library schema: `artists`, `albums`, `tracks` tables. Implement `rsp scan` to scan all paths in config's `music_paths`, populate/update database. Support `--force` full rescan, `--paths` override config paths. Auto-scan triggers on first run (if DB empty) and periodically per `scan_interval` config.
7
8## Phase 2: Daemon
91. Unix socket daemon (path: `$XDG_RUNTIME_DIR/rsplayer/daemon.sock`) for IPC.
102. Audio engine: Symphonia decode + CPAL output, support play/pause/stop/volume/seek.
113. Queue: in-memory, `last` (append to end, default), `now` (play immediately, ending current song playback), `next` (insert after current).
124. Shuffle modes: `none` (linear playback), `all` (shuffle all tracks), `album` (shuffle within album).
135. Scrobbling: Last.fm and ListenBrainz support via configured credentials.
14
15## Phase 3: CLI & Completion
161. Clap-based CLI with all subcommands. Generate shell completions for bash/zsh/fish, suggest database artists/albums/tracks, file paths, and config keys/values.
172. `rsp status`: show player state, current track, progress, volume, next 2-3 queue items.
183. `rsp queue`: no arguments display current queue; with arguments add items (default `last` behavior).
194. `rsp settings` subcommands: `get`, `set`, `list`, `unset` config values (music paths, scrobbling credentials, scan interval, etc.) without manual file editing.
20
21## Phase 4: Polish
221. Direct file/folder playback: prioritize database metadata if path matches known artist/album.
232. Logging, systemd service unit, unit tests.
243. Periodic rescan logic per config's `scan_interval`.
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..376841e
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,51 @@
1# TODO
2
3## Completed ✓
4
5- [x] **Fix SampleRate access in audio.rs:104** - `config.sample_rate().0` is invalid because `cpal::SampleRate` is a type alias for `u32`, not a tuple struct.
6
7- [x] **Implement previous track logic** (`src/playback.rs:99`) - Implemented basic previous() that restarts current track if playing/paused.
8
9- [x] **Remove unused std::thread import** (`src/playback.rs:2`) - Removed commented out import.
10
11- [x] **Run clippy** - All linting checks pass.
12
13- [x] **Run tests** - All tests pass (no tests exist yet).
14
15- [x] **Check formatting** - Code formatting is correct.
16
17- [x] **Implement resume functionality** - Implemented pause/resume using cpal stream pause()/play() methods.
18
19- [x] **Fix audio playback speed and buffer underrun** - Implemented proper sample buffering between callbacks to prevent audio skipping and buffer underrun errors.
20
21- [x] **Add Resume CLI subcommand** - Added `Resume` subcommand to CLI so users can resume playback after pause.
22
23- [x] **Implement shuffle mode** - Added shuffle on/off/album functionality with queue management in daemon. Supports `rsplayer shuffle on/off/album` commands.
24
25- [x] **Implement next/previous commands** - Next and previous track commands now work with the queue system.
26
27- [x] **Queue entire directory** - When playing a folder, all audio files are now queued instead of just the first one.
28
29- [x] **Auto-play next track** - Daemon automatically plays next track when current one ends (using `poll_track_ended()`).
30
31- [x] **Queue display with pager** - `rsplayer queue` shows all queued tracks with pager support for long lists.
32
33- [x] **Album shuffle mode structure** - `ShuffleMode::Album` added to enum and queue logic (behaves like `All` until database ready).
34
35## Potential Improvements
36
37- [ ] **Add error handling for audio playback** - Consider more robust error handling in the audio thread.
38
39- [ ] **Add unit tests for Player struct** - The `Player` struct in `playback.rs` has no corresponding tests.
40
41- [ ] **Persist queue state** - Save queue to disk so it survives daemon restarts.
42
43- [ ] **Album shuffle mode (database)** - Implement proper `ShuffleMode::Album` when database is ready (shuffle tracks within same album/folder).
44
45- [ ] **Scrobbling (Last.fm/ListenBrainz)** - Add scrobbling support via configured credentials.
46
47- [ ] **Database integration** - Scan music paths, populate SQLite database, use for metadata.
48
49- [ ] **Shell completion scripts** - Generate bash/zsh/fish completions using clap.
50
51- [ ] **Systemd service unit** - Create systemd service file for auto-start.
diff --git a/src/audio.rs b/src/audio.rs
new file mode 100644
index 0000000..86c88f7
--- /dev/null
+++ b/src/audio.rs
@@ -0,0 +1,418 @@
1use std::path::PathBuf;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::mpsc::{self, Receiver, Sender};
4use std::sync::{Arc, Mutex};
5use std::thread;
6
7#[allow(dead_code)]
8pub enum AudioCommand {
9 Play(PathBuf),
10 Pause,
11 Resume,
12 Stop,
13 SetVolume(f32),
14}
15
16pub struct AudioEngine {
17 command_tx: Sender<AudioCommand>,
18 is_playing: Arc<AtomicBool>,
19 current_track: Arc<Mutex<Option<std::path::PathBuf>>>,
20 track_ended: Arc<AtomicBool>,
21}
22
23impl AudioEngine {
24 pub fn new() -> Self {
25 let (tx, rx) = mpsc::channel();
26 let is_playing = Arc::new(AtomicBool::new(false));
27 let is_playing_clone = Arc::clone(&is_playing);
28 let current_track = Arc::new(Mutex::new(None));
29 let current_track_clone = Arc::clone(&current_track);
30 let track_ended = Arc::new(AtomicBool::new(false));
31 let track_ended_clone = Arc::clone(&track_ended);
32
33 let volume = Arc::new(Mutex::new(1.0f32));
34 let volume_clone = Arc::clone(&volume);
35
36 thread::spawn(move || {
37 audio_thread(
38 rx,
39 is_playing_clone,
40 current_track_clone,
41 volume_clone,
42 track_ended_clone,
43 );
44 });
45
46 AudioEngine {
47 command_tx: tx,
48 is_playing,
49 current_track,
50 track_ended,
51 }
52 }
53
54 pub fn play(&self, path: PathBuf) {
55 self.track_ended.store(false, Ordering::SeqCst);
56 if let Ok(mut track) = self.current_track.lock() {
57 *track = Some(path.clone());
58 }
59 let _ = self.command_tx.send(AudioCommand::Play(path));
60 }
61
62 pub fn pause(&self) {
63 let _ = self.command_tx.send(AudioCommand::Pause);
64 }
65
66 pub fn resume(&self) {
67 let _ = self.command_tx.send(AudioCommand::Resume);
68 }
69
70 pub fn stop(&self) {
71 let _ = self.command_tx.send(AudioCommand::Stop);
72 }
73
74 pub fn set_volume(&self, volume: f32) {
75 let _ = self.command_tx.send(AudioCommand::SetVolume(volume));
76 }
77
78 pub fn is_playing(&self) -> bool {
79 self.is_playing.load(Ordering::SeqCst)
80 }
81
82 pub fn current_track(&self) -> Option<std::path::PathBuf> {
83 self.current_track.lock().ok().and_then(|g| g.clone())
84 }
85
86 pub fn poll_track_ended(&self) -> bool {
87 self.track_ended.swap(false, Ordering::SeqCst)
88 }
89}
90
91struct AudioData {
92 format: Box<dyn symphonia::core::formats::FormatReader>,
93 decoder: Box<dyn symphonia::core::codecs::Decoder>,
94 sample_rate: u32,
95 channels: u16,
96 sample_buffer: Vec<f32>,
97 buffer_pos: usize,
98 resample_ratio: Option<f64>,
99}
100
101fn resample_linear(input: &[f32], ratio: f64, channels: usize) -> Vec<f32> {
102 if ratio == 1.0 {
103 return input.to_vec();
104 }
105 let input_frames = input.len() / channels;
106 let output_frames = (input_frames as f64 * ratio).ceil() as usize;
107 let mut output = Vec::with_capacity(output_frames * channels);
108 for i in 0..output_frames {
109 let input_pos = i as f64 / ratio;
110 let idx = input_pos.floor() as usize;
111 let frac = input_pos.fract() as f32;
112 for ch in 0..channels {
113 let cur = if idx * channels + ch < input.len() {
114 input[idx * channels + ch]
115 } else {
116 0.0
117 };
118 let next = if (idx + 1) * channels + ch < input.len() {
119 input[(idx + 1) * channels + ch]
120 } else {
121 cur
122 };
123 output.push(cur + frac * (next - cur));
124 }
125 }
126 output
127}
128
129fn audio_thread(
130 rx: Receiver<AudioCommand>,
131 is_playing: Arc<AtomicBool>,
132 current_track: Arc<Mutex<Option<std::path::PathBuf>>>,
133 volume: Arc<Mutex<f32>>,
134 track_ended: Arc<AtomicBool>,
135) {
136 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
137
138 let host = cpal::default_host();
139 let device = match host.default_output_device() {
140 Some(d) => d,
141 None => {
142 eprintln!("No output device available");
143 return;
144 }
145 };
146
147 let config = match device.default_output_config() {
148 Ok(c) => c,
149 Err(e) => {
150 eprintln!("Failed to get output config: {}", e);
151 return;
152 }
153 };
154
155 let device_sample_rate = config.sample_rate();
156 let channels = config.channels();
157 eprintln!(
158 "Default output: {} Hz, {} channels",
159 device_sample_rate, channels
160 );
161
162 let mut current_stream: Option<cpal::Stream> = None;
163 let audio_data: Arc<Mutex<Option<AudioData>>> = Arc::new(Mutex::new(None));
164
165 while let Ok(cmd) = rx.recv() {
166 match cmd {
167 AudioCommand::Play(path) => {
168 is_playing.store(true, Ordering::SeqCst);
169 if let Ok(mut track) = current_track.lock() {
170 *track = Some(path.clone());
171 }
172
173 // Drop the current stream before creating a new one
174 current_stream = None;
175
176 match open_audio_file(&path) {
177 Ok(mut data) => {
178 eprintln!("File: {} Hz, {} channels", data.sample_rate, data.channels);
179
180 if data.sample_rate != device_sample_rate {
181 eprintln!(
182 "Resampling from {} Hz to {} Hz",
183 data.sample_rate, device_sample_rate
184 );
185 data.resample_ratio =
186 Some(device_sample_rate as f64 / data.sample_rate as f64);
187 }
188
189 let stream_config: cpal::StreamConfig =
190 device.default_output_config().unwrap().into();
191
192 let audio_data_clone = Arc::clone(&audio_data);
193 let vol = Arc::clone(&volume);
194 let ended = Arc::clone(&track_ended);
195 let is_playing_clone = Arc::clone(&is_playing);
196
197 if let Ok(mut d) = audio_data.lock() {
198 *d = Some(data);
199 }
200
201 match device.build_output_stream(
202 &stream_config,
203 move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
204 let vol = vol.lock().map(|v| *v).unwrap_or(1.0);
205
206 if let Ok(mut audio_data) = audio_data_clone.lock() {
207 if let Some(ref mut state) = *audio_data {
208 let channels = state.channels as usize;
209 let mut offset = 0usize;
210
211 // Keep decoding until we fill the buffer or run out of data
212 while offset < data.len() {
213 // First, copy any buffered samples
214 if state.buffer_pos < state.sample_buffer.len() {
215 let remaining =
216 &state.sample_buffer[state.buffer_pos..];
217 let to_copy =
218 remaining.len().min(data.len() - offset);
219 data[offset..offset + to_copy]
220 .copy_from_slice(&remaining[..to_copy]);
221 state.buffer_pos += to_copy;
222 offset += to_copy;
223
224 // If we've consumed all buffered samples, clear the buffer
225 if state.buffer_pos >= state.sample_buffer.len() {
226 state.sample_buffer.clear();
227 state.buffer_pos = 0;
228 }
229 continue; // Try to fill more of the buffer
230 }
231
232 // Decode more data
233 match state.format.next_packet() {
234 Ok(packet) => {
235 match state.decoder.decode(&packet) {
236 Ok(decoded) => {
237 let spec = *decoded.spec();
238 let mut sample_buf =
239 symphonia::core::audio::SampleBuffer::<
240 f32,
241 >::new(
242 decoded.capacity() as u64, spec
243 );
244 sample_buf
245 .copy_interleaved_ref(decoded);
246 let samples = sample_buf.samples();
247
248 // Resample if needed
249 let resampled = if let Some(ratio) =
250 state.resample_ratio
251 {
252 resample_linear(
253 samples, ratio, channels,
254 )
255 } else {
256 samples.to_vec()
257 };
258
259 // Copy what we can to output
260 let to_copy = resampled
261 .len()
262 .min(data.len() - offset);
263 data[offset..offset + to_copy]
264 .copy_from_slice(
265 &resampled[..to_copy],
266 );
267 offset += to_copy;
268
269 // Buffer the rest
270 if to_copy < resampled.len() {
271 state.sample_buffer.clear();
272 state
273 .sample_buffer
274 .extend_from_slice(
275 &resampled[to_copy..],
276 );
277 state.buffer_pos = 0;
278 }
279 }
280 Err(_) => break,
281 }
282 }
283 Err(_) => break,
284 }
285 }
286
287 // Apply volume
288 for sample in data.iter_mut().take(offset) {
289 *sample *= vol;
290 }
291
292 // Fill remaining with silence
293 for sample in data.iter_mut().skip(offset) {
294 *sample = 0.0;
295 }
296
297 // If no more data and offset is 0, track ended
298 if offset == 0 && state.sample_buffer.is_empty() {
299 ended.store(true, Ordering::SeqCst);
300 is_playing_clone.store(false, Ordering::SeqCst);
301 // Clear audio data so we don't keep triggering ended
302 if let Ok(mut audio_data) = audio_data_clone.lock() {
303 *audio_data = None;
304 }
305 }
306 } else {
307 for sample in data.iter_mut() {
308 *sample = 0.0;
309 }
310 }
311 }
312 },
313 |err| {
314 eprintln!("Stream error: {}", err);
315 },
316 None,
317 ) {
318 Ok(stream) => {
319 if let Err(e) = stream.play() {
320 eprintln!("Failed to start stream: {}", e);
321 is_playing.store(false, Ordering::SeqCst);
322 track_ended.store(true, Ordering::SeqCst);
323 } else {
324 current_stream = Some(stream);
325 }
326 }
327 Err(e) => {
328 eprintln!("Failed to create stream: {}", e);
329 is_playing.store(false, Ordering::SeqCst);
330 track_ended.store(true, Ordering::SeqCst);
331 }
332 }
333 }
334 Err(e) => {
335 eprintln!("Failed to open audio: {}", e);
336 is_playing.store(false, Ordering::SeqCst);
337 track_ended.store(true, Ordering::SeqCst);
338 }
339 }
340 }
341 AudioCommand::Pause => {
342 if let Some(ref stream) = current_stream {
343 if let Err(e) = stream.pause() {
344 eprintln!("Failed to pause stream: {}", e);
345 }
346 is_playing.store(false, Ordering::SeqCst);
347 }
348 }
349 AudioCommand::Resume => {
350 if let Some(ref stream) = current_stream {
351 if let Err(e) = stream.play() {
352 eprintln!("Failed to resume stream: {}", e);
353 } else {
354 is_playing.store(true, Ordering::SeqCst);
355 }
356 }
357 }
358 AudioCommand::Stop => {
359 current_stream = None;
360 if let Ok(mut d) = audio_data.lock() {
361 *d = None;
362 }
363 is_playing.store(false, Ordering::SeqCst);
364 if let Ok(mut track) = current_track.lock() {
365 *track = None;
366 }
367 }
368 AudioCommand::SetVolume(vol) => {
369 if let Ok(mut volume_ref) = volume.lock() {
370 *volume_ref = vol;
371 }
372 }
373 }
374 }
375}
376
377fn open_audio_file(path: &PathBuf) -> Result<AudioData, Box<dyn std::error::Error>> {
378 use symphonia::core::audio::Channels;
379 use symphonia::core::io::MediaSourceStream;
380
381 let file = std::fs::File::open(path)?;
382 let mss = MediaSourceStream::new(Box::new(file), Default::default());
383
384 let hint = symphonia::core::probe::Hint::new();
385 let probed = symphonia::default::get_probe().format(
386 &hint,
387 mss,
388 &Default::default(),
389 &Default::default(),
390 )?;
391
392 let format = probed.format;
393 let track = format
394 .tracks()
395 .iter()
396 .find(|t| t.codec_params.sample_rate.is_some())
397 .ok_or("No suitable track found")?;
398
399 let sample_rate = track.codec_params.sample_rate.unwrap_or(44100);
400 let channels = track
401 .codec_params
402 .channels
403 .unwrap_or(Channels::FRONT_LEFT | Channels::FRONT_RIGHT)
404 .count() as u16;
405
406 let decoder =
407 symphonia::default::get_codecs().make(&track.codec_params, &Default::default())?;
408
409 Ok(AudioData {
410 format,
411 decoder,
412 sample_rate,
413 channels,
414 sample_buffer: Vec::new(),
415 buffer_pos: 0,
416 resample_ratio: None,
417 })
418}
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..13ef67c
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,186 @@
1use crate::config::Config;
2use crate::database::Database;
3use clap::{Parser, Subcommand};
4use std::path::PathBuf;
5
6#[derive(Parser)]
7#[command(name = "rsp")]
8#[command(about = "Rust music player daemon", long_about = None)]
9pub struct Cli {
10 #[command(subcommand)]
11 pub command: Commands,
12}
13
14#[derive(Subcommand)]
15pub enum Commands {
16 /// Start the daemon
17 Start,
18 /// Stop the daemon
19 Stop,
20 /// Restart the daemon (kill existing and start new)
21 Restart,
22 /// Play a track, album, or artist
23 Play {
24 #[arg(value_name = "QUERY")]
25 query: Option<String>,
26 },
27 /// Pause playback
28 Pause,
29 /// Resume playback
30 Resume,
31 /// Stop playback
32 StopPlayback,
33 /// Skip to next track
34 Next,
35 /// Skip to previous track
36 Previous,
37 /// Set volume (0-100)
38 Volume {
39 #[arg(value_name = "LEVEL")]
40 level: Option<u8>,
41 },
42 /// Shuffle mode
43 Shuffle {
44 #[arg(value_name = "MODE")]
45 mode: Option<String>,
46 },
47 /// Queue management
48 Queue {
49 #[arg(value_name = "POSITION")]
50 position: Option<String>,
51 #[arg(value_name = "QUERY")]
52 query: Option<String>,
53 },
54 /// Show player status
55 Status,
56 /// Scan music paths to populate database
57 Scan {
58 #[arg(long)]
59 force: bool,
60 #[arg(long, value_name = "PATHS")]
61 paths: Option<Vec<PathBuf>>,
62 },
63 /// Settings management
64 Settings {
65 #[command(subcommand)]
66 command: SettingsCommands,
67 },
68}
69
70#[derive(Subcommand)]
71pub enum SettingsCommands {
72 /// Get a setting value
73 Get {
74 #[arg(value_name = "KEY")]
75 key: String,
76 },
77 /// Set a setting value
78 Set {
79 #[arg(value_name = "KEY")]
80 key: String,
81 #[arg(value_name = "VALUE")]
82 value: String,
83 },
84 /// List all settings
85 List,
86 /// Unset a setting
87 Unset {
88 #[arg(value_name = "KEY")]
89 key: String,
90 },
91}
92
93impl Cli {
94 pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
95 match &self.command {
96 Commands::Start => {
97 crate::daemon::start_daemon()?;
98 }
99 Commands::Stop => {
100 crate::daemon::stop_daemon()?;
101 }
102 Commands::Restart => {
103 crate::daemon::restart_daemon()?;
104 }
105 Commands::Play { query } => {
106 if let Some(q) = query {
107 crate::daemon::send_command(format!("play {}", q))?;
108 } else {
109 crate::daemon::send_command("play".to_string())?;
110 }
111 }
112 Commands::Pause => {
113 crate::daemon::send_command("pause".to_string())?;
114 }
115 Commands::Resume => {
116 crate::daemon::send_command("resume".to_string())?;
117 }
118 Commands::StopPlayback => {
119 crate::daemon::send_command("stop".to_string())?;
120 }
121 Commands::Next => {
122 crate::daemon::send_command("next".to_string())?;
123 }
124 Commands::Previous => {
125 crate::daemon::send_command("previous".to_string())?;
126 }
127 Commands::Volume { level } => {
128 if let Some(l) = level {
129 crate::daemon::send_command(format!("volume {}", l))?;
130 } else {
131 crate::daemon::send_command("volume".to_string())?;
132 }
133 }
134 Commands::Shuffle { mode } => {
135 if let Some(m) = mode {
136 crate::daemon::send_command(format!("shuffle {}", m))?;
137 } else {
138 crate::daemon::send_command("shuffle".to_string())?;
139 }
140 }
141 Commands::Queue { position, query } => match (position, query) {
142 (None, None) => {
143 crate::daemon::send_command("queue".to_string())?;
144 }
145 (pos, Some(q)) => {
146 let pos = pos.as_deref().unwrap_or("last");
147 crate::daemon::send_command(format!("queue {} {}", pos, q))?;
148 }
149 _ => {}
150 },
151 Commands::Status => {
152 crate::daemon::send_command("status".to_string())?;
153 }
154 Commands::Scan { force, paths } => {
155 let config = Config::load()?;
156 let db = Database::open(&config)?;
157 if let Some(custom_paths) = paths {
158 db.scan_paths(custom_paths, *force)?;
159 } else {
160 db.scan_paths(&config.music_paths, *force)?;
161 }
162 println!("Scan complete");
163 }
164 Commands::Settings { command } => {
165 let mut config = Config::load()?;
166 match command {
167 SettingsCommands::Get { key } => {
168 println!("{}", config.get(key)?);
169 }
170 SettingsCommands::Set { key, value } => {
171 config.set(key, value)?;
172 config.save()?;
173 }
174 SettingsCommands::List => {
175 config.list();
176 }
177 SettingsCommands::Unset { key } => {
178 config.unset(key)?;
179 config.save()?;
180 }
181 }
182 }
183 }
184 Ok(())
185 }
186}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..cda59f4
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,180 @@
1use directories::ProjectDirs;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Serialize, Deserialize, Default)]
7pub struct Config {
8 #[serde(skip)]
9 pub config_path: PathBuf,
10 pub music_paths: Vec<PathBuf>,
11 pub scan_interval: Option<u64>,
12 pub socket_path: Option<PathBuf>,
13 pub lastfm_api_key: Option<String>,
14 pub lastfm_api_secret: Option<String>,
15 pub lastfm_session_key: Option<String>,
16 pub listenbrainz_token: Option<String>,
17 pub volume: Option<u8>,
18 pub shuffle: Option<String>,
19}
20
21impl Config {
22 pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
23 let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer")
24 .ok_or("Failed to get project directories")?;
25
26 let config_dir = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
27 PathBuf::from(xdg).join("rsplayer")
28 } else {
29 proj_dirs.config_dir().to_path_buf()
30 };
31
32 let config_path = config_dir.join("config.toml");
33
34 if !config_path.exists() {
35 fs::create_dir_all(&config_dir)?;
36 let default = Self {
37 config_path: config_path.clone(),
38 music_paths: vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))],
39 scan_interval: Some(3600),
40 socket_path: None,
41 ..Default::default()
42 };
43 default.save()?;
44 return Ok(default);
45 }
46
47 let content = fs::read_to_string(&config_path)?;
48 let mut config: Self = toml::from_str(&content)?;
49 config.config_path = config_path;
50 Ok(config)
51 }
52
53 pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
54 let content = toml::to_string_pretty(self)?;
55 if let Some(parent) = self.config_path.parent() {
56 fs::create_dir_all(parent)?;
57 }
58 fs::write(&self.config_path, content)?;
59 Ok(())
60 }
61
62 pub fn get(&self, key: &str) -> Result<String, Box<dyn std::error::Error>> {
63 match key {
64 "music_paths" => Ok(self
65 .music_paths
66 .iter()
67 .map(|p| p.display().to_string())
68 .collect::<Vec<_>>()
69 .join(", ")),
70 "scan_interval" => Ok(self
71 .scan_interval
72 .map(|i| i.to_string())
73 .unwrap_or_else(|| "not set".to_string())),
74 "socket_path" => Ok(self
75 .socket_path
76 .as_ref()
77 .map(|p| p.display().to_string())
78 .unwrap_or_else(|| "default".to_string())),
79 "lastfm_api_key" => Ok(self
80 .lastfm_api_key
81 .clone()
82 .unwrap_or_else(|| "not set".to_string())),
83 "lastfm_api_secret" => Ok(self
84 .lastfm_api_secret
85 .clone()
86 .unwrap_or_else(|| "not set".to_string())),
87 "lastfm_session_key" => Ok(self
88 .lastfm_session_key
89 .clone()
90 .unwrap_or_else(|| "not set".to_string())),
91 "listenbrainz_token" => Ok(self
92 .listenbrainz_token
93 .clone()
94 .unwrap_or_else(|| "not set".to_string())),
95 "volume" => Ok(self
96 .volume
97 .map(|v| v.to_string())
98 .unwrap_or_else(|| "not set".to_string())),
99 "shuffle" => Ok(self.shuffle.clone().unwrap_or_else(|| "off".to_string())),
100 _ => Err(format!("Unknown setting: {}", key).into()),
101 }
102 }
103
104 pub fn set(&mut self, key: &str, value: &str) -> Result<(), Box<dyn std::error::Error>> {
105 match key {
106 "music_paths" => {
107 self.music_paths = value.split(',').map(|p| PathBuf::from(p.trim())).collect();
108 }
109 "scan_interval" => {
110 self.scan_interval = Some(value.parse()?);
111 }
112 "socket_path" => {
113 self.socket_path = Some(PathBuf::from(value));
114 }
115 "lastfm_api_key" => {
116 self.lastfm_api_key = Some(value.to_string());
117 }
118 "lastfm_api_secret" => {
119 self.lastfm_api_secret = Some(value.to_string());
120 }
121 "lastfm_session_key" => {
122 self.lastfm_session_key = Some(value.to_string());
123 }
124 "listenbrainz_token" => {
125 self.listenbrainz_token = Some(value.to_string());
126 }
127 "volume" => {
128 self.volume = Some(value.parse()?);
129 }
130 "shuffle" => {
131 if value == "on" || value == "all" || value == "album" {
132 self.shuffle = Some(value.to_string());
133 } else if value == "off" || value == "none" {
134 self.shuffle = None;
135 } else {
136 return Err(format!("Invalid shuffle value: {}", value).into());
137 }
138 }
139 _ => return Err(format!("Unknown setting: {}", key).into()),
140 }
141 Ok(())
142 }
143
144 pub fn list(&self) {
145 println!("music_paths = {:?}", self.music_paths);
146 println!("scan_interval = {:?}", self.scan_interval);
147 println!("socket_path = {:?}", self.socket_path);
148 println!("lastfm_api_key = {:?}", self.lastfm_api_key);
149 println!("lastfm_api_secret = {:?}", self.lastfm_api_secret);
150 println!("lastfm_session_key = {:?}", self.lastfm_session_key);
151 println!("listenbrainz_token = {:?}", self.listenbrainz_token);
152 println!("volume = {:?}", self.volume);
153 println!("shuffle = {:?}", self.shuffle);
154 }
155
156 pub fn unset(&mut self, key: &str) -> Result<(), Box<dyn std::error::Error>> {
157 match key {
158 "music_paths" => self.music_paths.clear(),
159 "scan_interval" => self.scan_interval = None,
160 "socket_path" => self.socket_path = None,
161 "lastfm_api_key" => self.lastfm_api_key = None,
162 "lastfm_api_secret" => self.lastfm_api_secret = None,
163 "lastfm_session_key" => self.lastfm_session_key = None,
164 "listenbrainz_token" => self.listenbrainz_token = None,
165 "volume" => self.volume = None,
166 "shuffle" => self.shuffle = None,
167 _ => return Err(format!("Unknown setting: {}", key).into()),
168 }
169 Ok(())
170 }
171
172 pub fn get_socket_path(&self) -> PathBuf {
173 self.socket_path.clone().unwrap_or_else(|| {
174 let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
175 .map(PathBuf::from)
176 .unwrap_or_else(|_| PathBuf::from("/tmp"));
177 runtime_dir.join("rsplayer").join("daemon.sock")
178 })
179 }
180}
diff --git a/src/daemon.rs b/src/daemon.rs
new file mode 100644
index 0000000..8ac3a89
--- /dev/null
+++ b/src/daemon.rs
@@ -0,0 +1,574 @@
1use crate::audio::AudioEngine;
2use crate::config::Config;
3use std::io::{Read, Write};
4use std::os::unix::net::{UnixListener, UnixStream};
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use std::thread;
9use std::time::Duration;
10
11#[derive(Debug, Clone)]
12enum ShuffleMode {
13 None,
14 All,
15 Album,
16}
17
18struct Queue {
19 tracks: Vec<PathBuf>,
20 position: usize,
21 shuffle: ShuffleMode,
22 shuffled_indices: Vec<usize>,
23}
24
25impl Queue {
26 fn new() -> Self {
27 Queue {
28 tracks: Vec::new(),
29 position: 0,
30 shuffle: ShuffleMode::None,
31 shuffled_indices: Vec::new(),
32 }
33 }
34
35 fn add_track(&mut self, path: PathBuf) {
36 self.tracks.push(path);
37 self.update_shuffle_indices();
38 }
39
40 fn set_shuffle(&mut self, mode: ShuffleMode) {
41 self.shuffle = mode.clone();
42 self.update_shuffle_indices();
43 }
44
45 fn update_shuffle_indices(&mut self) {
46 self.shuffled_indices = (0..self.tracks.len()).collect();
47 if let ShuffleMode::All = self.shuffle {
48 use rand::seq::SliceRandom;
49 let mut rng = rand::rng();
50 self.shuffled_indices.shuffle(&mut rng);
51 }
52 }
53
54 fn current(&self) -> Option<&PathBuf> {
55 if self.tracks.is_empty() {
56 None
57 } else if let ShuffleMode::All = self.shuffle {
58 self.shuffled_indices
59 .get(self.position)
60 .and_then(|&idx| self.tracks.get(idx))
61 } else if let ShuffleMode::Album = self.shuffle {
62 // TODO: When database is ready, only shuffle tracks in same album/folder
63 // For now, behave like All
64 self.shuffled_indices
65 .get(self.position)
66 .and_then(|&idx| self.tracks.get(idx))
67 } else {
68 self.tracks.get(self.position)
69 }
70 }
71
72 fn next(&mut self) -> Option<PathBuf> {
73 let max_pos = self.tracks.len().saturating_sub(1);
74 if self.position < max_pos {
75 self.position += 1;
76 self.current().cloned()
77 } else {
78 None
79 }
80 }
81
82 fn previous(&mut self) -> Option<PathBuf> {
83 if self.position > 0 {
84 self.position -= 1;
85 self.current().cloned()
86 } else {
87 None
88 }
89 }
90}
91
92pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
93 let config = Config::load()?;
94 let socket_path = config.get_socket_path();
95 let pid_path = get_pid_path(&config);
96
97 // Check if daemon is already running
98 if let Ok(pid) = check_running_daemon(&pid_path) {
99 return Err(format!("Daemon already running with PID {}", pid).into());
100 }
101
102 // Clean up stale socket file if it exists
103 if socket_path.exists() {
104 std::fs::remove_file(&socket_path).ok();
105 }
106
107 // Create runtime directory
108 if let Some(parent) = socket_path.parent() {
109 std::fs::create_dir_all(parent)?;
110 }
111 if let Some(parent) = pid_path.parent() {
112 std::fs::create_dir_all(parent)?;
113 }
114
115 // Write PID file
116 let pid = std::process::id();
117 std::fs::write(&pid_path, pid.to_string())?;
118 println!("Daemon started with PID {}", pid);
119
120 // Shared shutdown flag - can be triggered by signal or command
121 let shutdown_flag = Arc::new(AtomicBool::new(false));
122 let shutdown_flag_signal = Arc::clone(&shutdown_flag);
123
124 // Handle SIGTERM and SIGINT
125 ctrlc::set_handler(move || {
126 println!("Received shutdown signal");
127 shutdown_flag_signal.store(true, Ordering::SeqCst);
128 })?;
129
130 let listener = UnixListener::bind(&socket_path)?;
131 println!("Listening on {:?}", socket_path);
132
133 let audio_engine = Arc::new(AudioEngine::new());
134 let queue = Arc::new(Mutex::new(Queue::new()));
135
136 // Load and apply saved settings
137 let config = Config::load().unwrap_or_default();
138 if let Some(vol) = config.volume {
139 audio_engine.set_volume(vol as f32 / 100.0);
140 }
141 if let Some(ref shuffle_str) = config.shuffle {
142 if let Ok(mut q) = queue.lock() {
143 match shuffle_str.as_str() {
144 "on" | "all" => q.set_shuffle(ShuffleMode::All),
145 "album" => q.set_shuffle(ShuffleMode::Album),
146 _ => {}
147 }
148 }
149 }
150
151 // Accept connections until shutdown
152 listener.set_nonblocking(true)?;
153 loop {
154 // Check for shutdown signal
155 if shutdown_flag.load(Ordering::SeqCst) {
156 break;
157 }
158
159 // Check if track ended and play next
160 if audio_engine.poll_track_ended() {
161 if let Ok(mut q) = queue.lock() {
162 if let Some(next_track) = q.next() {
163 audio_engine.play(next_track);
164 } else {
165 audio_engine.stop();
166 }
167 }
168 }
169
170 match listener.accept() {
171 Ok((stream, _)) => {
172 let audio_clone = Arc::clone(&audio_engine);
173 let shutdown_flag_clone = Arc::clone(&shutdown_flag);
174 let queue_clone = Arc::clone(&queue);
175 thread::spawn(move || {
176 handle_client(stream, audio_clone, shutdown_flag_clone, queue_clone);
177 });
178 }
179 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
180 thread::sleep(Duration::from_millis(50));
181 continue;
182 }
183 Err(e) => eprintln!("Connection failed: {}", e),
184 }
185
186 thread::sleep(Duration::from_millis(50));
187 }
188
189 // Cleanup
190 cleanup_daemon(&socket_path, &pid_path);
191 println!("Daemon stopped");
192 Ok(())
193}
194
195pub fn stop_daemon() -> Result<(), Box<dyn std::error::Error>> {
196 let config = Config::load()?;
197 let pid_path = get_pid_path(&config);
198
199 match check_running_daemon(&pid_path) {
200 Ok(pid) => {
201 // Try to send stop command first for graceful shutdown
202 if let Ok(()) = send_command_raw("stop_daemon".to_string()) {
203 // Wait for daemon to stop
204 for _ in 0..50 {
205 thread::sleep(Duration::from_millis(100));
206 if !is_process_running(pid) {
207 break;
208 }
209 }
210 }
211
212 // Force kill if still running
213 if is_process_running(pid) {
214 unsafe {
215 libc::kill(pid as i32, libc::SIGTERM);
216 }
217 thread::sleep(Duration::from_millis(500));
218
219 // Force kill if still alive
220 if is_process_running(pid) {
221 unsafe {
222 libc::kill(pid as i32, libc::SIGKILL);
223 }
224 }
225 }
226
227 // Clean up files
228 let socket_path = config.get_socket_path();
229 std::fs::remove_file(&socket_path).ok();
230 std::fs::remove_file(&pid_path).ok();
231
232 println!("Daemon stopped");
233 Ok(())
234 }
235 Err(e) => Err(format!("Daemon not running: {}", e).into()),
236 }
237}
238
239pub fn restart_daemon() -> Result<(), Box<dyn std::error::Error>> {
240 // Stop existing daemon
241 if let Ok(()) = stop_daemon() {
242 // Wait a bit for cleanup
243 thread::sleep(Duration::from_millis(500));
244 }
245
246 // Start new daemon in background
247 let current_exe = std::env::current_exe()?;
248 std::process::Command::new(current_exe)
249 .arg("start")
250 .spawn()?;
251
252 println!("Daemon restarted");
253 Ok(())
254}
255
256pub fn send_command(cmd: String) -> Result<(), Box<dyn std::error::Error>> {
257 send_command_raw(cmd)?;
258 Ok(())
259}
260
261fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> {
262 let config = Config::load()?;
263 let socket_path = config.get_socket_path();
264
265 let mut stream = UnixStream::connect(socket_path)?;
266 stream.write_all(cmd.as_bytes())?;
267
268 let mut response = String::new();
269 stream.read_to_string(&mut response)?;
270 print!("{}", response);
271
272 Ok(())
273}
274
275fn handle_client(
276 mut stream: UnixStream,
277 audio: Arc<AudioEngine>,
278 shutdown_flag: Arc<AtomicBool>,
279 queue: Arc<Mutex<Queue>>,
280) {
281 let mut buffer = [0; 1024];
282 if let Ok(size) = stream.read(&mut buffer) {
283 let cmd = String::from_utf8_lossy(&buffer[..size]);
284 let response = process_command(cmd.trim(), &audio, &shutdown_flag, &queue);
285 let _ = stream.write_all(response.as_bytes());
286 }
287}
288
289fn process_command(
290 cmd: &str,
291 audio: &AudioEngine,
292 shutdown_flag: &Arc<AtomicBool>,
293 queue: &Arc<Mutex<Queue>>,
294) -> String {
295 match cmd {
296 "status" => {
297 if audio.is_playing() {
298 if let Some(track) = audio.current_track() {
299 format!("Playing\nTrack: {}\n", track.display())
300 } else {
301 "Playing\n".to_string()
302 }
303 } else {
304 "Stopped\n".to_string()
305 }
306 }
307 "pause" => {
308 audio.pause();
309 "Paused\n".to_string()
310 }
311 "resume" => {
312 audio.resume();
313 "Resumed\n".to_string()
314 }
315 "stop" => {
316 audio.stop();
317 // Clear queue on stop
318 if let Ok(mut q) = queue.lock() {
319 q.tracks.clear();
320 q.position = 0;
321 }
322 "Stopped\n".to_string()
323 }
324 "stop_daemon" => {
325 // Signal the daemon to shut down
326 shutdown_flag.store(true, Ordering::SeqCst);
327 "Shutting down\n".to_string()
328 }
329 "next" => {
330 if let Ok(mut q) = queue.lock() {
331 if let Some(next_track) = q.next() {
332 audio.play(next_track);
333 "Playing next track\n".to_string()
334 } else {
335 audio.stop();
336 "Queue empty, stopped\n".to_string()
337 }
338 } else {
339 "Error accessing queue\n".to_string()
340 }
341 }
342 "previous" => {
343 if let Ok(mut q) = queue.lock() {
344 if let Some(prev_track) = q.previous() {
345 audio.play(prev_track);
346 "Playing previous track\n".to_string()
347 } else {
348 "No previous track\n".to_string()
349 }
350 } else {
351 "Error accessing queue\n".to_string()
352 }
353 }
354 "shuffle" => {
355 if let Ok(q) = queue.lock() {
356 match q.shuffle {
357 ShuffleMode::None => "Shuffle: off\n".to_string(),
358 ShuffleMode::All => "Shuffle: on (all)\n".to_string(),
359 ShuffleMode::Album => "Shuffle: on (album)\n".to_string(),
360 }
361 } else {
362 "Error accessing queue\n".to_string()
363 }
364 }
365 _ if cmd.starts_with("shuffle ") => {
366 let mode_str = cmd[8..].trim();
367 if let Ok(mut q) = queue.lock() {
368 let result = if mode_str == "on" || mode_str == "all" {
369 q.set_shuffle(ShuffleMode::All);
370 "Shuffle: on (all)\n".to_string()
371 } else if mode_str == "album" {
372 q.set_shuffle(ShuffleMode::Album);
373 "Shuffle: on (album)\n".to_string()
374 } else if mode_str == "off" || mode_str == "none" {
375 q.set_shuffle(ShuffleMode::None);
376 "Shuffle: off\n".to_string()
377 } else {
378 "Usage: shuffle [on|off|album]\n".to_string()
379 };
380 // Save to config
381 if let Ok(mut config) = Config::load() {
382 if mode_str == "off" || mode_str == "none" {
383 config.shuffle = None;
384 } else {
385 config.shuffle = Some(mode_str.to_string());
386 }
387 let _ = config.save();
388 }
389 result
390 } else {
391 "Error accessing queue\n".to_string()
392 }
393 }
394 _ if cmd.starts_with("play ") => {
395 let path_str = &cmd[5..].trim();
396 let path = std::path::PathBuf::from(path_str);
397 if path.is_file() {
398 // Add to queue and play
399 if let Ok(mut q) = queue.lock() {
400 q.add_track(path.clone());
401 }
402 audio.play(path);
403 format!("Playing: {}\n", path_str)
404 } else if path.is_dir() {
405 // Find all audio files in directory
406 match find_audio_in_dir(&path) {
407 Some(files) if !files.is_empty() => {
408 if let Ok(mut q) = queue.lock() {
409 for file in &files {
410 q.add_track(file.clone());
411 }
412 // If shuffle is on, shuffled_indices is already set
413 // Play the first track from the shuffled list
414 if let Some(track) = q.current().cloned() {
415 audio.play(track);
416 }
417 }
418 format!("Queued {} tracks from: {}\n", files.len(), path_str)
419 }
420 _ => format!("Error: No audio files found in '{}'\n", path_str),
421 }
422 } else {
423 format!("File not found: {}\n", path_str)
424 }
425 }
426 _ if cmd.starts_with("volume ") => {
427 if let Ok(level) = cmd[7..].trim().parse::<u8>() {
428 audio.set_volume(level as f32 / 100.0);
429 // Save to config
430 if let Ok(mut config) = Config::load() {
431 config.volume = Some(level);
432 let _ = config.save();
433 }
434 format!("Volume set to {}\n", level)
435 } else {
436 "Usage: volume <0-100>\n".to_string()
437 }
438 }
439 _ if cmd.starts_with("shuffle ") => {
440 let mode_str = cmd[8..].trim();
441 if let Ok(mut q) = queue.lock() {
442 if mode_str == "on" || mode_str == "all" {
443 q.set_shuffle(ShuffleMode::All);
444 "Shuffle: on\n".to_string()
445 } else if mode_str == "off" || mode_str == "none" {
446 q.set_shuffle(ShuffleMode::None);
447 "Shuffle: off\n".to_string()
448 } else {
449 "Usage: shuffle [on|off]\n".to_string()
450 }
451 } else {
452 "Error accessing queue\n".to_string()
453 }
454 }
455 _ if cmd.starts_with("queue") => {
456 if let Ok(q) = queue.lock() {
457 if q.tracks.is_empty() {
458 "Queue is empty\n".to_string()
459 } else {
460 let mut output = String::new();
461 let current_position = q.position;
462 // Use shuffled_indices for display order if shuffle is on
463 let indices: Vec<usize> = if let ShuffleMode::None = q.shuffle {
464 (0..q.tracks.len()).collect()
465 } else {
466 q.shuffled_indices.clone()
467 };
468 for (display_idx, &track_idx) in indices.iter().enumerate() {
469 if let Some(track) = q.tracks.get(track_idx) {
470 if display_idx == current_position {
471 output.push_str(&format!("> {} (playing)\n", track.display()));
472 } else {
473 output.push_str(&format!(" {}\n", track.display()));
474 }
475 }
476 }
477 // Use pager if output is long (more than 24 lines)
478 if output.lines().count() > 24 {
479 use std::process::{Command, Stdio};
480 let pager = Command::new("less").arg("-R").stdin(Stdio::piped()).spawn();
481 if let Ok(mut child) = pager {
482 use std::io::Write;
483 let _ = child.stdin.as_mut().unwrap().write_all(output.as_bytes());
484 let _ = child.wait();
485 "".to_string()
486 } else {
487 output
488 }
489 } else {
490 output
491 }
492 }
493 } else {
494 "Error accessing queue\n".to_string()
495 }
496 }
497 _ => "Unknown command\n".to_string(),
498 }
499}
500
501fn find_audio_in_dir(dir: &Path) -> Option<Vec<std::path::PathBuf>> {
502 use std::ffi::OsStr;
503
504 let mut results = Vec::new();
505
506 if let Ok(entries) = std::fs::read_dir(dir) {
507 let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
508 entries.sort_by_key(|e| e.file_name());
509
510 for entry in entries {
511 let path = entry.path();
512 if path.is_file() {
513 if let Some(ext) = path.extension().and_then(OsStr::to_str) {
514 if matches!(
515 ext.to_lowercase().as_str(),
516 "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus"
517 ) {
518 results.push(path);
519 }
520 }
521 } else if path.is_dir() {
522 if let Some(mut found) = find_audio_in_dir(&path) {
523 results.append(&mut found);
524 }
525 }
526 }
527 }
528
529 if results.is_empty() {
530 None
531 } else {
532 Some(results)
533 }
534}
535
536/// Get the PID file path for the daemon
537fn get_pid_path(_config: &Config) -> std::path::PathBuf {
538 let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
539 .map(std::path::PathBuf::from)
540 .unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
541 runtime_dir.join("rsplayer").join("rsplayer.pid")
542}
543
544/// Check if a process with the given PID is running
545fn is_process_running(pid: u32) -> bool {
546 unsafe {
547 // send signal 0 to check if process exists
548 libc::kill(pid as i32, 0) == 0
549 }
550}
551
552/// Check if daemon is running by reading PID file
553fn check_running_daemon(pid_path: &std::path::Path) -> Result<u32, Box<dyn std::error::Error>> {
554 if !pid_path.exists() {
555 return Err("PID file not found".into());
556 }
557
558 let contents = std::fs::read_to_string(pid_path)?;
559 let pid = contents.trim().parse::<u32>()?;
560
561 if is_process_running(pid) {
562 Ok(pid)
563 } else {
564 // Stale PID file, clean it up
565 std::fs::remove_file(pid_path).ok();
566 Err("Stale PID file, daemon not running".into())
567 }
568}
569
570/// Clean up daemon resources (socket and PID file)
571fn cleanup_daemon(socket_path: &std::path::Path, pid_path: &std::path::Path) {
572 std::fs::remove_file(socket_path).ok();
573 std::fs::remove_file(pid_path).ok();
574}
diff --git a/src/database.rs b/src/database.rs
new file mode 100644
index 0000000..9d1d5f2
--- /dev/null
+++ b/src/database.rs
@@ -0,0 +1,259 @@
1use directories::ProjectDirs;
2use rusqlite::{Connection, Result as SqlResult};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7#[derive(Debug, Clone)]
8#[allow(dead_code)]
9pub struct Artist {
10 pub id: i64,
11 pub name: String,
12}
13
14#[derive(Debug, Clone)]
15#[allow(dead_code)]
16pub struct Album {
17 pub id: i64,
18 pub artist_id: i64,
19 pub title: String,
20}
21
22#[derive(Debug, Clone)]
23#[allow(dead_code)]
24pub struct Track {
25 pub id: i64,
26 pub album_id: i64,
27 pub title: String,
28 pub path: PathBuf,
29 pub duration: Option<i64>,
30 pub track_number: Option<i32>,
31 pub artist_name: Option<String>,
32 pub album_title: Option<String>,
33}
34
35pub struct Database {
36 conn: Connection,
37}
38
39#[allow(dead_code)]
40impl Database {
41 pub fn open(_config: &crate::config::Config) -> SqlResult<Self> {
42 let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer").ok_or_else(|| {
43 rusqlite::Error::InvalidPath("Failed to get project directories".into())
44 })?;
45
46 let data_dir = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
47 PathBuf::from(xdg).join("rsplayer")
48 } else {
49 proj_dirs.data_dir().to_path_buf()
50 };
51
52 fs::create_dir_all(&data_dir).ok();
53 let db_path = data_dir.join("library.db");
54
55 let conn = Connection::open(&db_path)?;
56 let db = Database { conn };
57 db.init_schema()?;
58 Ok(db)
59 }
60
61 fn init_schema(&self) -> SqlResult<()> {
62 self.conn.execute(
63 "CREATE TABLE IF NOT EXISTS artists (
64 id INTEGER PRIMARY KEY,
65 name TEXT UNIQUE NOT NULL
66 )",
67 [],
68 )?;
69
70 self.conn.execute(
71 "CREATE TABLE IF NOT EXISTS albums (
72 id INTEGER PRIMARY KEY,
73 artist_id INTEGER NOT NULL,
74 title TEXT NOT NULL,
75 FOREIGN KEY (artist_id) REFERENCES artists(id),
76 UNIQUE(artist_id, title)
77 )",
78 [],
79 )?;
80
81 self.conn.execute(
82 "CREATE TABLE IF NOT EXISTS tracks (
83 id INTEGER PRIMARY KEY,
84 album_id INTEGER NOT NULL,
85 title TEXT NOT NULL,
86 path TEXT UNIQUE NOT NULL,
87 duration INTEGER,
88 track_number INTEGER,
89 last_scanned INTEGER,
90 FOREIGN KEY (album_id) REFERENCES albums(id)
91 )",
92 [],
93 )?;
94
95 Ok(())
96 }
97
98 pub fn scan_paths(&self, paths: &[PathBuf], force: bool) -> SqlResult<()> {
99 for path in paths {
100 self.scan_directory(path, force)?;
101 }
102 Ok(())
103 }
104
105 fn scan_directory(&self, dir: &Path, force: bool) -> SqlResult<()> {
106 if !dir.exists() {
107 return Ok(());
108 }
109
110 let now = SystemTime::now()
111 .duration_since(SystemTime::UNIX_EPOCH)
112 .unwrap()
113 .as_secs() as i64;
114
115 self.scan_recursive(dir, now, force)?;
116 self.cleanup_old_tracks(now)?;
117 Ok(())
118 }
119
120 fn scan_recursive(&self, dir: &Path, scan_time: i64, force: bool) -> SqlResult<()> {
121 let entries = fs::read_dir(dir).ok();
122 if let Some(entries) = entries {
123 for entry in entries.flatten() {
124 let path = entry.path();
125 if path.is_dir() {
126 self.scan_recursive(&path, scan_time, force)?;
127 } else if is_audio_file(&path) {
128 self.process_audio_file(&path, scan_time, force)?;
129 }
130 }
131 }
132 Ok(())
133 }
134
135 fn process_audio_file(&self, path: &Path, scan_time: i64, force: bool) -> SqlResult<()> {
136 let path_str = path.to_string_lossy().to_string();
137
138 if !force {
139 let count: i64 = self
140 .conn
141 .query_row(
142 "SELECT COUNT(*) FROM tracks WHERE path = ? AND last_scanned = ?",
143 rusqlite::params![path_str, scan_time],
144 |row| row.get(0),
145 )
146 .unwrap_or(0);
147 if count > 0 {
148 return Ok(());
149 }
150 }
151
152 let metadata = extract_metadata(path);
153 let (artist_name, album_title, track_title, track_number, duration) = metadata;
154 let duration_i64 = duration.map(|d| d as i64);
155
156 let artist_id = self.get_or_create_artist(&artist_name)?;
157 let album_id = self.get_or_create_album(artist_id, &album_title)?;
158
159 self.conn.execute(
160 "INSERT OR REPLACE INTO tracks (album_id, title, path, duration, track_number, last_scanned)
161 VALUES (?, ?, ?, ?, ?, ?)",
162 rusqlite::params![album_id, track_title, path_str, duration_i64, track_number, scan_time],
163 )?;
164
165 Ok(())
166 }
167
168 fn get_or_create_artist(&self, name: &str) -> SqlResult<i64> {
169 self.conn
170 .execute("INSERT OR IGNORE INTO artists (name) VALUES (?)", [name])?;
171 self.conn
172 .query_row("SELECT id FROM artists WHERE name = ?", [name], |row| {
173 row.get(0)
174 })
175 }
176
177 fn get_or_create_album(&self, artist_id: i64, title: &str) -> SqlResult<i64> {
178 self.conn.execute(
179 "INSERT OR IGNORE INTO albums (artist_id, title) VALUES (?, ?)",
180 rusqlite::params![artist_id, title],
181 )?;
182 self.conn.query_row(
183 "SELECT id FROM albums WHERE artist_id = ? AND title = ?",
184 rusqlite::params![artist_id, title],
185 |row| row.get(0),
186 )
187 }
188
189 fn cleanup_old_tracks(&self, scan_time: i64) -> SqlResult<()> {
190 self.conn
191 .execute("DELETE FROM tracks WHERE last_scanned < ?", [scan_time])?;
192 Ok(())
193 }
194
195 pub fn search_tracks(&self, query: &str) -> SqlResult<Vec<Track>> {
196 let mut stmt = self.conn.prepare(
197 "SELECT t.id, t.album_id, t.title, t.path, t.duration, t.track_number, ar.name, a.title
198 FROM tracks t
199 JOIN albums a ON t.album_id = a.id
200 JOIN artists ar ON a.artist_id = ar.id
201 WHERE t.title LIKE ? OR a.title LIKE ? OR ar.name LIKE ?
202 ORDER BY ar.name, a.title, t.track_number",
203 )?;
204
205 let search = format!("%{}%", query);
206 let rows = stmt.query_map([&search, &search, &search], |row| {
207 Ok(Track {
208 id: row.get(0)?,
209 album_id: row.get(1)?,
210 title: row.get(2)?,
211 path: PathBuf::from(row.get::<_, String>(3)?),
212 duration: row.get(4)?,
213 track_number: row.get(5)?,
214 artist_name: Some(row.get(6)?),
215 album_title: Some(row.get(7)?),
216 })
217 })?;
218
219 rows.collect()
220 }
221}
222
223fn is_audio_file(path: &Path) -> bool {
224 path.extension()
225 .and_then(|ext| ext.to_str())
226 .map(|ext| {
227 matches!(
228 ext.to_lowercase().as_str(),
229 "mp3" | "flac" | "ogg" | "m4a" | "wav" | "opus"
230 )
231 })
232 .unwrap_or(false)
233}
234
235fn extract_metadata(path: &Path) -> (String, String, String, Option<i32>, Option<u64>) {
236 let file_stem = path
237 .file_stem()
238 .and_then(|s| s.to_str())
239 .unwrap_or("Unknown");
240 let parent = path
241 .parent()
242 .and_then(|p| p.file_name())
243 .and_then(|s| s.to_str())
244 .unwrap_or("Unknown");
245 let grandparent = path
246 .parent()
247 .and_then(|p| p.parent())
248 .and_then(|p| p.file_name())
249 .and_then(|s| s.to_str())
250 .unwrap_or("Unknown");
251
252 (
253 grandparent.to_string(),
254 parent.to_string(),
255 file_stem.to_string(),
256 None,
257 None,
258 )
259}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..2b88c44
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,18 @@
1mod audio;
2mod cli;
3mod config;
4mod daemon;
5mod database;
6mod playback;
7mod scrobbler;
8
9use clap::Parser;
10use cli::Cli;
11
12fn main() {
13 let cli = Cli::parse();
14 if let Err(e) = cli.run() {
15 eprintln!("Error: {}", e);
16 std::process::exit(1);
17 }
18}
diff --git a/src/playback.rs b/src/playback.rs
new file mode 100644
index 0000000..76c0f44
--- /dev/null
+++ b/src/playback.rs
@@ -0,0 +1,131 @@
1use std::path::PathBuf;
2
3#[derive(Debug, Clone, PartialEq)]
4#[allow(dead_code)]
5pub enum PlaybackState {
6 Stopped,
7 Playing,
8 Paused,
9}
10
11#[derive(Debug, Clone)]
12#[allow(dead_code)]
13pub struct Track {
14 pub path: PathBuf,
15 pub title: String,
16 pub artist: String,
17 pub album: String,
18 pub duration: Option<u64>,
19}
20
21#[derive(Debug, Clone)]
22#[allow(dead_code)]
23pub enum ShuffleMode {
24 None,
25 All,
26 Album,
27}
28
29#[allow(dead_code)]
30pub struct Player {
31 state: PlaybackState,
32 current_track: Option<Track>,
33 queue: Vec<Track>,
34 volume: u8,
35 shuffle: ShuffleMode,
36}
37
38#[allow(dead_code)]
39impl Player {
40 pub fn new() -> Self {
41 Player {
42 state: PlaybackState::Stopped,
43 current_track: None,
44 queue: Vec::new(),
45 volume: 70,
46 shuffle: ShuffleMode::None,
47 }
48 }
49
50 pub fn state(&self) -> &PlaybackState {
51 &self.state
52 }
53
54 pub fn volume(&self) -> u8 {
55 self.volume
56 }
57
58 pub fn play(&mut self) {
59 if self.state == PlaybackState::Paused {
60 self.resume();
61 } else if self.current_track.is_none() && !self.queue.is_empty() {
62 self.current_track = Some(self.queue.remove(0));
63 self.state = PlaybackState::Playing;
64 }
65 }
66
67 pub fn play_track(&mut self, track: Track) {
68 self.current_track = Some(track);
69 self.state = PlaybackState::Playing;
70 }
71
72 pub fn pause(&mut self) {
73 if self.state == PlaybackState::Playing {
74 self.state = PlaybackState::Paused;
75 }
76 }
77
78 pub fn resume(&mut self) {
79 if self.state == PlaybackState::Paused {
80 self.state = PlaybackState::Playing;
81 }
82 }
83
84 pub fn stop(&mut self) {
85 self.state = PlaybackState::Stopped;
86 self.current_track = None;
87 }
88
89 pub fn next(&mut self) {
90 if !self.queue.is_empty() {
91 self.current_track = Some(self.queue.remove(0));
92 } else {
93 self.stop();
94 }
95 }
96
97 pub fn previous(&mut self) {
98 if (self.state == PlaybackState::Playing || self.state == PlaybackState::Paused)
99 && self.current_track.is_some()
100 {
101 self.state = PlaybackState::Playing;
102 }
103 }
104
105 pub fn set_volume(&mut self, level: u8) {
106 self.volume = level.min(100);
107 }
108
109 pub fn set_shuffle(&mut self, mode: ShuffleMode) {
110 self.shuffle = mode;
111 }
112
113 pub fn queue_track(&mut self, track: Track, position: &str) {
114 match position {
115 "now" => self.queue.insert(0, track),
116 "next" => {
117 let idx = 1.min(self.queue.len());
118 self.queue.insert(idx, track);
119 }
120 _ => self.queue.push(track),
121 }
122 }
123
124 pub fn dequeue_next(&mut self) -> Option<Track> {
125 if !self.queue.is_empty() {
126 Some(self.queue.remove(0))
127 } else {
128 None
129 }
130 }
131}
diff --git a/src/scrobbler.rs b/src/scrobbler.rs
new file mode 100644
index 0000000..09a7f55
--- /dev/null
+++ b/src/scrobbler.rs
@@ -0,0 +1,75 @@
1use reqwest::Client;
2
3#[allow(dead_code)]
4pub struct Scrobbler {
5 lastfm_api_key: Option<String>,
6 lastfm_api_secret: Option<String>,
7 lastfm_session_key: Option<String>,
8 listenbrainz_token: Option<String>,
9 client: Client,
10}
11
12#[allow(dead_code)]
13impl Scrobbler {
14 pub fn new(
15 lastfm_api_key: Option<String>,
16 lastfm_api_secret: Option<String>,
17 lastfm_session_key: Option<String>,
18 listenbrainz_token: Option<String>,
19 ) -> Self {
20 Scrobbler {
21 lastfm_api_key,
22 lastfm_api_secret,
23 lastfm_session_key,
24 listenbrainz_token,
25 client: Client::new(),
26 }
27 }
28
29 pub fn now_playing(&self, artist: &str, track: &str, album: Option<&str>) {
30 if let Some(api_key) = &self.lastfm_api_key {
31 let _ = self.send_lastfm_now_playing(api_key, artist, track, album);
32 }
33 }
34
35 pub fn scrobble(&self, artist: &str, track: &str, album: Option<&str>, timestamp: u64) {
36 if let Some(api_key) = &self.lastfm_api_key {
37 let _ = self.send_lastfm_scrobble(api_key, artist, track, album, timestamp);
38 }
39 if let Some(token) = &self.listenbrainz_token {
40 let _ = self.send_listenbrainz_scrobble(token, artist, track, album, timestamp);
41 }
42 }
43
44 fn send_lastfm_now_playing(
45 &self,
46 _api_key: &str,
47 _artist: &str,
48 _track: &str,
49 _album: Option<&str>,
50 ) -> Result<(), Box<dyn std::error::Error>> {
51 Ok(())
52 }
53
54 fn send_lastfm_scrobble(
55 &self,
56 _api_key: &str,
57 _artist: &str,
58 _track: &str,
59 _album: Option<&str>,
60 _timestamp: u64,
61 ) -> Result<(), Box<dyn std::error::Error>> {
62 Ok(())
63 }
64
65 fn send_listenbrainz_scrobble(
66 &self,
67 _token: &str,
68 _artist: &str,
69 _track: &str,
70 _album: Option<&str>,
71 _timestamp: u64,
72 ) -> Result<(), Box<dyn std::error::Error>> {
73 Ok(())
74 }
75}