• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

johnallen3d / mpc-rs / #124

24 Sep 2023 03:06PM UTC coverage: 18.502% (-0.2%) from 18.708%
#124

push

johnallen3d
feat: add `disable` command

Relates to #16

8 of 8 new or added lines in 2 files covered. (100.0%)

84 of 454 relevant lines covered (18.5%)

0.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/client.rs
1
use std::fmt;
2
use std::path::Path;
3
use std::path::PathBuf;
4
use std::time::Duration;
5

6
use eyre::WrapErr;
7
use serde::Serialize;
8

9
use crate::song::Finder;
10
use crate::stats::Output;
11
use crate::stats::Outputs;
12
use crate::{
13
    args::{OnOff, OutputFormat},
14
    song::Current,
15
    song::Playlist,
16
    song::Playlists,
17
    song::Song,
18
    song::TrackList,
19
    stats::Stats,
20
    status::Status,
21
};
22

23
#[derive(Serialize)]
24
pub struct Versions {
25
    mpd: String,
26
    mp_cli: String,
27
}
28

29
impl fmt::Display for Versions {
30
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
31
        write!(f, "mpd={}\nmp-cli={}", self.mpd, self.mp_cli)
×
32
    }
33
}
34

35
pub struct Client {
36
    client: mpd::Client,
37
    format: OutputFormat,
38
}
39

