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

johnallen3d / mpc-rs / #177

27 Dec 2023 03:38PM UTC coverage: 19.014% (-1.1%) from 20.149%
#177

push

web-flow
feat: add `search` command (#83)

Relates to #16

0 of 40 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

135 of 710 relevant lines covered (19.01%)

1.15 hits per line

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

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

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

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

25
#[derive(PartialEq)]
26
enum Direction {
27
    Forward,
28
    Reverse,
29
}
30

31
#[derive(Serialize)]
32
pub struct Versions {
33
    mpd: String,
34
    mp_cli: String,
35
}
36

37
impl fmt::Display for Versions {
38
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
39
        write!(f, "mpd={}\nmp-cli={}", self.mpd, self.mp_cli)
×
40
    }
41
}
42

43
pub struct Client {
44
    client: mpd::Client,
45
    format: OutputFormat,
46
}
47

48
impl Client {
49
    pub fn new(
×
50
        bind_to_address: &str,
51
        port: &str,
52
        format: OutputFormat,
53
    ) -> eyre::Result<Client> {
54
        let client = mpd::Client::connect(format!("{bind_to_address}:{port}"))
×
55
            .wrap_err("Error connecting to mpd server".to_string())?;
×
56

57
        Ok(Self { client, format })
×
58
    }
59

60
    //
61
    // queue related commands
62
    //
63
    pub fn add(&mut self, path: &str) -> eyre::Result<Option<String>> {
×
64
        let music_dir = self.client.music_directory()?;
×
65

66
        let absolute_path = if path.starts_with(&music_dir) {
×
67
            path.to_string()
×
68
        } else {
69
            PathBuf::from(&music_dir)
×
70
                .join(path)
×
71
                .to_str()
72
                .unwrap()
73
                .to_string()
74
        };
75

76
        let mut finder = Finder::new(music_dir);
×
77

78
        finder.find(Path::new(Path::new(&absolute_path)))?;
×
79

80
        for file in finder.found {
×
81
            let song = mpd::song::Song {
82
                file: file.relative_path,
×
83
                ..Default::default()
84
            };
85

86
            self.client
×
87
                .push(song.clone())
×
88
                .wrap_err(format!("unkown or inalid path: {}", song.file))?;
×
89
        }
90

91
        Ok(None)
×
92
    }
93

94
    pub fn crop(&mut self) -> eyre::Result<Option<String>> {
×
95
        // determine current song position
96
        // remove all songs before current song
97
        // remove all songs from 1 onwards
98
        let status = self.status()?;
×
99
        let current_position = status.position;
×
100
        let length = status.queue_count;
×
101

102
        if length < 1 {
×
103
            return self.current_status();
×
104
        }
105

106
        self.client.delete(0..current_position)?;
×
107
        // it doesn't matter that the range is out of bounds
108
        self.client.delete(1..length)?;
×
109

110
        self.current_status()
×
111
    }
112

113
    pub fn del(
×
114
        &mut self,
115
        position: Option<u32>,
116
    ) -> eyre::Result<Option<String>> {
117
        let position = match position {
×
118
            Some(position) => position,
×
119
            None => self.status()?.position,
×
120
        };
121

122
        self.client.delete(position)?;
×
123

124
        self.current_status()
×
125
    }
126

127
    //
128
    // playback related commands
129
    //
130
    pub fn current(&mut self) -> eyre::Result<Option<String>> {
×
131
        let current = Current::from(self.status()?);
×
132

133
        let response = match self.format {
×
134
            OutputFormat::Json => serde_json::to_string(&current)?,
×
135
            OutputFormat::Text => current.to_string(),
×
136
        };
137

138
        Ok(Some(response))
×
139
    }
140

141
    pub fn play(
×
142
        &mut self,
143
        position: Option<u32>,
144
    ) -> eyre::Result<Option<String>> {
145
        if position.is_none() {
×
146
            self.client.play()?;
×
147
            return self.current_status();
×
148
        }
149
        // TODO: this is super hacky, can't find a "jump" in rust-mpd
150

151
        // pause
152
        // get current position
153
        // next/prev to desired position
154
        // play
155

156
        let position = position.unwrap();
×
157
        let current_position = self.status()?.position;
×
158

159
        self.pause()?;
×
160

161
        if current_position > position {
×
162
            for _ in (position..current_position).rev() {
×
163
                self.prev()?;
×
164
            }
165
        } else {
166
            for _ in (current_position..position).rev() {
×
167
                self.next()?;
×
168
            }
169
        }
170

171
        self.client.play()?;
×
172

173
        self.current_status()
×
174
    }
175

176
    // allowing because this follows an external api naming convention
177
    #[allow(clippy::should_implement_trait)]
178
    pub fn next(&mut self) -> eyre::Result<Option<String>> {
179
        self.client.next()?;
×
180

181
        self.current_status()
×
182
    }
183

184
    pub fn prev(&mut self) -> eyre::Result<Option<String>> {
185
        self.client.prev()?;
×
186

187
        self.current_status()
×
188
    }
189

190
    pub fn pause(&mut self) -> eyre::Result<Option<String>> {
191
        self.client.pause(true)?;
×
192

193
        self.current_status()
×
194
    }
195

196
    pub fn pause_if_playing(&mut self) -> eyre::Result<Option<String>> {
×
197
        match self.client.status()?.state {
×
198
            mpd::State::Play => self.pause(),
×
199
            mpd::State::Pause | mpd::State::Stop => Err(eyre::eyre!("")),
×
200
        }
201
    }
202

203
    pub fn cdprev(&mut self) -> eyre::Result<Option<String>> {
×
204
        let default_duration = Duration::from_secs(0);
×
205
        let status = &self.client.status()?;
×
206
        let current = status.elapsed.unwrap_or(default_duration).as_secs();
×
207

208
        if current < 3 {
×
209
            self.prev()
×
210
        } else {
211
            let place = match status.song {
×
212
                Some(ref song) => song.pos,
×
213
                None => 0,
×
214
            };
215
            self.client.seek(place, 0)?;
×
216

217
            self.current_status()
×
218
        }
219
    }
220

221
    pub fn toggle(&mut self) -> eyre::Result<Option<String>> {
×
222
        match self.client.status()?.state {
×
223
            mpd::State::Play => self.pause(),
×
224
            mpd::State::Pause | mpd::State::Stop => self.play(None),
×
225
        }
226
    }
227

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

231
        self.current_status()
×
232
    }
