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

johnallen3d / mpc-rs / #110

23 Sep 2023 01:18AM UTC coverage: 0.0%. Remained the same
#110

push

johnallen3d
feat: add `add` command

Relates to #16

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

0 of 376 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/src/client.rs
1
use std::convert::TryInto;
2
use std::fmt;
3
use std::time::Duration;
4

5
use chrono::{TimeZone, Utc};
6
use eyre::WrapErr;
7
use serde::Serialize;
8

9
use crate::args::{OnOff, OutputFormat};
10
use crate::se::serialize_playlists;
11

12
#[derive(Serialize)]
13
pub struct Status {
14
    volume: String,
15
    state: String,
16
    artist: String,
17
    title: String,
18
    position: u32,
19
    queue_count: u32,
20
    elapsed: Time,
21
    track_length: Time,
22
    repeat: OnOff,
23
    random: OnOff,
24
    single: OnOff,
25
    consume: OnOff,
26
}
27

28
impl fmt::Display for Status {
29
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
30
        write!(
×
31
            f,
×
32
            "volume={}\nstate={}\nartist={}\ntitle={}\nposition={}\nqueue_count={}\nelapsed={}\ntrack_length={}\nrepeat={}\nrandom={}\nsingle={}\nconsume={}",
33
            self.volume,
×
34
            self.state,
×
35
            self.artist,
×
36
            self.title,
×
37
            self.position,
×
38
            self.queue_count,
×
39
            self.elapsed,
×
40
            self.track_length,
×
41
            self.repeat,
×
42
            self.random,
×
43
            self.single,
×
44
            self.consume,
×
45
        )
46
    }
47
}
48

49
#[derive(Serialize)]
50
struct Song(mpd::song::Song);
51

52
#[derive(Serialize)]
53
struct TrackList {
54
    songs: Vec<Current>,
55
}
56

57
impl fmt::Display for TrackList {
58
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
59
        for (index, song) in self.songs.iter().enumerate() {
×
60
            write!(f, "{index}={song}")?;
×
61
        }
62

63
        Ok(())
×
64
    }
65
}
66

67
#[derive(Serialize)]
68
struct Current {
69
    artist: String,
70
    title: String,
71
}
72

73
impl From<Status> for Current {
74
    fn from(status: Status) -> Self {
×
75
        Current {
76
            artist: status.artist,
×
77
            title: status.title,
×
78
        }
79
    }
80
}
81

82
impl From<Song> for Current {
83
    fn from(song: Song) -> Self {
×
84
        Current {
85
            artist: song.0.artist.unwrap_or(String::new()),
×
86
            title: song.0.title.unwrap_or(String::new()),
×
87
        }
88
    }
89
}
90

91
impl fmt::Display for Current {
92
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
93
        writeln!(f, "{} - {}", self.artist, self.title)
×
94
    }
95
}
96

97
#[derive(Serialize)]
98
pub struct TrackTime {
99
    elapsed: Time,
100
    total: Time,
101
}
102

103
impl From<Option<(Duration, Duration)>> for TrackTime {
104
    fn from(time: Option<(Duration, Duration)>) -> Self {
×
105
        match time {
×
106
            Some((elapsed, total)) => TrackTime {
107
                elapsed: Time::from(elapsed),
×
108
                total: Time::from(total),
×
109
            },
110
            None => TrackTime {
111
                elapsed: Time::from(0),
×
112
                total: Time::from(0),
×
113
            },
114
        }
115
    }
116
}
117

118
#[derive(Serialize)]
119
pub struct Time(String);
120

121
impl From<Duration> for Time {
122
    fn from(duration: Duration) -> Self {
×
123
        Time(format!(
×
124
            "{:02}:{:02}",
×
125
            duration.as_secs() / 60,
×
126
            duration.as_secs() % 60
×
127
        ))
128
    }
129
}
130