40
impl Client {
41
    pub fn new(
×
42
        bind_to_address: &str,
43
        port: &str,
44
        format: OutputFormat,
45
    ) -> eyre::Result<Client> {
46
        let client = mpd::Client::connect(format!("{bind_to_address}:{port}"))
×
47
            .wrap_err("Error connecting to mpd server".to_string())?;
×
48

49
        Ok(Self { client, format })
×
50
    }
51

52
    //
53
    // queue related commands
54
    //
55
    pub fn add(&mut self, path: &str) -> eyre::Result<Option<String>> {
×
56
        let music_dir = self.client.music_directory()?;
×
57

58
        let absolute_path = if path.starts_with(&music_dir) {
×
59
            path.to_string()
×
60
        } else {
61
            PathBuf::from(&music_dir)
×
62
                .join(path)
×
63
                .to_str()
64
                .unwrap()
65
                .to_string()
66
        };
67

68
        let mut finder = Finder::new(music_dir);
×
69

70
        finder.find(Path::new(Path::new(&absolute_path)))?;
×
71

72
        for file in finder.found {
×
73
            let song = crate::mpd::song::Song {
74
                file: file.relative_path,
×
75
                ..Default::default()
76
            };
77

78
            self.client
×
79
                .push(song.clone())
×
80
                .wrap_err(format!("unkown or inalid path: {}", song.file))?;
×
81
        }
82

83
        Ok(None)
×
84
    }
85

86
    pub fn crop(&mut self) -> eyre::Result<Option<String>> {
×
87
        // determine current song position
88
        // remove all songs before current song
89
        // remove all songs from 1 onwards
90
        let status = self.status()?;
×
91
        let current_position = status.position;
×
92
        let length = status.queue_count;
×
93

94
        if length < 1 {
×
95
            return self.current_status();
×
96
        }
97

98
        self.client.delete(0..current_position)?;
×
99
        // it doesn't matter that the range is out of bounds
100
        self.client.delete(1..length)?;
×
101

102
        self.current_status()
×
103
    }
104

105
    pub fn del(
×
106
        &mut self,
107
        position: Option<u32>,
108
    ) -> eyre::Result<Option<String>> {
109
        let position = match position {
×
110
            Some(position) => position,
×
111
            None => self.status()?.position,
×
112
        };
113

114
        self.client.delete(position)?;
×
115

116
        self.current_status()
×
117
    }
118

119
    //
120
    // playback related commands
121
    //
122
    pub fn current(&mut self) -> eyre::Result<Option<String>> {
×
123
        let current = Current::from(self.status()?);
×
124

125
        let response = match self.format {
×
126
            OutputFormat::Json => serde_json::to_string(&current)?,
×
127
            OutputFormat::Text => current.to_string(),
×
128
        };
129

130
        Ok(Some(response))
×
131
    }
132

133
    pub fn play(
×
134
        &mut self,
135
        position: Option<u32>,
136
    ) -> eyre::Result<Option<String>> {
137
        if position.is_none() {
×
138
            self.client.play()?;
×
139
            return self.current_status();
×
140
        }
141
        // TODO: this is super hacky, can't find a "jump" in rust-mpd
142

143
        // pause
144
        // get current position
145
        // next/prev to desired position
146
        // play
147

148
        let position = position.unwrap();
×
149
        let current_position = self.status()?.position;
×
150

151
        self.pause()?;
×
152

153
        if current_position > position {
×
154
            for _ in (position..current_position).rev() {
×
155
                self.prev()?;
×
156
            }
157
        } else {
158
            for _ in (current_position..position).rev() {
×
159
                self.next()?;
×
160
            }
161
        }
162

163
        self.client.play()?;
×
164

165
        self.current_status()
×
166
    }
167

168
    pub fn next(&mut self) -> eyre::Result<Option<String>> {
169
        self.client.next()?;
×
170

171
        self.current_status()
×
172
    }
173

174
    pub fn prev(&mut self) -> eyre::Result<Option<String>> {
175
        self.client.prev()?;
×
176

177
        self.current_status()
×
178
    }
179

180
    pub fn pause(&mut self) -> eyre::Result<Option<String>> {
181
        self.client.pause(true)?;
×
182

183
        self.current_status()
×
184
    }
185

186
    pub fn pause_if_playing(&mut self) -> eyre::Result<Option<String>> {
×
187
        match self.client.status()?.state {
×
188
            mpd::State::Play => self.pause(),
×
189
            mpd::State::Pause | mpd::State::Stop => Err(eyre::eyre!("")),
×
190
        }
191
    }
192

193
    pub fn cdprev(&mut self) -> eyre::Result<Option<String>> {
×
194
        let default_duration = Duration::from_secs(0);
×
195
        let status = &self.client.status()?;
×
196
        let current = status.elapsed.unwrap_or(default_duration).as_secs();
×
197

198
        if current < 3 {
×
199
            self.prev()
×
200
        } else {
201
            let place = match status.song {
×
202
                Some(ref song) => song.pos,
×
203
                None => 0,
×
204
            };
205
            self.client.seek(place, 0)?;
×
206

207
            self.current_status()
×
208
        }
209
    }
210

211
    pub fn toggle(&mut self) -> eyre::Result<Option<String>> {
×
212
        match self.client.status()?.state {
×
213
            mpd::State::Play => self.pause(),
×
214
            mpd::State::Pause | mpd::State::Stop => self.play(None),
×
215
        }
216
    }
217

218
    pub fn stop(&mut self) -> eyre::Result<Option<String>> {
219
        self.client.stop()?;
×
220

221
        self.current_status()
×
222
    }
223

224
    //
225
    // playlist related commands
226
    //
227

228
    pub fn clear(&mut self) -> eyre::Result<Option<String>> {
229
        self.client.clear()?;
×
230

231
        self.current_status()
×
232
    }
233

234
    pub fn outputs(&mut self) -> eyre::Result<Option<String>> {
×
235
        let outputs = self.client.outputs()?;
×
236
        let outputs: Vec<Output> =
×
237
            outputs.into_iter().map(Output::from).collect();
×
238
        let outputs = Outputs { outputs };
×
239

240
        let response = match self.format {
×
241
            OutputFormat::Json => serde_json::to_string(&outputs)?,
×
242
            OutputFormat::Text => outputs.to_string(),
×
243
        };
244

245
        Ok(Some(response))
×
246
    }
247

248
    fn enable_or_disable(
×
249
        &mut self,
250
        enable: bool,
251
        args: Vec<String>,
252
    ) -> eyre::Result<Option<String>> {
253
        let mut only = false;
×
254
        let mut outputs = Vec::new();
×
255

256
        for arg in args {
×
257
            if arg == "only" {
×
258
                only = true;
×
259
            } else {
260
                outputs.push(arg);
×
261
            }
262
        }
263

264
        if only {
×
265
            // first disable all outputs
266
            for output in self.client.outputs()? {
×
267
                self.client.output(output, enable)?;
×
268
            }
269
        }
270

271
        for name in outputs {
×
272
            let id: u32 = if let Ok(parsed_id) = name.parse::<u32>() {
×
273
                parsed_id
×
274
            } else {
275
                self.client
×
276
                    .outputs()?
×
277
                    .iter()
×
278
                    .find(|&o| o.name == name)
×
279
                    .ok_or_else(|| eyre::eyre!("unknown output: {}", name))?
×
280
                    .id
281
            };
282

283
            self.client.output(id, enable)?;
×
284
        }
285

286
        self.outputs()
×
287
    }
288

289
    pub fn enable(
×
290
        &mut self,
291
        args: Vec<String>,
292
    ) -> eyre::Result<Option<String>> {
293
        self.enable_or_disable(true, args)
×
294
    }
295

296
    pub fn disable(
×
297
        &mut self,
298
        args: Vec<String>,
299
    ) -> eyre::Result<Option<String>> {
300
        self.enable_or_disable(false, args)
×
301
    }
302

303
    pub fn queued(&mut self) -> eyre::Result<Option<String>> {
×
304
        if let Some(song) =
×
305
            self.client.queue().map_err(|e| eyre::eyre!(e))?.get(0)
×
306
        {
307
            // safe to unwrap because we know we have a song
308
            let current = Current::from(Song {
×
309
                inner: song.clone(),
×
310
            });
311

312
            let response = match self.format {
×
313
                OutputFormat::Json => serde_json::to_string(&current)?,
×
314
                OutputFormat::Text => current.to_string(),
×
315
            };
316

317
            Ok(Some(response))
×
318
        } else {
319
            Ok(None)
×
320
        }
321
    }
322

323
    pub fn shuffle(&mut self) -> eyre::Result<Option<String>> {
324
        self.client.shuffle(..)?;
×
325

326
        self.current_status()
×
327
    }
328

329
    pub fn lsplaylists(&mut self) -> eyre::Result<Option<String>> {
×
330
        let playlists = self.client.playlists()?;
×
331
        let playlists: Vec<Playlist> = playlists
×
332
            .into_iter()
333
            .map(|p| Playlist::from(p.name))
×
334
            .collect();
335
        let playlists = Playlists { playlists };
×
336

337
        let response = match self.format {
×
338
            OutputFormat::Json => serde_json::to_string(&playlists)?,
×
339
            OutputFormat::Text => playlists.to_string(),
×
340
        };
341

342
        Ok(Some(response))
×
343
    }
344

345
    pub fn playlist(
×
346
        &mut self,
347
        name: Option<String>,
348
    ) -> eyre::Result<Option<String>> {
349
        // if given a name list songs in that playlist
350
        // if `None` list songs in current playlist
351
        let songs = match name {
×
352
            Some(name) => self.client.playlist(&name)?,
×
353
            None => self.client.queue()?,
×
354
        };
355

356
        let songs: Vec<Current> = songs
×
357
            .into_iter()
358
            .map(|s| Current::from(Song { inner: s }))
×
359
            .collect();
360
        let track_list = TrackList { songs };
×
361

362
        let response = match self.format {
×
363
            OutputFormat::Json => serde_json::to_string(&track_list)?,
×
364
            OutputFormat::Text => track_list.to_string(),
×
365
        };
366

367
        Ok(Some(response))
×
368
    }
369

370
    pub fn repeat(
×
371
        &mut self,
372
        state: Option<OnOff>,
373
    ) -> eyre::Result<Option<String>> {
374
        let state = match state {
×
375
            Some(state) => state == OnOff::On,
×
376
            None => !self.client.status()?.repeat,
×
377
        };
378

379
        self.client.repeat(state)?;
×
380

381
        self.current_status()
×
382
    }
383

384
    pub(crate) fn random(
×
385
        &mut self,
386
        state: Option<OnOff>,
387
    ) -> eyre::Result<Option<String>> {
388
        let state = match state {
×
389
            Some(state) => state == OnOff::On,
×
390
            None => !self.client.status()?.random,
×
391
        };
392

393
        self.client.random(state)?;
×
394

395
        self.current_status()
×
396
    }
397

398
    pub(crate) fn single(
×
399
        &mut self,
400
        state: Option<OnOff>,
401
    ) -> eyre::Result<Option<String>> {
402
        let state = match state {
×
403
            Some(state) => state == OnOff::On,
×
404
            None => !self.client.status()?.single,
×
405
        };
406

407
        self.client.single(state)?;
×
408

409
        self.current_status()
×
410
    }
411

412
    pub fn consume(
×
413
        &mut self,
414
        state: Option<OnOff>,
415
    ) -> eyre::Result<Option<String>> {
416
        let state = match state {
×
417
            Some(state) => state == OnOff::On,
×
418
            None => !self.client.status()?.consume,
×
419
        };
420

421
        self.client.consume(state)?;
×
422

423
        self.current_status()
×
424
    }
425

426
    pub fn crossfade(
×
427
        &mut self,
428
        seconds: Option<String>,
429
    ) -> eyre::Result<Option<String>> {
430
        let crossfade = match seconds {
×
431
            Some(secs) => secs.parse::<i64>().wrap_err(format!(
×
432
                "\"{secs}\" is not 0 or a positive number"
×
433
            ))?,
434
            None => 0,
×
435
        };
436

437
        self.client
×
438
            .crossfade(crossfade)
×
439
            .wrap_err(format!("\"{crossfade}\" is too large"))?;
×
440

441
        Ok(Some(format!("crossfade: {crossfade}")))
×
442
    }
443

444
    pub fn version(&mut self) -> eyre::Result<Option<String>> {
×
445
        let mpd = format!(
×
446
            "{}.{}.{}",
447
            self.client.version.0, self.client.version.1, self.client.version.2
×
448
        );
449
        let mp_cli = env!("CARGO_PKG_VERSION").to_string();
×
450

451
        let versions = Versions { mpd, mp_cli };
×
452

453
        let response = match self.format {
×
454
            OutputFormat::Json => serde_json::to_string(&versions)?,
×
455
            OutputFormat::Text => versions.to_string(),
×
456
        };
457

458
        Ok(Some(response))
×
459
    }
460

461
    pub fn stats(&mut self) -> eyre::Result<Option<String>> {
×
462
        let stats = Stats::new(self.client.stats()?);
×
463

464
        let response = match self.format {
×
465
            OutputFormat::Json => serde_json::to_string(&stats)?,
×
466
            OutputFormat::Text => stats.to_string(),
×
467
        };
468

469
        Ok(Some(response))
×
470
    }
471

472
    pub fn save(&mut self, name: &str) -> eyre::Result<Option<String>> {
473
        self.client
×
474
            .save(name)
×
475
            .wrap_err(format!("Playlist already exists: {name}"))?;
×
476

477
        Ok(None)
×
478
    }
479

480
    pub fn rm(&mut self, name: &str) -> eyre::Result<Option<String>> {
481
        self.client
×
482
            .pl_remove(name)
×
483
            .wrap_err(format!("Unknown playlist: {name}"))?;
×
484

485
        Ok(None)
×
486
    }
487

488
    //
489
    // volume related commands
490
    //
491

492
    pub fn set_volume(&mut self, input: &str) -> eyre::Result<Option<String>> {
×
493
        let current = self.client.status()?.volume;
×
494

495
        let target = match input {
×
496
            matched if matched.starts_with('+') => {
×
497
                if let Ok(volume) = matched[1..].parse::<i8>() {
×
498
                    current.checked_add(volume).unwrap_or(100).min(100)
×
499
                } else {
500
                    panic!("Invalid volume increment, must be between 1-100")
×
501
                }
502
            }
503
            matched if matched.starts_with('-') => {
×
504
                if let Ok(volume) = matched[1..].parse::<i8>() {
×
505
                    current.checked_sub(volume).unwrap_or(100).max(0)
×
506
                } else {
507
                    panic!("Invalid volume increment, must be between 1-100")
×
508
                }
509
            }
510
            _ => input.parse::<i8>().unwrap_or(0),
×
511
        };
512

513
        self.client
×
514
            .volume(target)
×
515
            .map(|_| None)
×
516
            .map_err(eyre::Report::from)
×
517
    }
518

519
    //
520
    // output related commands
521
    //
522

523
    fn status(&mut self) -> eyre::Result<Status> {
×
524
        let status = self.client.status()?;
×
525

526
        let volume = status.volume.to_string();
×
527

528
        let current_song = self.client.currentsong()?;
×
529

530
        let artist = current_song
×
531
            .as_ref()
532
            .and_then(|song| song.artist.as_ref())
×
533
            .map_or(String::new(), ToString::to_string);
×
534
        let title = current_song
×
535
            .as_ref()
536
            .and_then(|song| song.title.as_ref())
×
537
            .map_or(String::new(), ToString::to_string);
×
538

539
        let state = match status.state {
×
540
            mpd::State::Play => "play",
×
541
            mpd::State::Pause => "pause",
×
542
            mpd::State::Stop => "stop",
×
543
        }
544
        .to_string();
545

546
        let position = match status.song {
×
547
            Some(song) => song.pos,
×
548
            None => 0,
×
549
        };
550
        let time = crate::time::Track::from(status.time);
×
551

552
        Ok(Status {
×
553
            volume,
×
554
            state,
×
555
            artist,
×
556
            title,
×
557
            position,
×
558
            queue_count: status.queue_len,
×
559
            elapsed: time.elapsed,
×
560
            track_length: time.total,
×
561
            repeat: OnOff::from(status.repeat),
×
562
            random: OnOff::from(status.random),
×
563
            single: OnOff::from(status.single),
×
564
            consume: OnOff::from(status.consume),
×
565
        })
566
    }
567

568
    pub fn current_status(&mut self) -> eyre::Result<Option<String>> {
×
569
        let status = self.status()?;
×
570
        let response = match self.format {
×
571
            OutputFormat::Json => serde_json::to_string(&status)?,
×
572
            OutputFormat::Text => format!("{status}"),
×
573
        };
574

575
        Ok(Some(response))
×
576
    }
577
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc