aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--AGENTS.md4
-rw-r--r--Cargo.lock107
-rw-r--r--Cargo.toml2
-rw-r--r--TODO.md6
-rw-r--r--rsplayer.service4
-rw-r--r--src/cli.rs12
-rw-r--r--src/config.rs186
-rw-r--r--src/daemon.rs66
-rw-r--r--src/notification.rs23
-rw-r--r--src/queue.rs16
-rw-r--r--tests/config.rs16
-rw-r--r--tests/queue.rs26
12 files changed, 294 insertions, 174 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 4ec9a5a..b7ce695 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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 56Check @TODO.md
57- Album shuffle mode with proper album/folder grouping
58- Scrobbling (Last.fm/ListenBrainz)
diff --git a/Cargo.lock b/Cargo.lock
index 52ab8c8..4d6bee2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
1331name = "linux-raw-sys" 1331name = "linux-raw-sys"
1332version = "0.4.15"
1333source = "registry+https://github.com/rust-lang/crates.io-index"
1334checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
1335
1336[[package]]
1337name = "linux-raw-sys"
1338version = "0.12.1" 1332version = "0.12.1"
1339source = "registry+https://github.com/rust-lang/crates.io-index" 1333source = "registry+https://github.com/rust-lang/crates.io-index"
1340checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" 1334checksum = "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]]
2044name = "rustix" 2038name = "rustix"
2045version = "0.38.44"
2046source = "registry+https://github.com/rust-lang/crates.io-index"
2047checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
2048dependencies = [
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]]
2057name = "rustix"
2058version = "1.1.4" 2039version = "1.1.4"
2059source = "registry+https://github.com/rust-lang/crates.io-index" 2040source = "registry+https://github.com/rust-lang/crates.io-index"
2060checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" 2041checksum = "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]]
2623name = "terminal_size" 2604name = "terminal_size"
2624version = "0.3.0" 2605version = "0.4.4"
2625source = "registry+https://github.com/rust-lang/crates.io-index" 2606source = "registry+https://github.com/rust-lang/crates.io-index"
2626checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 2607checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
2627dependencies = [ 2608dependencies = [
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]]
3342name = "windows-sys" 3323name = "windows-sys"
3343version = "0.48.0"
3344source = "registry+https://github.com/rust-lang/crates.io-index"
3345checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
3346dependencies = [
3347 "windows-targets 0.48.5",
3348]
3349
3350[[package]]
3351name = "windows-sys"
3352version = "0.52.0" 3324version = "0.52.0"
3353source = "registry+https://github.com/rust-lang/crates.io-index" 3325source = "registry+https://github.com/rust-lang/crates.io-index"
3354checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3326checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
@@ -3391,21 +3363,6 @@ dependencies = [
3391 3363
3392[[package]] 3364[[package]]
3393name = "windows-targets" 3365name = "windows-targets"
3394version = "0.48.5"
3395source = "registry+https://github.com/rust-lang/crates.io-index"
3396checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
3397dependencies = [
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]]
3408name = "windows-targets"
3409version = "0.52.6" 3366version = "0.52.6"
3410source = "registry+https://github.com/rust-lang/crates.io-index" 3367source = "registry+https://github.com/rust-lang/crates.io-index"
3411checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3368checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
@@ -3472,12 +3429,6 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
3472 3429
3473[[package]] 3430[[package]]
3474name = "windows_aarch64_gnullvm" 3431name = "windows_aarch64_gnullvm"
3475version = "0.48.5"
3476source = "registry+https://github.com/rust-lang/crates.io-index"
3477checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
3478
3479[[package]]
3480name = "windows_aarch64_gnullvm"
3481version = "0.52.6" 3432version = "0.52.6"
3482source = "registry+https://github.com/rust-lang/crates.io-index" 3433source = "registry+https://github.com/rust-lang/crates.io-index"
3483checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3434checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
@@ -3496,12 +3447,6 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
3496 3447
3497[[package]] 3448[[package]]
3498name = "windows_aarch64_msvc" 3449name = "windows_aarch64_msvc"
3499version = "0.48.5"
3500source = "registry+https://github.com/rust-lang/crates.io-index"
3501checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
3502
3503[[package]]
3504name = "windows_aarch64_msvc"
3505version = "0.52.6" 3450version = "0.52.6"
3506source = "registry+https://github.com/rust-lang/crates.io-index" 3451source = "registry+https://github.com/rust-lang/crates.io-index"
3507checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3452checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
@@ -3520,12 +3465,6 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
3520 3465
3521[[package]] 3466[[package]]
3522name = "windows_i686_gnu" 3467name = "windows_i686_gnu"
3523version = "0.48.5"
3524source = "registry+https://github.com/rust-lang/crates.io-index"
3525checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
3526
3527[[package]]
3528name = "windows_i686_gnu"
3529version = "0.52.6" 3468version = "0.52.6"
3530source = "registry+https://github.com/rust-lang/crates.io-index" 3469source = "registry+https://github.com/rust-lang/crates.io-index"
3531checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3470checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -3556,12 +3495,6 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
3556 3495
3557[[package]] 3496[[package]]
3558name = "windows_i686_msvc" 3497name = "windows_i686_msvc"
3559version = "0.48.5"
3560source = "registry+https://github.com/rust-lang/crates.io-index"
3561checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
3562
3563[[package]]
3564name = "windows_i686_msvc"
3565version = "0.52.6" 3498version = "0.52.6"
3566source = "registry+https://github.com/rust-lang/crates.io-index" 3499source = "registry+https://github.com/rust-lang/crates.io-index"
3567checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3500checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
@@ -3580,12 +3513,6 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
3580 3513
3581[[package]] 3514[[package]]
3582name = "windows_x86_64_gnu" 3515name = "windows_x86_64_gnu"
3583version = "0.48.5"
3584source = "registry+https://github.com/rust-lang/crates.io-index"
3585checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
3586
3587[[package]]
3588name = "windows_x86_64_gnu"
3589version = "0.52.6" 3516version = "0.52.6"
3590source = "registry+https://github.com/rust-lang/crates.io-index" 3517source = "registry+https://github.com/rust-lang/crates.io-index"
3591checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3518checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
@@ -3604,12 +3531,6 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
3604 3531
3605[[package]] 3532[[package]]
3606name = "windows_x86_64_gnullvm" 3533name = "windows_x86_64_gnullvm"
3607version = "0.48.5"
3608source = "registry+https://github.com/rust-lang/crates.io-index"
3609checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
3610
3611[[package]]
3612name = "windows_x86_64_gnullvm"
3613version = "0.52.6" 3534version = "0.52.6"
3614source = "registry+https://github.com/rust-lang/crates.io-index" 3535source = "registry+https://github.com/rust-lang/crates.io-index"
3615checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3536checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
@@ -3628,12 +3549,6 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
3628 3549
3629[[package]] 3550[[package]]
3630name = "windows_x86_64_msvc" 3551name = "windows_x86_64_msvc"
3631version = "0.48.5"
3632source = "registry+https://github.com/rust-lang/crates.io-index"
3633checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
3634
3635[[package]]
3636name = "windows_x86_64_msvc"
3637version = "0.52.6" 3552version = "0.52.6"
3638source = "registry+https://github.com/rust-lang/crates.io-index" 3553source = "registry+https://github.com/rust-lang/crates.io-index"
3639checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3554checksum = "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",
diff --git a/Cargo.toml b/Cargo.toml
index 0fe9f43..33b6b24 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -33,7 +33,7 @@ libc = "0.2.186"
33ctrlc = "3.5.2" 33ctrlc = "3.5.2"
34rand = "0.10.1" 34rand = "0.10.1"
35notify-rust = "4.17.0" 35notify-rust = "4.17.0"
36terminal_size = "0.3" 36terminal_size = "0.4.4"
37 37
38[dev-dependencies] 38[dev-dependencies]
39tempfile = "3.17" 39tempfile = "3.17"
diff --git a/TODO.md b/TODO.md
index bdca569..c66ae50 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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]
6Type=simple 6Type=simple
7ExecStart=/usr/bin/rsp start 7ExecStart=/usr/bin/rsp start-daemon
8Restart=on-failure 8Restart=on-failure
9RestartSec=5 9RestartSec=5
10 10
11[Install] 11[Install]
12WantedBy=default.target \ No newline at end of file 12WantedBy=default.target
diff --git a/src/cli.rs b/src/cli.rs
index bd81983..e0cea63 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -14,9 +14,9 @@ pub struct Cli {
14#[derive(Subcommand)] 14#[derive(Subcommand)]
15pub enum Commands { 15pub 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 {
101impl Cli { 101impl 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
21impl Config { 21impl 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
13pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { 13pub 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
218fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { 221fn 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 @@
1use crate::queue::Track; 1use crate::queue::Track;
2 2
3pub fn notify_track(track: &Track) { 3pub 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
122pub struct Queue { 138pub 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() {
22fn test_config_get_not_set() { 22fn 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]
252fn 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]
265fn 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]
271fn 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]