131
impl From<u32> for Time {
132
    fn from(duration: u32) -> Self {
×
133
        Time(format!("{:02}:{:02}", duration / 60, duration % 60))
×
134
    }
135
}
136

137
impl fmt::Display for Time {
138
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
139
        write!(f, "{}", self.0)
×
140
    }
141
}
142

143
struct HumanReadableDuration(core::time::Duration);
144

145
impl From<Duration> for HumanReadableDuration {
146
    fn from(duration: Duration) -> Self {
×
147
        HumanReadableDuration(duration)
×
148
    }
149
}
150

151
impl ToString for HumanReadableDuration {
152
    fn to_string(&self) -> String {
×
153
        let total_seconds = self.0.as_secs();
×
154
        let days = total_seconds / 86400;
×
155
        let hours = (total_seconds % 86400) / 3600;
×
156
        let minutes = (total_seconds % 3600) / 60;
×
157
        let seconds = total_seconds % 60;
×
158

159
        format!("{days} days, {hours}:{minutes:02}:{seconds:02}")
×
160
    }
161
}
162
#[derive(Serialize)]
163
struct Stats {
164
    artists: u32,
165
    albums: u32,
166
    songs: u32,
167
    uptime: String,
168
    playtime: String,
169
    db_playtime: String,
170
    db_update: String,
171
}
172

173
impl Stats {
174
    pub fn new(stats: mpd::stats::Stats) -> Self {
×
175
        let seconds: i64 =
×
176
            stats.db_update.as_secs().try_into().unwrap_or(i64::MAX);
×
177
        let db_update = match Utc.timestamp_opt(seconds, 0) {
×
178
            chrono::LocalResult::Single(date_time) => {
×
179
                date_time.format("%a %b %d %H:%M:%S %Y").to_string()
×
180
            }
181
            _ => String::new(),
×
182
        };
183

184
        Self {
185
            artists: stats.artists,
×
186
            albums: stats.albums,
×
187
            songs: stats.songs,
×
188
            uptime: HumanReadableDuration::from(stats.uptime).to_string(),
×
189
            playtime: HumanReadableDuration::from(stats.playtime).to_string(),
×
190
            db_playtime: HumanReadableDuration::from(stats.db_playtime)
×
191
                .to_string(),
192
            db_update,
193
        }
194
    }
195
}
196

197
impl fmt::Display for Stats {
198
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
199
        write!(
×
200
            f,
×
201
            "artists={}\nalbums={}\nsongs={}\nuptime={}\nplaytime={}\ndb_playtime={}\ndb_update={}",
202
            self.artists,
×
203
            self.albums,
×
204
            self.songs,
×
205
            self.uptime,
×
206
            self.playtime,
×
207
            self.db_playtime,
×
208
            self.db_update,
×
209
            )
210
    }
211
}
212

213
#[derive(Serialize)]
214
pub struct Playlists {
215
    #[serde(serialize_with = "serialize_playlists")]
216
    playlists: Vec<Playlist>,
217
}
218

219
#[derive(Default, Serialize)]
220
pub struct Playlist {
221
    pub name: String,
222
    songs: Vec<Song>,
223
}
224

225
impl From<String> for Playlist {
226
    fn from(name: String) -> Self {
×
227
        Playlist {
228
            name,
229
            ..Default::default()
230
        }
231
    }
232
}
233

234
impl fmt::Display for Playlists {
235
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
236
        for (index, playlist) in self.playlists.iter().enumerate() {
×
237
            write!(f, "{}={}", index, playlist.name)?;
×
238
        }
239

240
        Ok(())
×
241
    }
242
}
243

244
#[derive(Serialize)]
245
pub struct Outputs {
246
    outputs: Vec<Output>,
247
}
248

249
#[derive(Serialize)]
250
pub struct Output(String);
251

252
impl From<String> for Output {
253
    fn from(name: String) -> Self {
×
254
        Output(name)
×
255
    }
256
}
257

