aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@noreply.codeberg.org>2026-07-06 15:59:34 +0200
committersalmonik <salmonik@noreply.codeberg.org>2026-07-06 15:59:34 +0200
commit338b4d600e2d41ef52858c73adf23473183a60a9 (patch)
treeced63779d57e44fa35a896b27bf07e9eee120d5f
parentfix -a to work properly (diff)
parentadd track index to MPRIS (diff)
downloadrsplayer-338b4d600e2d41ef52858c73adf23473183a60a9.tar.gz
rsplayer-338b4d600e2d41ef52858c73adf23473183a60a9.zip
Merge pull request 'mpris merge' (#2) from mpris into main
Reviewed-on: https://codeberg.org/salmonik/RSPlayer/pulls/2
-rw-r--r--Cargo.lock3
-rw-r--r--Cargo.toml3
-rw-r--r--src/daemon.rs13
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs1
-rw-r--r--src/mpris.rs470
-rw-r--r--src/notification.rs68
-rw-r--r--src/queue.rs3
-rw-r--r--tests/daemon.rs1
-rw-r--r--tests/queue.rs2
10 files changed, 536 insertions, 29 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 43b17e7..acf5ce5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
4 4
5[[package]] 5[[package]]
6name = "RSPlayer" 6name = "RSPlayer"
7version = "0.8.6" 7version = "0.9.1"
8dependencies = [ 8dependencies = [
9 "clap", 9 "clap",
10 "clap_complete", 10 "clap_complete",
@@ -23,6 +23,7 @@ dependencies = [
23 "serde_json", 23 "serde_json",
24 "symphonia", 24 "symphonia",
25 "toml", 25 "toml",
26 "zbus",
26] 27]
27 28
28[[package]] 29[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index b64a2ee..ce8ca48 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
1[package] 1[package]
2name = "RSPlayer" 2name = "RSPlayer"
3version = "0.8.6" 3version = "0.9.1"
4edition = "2024" 4edition = "2024"
5description = "Rust music player daemon with Unix socket IPC" 5description = "Rust music player daemon with Unix socket IPC"
6license = "GPL-3.0 or later" 6license = "GPL-3.0 or later"
@@ -34,6 +34,7 @@ rand = "0.10.1"
34notify-rust = "4.17.0" 34notify-rust = "4.17.0"
35md-5 = "0.11.0" 35md-5 = "0.11.0"
36hex = "0.4.3" 36hex = "0.4.3"
37zbus = { version = "5", default-features = false, features = ["blocking-api"] }
37 38
38[profile.release] 39[profile.release]
39opt-level = "z" 40opt-level = "z"
diff --git a/src/daemon.rs b/src/daemon.rs
index 61d021b..1c918a8 100644
--- a/src/daemon.rs
+++ b/src/daemon.rs
@@ -102,7 +102,7 @@ impl ScrobbleState {
102 } 102 }
103} 103}
104 104
105fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) { 105pub fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) {
106 if let Some(ref track) = state.track 106 if let Some(ref track) = state.track
107 && track.is_stream 107 && track.is_stream
108 { 108 {
@@ -137,7 +137,7 @@ fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState
137 state.clear(); 137 state.clear();
138} 138}
139 139
140fn send_now_playing(scrobbler: &RwLock<Scrobbler>, track: &Track) { 140pub fn send_now_playing(scrobbler: &RwLock<Scrobbler>, track: &Track) {
141 if track.is_stream { 141 if track.is_stream {
142 crate::debug_eprintln(format_args!( 142 crate::debug_eprintln(format_args!(
143 "scrobble: skipping now_playing for stream track" 143 "scrobble: skipping now_playing for stream track"
@@ -246,6 +246,15 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> {
246 // Save full config 246 // Save full config
247 config.save()?; 247 config.save()?;
248 248
249 // Start MPRIS D-Bus server for external control (playerctl, etc.)
250 crate::mpris::start_mpris_server(
251 Arc::clone(&audio_engine),
252 Arc::clone(&queue),
253 Arc::clone(&scrobbler),
254 Arc::clone(&scrobble_state),
255 Arc::clone(&shutdown_flag),
256 );
257
249 // Accept connections until shutdown 258 // Accept connections until shutdown
250 listener.set_nonblocking(true)?; 259 listener.set_nonblocking(true)?;
251 // Helper: poll until a specific track actually starts playing or timeout 260 // Helper: poll until a specific track actually starts playing or timeout
diff --git a/src/lib.rs b/src/lib.rs
index cc1f70a..f08e8ea 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,6 +17,7 @@ pub mod cli;
17pub mod config; 17pub mod config;
18pub mod daemon; 18pub mod daemon;
19pub mod database; 19pub mod database;
20pub mod mpris;
20pub mod notification; 21pub mod notification;
21pub mod playback; 22pub mod playback;
22pub mod queue; 23pub mod queue;
diff --git a/src/main.rs b/src/main.rs
index d472b2d..fd60bef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,7 @@ mod cli;
10mod config; 10mod config;
11mod daemon; 11mod daemon;
12mod database; 12mod database;
13mod mpris;
13mod notification; 14mod notification;
14mod playback; 15mod playback;
15mod queue; 16mod queue;
diff --git a/src/mpris.rs b/src/mpris.rs
new file mode 100644
index 0000000..a7ac360
--- /dev/null
+++ b/src/mpris.rs
@@ -0,0 +1,470 @@
1/*
2 * This file is a part of RSPlayer
3 * Licensed under the GPL-3.0 License
4 * Copyright (C) 2026 Salmonik @ salmonik@tuta.io
5 * https://codeberg.org/salmonik/RSPlayer
6*/
7
8//! MPRIS D-Bus interface for external control via playerctl, KDE Connect, etc.
9//!
10//! Implements org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player on the
11//! session D-Bus bus. Uses zbus blocking API so no async runtime is required
12//! in the caller.
13
14use crate::audio::AudioEngine;
15use crate::config::Config;
16use crate::daemon::ScrobbleState;
17use crate::notification;
18use crate::queue::{Queue, ShuffleMode, Track};
19use crate::scrobbler::Scrobbler;
20use std::collections::HashMap;
21use std::path::PathBuf;
22use std::sync::atomic::AtomicBool;
23use std::sync::{Arc, Mutex, RwLock};
24use std::thread;
25use std::time::Duration;
26use zbus::blocking::Connection;
27use zbus::interface;
28use zbus::zvariant::Value;
29
30// ---------------------------------------------------------------------------
31// org.mpris.MediaPlayer2 — root interface
32// ---------------------------------------------------------------------------
33
34pub struct Mpris2;
35
36#[interface(name = "org.mpris.MediaPlayer2")]
37impl Mpris2 {
38 #[zbus(property)]
39 fn can_quit(&self) -> bool {
40 true
41 }
42
43 #[zbus(property)]
44 fn can_raise(&self) -> bool {
45 false
46 }
47
48 #[zbus(property)]
49 fn has_track_list(&self) -> bool {
50 false
51 }
52
53 #[zbus(property)]
54 fn identity(&self) -> String {
55 "RSPlayer".to_string()
56 }
57
58 #[zbus(property)]
59 fn desktop_entry(&self) -> String {
60 "rsplayer".to_string()
61 }
62
63 #[zbus(property)]
64 fn supported_uri_schemes(&self) -> Vec<String> {
65 vec!["file".into(), "http".into(), "https".into()]
66 }
67
68 #[zbus(property)]
69 fn supported_mime_types(&self) -> Vec<String> {
70 vec![
71 "audio/mpeg".into(),
72 "audio/flac".into(),
73 "audio/ogg".into(),
74 "audio/wav".into(),
75 "audio/x-wav".into(),
76 "audio/vorbis".into(),
77 ]
78 }
79
80 fn quit(&self) -> zbus::fdo::Result<()> {
81 crate::debug_eprintln(format_args!("mpris: quit requested"));
82 std::process::exit(0);
83 }
84
85 fn raise(&self) {
86 // Terminal-based player — nothing to raise.
87 }
88}
89
90// ---------------------------------------------------------------------------
91// org.mpris.MediaPlayer2.Player — playback control
92// ---------------------------------------------------------------------------
93
94pub struct MprisPlayer {
95 audio: Arc<AudioEngine>,
96 queue: Arc<Mutex<Queue>>,
97 scrobbler: Arc<RwLock<Scrobbler>>,
98 scrobble_state: Arc<Mutex<ScrobbleState>>,
99}
100
101/// Build the `Metadata` map (a{sv}) required by MPRIS, using only owned values
102/// so no borrowing from locals.
103fn build_metadata(track: &Track) -> HashMap<String, Value<'static>> {
104 let mut map: HashMap<String, Value<'static>> = HashMap::new();
105
106 map.insert(
107 "mpris:trackid".into(),
108 Value::new("/org/mpris/MediaPlayer2/TrackList/CurrentTrack"),
109 );
110
111 let length_us = track
112 .duration_secs
113 .map(|d| (d * 1_000_000.0) as i64)
114 .unwrap_or(0);
115 map.insert("mpris:length".into(), Value::new(length_us));
116 map.insert("mpris:artUrl".into(), Value::new(""));
117
118 map.insert(
119 "xesam:title".into(),
120 Value::new(track.title.clone().unwrap_or_default()),
121 );
122
123 let artist = track.artist.clone().unwrap_or_default();
124 map.insert("xesam:artist".into(), Value::new(vec![artist]));
125
126 map.insert(
127 "xesam:album".into(),
128 Value::new(track.album.clone().unwrap_or_default()),
129 );
130
131 map.insert(
132 "xesam:trackNumber".into(),
133 Value::new(track.track_number.unwrap_or(0)),
134 );
135
136 map
137}
138
139fn empty_metadata() -> HashMap<String, Value<'static>> {
140 let mut map: HashMap<String, Value<'static>> = HashMap::new();
141 map.insert(
142 "mpris:trackid".into(),
143 Value::new("/org/mpris/MediaPlayer2/TrackList/NoTrack"),
144 );
145 map
146}
147
148fn playback_status(audio: &AudioEngine) -> String {
149 if audio.is_playing() {
150 "Playing"
151 } else if audio.current_track().is_some() {
152 "Paused"
153 } else {
154 "Stopped"
155 }
156 .to_string()
157}
158
159#[interface(name = "org.mpris.MediaPlayer2.Player")]
160impl MprisPlayer {
161 // -- Methods --
162
163 fn next(&self) {
164 crate::debug_eprintln(format_args!("mpris: next"));
165 if let Ok(mut s) = self.scrobble_state.lock() {
166 crate::daemon::try_scrobble_current(&self.scrobbler, &mut s);
167 }
168 if let Ok(mut q) = self.queue.lock() {
169 if let Some(next_path) = q.next() {
170 self.audio.play(next_path.clone());
171 let track = Track::from_path(next_path);
172 crate::daemon::send_now_playing(&self.scrobbler, &track);
173 if let Ok(mut s) = self.scrobble_state.lock() {
174 s.start(track.clone());
175 }
176 let fmt = Config::load().ok().and_then(|c| c.display_format.clone());
177 notification::notify_track(&track, fmt.as_deref());
178 } else {
179 self.audio.stop();
180 notification::notify_queue_ended();
181 q.clear();
182 }
183 }
184 }
185
186 fn previous(&self) {
187 crate::debug_eprintln(format_args!("mpris: previous"));
188 if let Ok(mut s) = self.scrobble_state.lock() {
189 crate::daemon::try_scrobble_current(&self.scrobbler, &mut s);
190 }
191 if let Ok(mut q) = self.queue.lock()
192 && let Some(prev_path) = q.previous()
193 {
194 self.audio.play(prev_path.clone());
195 let track = Track::from_path(prev_path);
196 crate::daemon::send_now_playing(&self.scrobbler, &track);
197 if let Ok(mut s) = self.scrobble_state.lock() {
198 s.start(track.clone());
199 }
200 let fmt = Config::load().ok().and_then(|c| c.display_format.clone());
201 notification::notify_track(&track, fmt.as_deref());
202 }
203 }
204
205 fn pause(&self) {
206 crate::debug_eprintln(format_args!("mpris: pause"));
207 self.audio.pause();
208 notification::notify_change("Paused");
209 }
210
211 fn play_pause(&self) {
212 crate::debug_eprintln(format_args!("mpris: play_pause"));
213 if self.audio.is_playing() {
214 self.audio.pause();
215 notification::notify_change("Paused");
216 } else {
217 self.audio.resume();
218 notification::notify_change("Resumed");
219 }
220 }
221
222 fn stop(&self) {
223 crate::debug_eprintln(format_args!("mpris: stop"));
224 if let Ok(mut s) = self.scrobble_state.lock() {
225 crate::daemon::try_scrobble_current(&self.scrobbler, &mut s);
226 }
227 self.audio.stop();
228 if let Ok(mut q) = self.queue.lock() {
229 q.clear();
230 }
231 notification::notify_change("Stopped");
232 }
233
234 fn play(&self) {
235 crate::debug_eprintln(format_args!("mpris: play"));
236 if self.audio.current_track().is_some() {
237 self.audio.resume();
238 notification::notify_change("Resumed");
239 } else if let Ok(q) = self.queue.lock()
240 && let Some(track) = q.current()
241 {
242 self.audio.play(track.path.clone());
243 crate::daemon::send_now_playing(&self.scrobbler, track);
244 if let Ok(mut s) = self.scrobble_state.lock() {
245 s.start(track.clone());
246 }
247 let fmt = Config::load().ok().and_then(|c| c.display_format.clone());
248 notification::notify_track(track, fmt.as_deref());
249 }
250 }
251
252 fn seek(&self, _offset: i64) {
253 // Seeking not implemented yet.
254 }
255
256 fn set_position(&self, _track_id: zbus::zvariant::ObjectPath<'_>, _position: i64) {
257 // Seeking not implemented yet.
258 }
259
260 fn open_uri(&self, uri: String) {
261 crate::debug_eprintln(format_args!("mpris: open_uri {}", uri));
262 let path_str = uri
263 .strip_prefix("file://")
264 .map(|s| s.to_string())
265 .unwrap_or_else(|| uri.clone());
266
267 let path = PathBuf::from(&path_str);
268 if let Ok(mut q) = self.queue.lock() {
269 let was_empty = q.is_empty();
270 let was_playing = self.audio.is_playing();
271
272 if path_str.starts_with("http://") || path_str.starts_with("https://") {
273 let track = Track::from_url(&path_str);
274 q.push_track(track.clone());
275 if was_empty
276 && !was_playing
277 && let Some(t) = q.current()
278 {
279 self.audio.play(t.path.clone());
280 crate::daemon::send_now_playing(&self.scrobbler, t);
281 if let Ok(mut s) = self.scrobble_state.lock() {
282 s.start(t.clone());
283 }
284 let fmt = Config::load().ok().and_then(|c| c.display_format.clone());
285 notification::notify_track(t, fmt.as_deref());
286 }
287 } else if path.is_file() {
288 q.add_track(path.clone());
289 if was_empty && !was_playing {
290 let track = Track::from_path(path);
291 self.audio.play(track.path.clone());
292 crate::daemon::send_now_playing(&self.scrobbler, &track);
293 if let Ok(mut s) = self.scrobble_state.lock() {
294 s.start(track.clone());
295 }
296 let fmt = Config::load().ok().and_then(|c| c.display_format.clone());
297 notification::notify_track(&track, fmt.as_deref());
298 }
299 }
300 }
301 }
302
303 // -- Properties --
304
305 #[zbus(property)]
306 fn playback_status(&self) -> String {
307 playback_status(&self.audio)
308 }
309
310 #[zbus(property)]
311 fn loop_status(&self) -> String {
312 "None".to_string()
313 }
314
315 #[zbus(property)]
316 fn rate(&self) -> f64 {
317 1.0
318 }
319
320 #[zbus(property)]
321 fn shuffle(&self) -> bool {
322 self.queue
323 .lock()
324 .map(|q| !matches!(q.shuffle_mode(), ShuffleMode::None))
325 .unwrap_or(false)
326 }
327
328 #[zbus(property)]
329 fn volume(&self) -> f64 {
330 self.audio.get_volume() as f64 / 100.0
331 }
332
333 #[zbus(property)]
334 #[zbus(name = "Volume")]
335 fn set_volume(&self, value: f64) {
336 let level = (value * 100.0).clamp(0.0, 100.0) as u8;
337 self.audio.set_volume(level as f32 / 100.0);
338 if let Ok(mut config) = Config::load() {
339 config.volume = Some(level);
340 let _ = config.save();
341 }
342 }
343
344 #[zbus(property)]
345 fn position(&self) -> i64 {
346 (self.audio.position() * 1_000_000.0) as i64
347 }
348
349 #[zbus(property)]
350 fn minimum_rate(&self) -> f64 {
351 1.0
352 }
353
354 #[zbus(property)]
355 fn maximum_rate(&self) -> f64 {
356 1.0
357 }
358
359 #[zbus(property)]
360 fn can_go_next(&self) -> bool {
361 true
362 }
363
364 #[zbus(property)]
365 fn can_go_previous(&self) -> bool {
366 true
367 }
368
369 #[zbus(property)]
370 fn can_play(&self) -> bool {
371 true
372 }
373
374 #[zbus(property)]
375 fn can_pause(&self) -> bool {
376 true
377 }
378
379 #[zbus(property)]
380 fn can_seek(&self) -> bool {
381 false
382 }
383
384 #[zbus(property)]
385 fn can_control(&self) -> bool {
386 true
387 }
388
389 #[zbus(property)]
390 fn metadata(&self) -> HashMap<String, Value<'static>> {
391 if let Some(track_path) = self.audio.current_track() {
392 let track = Track::from_path(track_path);
393 build_metadata(&track)
394 } else if let Ok(q) = self.queue.lock()
395 && let Some(track) = q.current()
396 {
397 build_metadata(track)
398 } else {
399 empty_metadata()
400 }
401 }
402}
403
404// ---------------------------------------------------------------------------
405// Public entry-point: spawn the MPRIS D-Bus server in a background thread
406// ---------------------------------------------------------------------------
407
408pub fn start_mpris_server(
409 audio: Arc<AudioEngine>,
410 queue: Arc<Mutex<Queue>>,
411 scrobbler: Arc<RwLock<Scrobbler>>,
412 scrobble_state: Arc<Mutex<ScrobbleState>>,
413 shutdown_flag: Arc<AtomicBool>,
414) {
415 thread::Builder::new()
416 .name("mpris".into())
417 .spawn(move || {
418 let conn = match Connection::session() {
419 Ok(c) => c,
420 Err(e) => {
421 crate::debug_eprintln(format_args!(
422 "mpris: failed to connect to session bus: {}",
423 e
424 ));
425 return;
426 }
427 };
428
429 // Register both MPRIS interfaces on the same object path
430 let player = MprisPlayer {
431 audio,
432 queue,
433 scrobbler,
434 scrobble_state,
435 };
436
437 if let Err(e) = conn.object_server().at("/org/mpris/MediaPlayer2", Mpris2) {
438 crate::debug_eprintln(format_args!(
439 "mpris: failed to register MediaPlayer2: {}",
440 e
441 ));
442 return;
443 }
444
445 if let Err(e) = conn.object_server().at("/org/mpris/MediaPlayer2", player) {
446 crate::debug_eprintln(format_args!(
447 "mpris: failed to register MediaPlayer2.Player: {}",
448 e
449 ));
450 return;
451 }
452
453 if let Err(e) = conn.request_name("org.mpris.MediaPlayer2.rsplayer") {
454 crate::debug_eprintln(format_args!("mpris: failed to request bus name: {}", e));
455 return;
456 }
457
458 crate::debug_eprintln(format_args!(
459 "mpris: serving on org.mpris.MediaPlayer2.rsplayer"
460 ));
461
462 // Keep the connection alive until shutdown.
463 while !shutdown_flag.load(std::sync::atomic::Ordering::SeqCst) {
464 thread::sleep(Duration::from_millis(500));
465 }
466
467 crate::debug_eprintln(format_args!("mpris: shutting down"));
468 })
469 .expect("failed to spawn mpris thread");
470}
diff --git a/src/notification.rs b/src/notification.rs
index b2d2b56..9c19b0b 100644
--- a/src/notification.rs
+++ b/src/notification.rs
@@ -22,41 +22,59 @@ pub fn notify_track(track: &Track, format: Option<&str>) {
22 }, 22 },
23 }; 23 };
24 24
25 let _ = notify_rust::Notification::new() 25 // show() is a synchronous D-Bus call that blocks until the notification
26 .summary("Now Playing") 26 // daemon responds. Spawn it on a separate thread so it never blocks the
27 .body(&body) 27 // player event loop or CLI command processing.
28 .appname("RSPlayer") 28 std::thread::spawn(move || {
29 .show(); 29 let _ = notify_rust::Notification::new()
30 .summary("Now Playing")
31 .body(&body)
32 .appname("RSPlayer")
33 .show();
34 });
30} 35}
31 36
32pub fn notify_queue_ended() { 37pub fn notify_queue_ended() {
33 let _ = notify_rust::Notification::new() 38 std::thread::spawn(|| {
34 .summary("Playback Ended") 39 let _ = notify_rust::Notification::new()
35 .body("The queue is empty") 40 .summary("Playback Ended")
36 .appname("RSPlayer") 41 .body("The queue is empty")
37 .show(); 42 .appname("RSPlayer")
43 .show();
44 });
38} 45}
39 46
40pub fn notify_change(state: &str) { 47pub fn notify_change(state: &str) {
41 let _ = notify_rust::Notification::new() 48 let summary = state.to_owned();
42 .summary(state) 49 let body = format!("Playback {}", state.to_lowercase());
43 .body(&format!("Playback {}", state.to_lowercase())) 50 std::thread::spawn(move || {
44 .appname("RSPlayer") 51 let _ = notify_rust::Notification::new()
45 .show(); 52 .summary(&summary)
53 .body(&body)
54 .appname("RSPlayer")
55 .show();
56 });
46} 57}
47 58
48pub fn notify_error(context: &str) { 59pub fn notify_error(context: &str) {
49 let _ = notify_rust::Notification::new() 60 let body = format!("{} - Check the logs for details", context);
50 .summary("RSPlayer Error") 61 std::thread::spawn(move || {
51 .body(&format!("{} - Check the logs for details", context)) 62 let _ = notify_rust::Notification::new()
52 .appname("RSPlayer") 63 .summary("RSPlayer Error")
53 .show(); 64 .body(&body)
65 .appname("RSPlayer")
66 .show();
67 });
54} 68}
55 69
56pub fn notify(summary: &str, body: &str) { 70pub fn notify(summary: &str, body: &str) {
57 let _ = notify_rust::Notification::new() 71 let summary = summary.to_owned();
58 .summary(summary) 72 let body = body.to_owned();
59 .body(body) 73 std::thread::spawn(move || {
60 .appname("RSPlayer") 74 let _ = notify_rust::Notification::new()
61 .show(); 75 .summary(&summary)
76 .body(&body)
77 .appname("RSPlayer")
78 .show();
79 });
62} 80}
diff --git a/src/queue.rs b/src/queue.rs
index 7675ddd..1f2f0fc 100644
--- a/src/queue.rs
+++ b/src/queue.rs
@@ -98,6 +98,7 @@ pub struct Track {
98 pub title: Option<String>, 98 pub title: Option<String>,
99 pub artist: Option<String>, 99 pub artist: Option<String>,
100 pub album: Option<String>, 100 pub album: Option<String>,
101 pub track_number: Option<i32>,
101 pub duration_secs: Option<f64>, 102 pub duration_secs: Option<f64>,
102 pub is_stream: bool, 103 pub is_stream: bool,
103} 104}
@@ -115,6 +116,7 @@ impl Track {
115 title: metadata.and_then(|m| m.title.clone()), 116 title: metadata.and_then(|m| m.title.clone()),
116 artist: metadata.and_then(|m| m.artist.clone()), 117 artist: metadata.and_then(|m| m.artist.clone()),
117 album: metadata.and_then(|m| m.album.clone()), 118 album: metadata.and_then(|m| m.album.clone()),
119 track_number: metadata.and_then(|m| m.track_number),
118 duration_secs: metadata.and_then(|m| m.duration_secs), 120 duration_secs: metadata.and_then(|m| m.duration_secs),
119 is_stream: false, 121 is_stream: false,
120 } 122 }
@@ -139,6 +141,7 @@ impl Track {
139 title: Some(title), 141 title: Some(title),
140 artist: None, 142 artist: None,
141 album: None, 143 album: None,
144 track_number: None,
142 duration_secs: None, 145 duration_secs: None,
143 is_stream: true, 146 is_stream: true,
144 } 147 }
diff --git a/tests/daemon.rs b/tests/daemon.rs
index e5cb270..01241fd 100644
--- a/tests/daemon.rs
+++ b/tests/daemon.rs
@@ -26,6 +26,7 @@ fn test_scrobble_state_clear() {
26 title: Some("Test".into()), 26 title: Some("Test".into()),
27 artist: Some("Artist".into()), 27 artist: Some("Artist".into()),
28 album: None, 28 album: None,
29 track_number: None,
29 duration_secs: Some(200.0), 30 duration_secs: Some(200.0),
30 is_stream: false, 31 is_stream: false,
31 }; 32 };
diff --git a/tests/queue.rs b/tests/queue.rs
index 59311d2..2eaa6f3 100644
--- a/tests/queue.rs
+++ b/tests/queue.rs
@@ -14,6 +14,7 @@ fn track(path: &str, title: Option<&str>, artist: Option<&str>, album: Option<&s
14 title: title.map(|s| s.to_string()), 14 title: title.map(|s| s.to_string()),
15 artist: artist.map(|s| s.to_string()), 15 artist: artist.map(|s| s.to_string()),
16 album: album.map(|s| s.to_string()), 16 album: album.map(|s| s.to_string()),
17 track_number: None,
17 duration_secs: None, 18 duration_secs: None,
18 is_stream: false, 19 is_stream: false,
19 } 20 }
@@ -25,6 +26,7 @@ fn track_no_meta(path: &str) -> Track {
25 title: None, 26 title: None,
26 artist: None, 27 artist: None,
27 album: None, 28 album: None,
29 track_number: None,
28 duration_secs: None, 30 duration_secs: None,
29 is_stream: false, 31 is_stream: false,
30 } 32 }