233

234
    pub fn seek(&mut self, position: &str) -> eyre::Result<Option<String>> {
×
235
        let current_status = self.status()?;
×
236

237
        // valid position syntax: [+-][HH:MM:SS]|<0-100>%
238
        let place = if position.contains('%') {
×
239
            let position = position.replace('%', "");
×
240

241
            let percent = position.parse::<u8>().wrap_err(format!(
×
242
                "\"{position}\" must be a value between 0 and 100"
×
243
            ))?;
244
            if percent > 100 {
×
245
                return Err(eyre::eyre!(
×
246
                    "\"{position}\" must be a value between 0 and 100"
×
247
                ));
248
            }
249

250
            let length = current_status.track_length.as_secs;
×
251
            let percent = i64::try_from(percent)?;
×
252

253
            length * percent / 100
×
254
        } else if position.contains('+') || position.contains('-') {
×
255
            current_status.elapsed.compute_offset(position)
×
256
        } else {
257
            time::Time::from(position.to_string()).as_secs
×
258
        };
259

260
        let position = self.status()?.position;
×
261

262
        self.client.seek(position, place)?;
×
263

264
        self.stats()
×
265
    }
266

267
    pub fn seekthrough(
×
268
        &mut self,
269
        position: &str,
270
    ) -> eyre::Result<Option<String>> {
271
        let mut direction = Direction::Forward;
×
272

273
        // valid position syntax: [+-][HH:MM:SS]
274
        let mut place = if position.contains('%') {
×
275
            return Err(eyre::eyre!(
×
276
                "seekthrough does not support percentage based seeking"
×
277
            ));
278
        } else {
279
            // if `-` present then back otherwise assume forward
280
            if position.contains('-') {
×
281
                direction = Direction::Reverse;
×
282
            }
283
            time::Time::from(position.to_string()).as_secs
×
284
        };
285

286
        let queue = self.client.queue()?;
×
287
        let start = usize::try_from(self.status()?.position)?;
×
288
        let mut elapsed = self.status()?.elapsed.as_secs;
×
289

290
        match direction {
×
291
            Direction::Forward => {
292
                for song in queue.iter().cycle().skip(start) {
×
293
                    let current_song_duration =
×
294
                        i64::try_from(song.duration.unwrap().as_secs())?;
×
295
                    let remainder = current_song_duration - elapsed - place;
×
296

297
                    // seek position fits the current song
298
                    if remainder >= 0 {
×
299
                        let position = song.place.unwrap().id;
×
300
                        self.client.seek(position, elapsed + place)?;
×
301
                        break;
×
302
                    }
303

304
                    place = remainder.abs();
×
305
                    elapsed = 0;
×
306
                }
307
            }
308
            Direction::Reverse => {
309
                // queue is reversed so we need to start from the end
310
                let start = queue.len() - start - 1;
×
311

312
                for song in queue.iter().rev().cycle().skip(start) {
×
313
                    let current_song_duration =
×
314
                        i64::try_from(song.duration.unwrap().as_secs())?;
×
315

316
                    let remainder = if elapsed > 0 {
×
317
                        elapsed - place
×
318
                    } else {
319
                        current_song_duration - place
×
320
                    };
321

322
                    // seek position fits the current song
323
                    if remainder >= 0 {
×
324
                        let position = song.place.unwrap().id;
×
325
                        self.client.seek(position, remainder)?;
×
326
                        break;
×
327
                    }
328

329
                    place = remainder.abs();
×
330
                    elapsed = 0;
×
331
                }
332
            }
333
        }
334

335
        self.stats()
×
336
    }
337

338
    //
339
    // playlist related commands
340
    //
341

342
    pub fn clear(&mut self) -> eyre::Result<Option<String>> {
343
        self.client.clear()?;
×
344

345
        self.current_status()
×
346
    }
347

348
    pub fn outputs(&mut self) -> eyre::Result<Option<String>> {
×
349
        let outputs = self.client.outputs()?;
×
350
        let outputs: Vec<Output> =
×
351
            outputs.into_iter().map(Output::from).collect();
×
352
        let outputs = Outputs { outputs };
×
353

354
        let response = match self.format {
×
355
            OutputFormat::Json => serde_json::to_string(&outputs)?,
×
356
            OutputFormat::Text => outputs.to_string(),
×
357
        };
358

359
        Ok(Some(response))
×
360
    }
361

362
    fn output_for(&mut self, name_or_id: &str) -> Result<u32, eyre::Error> {
×
363
        let id: u32 = if let Ok(parsed_id) = name_or_id.parse::<u32>() {
×
364
            parsed_id
×
365
        } else {
366
            self.client
×
367
                .outputs()?
×
368
                .iter()
×
369
                .find(|&o| o.name == name_or_id)
×
370
                .ok_or_else(|| eyre::eyre!("unknown output: {}", name_or_id))?
×
371
                .id
372
        };
373

374
        Ok(id)
×
375
    }
376

377
    fn enable_or_disable(
×
378
        &mut self,
379
        enable: bool,
380
        args: Vec<String>,
381
    ) -> eyre::Result<Option<String>> {
382
        let mut only = false;
×
383
        let mut outputs = Vec::new();
×
384

385
        for arg in args {
×
386
            if arg == "only" {
×
387
                only = true;
×
388
            } else {
389
                outputs.push(arg);
×
390
            }
391
        }
392

393
        if only {
×
394
            // first disable all outputs
395
            for output in self.client.outputs()? {
×
396
                self.client.output(output, enable)?;
×
397
            }
398
        }
399

400
        for name_or_id in outputs {
×
401
            let id = self.output_for(&name_or_id)?;
×
402

403
            self.client.output(id, enable)?;
×
404
        }
405

406
        self.outputs()
×
407
    }