258
impl fmt::Display for Outputs {
259
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
260
        for (index, output) in self.outputs.iter().enumerate() {
×
261
            write!(f, "{}={}", index, output.0)?;
×
262
        }
263

264
        Ok(())
×
265
    }
266
}
267

268
#[derive(Serialize)]
269
pub struct Versions {
270
    mpd: String,
271
    mp_cli: String,
272
}
273

274
impl fmt::Display for Versions {
275
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
276
        write!(f, "mpd={}\nmp-cli={}", self.mpd, self.mp_cli)
×
277
    }
278
}
279

280
pub struct Client {
281
    client: mpd::Client,
282
    format: OutputFormat,
283
}
284

285
impl Client {
286
    pub fn new(
×
287
        bind_to_address: &str,
288
        port: &str,
289
        format: OutputFormat,
290
    ) -> eyre::Result<Client> {
291
        let client = mpd::Client::connect(format!("{bind_to_address}:{port}"))
×
292
            .wrap_err("Error connecting to mpd server".to_string())?;
×
293

294
        Ok(Self { client, format })
×
295
    }
296

297
    //
298
    // queue related commands
299
    //
300
    pub fn add(&mut self, path: &str) -> eyre::Result<Option<String>> {
×
301
        let music_dir = self.client.music_directory()?;
×
302

303
        let mut file = match path.strip_prefix(&music_dir) {
×
304
            Some(remainder) => remainder,
×
305
            None => path,
×
306
        }
307
        .to_string();
308

309
        if file.starts_with('/') {
×
310
            file.remove(0);
×
311
        }
312

313
        let song = crate::mpd::song::Song {
314
            file: file.clone(),
×
315
            ..Default::default()
316
        };
317

318
        self.client
×
319
            .push(song)
×
320
            .wrap_err(format!("unkown or inalid path: {file}"))?;
×
321

322
        Ok(None)
×
323
    }
324

325
    pub fn crop(&mut self) -> eyre::Result<Option<String>> {
×
326
        // determine current song position
327
        // remove all songs before current song
328
        // remove all songs from 1 onwards
329
        let status = self.status()?;
×
330
        let current_position = status.position;
×
331
        let length = status.queue_count;
×
332

333
        if length < 1 {
×
334
            return self.current_status();
×
335
        }
336

337
        self.client.delete(0..current_position)?;
×
338
        // it doesn't matter that the range is out of bounds
339
        self.client.delete(1..length)?;
×
340

341
        self.current_status()
×
342
    }
343

344
    pub fn del(
×
345
        &mut self,
346
        position: Option<u32>,
347
    ) -> eyre::Result<Option<String>> {
348
        let position = match position {
×
349
            Some(position) => position,
×
350
            None => self.status()?.position,
×
351
        };
352

353
        self.client.delete(position)?;
×
354

355
        self.current_status()
×
356
    }
357

358
    //
359
    // playback related commands
360
    //
361
    pub fn current(&mut self) -> eyre::Result<Option<String>> {
×
362
        let current = Current::from(self.status()?);
×
363

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

369
        Ok(Some(response))
×
370
    }
371

372
    pub fn play(
×
373
        &mut self,
374
        position: Option<u32>,
375
    ) -> eyre::Result<Option<String>> {
376
        if position.is_none() {
×
377
            self.client.play()?;
×
378
            return self.current_status();
×
379
        }
380
        // TODO: this is super hacky, can't find a "jump" in rust-mpd
381

382
        // pause
383
        // get current position
384
        // next/prev to desired position
385
        // play
386

387
        let position = position.unwrap();
×
388
        let current_position = self.status()?.position;
×
389

390
        self.pause()?;
×
391

392
        if current_position > position {
×
393
            for _ in (position..current_position).rev() {
×
394
                self.prev()?;
×
395
            }
396
        } else {
397
            for _ in (current_position..position).rev() {
×
398
                self.next()?;
×
399
            }
400
        }
401

402
        self.client.play()?;
×
403

404
        self.current_status()
×
405
    }
406

407
    pub fn next(&mut self) -> eyre::Result<Option<String>> {
408
        self.client.next()?;
×
409

410
        self.current_status()
×
411
    }
412

413
    pub fn prev(&mut self) -> eyre::Result<Option<String>> {
414
        self.client.prev()?;
×
415

416
        self.current_status()
×
417
    }
418

419
    pub fn pause(&mut self) -> eyre::Result<Option<String>> {
420
        self.client.pause(true)?;
×
421

422
        self.current_status()
×
423
    }
424

425
    pub fn pause_if_playing(&mut self) -> eyre::Result<Option<String>> {
×
426
        match self.client.status()?.state {
×
427
            mpd::State::Play => self.pause(),
×
428
            mpd::State::Pause | mpd::State::Stop => Err(eyre::eyre!("")),
×
429
        }
430
    }
431

432
    pub fn cdprev(&mut self) -> eyre::Result<Option<String>> {
×
433
        let default_duration = Duration::from_secs(0);
×
434
        let status = &self.client.status()?;
×
435
        let current = status.elapsed.unwrap_or(default_duration).as_secs();
×
436

437
        if current < 3 {
×
438
            self.prev()
×
439
        } else {
440
            let place = match status.song {
×
441
                Some(ref song) => song.pos,
×
442
                None => 0,
×
443
            };
444
            self.client.seek(place, 0)?;
×
445

446
            self.current_status()
×
447
        }
448
    }
449

450
    pub fn toggle(&mut self) -> eyre::Result<Option<String>> {
×
451
        match self.client.status()?.state {
×
452
            mpd::State::Play => self.pause(),
×
453
            mpd::State::Pause | mpd::State::Stop => self.play(None),
×
454
        }
455
    }
456

457
    pub fn stop(&mut self) -> eyre::Result<Option<String>> {
458
        self.client.stop()?;
×
459

460
        self.current_status()
×
461
    }
462

463
    //
464
    // playlist related commands
465
    //
466

467
    pub fn clear(&mut self) -> eyre::Result<Option<String>> {
468
        self.client.clear()?;
×
469

470
        self.current_status()
×
471
    }
472

473
    pub fn outputs(&mut self) -> eyre::Result<Option<String>> {
×
474
        let outputs = self.client.outputs()?;
×
475
        let outputs: Vec<Output> =
×
476
            outputs.into_iter().map(|p| Output::from(p.name)).collect();
×
477
        let outputs = Outputs { outputs };
×
478

479
        let response = match self.format {
×
480
            OutputFormat::Json => serde_json::to_string(&outputs)?,
×
481
            OutputFormat::Text => outputs.to_string(),
×
482
        };
483

484
        Ok(Some(response))
×
485
    }
486

487
    pub fn queued(&mut self) -> eyre::Result<Option<String>> {
×
488
        if let Some(song) =
×
489
            self.client.queue().map_err(|e| eyre::eyre!(e))?.get(0)
×
490
        {
491
            // safe to unwrap because we know we have a song
492
            let current = Current::from(Song(song.clone()));
×
493

494
            let response = match self.format {
×
495
                OutputFormat::Json => serde_json::to_string(&current)?,
×
496
                OutputFormat::Text => current.to_string(),
×
497
            };
498

499
            Ok(Some(response))
×
500
        } else {
501
            Ok(None)
×
502
        }
503
    }
504

505
    pub fn shuffle(&mut self) -> eyre::Result<Option<String>> {
506
        self.client.shuffle(..)?;
×
507

508
        self.current_status()
×
509
    }
510

511
    pub fn lsplaylists(&mut self) -> eyre::Result<Option<String>> {
×
512
        let playlists = self.client.playlists()?;
×
513
        let playlists: Vec<Playlist> = playlists
×
514
            .into_iter()
515
            .map(|p| Playlist::from(p.name))
×
516
            .collect();
517
        let playlists = Playlists { playlists };
×
518

519
        let response = match self.format {
×
520
            OutputFormat::Json => serde_json::to_string(&playlists)?,
×
521
            OutputFormat::Text => playlists.to_string(),
×
522
        };
523

524
        Ok(Some(response))
×
525
    }
526

527
    pub fn playlist(
×
528
        &mut self,
529
        name: Option<String>,
530
    ) -> eyre::Result<Option<String>> {
531
        // if given a name list songs in that playlist
532
        // if `None` list songs in current playlist
533
        let songs = match name {
×
534
            Some(name) => self.client.playlist(&name)?,
×
535
            None => self.client.queue()?,
×
536
        };
537

538
        let songs: Vec<Current> =
×
539
            songs.into_iter().map(|s| Current::from(Song(s))).collect();
×
540
        let track_list = TrackList { songs };
×
541

542
        let response = match self.format {
×
543
            OutputFormat::Json => serde_json::to_string(&track_list)?,
×
544
            OutputFormat::Text => track_list.to_string(),
×
545
        };
546

547
        Ok(Some(response))
×
548
    }
549

550
    pub fn repeat(
×
551
        &mut self,
552
        state: Option<OnOff>,
553
    ) -> eyre::Result<Option<String>> {
554
        let state = match state {
×
555
            Some(state) => state == OnOff::On,
×
556
            None => !self.client.status()?.repeat,
×
557
        };
558

559
        self.client.repeat(state)?;
×
560

561
        self.current_status()
×
562
    }
563

564
    pub(crate) fn random(
×
565
        &mut self,
566
        state: Option<OnOff>,
567
    ) -> eyre::Result<Option<String>> {
568
        let state = match state {
×
569
            Some(state) => state == OnOff::On,
×
570
            None => !self.client.status()?.random,
×
571
        };
572

573
        self.client.random(state)?;
×
574

575
        self.current_status()
×
576
    }
577

578
    pub(crate) fn single(
×
579
        &mut self,
580
        state: Option<OnOff>,
581
    ) -> eyre::Result<Option<String>> {
582
        let state = match state {
×
583
            Some(state) => state == OnOff::On,
×
584
            None => !self.client.status()?.single,
×
585
        };
586

587
        self.client.single(state)?;
×
588

589
        self.current_status()
×
590
    }
591

592
    pub fn consume(
×
593
        &mut self,
594
        state: Option<OnOff>,
595
    ) -> eyre::Result<Option<String>> {
596
        let state = match state {
×
597
            Some(state) => state == OnOff::On,
×
598
            None => !self.client.status()?.consume,
×
599
        };
600

601
        self.client.consume(state)?;
×
602

603
        self.current_status()
×
604
    }
605

606
    pub fn crossfade(
×
607
        &mut self,
608
        seconds: Option<String>,
609
    ) -> eyre::Result<Option<String>> {
610
        let crossfade = match seconds {
×
611
            Some(secs) => secs.parse::<i64>().wrap_err(format!(
×
612
                "\"{secs}\" is not 0 or a positive number"
×
613
            ))?,
614
            None => 0,
×
615
        };
616

617
        self.client
×
618
            .crossfade(crossfade)
×
619
            .wrap_err(format!("\"{crossfade}\" is too large"))?;
×
620

621
        Ok(Some(format!("crossfade: {crossfade}")))
×
622
    }
623

624
    pub fn version(&mut self) -> eyre::Result<Option<String>> {
×
625
        let mpd = format!(
×
626
            "{}.{}.{}",
627
            self.client.version.0, self.client.version.1, self.client.version.2
×
628
        );
629
        let mp_cli = env!("CARGO_PKG_VERSION").to_string();
×
630

631
        let versions = Versions { mpd, mp_cli };
×
632

633
        let response = match self.format {
×
634
            OutputFormat::Json => serde_json::to_string(&versions)?,
×
635
            OutputFormat::Text => versions.to_string(),
×
636
        };
637

638
        Ok(Some(response))
×
639
    }
640

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

644
        let response = match self.format {
×
645
            OutputFormat::Json => serde_json::to_string(&stats)?,
×
646
            OutputFormat::Text => stats.to_string(),
×
647
        };
648

649
        Ok(Some(response))
×
650
    }
651

652
    pub fn save(&mut self, name: &str) -> eyre::Result<Option<String>> {
653
        self.client
×
654
            .save(name)
×
655
            .wrap_err(format!("Playlist already exists: {name}"))?;
×
656

657
        Ok(None)
×
658
    }
659

660
    pub fn rm(&mut self, name: &str) -> eyre::Result<Option<String>> {
661
        self.client
×
662
            .pl_remove(name)
×
663
            .wrap_err(format!("Unknown playlist: {name}"))?;
×
664

665
        Ok(None)
×
666
    }
667

668
    //
669
    // volume related commands
670
    //
671

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

675
        let target = match input {
×
676
            matched if matched.starts_with('+') => {
×
677
                if let Ok(volume) = matched[1..].parse::<i8>() {
×
678
                    current.checked_add(volume).unwrap_or(100).min(100)
×
679
                } else {
680
                    panic!("Invalid volume increment, must be between 1-100")
×
681
                }
682
            }
683
            matched if matched.starts_with('-') => {
×
684
                if let Ok(volume) = matched[1..].parse::<i8>() {
×
685
                    current.checked_sub(volume).unwrap_or(100).max(0)
×
686
                } else {
687
                    panic!("Invalid volume increment, must be between 1-100")
×
688
                }
689
            }
690
            _ => input.parse::<i8>().unwrap_or(0),
×
691
        };
