diff options
Diffstat (limited to '')
| -rw-r--r-- | AGENTS.md | 4 | ||||
| -rw-r--r-- | Cargo.lock | 107 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | TODO.md | 6 | ||||
| -rw-r--r-- | rsplayer.service | 4 | ||||
| -rw-r--r-- | src/cli.rs | 12 | ||||
| -rw-r--r-- | src/config.rs | 186 | ||||
| -rw-r--r-- | src/daemon.rs | 66 | ||||
| -rw-r--r-- | src/notification.rs | 23 | ||||
| -rw-r--r-- | src/queue.rs | 16 | ||||
| -rw-r--r-- | tests/config.rs | 16 | ||||
| -rw-r--r-- | tests/queue.rs | 26 |
12 files changed, 294 insertions, 174 deletions
| @@ -53,6 +53,4 @@ Run these before completing any task: | |||
| 53 | - [x] Desktop notifications for track changes, queue end, and errors | 53 | - [x] Desktop notifications for track changes, queue end, and errors |
| 54 | 54 | ||
| 55 | ## Remaining Work | 55 | ## Remaining Work |
| 56 | - Queue state persistence to disk | 56 | Check @TODO.md |
| 57 | - Album shuffle mode with proper album/folder grouping | ||
| 58 | - Scrobbling (Last.fm/ListenBrainz) | ||
| @@ -146,7 +146,7 @@ dependencies = [ | |||
| 146 | "futures-lite", | 146 | "futures-lite", |
| 147 | "parking", | 147 | "parking", |
| 148 | "polling", | 148 | "polling", |
| 149 | "rustix 1.1.4", | 149 | "rustix", |
| 150 | "slab", | 150 | "slab", |
| 151 | "windows-sys 0.61.2", | 151 | "windows-sys 0.61.2", |
| 152 | ] | 152 | ] |
| @@ -177,7 +177,7 @@ dependencies = [ | |||
| 177 | "cfg-if", | 177 | "cfg-if", |
| 178 | "event-listener", | 178 | "event-listener", |
| 179 | "futures-lite", | 179 | "futures-lite", |
| 180 | "rustix 1.1.4", | 180 | "rustix", |
| 181 | ] | 181 | ] |
| 182 | 182 | ||
| 183 | [[package]] | 183 | [[package]] |
| @@ -203,7 +203,7 @@ dependencies = [ | |||
| 203 | "cfg-if", | 203 | "cfg-if", |
| 204 | "futures-core", | 204 | "futures-core", |
| 205 | "futures-io", | 205 | "futures-io", |
| 206 | "rustix 1.1.4", | 206 | "rustix", |
| 207 | "signal-hook-registry", | 207 | "signal-hook-registry", |
| 208 | "slab", | 208 | "slab", |
| 209 | "windows-sys 0.61.2", | 209 | "windows-sys 0.61.2", |
| @@ -1329,12 +1329,6 @@ dependencies = [ | |||
| 1329 | 1329 | ||
| 1330 | [[package]] | 1330 | [[package]] |
| 1331 | name = "linux-raw-sys" | 1331 | name = "linux-raw-sys" |
| 1332 | version = "0.4.15" | ||
| 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 1334 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" | ||
| 1335 | |||
| 1336 | [[package]] | ||
| 1337 | name = "linux-raw-sys" | ||
| 1338 | version = "0.12.1" | 1332 | version = "0.12.1" |
| 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" | 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1340 | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | 1334 | checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" |
| @@ -1713,7 +1707,7 @@ dependencies = [ | |||
| 1713 | "concurrent-queue", | 1707 | "concurrent-queue", |
| 1714 | "hermit-abi", | 1708 | "hermit-abi", |
| 1715 | "pin-project-lite", | 1709 | "pin-project-lite", |
| 1716 | "rustix 1.1.4", | 1710 | "rustix", |
| 1717 | "windows-sys 0.61.2", | 1711 | "windows-sys 0.61.2", |
| 1718 | ] | 1712 | ] |
| 1719 | 1713 | ||
| @@ -2042,19 +2036,6 @@ dependencies = [ | |||
| 2042 | 2036 | ||
| 2043 | [[package]] | 2037 | [[package]] |
| 2044 | name = "rustix" | 2038 | name = "rustix" |
| 2045 | version = "0.38.44" | ||
| 2046 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2047 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" | ||
| 2048 | dependencies = [ | ||
| 2049 | "bitflags 2.11.1", | ||
| 2050 | "errno", | ||
| 2051 | "libc", | ||
| 2052 | "linux-raw-sys 0.4.15", | ||
| 2053 | "windows-sys 0.52.0", | ||
| 2054 | ] | ||
| 2055 | |||
| 2056 | [[package]] | ||
| 2057 | name = "rustix" | ||
| 2058 | version = "1.1.4" | 2039 | version = "1.1.4" |
| 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2040 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2060 | checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" | 2041 | checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" |
| @@ -2062,7 +2043,7 @@ dependencies = [ | |||
| 2062 | "bitflags 2.11.1", | 2043 | "bitflags 2.11.1", |
| 2063 | "errno", | 2044 | "errno", |
| 2064 | "libc", | 2045 | "libc", |
| 2065 | "linux-raw-sys 0.12.1", | 2046 | "linux-raw-sys", |
| 2066 | "windows-sys 0.61.2", | 2047 | "windows-sys 0.61.2", |
| 2067 | ] | 2048 | ] |
| 2068 | 2049 | ||
| @@ -2615,18 +2596,18 @@ dependencies = [ | |||
| 2615 | "fastrand", | 2596 | "fastrand", |
| 2616 | "getrandom 0.4.2", | 2597 | "getrandom 0.4.2", |
| 2617 | "once_cell", | 2598 | "once_cell", |
| 2618 | "rustix 1.1.4", | 2599 | "rustix", |
| 2619 | "windows-sys 0.61.2", | 2600 | "windows-sys 0.61.2", |
| 2620 | ] | 2601 | ] |
| 2621 | 2602 | ||
| 2622 | [[package]] | 2603 | [[package]] |
| 2623 | name = "terminal_size" | 2604 | name = "terminal_size" |
| 2624 | version = "0.3.0" | 2605 | version = "0.4.4" |
| 2625 | source = "registry+https://github.com/rust-lang/crates.io-index" | 2606 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2626 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" | 2607 | checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" |
| 2627 | dependencies = [ | 2608 | dependencies = [ |
| 2628 | "rustix 0.38.44", | 2609 | "rustix", |
| 2629 | "windows-sys 0.48.0", | 2610 | "windows-sys 0.61.2", |
| 2630 | ] | 2611 | ] |
| 2631 | 2612 | ||
| 2632 | [[package]] | 2613 | [[package]] |
| @@ -3340,15 +3321,6 @@ dependencies = [ | |||
| 3340 | 3321 | ||
| 3341 | [[package]] | 3322 | [[package]] |
| 3342 | name = "windows-sys" | 3323 | name = "windows-sys" |
| 3343 | version = "0.48.0" | ||
| 3344 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3345 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" | ||
| 3346 | dependencies = [ | ||
| 3347 | "windows-targets 0.48.5", | ||
| 3348 | ] | ||
| 3349 | |||
| 3350 | [[package]] | ||
| 3351 | name = "windows-sys" | ||
| 3352 | version = "0.52.0" | 3324 | version = "0.52.0" |
| 3353 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3325 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3354 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | 3326 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" |
| @@ -3391,21 +3363,6 @@ dependencies = [ | |||
| 3391 | 3363 | ||
| 3392 | [[package]] | 3364 | [[package]] |
| 3393 | name = "windows-targets" | 3365 | name = "windows-targets" |
| 3394 | version = "0.48.5" | ||
| 3395 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3396 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" | ||
| 3397 | dependencies = [ | ||
| 3398 | "windows_aarch64_gnullvm 0.48.5", | ||
| 3399 | "windows_aarch64_msvc 0.48.5", | ||
| 3400 | "windows_i686_gnu 0.48.5", | ||
| 3401 | "windows_i686_msvc 0.48.5", | ||
| 3402 | "windows_x86_64_gnu 0.48.5", | ||
| 3403 | "windows_x86_64_gnullvm 0.48.5", | ||
| 3404 | "windows_x86_64_msvc 0.48.5", | ||
| 3405 | ] | ||
| 3406 | |||
| 3407 | [[package]] | ||
| 3408 | name = "windows-targets" | ||
| 3409 | version = "0.52.6" | 3366 | version = "0.52.6" |
| 3410 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3367 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3411 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | 3368 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" |
| @@ -3472,12 +3429,6 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" | |||
| 3472 | 3429 | ||
| 3473 | [[package]] | 3430 | [[package]] |
| 3474 | name = "windows_aarch64_gnullvm" | 3431 | name = "windows_aarch64_gnullvm" |
| 3475 | version = "0.48.5" | ||
| 3476 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3477 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" | ||
| 3478 | |||
| 3479 | [[package]] | ||
| 3480 | name = "windows_aarch64_gnullvm" | ||
| 3481 | version = "0.52.6" | 3432 | version = "0.52.6" |
| 3482 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3433 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3483 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | 3434 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" |
| @@ -3496,12 +3447,6 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" | |||
| 3496 | 3447 | ||
| 3497 | [[package]] | 3448 | [[package]] |
| 3498 | name = "windows_aarch64_msvc" | 3449 | name = "windows_aarch64_msvc" |
| 3499 | version = "0.48.5" | ||
| 3500 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3501 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" | ||
| 3502 | |||
| 3503 | [[package]] | ||
| 3504 | name = "windows_aarch64_msvc" | ||
| 3505 | version = "0.52.6" | 3450 | version = "0.52.6" |
| 3506 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3451 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3507 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | 3452 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" |
| @@ -3520,12 +3465,6 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" | |||
| 3520 | 3465 | ||
| 3521 | [[package]] | 3466 | [[package]] |
| 3522 | name = "windows_i686_gnu" | 3467 | name = "windows_i686_gnu" |
| 3523 | version = "0.48.5" | ||
| 3524 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3525 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" | ||
| 3526 | |||
| 3527 | [[package]] | ||
| 3528 | name = "windows_i686_gnu" | ||
| 3529 | version = "0.52.6" | 3468 | version = "0.52.6" |
| 3530 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3469 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3531 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | 3470 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" |
| @@ -3556,12 +3495,6 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" | |||
| 3556 | 3495 | ||
| 3557 | [[package]] | 3496 | [[package]] |
| 3558 | name = "windows_i686_msvc" | 3497 | name = "windows_i686_msvc" |
| 3559 | version = "0.48.5" | ||
| 3560 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3561 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" | ||
| 3562 | |||
| 3563 | [[package]] | ||
| 3564 | name = "windows_i686_msvc" | ||
| 3565 | version = "0.52.6" | 3498 | version = "0.52.6" |
| 3566 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3499 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3567 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | 3500 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" |
| @@ -3580,12 +3513,6 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" | |||
| 3580 | 3513 | ||
| 3581 | [[package]] | 3514 | [[package]] |
| 3582 | name = "windows_x86_64_gnu" | 3515 | name = "windows_x86_64_gnu" |
| 3583 | version = "0.48.5" | ||
| 3584 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3585 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" | ||
| 3586 | |||
| 3587 | [[package]] | ||
| 3588 | name = "windows_x86_64_gnu" | ||
| 3589 | version = "0.52.6" | 3516 | version = "0.52.6" |
| 3590 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3517 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3591 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | 3518 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" |
| @@ -3604,12 +3531,6 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" | |||
| 3604 | 3531 | ||
| 3605 | [[package]] | 3532 | [[package]] |
| 3606 | name = "windows_x86_64_gnullvm" | 3533 | name = "windows_x86_64_gnullvm" |
| 3607 | version = "0.48.5" | ||
| 3608 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3609 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" | ||
| 3610 | |||
| 3611 | [[package]] | ||
| 3612 | name = "windows_x86_64_gnullvm" | ||
| 3613 | version = "0.52.6" | 3534 | version = "0.52.6" |
| 3614 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3535 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3615 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | 3536 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" |
| @@ -3628,12 +3549,6 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" | |||
| 3628 | 3549 | ||
| 3629 | [[package]] | 3550 | [[package]] |
| 3630 | name = "windows_x86_64_msvc" | 3551 | name = "windows_x86_64_msvc" |
| 3631 | version = "0.48.5" | ||
| 3632 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3633 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" | ||
| 3634 | |||
| 3635 | [[package]] | ||
| 3636 | name = "windows_x86_64_msvc" | ||
| 3637 | version = "0.52.6" | 3552 | version = "0.52.6" |
| 3638 | source = "registry+https://github.com/rust-lang/crates.io-index" | 3553 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3639 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | 3554 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" |
| @@ -3798,7 +3713,7 @@ dependencies = [ | |||
| 3798 | "hex", | 3713 | "hex", |
| 3799 | "libc", | 3714 | "libc", |
| 3800 | "ordered-stream", | 3715 | "ordered-stream", |
| 3801 | "rustix 1.1.4", | 3716 | "rustix", |
| 3802 | "serde", | 3717 | "serde", |
| 3803 | "serde_repr", | 3718 | "serde_repr", |
| 3804 | "tracing", | 3719 | "tracing", |
| @@ -33,7 +33,7 @@ libc = "0.2.186" | |||
| 33 | ctrlc = "3.5.2" | 33 | ctrlc = "3.5.2" |
| 34 | rand = "0.10.1" | 34 | rand = "0.10.1" |
| 35 | notify-rust = "4.17.0" | 35 | notify-rust = "4.17.0" |
| 36 | terminal_size = "0.3" | 36 | terminal_size = "0.4.4" |
| 37 | 37 | ||
| 38 | [dev-dependencies] | 38 | [dev-dependencies] |
| 39 | tempfile = "3.17" | 39 | tempfile = "3.17" |
| @@ -60,6 +60,8 @@ | |||
| 60 | 60 | ||
| 61 | - [x] **Shuffle first song** - `update_shuffle_indices` now shuffles all indices when position is 0; directory playback uses `q.current()` instead of `files.first()`. | 61 | - [x] **Shuffle first song** - `update_shuffle_indices` now shuffles all indices when position is 0; directory playback uses `q.current()` instead of `files.first()`. |
| 62 | 62 | ||
| 63 | - [x] **Customize status/notification display** - Added `display_format` config with `$ARTIST`, `$ALBUM`, `$TRACK` variables. Unified format for queue display, status, and notifications. | ||
| 64 | |||
| 63 | ## Remaining Work | 65 | ## Remaining Work |
| 64 | 66 | ||
| 65 | ### Phase 1-3: Completed ✓ | 67 | ### Phase 1-3: Completed ✓ |
| @@ -75,6 +77,6 @@ | |||
| 75 | 77 | ||
| 76 | - [ ] **Scrobbling (Last.fm/ListenBrainz)** - Add scrobbling support via configured credentials. | 78 | - [ ] **Scrobbling (Last.fm/ListenBrainz)** - Add scrobbling support via configured credentials. |
| 77 | 79 | ||
| 78 | - [ ] **Customize status display** - Variables like $ARTIST, $ALBUM, $TRACK available in config so `queue`, `status` and notifs can be customized | ||
| 79 | |||
| 80 | - [ ] **Dynamic autocompletion of artists, albums and tracks** - Let shell communicate with the daemon so it returns autocompletions for shell | 80 | - [ ] **Dynamic autocompletion of artists, albums and tracks** - Let shell communicate with the daemon so it returns autocompletions for shell |
| 81 | |||
| 82 | - [ ] **Cache config in daemon** - Avoid repeated Config::load() calls in daemon hot paths; pass display_format through command chain instead | ||
diff --git a/rsplayer.service b/rsplayer.service index 4f6b36d..7a64671 100644 --- a/rsplayer.service +++ b/rsplayer.service | |||
| @@ -4,9 +4,9 @@ After=network.target sound.target | |||
| 4 | 4 | ||
| 5 | [Service] | 5 | [Service] |
| 6 | Type=simple | 6 | Type=simple |
| 7 | ExecStart=/usr/bin/rsp start | 7 | ExecStart=/usr/bin/rsp start-daemon |
| 8 | Restart=on-failure | 8 | Restart=on-failure |
| 9 | RestartSec=5 | 9 | RestartSec=5 |
| 10 | 10 | ||
| 11 | [Install] | 11 | [Install] |
| 12 | WantedBy=default.target \ No newline at end of file | 12 | WantedBy=default.target |
| @@ -14,9 +14,9 @@ pub struct Cli { | |||
| 14 | #[derive(Subcommand)] | 14 | #[derive(Subcommand)] |
| 15 | pub enum Commands { | 15 | pub enum Commands { |
| 16 | /// Start the daemon | 16 | /// Start the daemon |
| 17 | Start, | 17 | StartDaemon, |
| 18 | /// Stop the daemon | 18 | /// Stop the daemon |
| 19 | Stop, | 19 | StopDaemon, |
| 20 | /// Restart the daemon (kill existing and start new) | 20 | /// Restart the daemon (kill existing and start new) |
| 21 | Restart, | 21 | Restart, |
| 22 | /// Play a track, album, or artist | 22 | /// Play a track, album, or artist |
| @@ -37,7 +37,7 @@ pub enum Commands { | |||
| 37 | /// Toggle play/pause | 37 | /// Toggle play/pause |
| 38 | Toggle, | 38 | Toggle, |
| 39 | /// Stop playback | 39 | /// Stop playback |
| 40 | StopPlayback, | 40 | Stop, |
| 41 | /// Skip to next track | 41 | /// Skip to next track |
| 42 | Next, | 42 | Next, |
| 43 | /// Skip to previous track | 43 | /// Skip to previous track |
| @@ -101,10 +101,10 @@ pub enum SettingsCommands { | |||
| 101 | impl Cli { | 101 | impl Cli { |
| 102 | pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> { | 102 | pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> { |
| 103 | match &self.command { | 103 | match &self.command { |
| 104 | Commands::Start => { | 104 | Commands::StartDaemon => { |
| 105 | crate::daemon::start_daemon()?; | 105 | crate::daemon::start_daemon()?; |
| 106 | } | 106 | } |
| 107 | Commands::Stop => { | 107 | Commands::StopDaemon => { |
| 108 | crate::daemon::stop_daemon()?; | 108 | crate::daemon::stop_daemon()?; |
| 109 | } | 109 | } |
| 110 | Commands::Restart => { | 110 | Commands::Restart => { |
| @@ -135,7 +135,7 @@ impl Cli { | |||
| 135 | Commands::Toggle => { | 135 | Commands::Toggle => { |
| 136 | crate::daemon::send_command("toggle".to_string())?; | 136 | crate::daemon::send_command("toggle".to_string())?; |
| 137 | } | 137 | } |
| 138 | Commands::StopPlayback => { | 138 | Commands::Stop => { |
| 139 | crate::daemon::send_command("stop".to_string())?; | 139 | crate::daemon::send_command("stop".to_string())?; |
| 140 | } | 140 | } |
| 141 | Commands::Next => { | 141 | Commands::Next => { |
diff --git a/src/config.rs b/src/config.rs index 42573ed..5fd2f1b 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -9,13 +9,13 @@ pub struct Config { | |||
| 9 | pub config_path: PathBuf, | 9 | pub config_path: PathBuf, |
| 10 | pub music_paths: Vec<PathBuf>, | 10 | pub music_paths: Vec<PathBuf>, |
| 11 | pub scan_interval: Option<u64>, | 11 | pub scan_interval: Option<u64>, |
| 12 | pub socket_path: Option<PathBuf>, | ||
| 13 | pub lastfm_api_key: Option<String>, | 12 | pub lastfm_api_key: Option<String>, |
| 14 | pub lastfm_api_secret: Option<String>, | 13 | pub lastfm_api_secret: Option<String>, |
| 15 | pub lastfm_session_key: Option<String>, | 14 | pub lastfm_session_key: Option<String>, |
| 16 | pub listenbrainz_token: Option<String>, | 15 | pub listenbrainz_token: Option<String>, |
| 17 | pub volume: Option<u8>, | 16 | pub volume: Option<u8>, |
| 18 | pub shuffle: Option<String>, | 17 | pub shuffle: Option<String>, |
| 18 | pub display_format: Option<String>, | ||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | impl Config { | 21 | impl Config { |
| @@ -43,8 +43,13 @@ impl Config { | |||
| 43 | config_path: config_path.clone(), | 43 | config_path: config_path.clone(), |
| 44 | music_paths: vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))], | 44 | music_paths: vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))], |
| 45 | scan_interval: Some(3600), | 45 | scan_interval: Some(3600), |
| 46 | socket_path: None, | 46 | lastfm_api_key: None, |
| 47 | ..Default::default() | 47 | lastfm_api_secret: None, |
| 48 | lastfm_session_key: None, | ||
| 49 | listenbrainz_token: None, | ||
| 50 | volume: Some(50), | ||
| 51 | shuffle: None, | ||
| 52 | display_format: Some(String::from("$ARTIST - $TRACK")), | ||
| 48 | }; | 53 | }; |
| 49 | default.save()?; | 54 | default.save()?; |
| 50 | return Ok(default); | 55 | return Ok(default); |
| @@ -53,15 +58,72 @@ impl Config { | |||
| 53 | let content = fs::read_to_string(&config_path)?; | 58 | let content = fs::read_to_string(&config_path)?; |
| 54 | let mut config: Self = toml::from_str(&content)?; | 59 | let mut config: Self = toml::from_str(&content)?; |
| 55 | config.config_path = config_path; | 60 | config.config_path = config_path; |
| 61 | if config.music_paths.is_empty() { | ||
| 62 | config.music_paths = | ||
| 63 | vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))]; | ||
| 64 | } | ||
| 65 | if config.scan_interval.is_none() { | ||
| 66 | config.scan_interval = Some(3600); | ||
| 67 | } | ||
| 68 | config.save()?; | ||
| 56 | Ok(config) | 69 | Ok(config) |
| 57 | } | 70 | } |
| 58 | 71 | ||
| 59 | pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { | 72 | pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { |
| 60 | let content = toml::to_string_pretty(self)?; | 73 | let mut output = String::new(); |
| 74 | let paths_str = self | ||
| 75 | .music_paths | ||
| 76 | .iter() | ||
| 77 | .map(|p| format!("\"{}\"", p.display())) | ||
| 78 | .collect::<Vec<_>>() | ||
| 79 | .join(", "); | ||
| 80 | output.push_str(&format!("music_paths = [{}]\n", paths_str)); | ||
| 81 | output.push_str(&format!( | ||
| 82 | "scan_interval = {}\n", | ||
| 83 | self.scan_interval.unwrap_or(3600) | ||
| 84 | )); | ||
| 85 | let api_key = self | ||
| 86 | .lastfm_api_key | ||
| 87 | .as_ref() | ||
| 88 | .filter(|s| !s.is_empty()) | ||
| 89 | .map(|s| s.as_str()) | ||
| 90 | .unwrap_or(""); | ||
| 91 | output.push_str(&format!("lastfm_api_key = \"{}\"\n", api_key)); | ||
| 92 | let api_secret = self | ||
| 93 | .lastfm_api_secret | ||
| 94 | .as_ref() | ||
| 95 | .filter(|s| !s.is_empty()) | ||
| 96 | .map(|s| s.as_str()) | ||
| 97 | .unwrap_or(""); | ||
| 98 | output.push_str(&format!("lastfm_api_secret = \"{}\"\n", api_secret)); | ||
| 99 | let session_key = self | ||
| 100 | .lastfm_session_key | ||
| 101 | .as_ref() | ||
| 102 | .filter(|s| !s.is_empty()) | ||
| 103 | .map(|s| s.as_str()) | ||
| 104 | .unwrap_or(""); | ||
| 105 | output.push_str(&format!("lastfm_session_key = \"{}\"\n", session_key)); | ||
| 106 | let lb_token = self | ||
| 107 | .listenbrainz_token | ||
| 108 | .as_ref() | ||
| 109 | .filter(|s| !s.is_empty()) | ||
| 110 | .map(|s| s.as_str()) | ||
| 111 | .unwrap_or(""); | ||
| 112 | output.push_str(&format!("listenbrainz_token = \"{}\"\n", lb_token)); | ||
| 113 | output.push_str(&format!("volume = {}\n", self.volume.unwrap_or(0))); | ||
| 114 | let shuffle = self | ||
| 115 | .shuffle | ||
| 116 | .as_ref() | ||
| 117 | .filter(|s| !s.is_empty()) | ||
| 118 | .map(|s| s.as_str()) | ||
| 119 | .unwrap_or(""); | ||
| 120 | output.push_str(&format!("shuffle = \"{}\"\n", shuffle)); | ||
| 121 | let fmt = self.display_format.clone().unwrap_or_default(); | ||
| 122 | output.push_str(&format!("display_format = \"{}\"\n", fmt)); | ||
| 61 | if let Some(parent) = self.config_path.parent() { | 123 | if let Some(parent) = self.config_path.parent() { |
| 62 | fs::create_dir_all(parent)?; | 124 | fs::create_dir_all(parent)?; |
| 63 | } | 125 | } |
| 64 | fs::write(&self.config_path, content)?; | 126 | fs::write(&self.config_path, output)?; |
| 65 | Ok(()) | 127 | Ok(()) |
| 66 | } | 128 | } |
| 67 | 129 | ||
| @@ -77,32 +139,39 @@ impl Config { | |||
| 77 | .scan_interval | 139 | .scan_interval |
| 78 | .map(|i| i.to_string()) | 140 | .map(|i| i.to_string()) |
| 79 | .unwrap_or_else(|| "not set".to_string())), | 141 | .unwrap_or_else(|| "not set".to_string())), |
| 80 | "socket_path" => Ok(self | ||
| 81 | .socket_path | ||
| 82 | .as_ref() | ||
| 83 | .map(|p| p.display().to_string()) | ||
| 84 | .unwrap_or_else(|| "default".to_string())), | ||
| 85 | "lastfm_api_key" => Ok(self | 142 | "lastfm_api_key" => Ok(self |
| 86 | .lastfm_api_key | 143 | .lastfm_api_key |
| 87 | .clone() | 144 | .as_ref() |
| 145 | .filter(|s| !s.is_empty()) | ||
| 146 | .map(|_| "<set>".to_string()) | ||
| 88 | .unwrap_or_else(|| "not set".to_string())), | 147 | .unwrap_or_else(|| "not set".to_string())), |
| 89 | "lastfm_api_secret" => Ok(self | 148 | "lastfm_api_secret" => Ok(self |
| 90 | .lastfm_api_secret | 149 | .lastfm_api_secret |
| 91 | .clone() | 150 | .as_ref() |
| 151 | .filter(|s| !s.is_empty()) | ||
| 152 | .map(|_| "<set>".to_string()) | ||
| 92 | .unwrap_or_else(|| "not set".to_string())), | 153 | .unwrap_or_else(|| "not set".to_string())), |
| 93 | "lastfm_session_key" => Ok(self | 154 | "lastfm_session_key" => Ok(self |
| 94 | .lastfm_session_key | 155 | .lastfm_session_key |
| 95 | .clone() | 156 | .as_ref() |
| 157 | .filter(|s| !s.is_empty()) | ||
| 158 | .map(|_| "<set>".to_string()) | ||
| 96 | .unwrap_or_else(|| "not set".to_string())), | 159 | .unwrap_or_else(|| "not set".to_string())), |
| 97 | "listenbrainz_token" => Ok(self | 160 | "listenbrainz_token" => Ok(self |
| 98 | .listenbrainz_token | 161 | .listenbrainz_token |
| 99 | .clone() | 162 | .as_ref() |
| 163 | .filter(|s| !s.is_empty()) | ||
| 164 | .map(|_| "<set>".to_string()) | ||
| 100 | .unwrap_or_else(|| "not set".to_string())), | 165 | .unwrap_or_else(|| "not set".to_string())), |
| 101 | "volume" => Ok(self | 166 | "volume" => Ok(self |
| 102 | .volume | 167 | .volume |
| 103 | .map(|v| v.to_string()) | 168 | .map(|v| v.to_string()) |
| 104 | .unwrap_or_else(|| "not set".to_string())), | 169 | .unwrap_or_else(|| "not set".to_string())), |
| 105 | "shuffle" => Ok(self.shuffle.clone().unwrap_or_else(|| "off".to_string())), | 170 | "shuffle" => Ok(self.shuffle.clone().unwrap_or_else(|| "off".to_string())), |
| 171 | "display_format" => Ok(self | ||
| 172 | .display_format | ||
| 173 | .clone() | ||
| 174 | .unwrap_or_else(|| "not set".to_string())), | ||
| 106 | _ => Err(format!("Unknown setting: {}", key).into()), | 175 | _ => Err(format!("Unknown setting: {}", key).into()), |
| 107 | } | 176 | } |
| 108 | } | 177 | } |
| @@ -115,9 +184,6 @@ impl Config { | |||
| 115 | "scan_interval" => { | 184 | "scan_interval" => { |
| 116 | self.scan_interval = Some(value.parse()?); | 185 | self.scan_interval = Some(value.parse()?); |
| 117 | } | 186 | } |
| 118 | "socket_path" => { | ||
| 119 | self.socket_path = Some(PathBuf::from(value)); | ||
| 120 | } | ||
| 121 | "lastfm_api_key" => { | 187 | "lastfm_api_key" => { |
| 122 | self.lastfm_api_key = Some(value.to_string()); | 188 | self.lastfm_api_key = Some(value.to_string()); |
| 123 | } | 189 | } |
| @@ -142,45 +208,97 @@ impl Config { | |||
| 142 | return Err(format!("Invalid shuffle value: {}", value).into()); | 208 | return Err(format!("Invalid shuffle value: {}", value).into()); |
| 143 | } | 209 | } |
| 144 | } | 210 | } |
| 211 | "display_format" => { | ||
| 212 | self.display_format = Some(value.to_string()); | ||
| 213 | } | ||
| 145 | _ => return Err(format!("Unknown setting: {}", key).into()), | 214 | _ => return Err(format!("Unknown setting: {}", key).into()), |
| 146 | } | 215 | } |
| 147 | Ok(()) | 216 | Ok(()) |
| 148 | } | 217 | } |
| 149 | 218 | ||
| 150 | pub fn list(&self) { | 219 | pub fn list(&self) { |
| 151 | println!("music_paths = {:?}", self.music_paths); | 220 | println!( |
| 152 | println!("scan_interval = {:?}", self.scan_interval); | 221 | "music_paths = {}", |
| 153 | println!("socket_path = {:?}", self.socket_path); | 222 | self.music_paths |
| 154 | println!("lastfm_api_key = {:?}", self.lastfm_api_key); | 223 | .iter() |
| 155 | println!("lastfm_api_secret = {:?}", self.lastfm_api_secret); | 224 | .map(|p| p.display().to_string()) |
| 156 | println!("lastfm_session_key = {:?}", self.lastfm_session_key); | 225 | .collect::<Vec<_>>() |
| 157 | println!("listenbrainz_token = {:?}", self.listenbrainz_token); | 226 | .join(", ") |
| 158 | println!("volume = {:?}", self.volume); | 227 | ); |
| 159 | println!("shuffle = {:?}", self.shuffle); | 228 | println!( |
| 229 | "scan_interval = {}", | ||
| 230 | self.scan_interval | ||
| 231 | .map_or("not set".to_string(), |v| v.to_string()) | ||
| 232 | ); | ||
| 233 | println!( | ||
| 234 | "lastfm_api_key = {}", | ||
| 235 | self.lastfm_api_key | ||
| 236 | .as_ref() | ||
| 237 | .filter(|s| !s.is_empty()) | ||
| 238 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 239 | ); | ||
| 240 | println!( | ||
| 241 | "lastfm_api_secret = {}", | ||
| 242 | self.lastfm_api_secret | ||
| 243 | .as_ref() | ||
| 244 | .filter(|s| !s.is_empty()) | ||
| 245 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 246 | ); | ||
| 247 | println!( | ||
| 248 | "lastfm_session_key = {}", | ||
| 249 | self.lastfm_session_key | ||
| 250 | .as_ref() | ||
| 251 | .filter(|s| !s.is_empty()) | ||
| 252 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 253 | ); | ||
| 254 | println!( | ||
| 255 | "listenbrainz_token = {}", | ||
| 256 | self.listenbrainz_token | ||
| 257 | .as_ref() | ||
| 258 | .filter(|s| !s.is_empty()) | ||
| 259 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 260 | ); | ||
| 261 | println!( | ||
| 262 | "volume = {}", | ||
| 263 | self.volume | ||
| 264 | .filter(|&v| v != 0) | ||
| 265 | .map_or("not set".to_string(), |v| v.to_string()) | ||
| 266 | ); | ||
| 267 | println!( | ||
| 268 | "shuffle = {}", | ||
| 269 | self.shuffle | ||
| 270 | .clone() | ||
| 271 | .filter(|s| !s.is_empty()) | ||
| 272 | .unwrap_or_else(|| "off".to_string()) | ||
| 273 | ); | ||
| 274 | println!( | ||
| 275 | "display_format = {}", | ||
| 276 | self.display_format | ||
| 277 | .clone() | ||
| 278 | .unwrap_or_else(|| "not set".to_string()) | ||
| 279 | ); | ||
| 160 | } | 280 | } |
| 161 | 281 | ||
| 162 | pub fn unset(&mut self, key: &str) -> Result<(), Box<dyn std::error::Error>> { | 282 | pub fn unset(&mut self, key: &str) -> Result<(), Box<dyn std::error::Error>> { |
| 163 | match key { | 283 | match key { |
| 164 | "music_paths" => self.music_paths.clear(), | 284 | "music_paths" => self.music_paths.clear(), |
| 165 | "scan_interval" => self.scan_interval = None, | 285 | "scan_interval" => self.scan_interval = None, |
| 166 | "socket_path" => self.socket_path = None, | ||
| 167 | "lastfm_api_key" => self.lastfm_api_key = None, | 286 | "lastfm_api_key" => self.lastfm_api_key = None, |
| 168 | "lastfm_api_secret" => self.lastfm_api_secret = None, | 287 | "lastfm_api_secret" => self.lastfm_api_secret = None, |
| 169 | "lastfm_session_key" => self.lastfm_session_key = None, | 288 | "lastfm_session_key" => self.lastfm_session_key = None, |
| 170 | "listenbrainz_token" => self.listenbrainz_token = None, | 289 | "listenbrainz_token" => self.listenbrainz_token = None, |
| 171 | "volume" => self.volume = None, | 290 | "volume" => self.volume = None, |
| 172 | "shuffle" => self.shuffle = None, | 291 | "shuffle" => self.shuffle = None, |
| 292 | "display_format" => self.display_format = None, | ||
| 173 | _ => return Err(format!("Unknown setting: {}", key).into()), | 293 | _ => return Err(format!("Unknown setting: {}", key).into()), |
| 174 | } | 294 | } |
| 175 | Ok(()) | 295 | Ok(()) |
| 176 | } | 296 | } |
| 177 | 297 | ||
| 178 | pub fn get_socket_path(&self) -> PathBuf { | 298 | pub fn get_socket_path() -> PathBuf { |
| 179 | self.socket_path.clone().unwrap_or_else(|| { | 299 | let runtime_dir = std::env::var("XDG_RUNTIME_DIR") |
| 180 | let runtime_dir = std::env::var("XDG_RUNTIME_DIR") | 300 | .map(PathBuf::from) |
| 181 | .map(PathBuf::from) | 301 | .unwrap_or_else(|_| PathBuf::from("/tmp")); |
| 182 | .unwrap_or_else(|_| PathBuf::from("/tmp")); | 302 | runtime_dir.join("rsplayer").join("daemon.sock") |
| 183 | runtime_dir.join("rsplayer").join("daemon.sock") | ||
| 184 | }) | ||
| 185 | } | 303 | } |
| 186 | } | 304 | } |
diff --git a/src/daemon.rs b/src/daemon.rs index b79e67a..acc7729 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -12,7 +12,7 @@ use std::time::Duration; | |||
| 12 | 12 | ||
| 13 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | 13 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { |
| 14 | let config = Config::load()?; | 14 | let config = Config::load()?; |
| 15 | let socket_path = config.get_socket_path(); | 15 | let socket_path = Config::get_socket_path(); |
| 16 | let pid_path = get_pid_path(&config); | 16 | let pid_path = get_pid_path(&config); |
| 17 | 17 | ||
| 18 | // Check if daemon is already running | 18 | // Check if daemon is already running |
| @@ -55,7 +55,6 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 55 | let queue = Arc::new(Mutex::new(Queue::new())); | 55 | let queue = Arc::new(Mutex::new(Queue::new())); |
| 56 | 56 | ||
| 57 | // Load and apply saved settings | 57 | // Load and apply saved settings |
| 58 | let config = Config::load().unwrap_or_default(); | ||
| 59 | if let Some(vol) = config.volume { | 58 | if let Some(vol) = config.volume { |
| 60 | audio_engine.set_volume(vol as f32 / 100.0); | 59 | audio_engine.set_volume(vol as f32 / 100.0); |
| 61 | } | 60 | } |
| @@ -69,6 +68,9 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 69 | } | 68 | } |
| 70 | } | 69 | } |
| 71 | 70 | ||
| 71 | // Save full config | ||
| 72 | config.save()?; | ||
| 73 | |||
| 72 | // Accept connections until shutdown | 74 | // Accept connections until shutdown |
| 73 | listener.set_nonblocking(true)?; | 75 | listener.set_nonblocking(true)?; |
| 74 | // Helper: poll until a specific track actually starts playing or timeout | 76 | // Helper: poll until a specific track actually starts playing or timeout |
| @@ -106,7 +108,8 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 106 | audio_engine.play(next_track.clone()); | 108 | audio_engine.play(next_track.clone()); |
| 107 | if wait_for_start(&audio_engine, &next_track, 500) { | 109 | if wait_for_start(&audio_engine, &next_track, 500) { |
| 108 | started = true; | 110 | started = true; |
| 109 | notification::notify_track(&Track::from_path(next_track)); | 111 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 112 | notification::notify_track(&Track::from_path(next_track), fmt.as_deref()); | ||
| 110 | break; | 113 | break; |
| 111 | } | 114 | } |
| 112 | } else { | 115 | } else { |
| @@ -182,7 +185,7 @@ pub fn stop_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 182 | } | 185 | } |
| 183 | 186 | ||
| 184 | // Clean up files | 187 | // Clean up files |
| 185 | let socket_path = config.get_socket_path(); | 188 | let socket_path = Config::get_socket_path(); |
| 186 | std::fs::remove_file(&socket_path).ok(); | 189 | std::fs::remove_file(&socket_path).ok(); |
| 187 | std::fs::remove_file(&pid_path).ok(); | 190 | std::fs::remove_file(&pid_path).ok(); |
| 188 | 191 | ||
| @@ -203,7 +206,7 @@ pub fn restart_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 203 | // Start new daemon in background | 206 | // Start new daemon in background |
| 204 | let current_exe = std::env::current_exe()?; | 207 | let current_exe = std::env::current_exe()?; |
| 205 | std::process::Command::new(current_exe) | 208 | std::process::Command::new(current_exe) |
| 206 | .arg("start") | 209 | .arg("start-daemon") |
| 207 | .spawn()?; | 210 | .spawn()?; |
| 208 | 211 | ||
| 209 | println!("Daemon restarted"); | 212 | println!("Daemon restarted"); |
| @@ -216,8 +219,7 @@ pub fn send_command(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | |||
| 216 | } | 219 | } |
| 217 | 220 | ||
| 218 | fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | 221 | fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { |
| 219 | let config = Config::load()?; | 222 | let socket_path = Config::get_socket_path(); |
| 220 | let socket_path = config.get_socket_path(); | ||
| 221 | 223 | ||
| 222 | let mut stream = UnixStream::connect(socket_path)?; | 224 | let mut stream = UnixStream::connect(socket_path)?; |
| 223 | stream.write_all(cmd.as_bytes())?; | 225 | stream.write_all(cmd.as_bytes())?; |
| @@ -254,7 +256,11 @@ fn process_command( | |||
| 254 | if audio.is_playing() { | 256 | if audio.is_playing() { |
| 255 | if let Some(track_path) = audio.current_track() { | 257 | if let Some(track_path) = audio.current_track() { |
| 256 | let track = Track::from_path(track_path); | 258 | let track = Track::from_path(track_path); |
| 257 | let track_str = track.display_string(0, 1); | 259 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 260 | let track_str = match fmt.as_deref() { | ||
| 261 | Some(f) => track.format(f), | ||
| 262 | None => track.display_string(0, 1), | ||
| 263 | }; | ||
| 258 | format!("Playing\nTrack: {}\n", track_str) | 264 | format!("Playing\nTrack: {}\n", track_str) |
| 259 | } else { | 265 | } else { |
| 260 | "Playing\n".to_string() | 266 | "Playing\n".to_string() |
| @@ -296,7 +302,11 @@ fn process_command( | |||
| 296 | if let Ok(mut q) = queue.lock() { | 302 | if let Ok(mut q) = queue.lock() { |
| 297 | if let Some(next_track) = q.next() { | 303 | if let Some(next_track) = q.next() { |
| 298 | audio.play(next_track.clone()); | 304 | audio.play(next_track.clone()); |
| 299 | notification::notify_track(&Track::from_path(next_track)); | 305 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 306 | notification::notify_track( | ||
| 307 | &Track::from_path(next_track.clone()), | ||
| 308 | fmt.as_deref(), | ||
| 309 | ); | ||
| 300 | "Playing next track\n".to_string() | 310 | "Playing next track\n".to_string() |
| 301 | } else { | 311 | } else { |
| 302 | audio.stop(); | 312 | audio.stop(); |
| @@ -311,7 +321,11 @@ fn process_command( | |||
| 311 | if let Ok(mut q) = queue.lock() { | 321 | if let Ok(mut q) = queue.lock() { |
| 312 | if let Some(prev_track) = q.previous() { | 322 | if let Some(prev_track) = q.previous() { |
| 313 | audio.play(prev_track.clone()); | 323 | audio.play(prev_track.clone()); |
| 314 | notification::notify_track(&Track::from_path(prev_track)); | 324 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 325 | notification::notify_track( | ||
| 326 | &Track::from_path(prev_track.clone()), | ||
| 327 | fmt.as_deref(), | ||
| 328 | ); | ||
| 315 | "Playing previous track\n".to_string() | 329 | "Playing previous track\n".to_string() |
| 316 | } else { | 330 | } else { |
| 317 | "No previous track\n".to_string() | 331 | "No previous track\n".to_string() |
| @@ -371,7 +385,13 @@ fn process_command( | |||
| 371 | } else { | 385 | } else { |
| 372 | ("last", rest) | 386 | ("last", rest) |
| 373 | }; | 387 | }; |
| 374 | let path_str = path_str.to_string(); | 388 | let path_str = if path_str.starts_with("~/") { |
| 389 | std::env::var("HOME") | ||
| 390 | .map(|h| path_str.replacen("~", &h, 1)) | ||
| 391 | .unwrap_or_else(|_| path_str.to_string()) | ||
| 392 | } else { | ||
| 393 | path_str.to_string() | ||
| 394 | }; | ||
| 375 | 395 | ||
| 376 | if path_str.is_empty() { | 396 | if path_str.is_empty() { |
| 377 | return "Usage: play [now|next|last] <path>\n".to_string(); | 397 | return "Usage: play [now|next|last] <path>\n".to_string(); |
| @@ -390,7 +410,9 @@ fn process_command( | |||
| 390 | } | 410 | } |
| 391 | if let Some(track) = q.track_at(q.position()) { | 411 | if let Some(track) = q.track_at(q.position()) { |
| 392 | audio.play(track.path.clone()); | 412 | audio.play(track.path.clone()); |
| 393 | notification::notify_track(track); | 413 | let fmt = |
| 414 | Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 415 | notification::notify_track(track, fmt.as_deref()); | ||
| 394 | } | 416 | } |
| 395 | format!("Playing now: {}\n", path_str) | 417 | format!("Playing now: {}\n", path_str) |
| 396 | } | 418 | } |
| @@ -405,7 +427,9 @@ fn process_command( | |||
| 405 | if was_empty && !was_playing { | 427 | if was_empty && !was_playing { |
| 406 | let track = Track::from_path(path.clone()); | 428 | let track = Track::from_path(path.clone()); |
| 407 | audio.play(path); | 429 | audio.play(path); |
| 408 | notification::notify_track(&track); | 430 | let fmt = |
| 431 | Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 432 | notification::notify_track(&track, fmt.as_deref()); | ||
| 409 | format!("Playing: {}\n", path_str) | 433 | format!("Playing: {}\n", path_str) |
| 410 | } else { | 434 | } else { |
| 411 | format!("Queued: {}\n", path_str) | 435 | format!("Queued: {}\n", path_str) |
| @@ -431,7 +455,10 @@ fn process_command( | |||
| 431 | } | 455 | } |
| 432 | if let Some(track) = q.track_at(q.position()) { | 456 | if let Some(track) = q.track_at(q.position()) { |
| 433 | audio.play(track.path.clone()); | 457 | audio.play(track.path.clone()); |
| 434 | notification::notify_track(track); | 458 | let fmt = Config::load() |
| 459 | .ok() | ||
| 460 | .and_then(|c| c.display_format.clone()); | ||
| 461 | notification::notify_track(track, fmt.as_deref()); | ||
| 435 | } | 462 | } |
| 436 | format!( | 463 | format!( |
| 437 | "Playing now: {} tracks from {}\n", | 464 | "Playing now: {} tracks from {}\n", |
| @@ -459,7 +486,10 @@ fn process_command( | |||
| 459 | && let Some(track) = q.current() | 486 | && let Some(track) = q.current() |
| 460 | { | 487 | { |
| 461 | audio.play(track.path.clone()); | 488 | audio.play(track.path.clone()); |
| 462 | notification::notify_track(track); | 489 | let fmt = Config::load() |
| 490 | .ok() | ||
| 491 | .and_then(|c| c.display_format.clone()); | ||
| 492 | notification::notify_track(track, fmt.as_deref()); | ||
| 463 | } | 493 | } |
| 464 | format!("Queued {} tracks from: {}\n", files.len(), path_str) | 494 | format!("Queued {} tracks from: {}\n", files.len(), path_str) |
| 465 | } | 495 | } |
| @@ -512,9 +542,13 @@ fn process_command( | |||
| 512 | q.shuffled_indices().to_vec() | 542 | q.shuffled_indices().to_vec() |
| 513 | }; | 543 | }; |
| 514 | let is_playing = audio.is_playing(); | 544 | let is_playing = audio.is_playing(); |
| 545 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 515 | for (display_idx, &track_idx) in indices.iter().enumerate() { | 546 | for (display_idx, &track_idx) in indices.iter().enumerate() { |
| 516 | if let Some(track) = q.track_at(track_idx) { | 547 | if let Some(track) = q.track_at(track_idx) { |
| 517 | let track_str = track.display_string(display_idx, q.len()); | 548 | let track_str = match fmt.as_deref() { |
| 549 | Some(f) => track.format(f), | ||
| 550 | None => track.display_string(display_idx, q.len()), | ||
| 551 | }; | ||
| 518 | if track_idx == playing_track_idx && is_playing { | 552 | if track_idx == playing_track_idx && is_playing { |
| 519 | output.push_str(&format!("> {} (playing)\n", track_str)); | 553 | output.push_str(&format!("> {} (playing)\n", track_str)); |
| 520 | } else { | 554 | } else { |
diff --git a/src/notification.rs b/src/notification.rs index 2f7baa2..4947e6f 100644 --- a/src/notification.rs +++ b/src/notification.rs | |||
| @@ -1,15 +1,18 @@ | |||
| 1 | use crate::queue::Track; | 1 | use crate::queue::Track; |
| 2 | 2 | ||
| 3 | pub fn notify_track(track: &Track) { | 3 | pub fn notify_track(track: &Track, format: Option<&str>) { |
| 4 | let body = match (track.artist.as_deref(), track.title.as_deref()) { | 4 | let body = match format { |
| 5 | (Some(a), Some(t)) => format!("{} - {}", a, t), | 5 | Some(fmt) => track.format(fmt), |
| 6 | (_, Some(t)) => t.to_string(), | 6 | None => match (track.artist.as_deref(), track.title.as_deref()) { |
| 7 | _ => track | 7 | (Some(a), Some(t)) => format!("{} - {}", a, t), |
| 8 | .path | 8 | (_, Some(t)) => t.to_string(), |
| 9 | .file_name() | 9 | _ => track |
| 10 | .and_then(|n| n.to_str()) | 10 | .path |
| 11 | .unwrap_or("Unknown") | 11 | .file_name() |
| 12 | .to_string(), | 12 | .and_then(|n| n.to_str()) |
| 13 | .unwrap_or("Unknown") | ||
| 14 | .to_string(), | ||
| 15 | }, | ||
| 13 | }; | 16 | }; |
| 14 | 17 | ||
| 15 | let _ = notify_rust::Notification::new() | 18 | let _ = notify_rust::Notification::new() |
diff --git a/src/queue.rs b/src/queue.rs index 2c20356..d42203f 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -117,6 +117,22 @@ impl Track { | |||
| 117 | format!("{:0width$}. {} - {}", index + 1, album_artist, title) | 117 | format!("{:0width$}. {} - {}", index + 1, album_artist, title) |
| 118 | } | 118 | } |
| 119 | } | 119 | } |
| 120 | |||
| 121 | pub fn format(&self, format_str: &str) -> String { | ||
| 122 | let artist = self.artist.clone().unwrap_or_default(); | ||
| 123 | let album = self.album.clone().unwrap_or_default(); | ||
| 124 | let title = self.title.clone().unwrap_or_else(|| { | ||
| 125 | self.path | ||
| 126 | .file_name() | ||
| 127 | .and_then(|n| n.to_str()) | ||
| 128 | .unwrap_or("Unknown") | ||
| 129 | .to_string() | ||
| 130 | }); | ||
| 131 | format_str | ||
| 132 | .replace("$ARTIST", &artist) | ||
| 133 | .replace("$ALBUM", &album) | ||
| 134 | .replace("$TRACK", &title) | ||
| 135 | } | ||
| 120 | } | 136 | } |
| 121 | 137 | ||
| 122 | pub struct Queue { | 138 | pub struct Queue { |
diff --git a/tests/config.rs b/tests/config.rs index 70c5034..1702db9 100644 --- a/tests/config.rs +++ b/tests/config.rs | |||
| @@ -7,13 +7,13 @@ fn test_config_get_scan_interval() { | |||
| 7 | config_path: PathBuf::from("/test/config.toml"), | 7 | config_path: PathBuf::from("/test/config.toml"), |
| 8 | music_paths: vec![], | 8 | music_paths: vec![], |
| 9 | scan_interval: Some(3600), | 9 | scan_interval: Some(3600), |
| 10 | socket_path: None, | ||
| 11 | lastfm_api_key: None, | 10 | lastfm_api_key: None, |
| 12 | lastfm_api_secret: None, | 11 | lastfm_api_secret: None, |
| 13 | lastfm_session_key: None, | 12 | lastfm_session_key: None, |
| 14 | listenbrainz_token: None, | 13 | listenbrainz_token: None, |
| 15 | volume: None, | 14 | volume: None, |
| 16 | shuffle: None, | 15 | shuffle: None, |
| 16 | display_format: Some(String::from("$ARTIST - $TRACK")), | ||
| 17 | }; | 17 | }; |
| 18 | assert_eq!(config.get("scan_interval").unwrap(), "3600"); | 18 | assert_eq!(config.get("scan_interval").unwrap(), "3600"); |
| 19 | } | 19 | } |
| @@ -22,7 +22,15 @@ fn test_config_get_scan_interval() { | |||
| 22 | fn test_config_get_not_set() { | 22 | fn test_config_get_not_set() { |
| 23 | let config = Config { | 23 | let config = Config { |
| 24 | config_path: PathBuf::from("/test/config.toml"), | 24 | config_path: PathBuf::from("/test/config.toml"), |
| 25 | ..Default::default() | 25 | music_paths: vec![], |
| 26 | scan_interval: None, | ||
| 27 | lastfm_api_key: None, | ||
| 28 | lastfm_api_secret: None, | ||
| 29 | lastfm_session_key: None, | ||
| 30 | listenbrainz_token: None, | ||
| 31 | volume: None, | ||
| 32 | shuffle: None, | ||
| 33 | display_format: None, | ||
| 26 | }; | 34 | }; |
| 27 | assert_eq!(config.get("scan_interval").unwrap(), "not set"); | 35 | assert_eq!(config.get("scan_interval").unwrap(), "not set"); |
| 28 | } | 36 | } |
| @@ -33,13 +41,13 @@ fn test_config_get_volume() { | |||
| 33 | config_path: PathBuf::from("/test/config.toml"), | 41 | config_path: PathBuf::from("/test/config.toml"), |
| 34 | music_paths: vec![], | 42 | music_paths: vec![], |
| 35 | scan_interval: None, | 43 | scan_interval: None, |
| 36 | socket_path: None, | ||
| 37 | lastfm_api_key: None, | 44 | lastfm_api_key: None, |
| 38 | lastfm_api_secret: None, | 45 | lastfm_api_secret: None, |
| 39 | lastfm_session_key: None, | 46 | lastfm_session_key: None, |
| 40 | listenbrainz_token: None, | 47 | listenbrainz_token: None, |
| 41 | volume: Some(75), | 48 | volume: Some(75), |
| 42 | shuffle: None, | 49 | shuffle: None, |
| 50 | display_format: None, | ||
| 43 | }; | 51 | }; |
| 44 | assert_eq!(config.get("volume").unwrap(), "75"); | 52 | assert_eq!(config.get("volume").unwrap(), "75"); |
| 45 | } | 53 | } |
| @@ -113,13 +121,13 @@ fn test_config_unset_volume() { | |||
| 113 | config_path: PathBuf::from("/test/config.toml"), | 121 | config_path: PathBuf::from("/test/config.toml"), |
| 114 | music_paths: vec![], | 122 | music_paths: vec![], |
| 115 | scan_interval: None, | 123 | scan_interval: None, |
| 116 | socket_path: None, | ||
| 117 | lastfm_api_key: None, | 124 | lastfm_api_key: None, |
| 118 | lastfm_api_secret: None, | 125 | lastfm_api_secret: None, |
| 119 | lastfm_session_key: None, | 126 | lastfm_session_key: None, |
| 120 | listenbrainz_token: None, | 127 | listenbrainz_token: None, |
| 121 | volume: Some(50), | 128 | volume: Some(50), |
| 122 | shuffle: None, | 129 | shuffle: None, |
| 130 | display_format: None, | ||
| 123 | }; | 131 | }; |
| 124 | config.unset("volume").unwrap(); | 132 | config.unset("volume").unwrap(); |
| 125 | assert_eq!(config.volume, None); | 133 | assert_eq!(config.volume, None); |
diff --git a/tests/queue.rs b/tests/queue.rs index 438fa23..a73c312 100644 --- a/tests/queue.rs +++ b/tests/queue.rs | |||
| @@ -248,6 +248,32 @@ fn test_track_display_index_offset() { | |||
| 248 | assert_eq!(t.display_string(4, 10), "05. Artist - Title"); | 248 | assert_eq!(t.display_string(4, 10), "05. Artist - Title"); |
| 249 | } | 249 | } |
| 250 | 250 | ||
| 251 | #[test] | ||
| 252 | fn test_track_format_variables() { | ||
| 253 | let t = track( | ||
| 254 | "/music/test.mp3", | ||
| 255 | Some("My Song"), | ||
| 256 | Some("John Doe"), | ||
| 257 | Some("Great Album"), | ||
| 258 | ); | ||
| 259 | assert_eq!(t.format("$ARTIST - $TRACK"), "John Doe - My Song"); | ||
| 260 | assert_eq!(t.format("$TRACK ($ALBUM)"), "My Song (Great Album)"); | ||
| 261 | assert_eq!(t.format("Now playing: $ARTIST"), "Now playing: John Doe"); | ||
| 262 | } | ||
| 263 | |||
| 264 | #[test] | ||
| 265 | fn test_track_format_empty_variables() { | ||
| 266 | let t = track_no_meta("/music/test.mp3"); | ||
| 267 | assert_eq!(t.format("$ARTIST - $TRACK"), " - test.mp3"); | ||
| 268 | } | ||
| 269 | |||
| 270 | #[test] | ||
| 271 | fn test_track_format_missing_variables() { | ||
| 272 | let t = track("/music/test.mp3", Some("My Song"), Some("John Doe"), None); | ||
| 273 | assert_eq!(t.format("Track: $TRACK"), "Track: My Song"); | ||
| 274 | assert_eq!(t.format("Album: $ALBUM"), "Album: "); | ||
| 275 | } | ||
| 276 | |||
| 251 | // ---------- Shuffle ---------- | 277 | // ---------- Shuffle ---------- |
| 252 | 278 | ||
| 253 | #[test] | 279 | #[test] |