408

409
    pub fn enable(
×
410
        &mut self,
411
        args: Vec<String>,
412
    ) -> eyre::Result<Option<String>> {
413
        self.enable_or_disable(true, args)
×
414
    }
415

416
    pub fn disable(
×
417
        &mut self,
418
        args: Vec<String>,
419
    ) -> eyre::Result<Option<String>> {
420
        self.enable_or_disable(false, args)
×
421
    }
422

423
    pub fn toggle_output(
×
424
        &mut self,
425
        args: Vec<String>,
426
    ) -> eyre::Result<Option<String>> {
427
        if args.is_empty() {
×
428
            return Err(eyre::eyre!("no outputs given"));
×
429
        }
430

431
        for name_or_id in args {
×
432
            let id = self.output_for(&name_or_id)?;
×
433

434
            self.client.out_toggle(id)?;
×
435
        }
436

437
        self.outputs()
×
438
    }
439

440
    pub fn queued(&mut self) -> eyre::Result<Option<String>> {
×
441
        if let Some(song) =
×
442
            self.client.queue().map_err(|e| eyre::eyre!(e))?.get(0)
×
443
        {
444
            // safe to unwrap because we know we have a song
445
            let current = Current::from(Song {
×
446
                inner: song.clone(),
×
447
            });
448

449
            let response = match self.format {
×
450
                OutputFormat::Json => serde_json::to_string(&current)?,
×
451
                OutputFormat::Text => current.to_string(),
×
452
            };
453

454
            Ok(Some(response))
×
455
        } else {
456
            Ok(None)
×
457
        }
458
    }
459

460
    pub fn shuffle(&mut self) -> eyre::Result<Option<String>> {
461
        self.client.shuffle(..)?;
×
462

463
        self.current_status()
×
464
    }
465

466
    pub fn lsplaylists(&mut self) -> eyre::Result<Option<String>> {
×
467
        let playlists = self.client.playlists()?;
×
468
        let playlists: Vec<Playlist> = playlists
×
469
            .into_iter()
470
            .map(|p| Playlist::from(p.name))
×
471
            .collect();
472
        let playlists = Playlists { playlists };
×
473

474
        let response = match self.format {
×
475
            OutputFormat::Json => serde_json::to_string(&playlists)?,
×
476
            OutputFormat::Text => playlists.to_string(),
×
477
        };
478

479
        Ok(Some(response))
×
480
    }
481

482
    pub fn load(
×
483
        &mut self,
484
        name: &String,
485
        range: Option<String>,
486
    ) -> eyre::Result<Option<String>> {
487
        match range {
×
488
            Some(range_str) => {
×
489
                let range_or_index = range::Parser::new(&range_str)?;
×
490

491
                if !range_or_index.is_range {
×
492
                    return Err(eyre::eyre!(INVALID_RANGE));
×
493
                }
494

495
                self.client.load(name, range_or_index.range)?;
×
496
            }
497
            None => {
498
                self.client.load(name, ..)?;
×
499
            }
500
        }
501

502
        Ok(Some(format!("loading: {name}")))
×
503
    }
504

505
    /// Retrieves a list of song files from a given directory
506
    fn files_for(
×
507
        &mut self,
508
        file: Option<&str>,
509
    ) -> Result<Vec<String>, eyre::Error> {
510
        let all_files = Listing::from(self.client.listall()?);
×
511

512
        let files = if let Some(ref file) = file {
×
513
            // TODO: this is inefficient but it's the only way I see at the moment
514
            all_files
×
515
                .listing
×
516
                .iter()
517
                .filter(|song| song.starts_with(file))
×
518
                .cloned()
519
                .collect::<Vec<_>>()
520
        } else {
521
            all_files.listing.clone()
×
522
        };
523

524
        Ok(files)
×
525
    }