692

693
        self.client
×
694
            .volume(target)
×
695
            .map(|_| None)
×
696
            .map_err(eyre::Report::from)
×
697
    }
698

699
    //
700
    // output related commands
701
    //
702

703
    fn status(&mut self) -> eyre::Result<Status> {
×
704
        let status = self.client.status()?;
×
705

706
        let volume = status.volume.to_string();
×
707

708
        let current_song = self.client.currentsong()?;
×
709

710
        let artist = current_song
×
711
            .as_ref()
712
            .and_then(|song| song.artist.as_ref())
×
713
            .map_or(String::new(), ToString::to_string);
×
714
        let title = current_song
×
715
            .as_ref()
716
            .and_then(|song| song.title.as_ref())
×
717
            .map_or(String::new(), ToString::to_string);
×
718

719
        let state = match status.state {
×
720
            mpd::State::Play => "play",
×
721
            mpd::State::Pause => "pause",
×
722
            mpd::State::Stop => "stop",
×
723
        }
724
        .to_string();
725

726
        let position = match status.song {
×
727
            Some(song) => song.pos,
×
728
            None => 0,
×
729
        };
730
        let time = TrackTime::from(status.time);
×
731

732
        Ok(Status {
×
733
            volume,
×
734
            state,
×
735
            artist,
×
736
            title,
×
737
            position,
×
738
            queue_count: status.queue_len,
×
739
            elapsed: time.elapsed,
×
740
            track_length: time.total,
×
741
            repeat: OnOff::from(status.repeat),
×
742
            random: OnOff::from(status.random),
×
743
            single: OnOff::from(status.single),
×
744
            consume: OnOff::from(status.consume),
×
745
        })
746
    }
747

748
    pub fn current_status(&mut self) -> eyre::Result<Option<String>> {
×
749
        let status = self.status()?;
×
750
        let response = match self.format {
×
751
            OutputFormat::Json => serde_json::to_string(&status)?,
×
752
            OutputFormat::Text => format!("{status}"),
×
753
        };
754

755
        Ok(Some(response))
×
756
    }
757
}
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