526

527
    pub fn insert(&mut self, uri: &str) -> eyre::Result<Option<String>> {
×
528
        let files = self.files_for(Some(uri))?;
×
529

530
        for file in &files {
×
531
            let song = mpd::song::Song {
532
                file: file.to_string(),
×
533
                ..Default::default()
534
            };
535

536
            self.client.insert(song, 0)?;
×
537
        }
538

539
        Ok(None)
×
540
    }
541

542
    pub fn prio(
×
543
        &mut self,
544
        priority: &str,
545
        position_or_range: &str,
546
    ) -> eyre::Result<Option<String>> {
547
        let priority = u8::try_from(priority.parse::<u32>()?).wrap_err(
×
548
            format!("\"{priority}\" must be a value between 0 and 255"),
×
549
        )?;
550

551
        let queue_size = u32::try_from(self.client.queue()?.len())?;
×
552
        let position_or_range = range::Parser::new(position_or_range)?;
×
553

554
        if position_or_range.index > queue_size {
×
555
            return Err(eyre::eyre!(
×
556
                "position ({}) must be less than or equal to the queue length {}",
×
557
                position_or_range.index,
×
558
                queue_size,
×
559
            ));
560
        }
561

562
        if position_or_range.is_range {
×
563
            self.client.priority(position_or_range.range, priority)?;
×
564
        } else {
565
            self.client.priority(position_or_range.index, priority)?;
×
566
        };
567

568
        Ok(None)
×
569
    }
570

571
    pub fn playlist(
×
572
        &mut self,
573
        name: Option<String>,
574
    ) -> eyre::Result<Option<String>> {
575
        // if given a name list songs in that playlist
576
        // if `None` list songs in current playlist
577
        let songs = match name {
×
578
            Some(name) => self.client.playlist(&name)?,
×
579
            None => self.client.queue()?,
×
580
        };
581

582
        let songs: Vec<Current> = songs
×
583
            .into_iter()
584
            .map(|s| Current::from(Song { inner: s }))
×
585
            .collect();
586
        let track_list = TrackList { songs };
×
587

588
        let response = match self.format {
×
589
            OutputFormat::Json => serde_json::to_string(&track_list)?,
×
590
            OutputFormat::Text => track_list.to_string(),
×
591
        };
592

593
        Ok(Some(response))
×
594
    }
595

596
    pub fn listall(
×
597
        &mut self,
598
        file: Option<&str>,
599
    ) -> eyre::Result<Option<String>> {
600
        let files = Listing::from(self.files_for(file)?);
×
601

602
        let response = match self.format {
×
603
            OutputFormat::Json => serde_json::to_string(&files)?,
×
604
            OutputFormat::Text => files.to_string(),
×
605
        };
606

607
        Ok(Some(response))
×
608
    }
609

610
    pub fn ls(
×
611
        &mut self,
612
        directory: Option<&str>,
613
    ) -> eyre::Result<Option<String>> {
614
        let directory = directory.unwrap_or("");
×
615
        let listing = self.client.listfiles(directory)?;
×
616
        let filter_for = if let Some(entry) = listing.first() {
×
617
            entry.0.as_str()
×
618
        } else {
619
            "directory"
×
620
        };
621

622
        let results = Listing::from(
623
            listing
×
624
                .clone()
×
625
                .into_iter()
×
626
                .filter(|(key, _)| key == filter_for)
×
627
                .map(|(_, value)| {
×
628
                    PathBuf::from(&directory)
×
629
                        .join(value)
×
630
                        .to_str()
×
631
                        .unwrap()
×
632
                        .to_string()
×
633
                })
634
                .collect::<Vec<String>>(),
×
635
        );
636

637
        let response = match self.format {
×
638
            OutputFormat::Json => serde_json::to_string(&results)?,
×
639
            OutputFormat::Text => results.to_string(),
×
640
        };
641

642
        Ok(Some(response))
×
643
    }
644

645
    pub fn repeat(
×
646
        &mut self,
647
        state: Option<OnOff>,
648
    ) -> eyre::Result<Option<String>> {
649
        let state = match state {
×
650
            Some(state) => state == OnOff::On,
×
651
            None => !self.client.status()?.repeat,
×
652
        };
653

654
        self.client.repeat(state)?;
×
655

656
        self.current_status()
×
657
    }
658

659
    pub fn random(
×
660
        &mut self,
661
        state: Option<OnOff>,
662
    ) -> eyre::Result<Option<String>> {
663
        let state = match state {
×
664
            Some(state) => state == OnOff::On,
×
665
            None => !self.client.status()?.random,
×
666
        };
667

668
        self.client.random(state)?;
×
669

670
        self.current_status()
×
671
    }
672

673
    pub fn single(
×
674
        &mut self,
675
        state: Option<OnOff>,
676
    ) -> eyre::Result<Option<String>> {
677
        let state = match state {
×
678
            Some(state) => state == OnOff::On,
×
679
            None => !self.client.status()?.single,
×
680
        };
681

682
        self.client.single(state)?;
×
683

684
        self.current_status()
×
685
    }
686

NEW
687
    pub fn search(
×
688
        &mut self,
689
        tag: &str,
690
        query: &str,
691
    ) -> eyre::Result<Option<String>> {
NEW
692
        let term = mpd::Term::Tag(tag.into());
×
NEW
693
        let mut binding = mpd::Query::new();
×
NEW
694
        let query = binding.and(term, query);
×
695

NEW
696
        let results = self.client.search(query, None)?;
×
697

NEW
698
        let files = Listing::from(results);
×
699

NEW
700
        let response = match self.format {
×
NEW
701
            OutputFormat::Json => serde_json::to_string(&files)?,
×
NEW
702
            OutputFormat::Text => files.to_string(),
×
703
        };
704

NEW
705
        Ok(Some(response))
×
706
    }
707

UNCOV
708
    pub fn consume(
×
709
        &mut self,
710
        state: Option<OnOff>,
711
    ) -> eyre::Result<Option<String>> {
712
        let state = match state {
×
713
            Some(state) => state == OnOff::On,
×
714
            None => !self.client.status()?.consume,
×
715
        };
716

717
        self.client.consume(state)?;
×
718

719
        self.current_status()
×
720
    }
721

722
    pub fn crossfade(
×
723
        &mut self,
724
        seconds: Option<String>,
725
    ) -> eyre::Result<Option<String>> {
726
        let crossfade = match seconds {
×
727
            Some(secs) => secs.parse::<i64>().wrap_err(format!(
×
728
                "\"{secs}\" is not 0 or a positive number"
×
729
            ))?,
730
            None => 0,
×
731
        };
732

733
        self.client
×
734
            .crossfade(crossfade)
×
735
            .wrap_err(format!("\"{crossfade}\" is too large"))?;
×
736

737
        Ok(Some(format!("crossfade: {crossfade}")))
×
738
    }
739

740
    pub fn version(&mut self) -> eyre::Result<Option<String>> {
×
741
        let mpd = format!(
×
742
            "{}.{}.{}",
743
            self.client.version.0, self.client.version.1, self.client.version.2
×
744
        );
745
        let mp_cli = env!("CARGO_PKG_VERSION").to_string();
×
746

747
        let versions = Versions { mpd, mp_cli };
×
748

749
        let response = match self.format {
×
750
            OutputFormat::Json => serde_json::to_string(&versions)?,
×
751
            OutputFormat::Text => versions.to_string(),
×
752
        };
753

754
        Ok(Some(response))
×
755
    }
756

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

760
        let response = match self.format {
×
761
            OutputFormat::Json => serde_json::to_string(&stats)?,
×
762
            OutputFormat::Text => stats.to_string(),
×
763
        };
764

765
        Ok(Some(response))
×
766
    }
767

768
    pub fn save(&mut self, name: &str) -> eyre::Result<Option<String>> {
769
        self.client
×
770
            .save(name)
×
771
            .wrap_err(format!("Playlist already exists: {name}"))?;
×
772

773
        Ok(None)
×
774
    }
775

776
    pub fn rm(&mut self, name: &str) -> eyre::Result<Option<String>> {
777
        self.client
×
778
            .pl_remove(name)
×
779
            .wrap_err(format!("Unknown playlist: {name}"))?;
×
780

781
        Ok(None)
×
782
    }
783

784
    //
785
    // volume related commands
786
    //
787

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

791
        let target = match input {
×
792
            matched if matched.starts_with('+') => {
×
793
                if let Ok(volume) = matched[1..].parse::<i8>() {
×
794
                    current.checked_add(volume).unwrap_or(100).min(100)
×
795
                } else {
796
                    panic!("Invalid volume increment, must be between 1-100")
×
797
                }
798
            }
799
            matched if matched.starts_with('-') => {
×
800
                if let Ok(volume) = matched[1..].parse::<i8>() {
×
801
                    current.checked_sub(volume).unwrap_or(100).max(0)
×
802
                } else {
803
                    panic!("Invalid volume increment, must be between 1-100")
×
804
                }
805
            }
806
            _ => input.parse::<i8>().unwrap_or(0),
×
807
        };
808

809
        self.client
×
810
            .volume(target)
×
811
            .map(|()| None)
×
812
            .map_err(eyre::Report::from)
×
813
    }
814

815
    //
816
    // output related commands
817
    //
818

819
    fn status(&mut self) -> eyre::Result<Status> {
×
820
        let status = self.client.status()?;
×
821

822
        let volume = status.volume.to_string();
×
823

824
        let current_song = self.client.currentsong()?;
×
825

826
        let artist = current_song
×
827
            .as_ref()
828
            .and_then(|song| song.artist.as_ref())
×
829
            .map_or(String::new(), ToString::to_string);
×
830

831
        let album = current_song
×
832
            .as_ref()
833
            .and_then(|song| {
×
834
                song.tags
×
835
                    .iter()
×
836
                    .find(|&(key, _)| key.to_lowercase() == "album")
×
837
            })
838
            .map_or_else(String::new, |(_, value)| value.clone());
×
839

840
        let title = current_song
×
841
            .as_ref()
842
            .and_then(|song| song.title.as_ref())
×
843
            .map_or(String::new(), ToString::to_string);
×
844

845
        let state = match status.state {
×
846
            mpd::State::Play => "play",
×
847
            mpd::State::Pause => "pause",
×
848
            mpd::State::Stop => "stop",
×
849
        }
850
        .to_string();
851

852
        let position = match status.song {
×
853
            Some(song) => song.pos,
×
854
            None => 0,
×
855
        };
856
        let time = crate::time::Track::from(status.time);
×
857

858
        Ok(Status {
×
859
            volume,
×
860
            state,
×
861
            artist,
×
862
            album,
×
863
            title,
×
864
            position,
×
865
            queue_count: status.queue_len,
×
866
            elapsed: time.elapsed,
×
867
            track_length: time.total,
×
868
            repeat: OnOff::from(status.repeat),
×
869
            random: OnOff::from(status.random),
×
870
            single: OnOff::from(status.single),
×
871
            consume: OnOff::from(status.consume),
×
872
        })
873
    }
874

875
    pub fn current_status(&mut self) -> eyre::Result<Option<String>> {
×
876
        let status = self.status()?;
×
877
        let response = match self.format {
×
878
            OutputFormat::Json => serde_json::to_string(&status)?,
×
879
            OutputFormat::Text => format!("{status}"),
×
880
        };
881

882
        Ok(Some(response))
×
883
    }
884
}
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

© 2025 Coveralls, Inc