mpd_easy/
client.rs

1use std::fmt;
2use std::path::{Path, PathBuf};
3use std::time::Duration;
4
5use eyre::WrapErr;
6use serde::Serialize;
7
8use 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
25const NO_OUT: String = String::new();
26
27#[derive(PartialEq)]
28enum Direction {
29    Forward,
30    Reverse,
31}
32
33#[derive(Serialize)]
34pub struct Versions {
35    mpd: String,
36    mp_cli: String,
37}
38
39impl fmt::Display for Versions {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        write!(f, "mpd={}\nmp-cli={}", self.mpd, self.mp_cli)
42    }
43}
44
45pub struct Client {
46    client: mpd::Client,
47    format: OutputFormat,
48}
49
50impl Client {
51    pub fn new(
52        bind_to_address: &str,
53        port: &str,
54        format: Option<OutputFormat>,
55    ) -> eyre::Result<Client> {
56        let format = format.unwrap_or(OutputFormat::Json);
57        let client = mpd::Client::connect(format!("{bind_to_address}:{port}"))
58            .wrap_err("Error connecting to mpd server".to_string())?;
59
60        Ok(Self { client, format })
61    }
62
63    //
64    // queue related commands
65    //
66    pub fn add(&mut self, path: &str) -> eyre::Result<Option<String>> {
67        let music_dir = self.client.music_directory()?;
68
69        let absolute_path = if path.starts_with(&music_dir) {
70            path.to_string()
71        } else {
72            PathBuf::from(&music_dir)
73                .join(path)
74                .to_str()
75                .unwrap()
76                .to_string()
77        };
78
79        let mut finder = Finder::new(music_dir);
80
81        finder.find(Path::new(Path::new(&absolute_path)))?;
82
83        for file in finder.found {
84            let song = mpd::song::Song {
85                file: file.relative_path,
86                ..Default::default()
87            };
88
89            self.client
90                .push(song.clone())
91                .wrap_err(format!("unkown or inalid path: {}", song.file))?;
92        }
93
94        Ok(None)
95    }
96
97    pub fn crop(&mut self) -> eyre::Result<Option<String>> {
98        // determine current song position
99        // remove all songs before current song
100        // remove all songs from 1 onwards
101        let status = self.status()?;
102        let current_position = status.position;
103        let length = status.queue_count;
104
105        if length < 1 {
106            return self.current_status();
107        }
108
109        self.client.delete(0..current_position)?;
110        // it doesn't matter that the range is out of bounds
111        self.client.delete(1..length)?;
112
113        self.current_status()
114    }
115
116    pub fn del(
117        &mut self,
118        position: Option<u32>,
119    ) -> eyre::Result<Option<String>> {
120        let position = match position {
121            Some(position) => position,
122            None => self.status()?.position,
123        };
124
125        self.client.delete(position)?;
126
127        self.current_status()
128    }
129
130    //
131    // playback related commands
132    //
133    pub fn current(&mut self) -> eyre::Result<Option<String>> {
134        let current = Current::from(self.status()?);
135
136        let response = match self.format {
137            OutputFormat::Json => serde_json::to_string(&current)?,
138            OutputFormat::Text => current.to_string(),
139            OutputFormat::None => NO_OUT,
140        };
141
142        Ok(Some(response))
143    }
144
145    pub fn play(
146        &mut self,
147        position: Option<u32>,
148    ) -> eyre::Result<Option<String>> {
149        if position.is_none() {
150            self.client.play()?;
151            return self.current_status();
152        }
153        // TODO: this is super hacky, can't find a "jump" in rust-mpd
154
155        // pause
156        // get current position
157        // next/prev to desired position
158        // play
159
160        let position = position.unwrap();
161        let current_position = self.status()?.position;
162
163        self.pause()?;
164
165        if current_position > position {
166            for _ in (position..current_position).rev() {
167                self.prev()?;
168            }
169        } else {
170            for _ in (current_position..position).rev() {
171                self.next()?;
172            }
173        }
174
175        self.client.play()?;
176
177        self.current_status()
178    }
179
180    // allowing because this follows an external api naming convention
181    #[allow(clippy::should_implement_trait)]
182    pub fn next(&mut self) -> eyre::Result<Option<String>> {
183        self.client.next()?;
184
185        self.current_status()
186    }
187
188    pub fn prev(&mut self) -> eyre::Result<Option<String>> {
189        self.client.prev()?;
190
191        self.current_status()
192    }
193
194    pub fn pause(&mut self) -> eyre::Result<Option<String>> {
195        self.client.pause(true)?;
196
197        self.current_status()
198    }
199
200    pub fn pause_if_playing(&mut self) -> eyre::Result<Option<String>> {
201        match self.client.status()?.state {
202            mpd::State::Play => self.pause(),
203            mpd::State::Pause | mpd::State::Stop => Err(eyre::eyre!("")),
204        }
205    }
206
207    pub fn cdprev(&mut self) -> eyre::Result<Option<String>> {
208        let default_duration = Duration::from_secs(0);
209        let status = &self.client.status()?;
210        let current = status.elapsed.unwrap_or(default_duration).as_secs();
211
212        if current < 3 {
213            self.prev()
214        } else {
215            let place = match status.song {
216                Some(ref song) => song.pos,
217                None => 0,
218            };
219            self.client.seek(place, 0)?;
220
221            self.current_status()
222        }
223    }
224
225    pub fn toggle(&mut self) -> eyre::Result<Option<String>> {
226        match self.client.status()?.state {
227            mpd::State::Play => self.pause(),
228            mpd::State::Pause | mpd::State::Stop => self.play(None),
229        }
230    }
231
232    pub fn stop(&mut self) -> eyre::Result<Option<String>> {
233        self.client.stop()?;
234
235        self.current_status()
236    }
237
238    pub fn seek(&mut self, position: &str) -> eyre::Result<Option<String>> {
239        let current_status = self.status()?;
240
241        // valid position syntax: [+-][HH:MM:SS]|<0-100>%
242        let place = if position.contains('%') {
243            let position = position.replace('%', "");
244
245            let percent = position.parse::<u8>().wrap_err(format!(
246                "\"{position}\" must be a value between 0 and 100"
247            ))?;
248            if percent > 100 {
249                return Err(eyre::eyre!(
250                    "\"{position}\" must be a value between 0 and 100"
251                ));
252            }
253
254            let length = current_status.track_length.as_secs;
255            let percent = i64::from(percent);
256
257            length * percent / 100
258        } else if position.contains('+') || position.contains('-') {
259            current_status.elapsed.compute_offset(position)
260        } else {
261            time::Time::from(position.to_string()).as_secs
262        };
263
264        let position = self.status()?.position;
265
266        self.client.seek(position, place)?;
267
268        self.stats()
269    }
270
271    pub fn seekthrough(
272        &mut self,
273        position: &str,
274    ) -> eyre::Result<Option<String>> {
275        let mut direction = Direction::Forward;
276
277        // valid position syntax: [+-][HH:MM:SS]
278        let mut place = if position.contains('%') {
279            return Err(eyre::eyre!(
280                "seekthrough does not support percentage based seeking"
281            ));
282        } else {
283            // if `-` present then back otherwise assume forward
284            if position.contains('-') {
285                direction = Direction::Reverse;
286            }
287            time::Time::from(position.to_string()).as_secs
288        };
289
290        let queue = self.client.queue()?;
291        let start = usize::try_from(self.status()?.position)?;
292        let mut elapsed = self.status()?.elapsed.as_secs;
293
294        match direction {
295            Direction::Forward => {
296                for song in queue.iter().cycle().skip(start) {
297                    let current_song_duration =
298                        i64::try_from(song.duration.unwrap().as_secs())?;
299                    let remainder = current_song_duration - elapsed - place;
300
301                    // seek position fits the current song
302                    if remainder >= 0 {
303                        let position = song.place.unwrap().id;
304                        self.client.seek(position, elapsed + place)?;
305                        break;
306                    }
307
308                    place = remainder.abs();
309                    elapsed = 0;
310                }
311            }
312            Direction::Reverse => {
313                // queue is reversed so we need to start from the end
314                let start = queue.len() - start - 1;
315
316                for song in queue.iter().rev().cycle().skip(start) {
317                    let current_song_duration =
318                        i64::try_from(song.duration.unwrap().as_secs())?;
319
320                    let remainder = if elapsed > 0 {
321                        elapsed - place
322                    } else {
323                        current_song_duration - place
324                    };
325
326                    // seek position fits the current song
327                    if remainder >= 0 {
328                        let position = song.place.unwrap().id;
329                        self.client.seek(position, remainder)?;
330                        break;
331                    }
332
333                    place = remainder.abs();
334                    elapsed = 0;
335                }
336            }
337        }
338
339        self.stats()
340    }
341
342    //
343    // playlist related commands
344    //
345
346    pub fn clear(&mut self) -> eyre::Result<Option<String>> {
347        self.client.clear()?;
348
349        self.current_status()
350    }
351
352    pub fn outputs(&mut self) -> eyre::Result<Option<String>> {
353        let outputs = self.client.outputs()?;
354        let outputs: Vec<Output> =
355            outputs.into_iter().map(Output::from).collect();
356        let outputs = Outputs { outputs };
357
358        let response = match self.format {
359            OutputFormat::Json => serde_json::to_string(&outputs)?,
360            OutputFormat::Text => outputs.to_string(),
361            OutputFormat::None => NO_OUT,
362        };
363
364        Ok(Some(response))
365    }
366
367    fn output_for(&mut self, name_or_id: &str) -> Result<u32, eyre::Error> {
368        let id: u32 = if let Ok(parsed_id) = name_or_id.parse::<u32>() {
369            parsed_id
370        } else {
371            self.client
372                .outputs()?
373                .iter()
374                .find(|&o| o.name == name_or_id)
375                .ok_or_else(|| eyre::eyre!("unknown output: {}", name_or_id))?
376                .id
377        };
378
379        Ok(id)
380    }
381
382    fn enable_or_disable(
383        &mut self,
384        enable: bool,
385        args: Vec<String>,
386    ) -> eyre::Result<Option<String>> {
387        let mut only = false;
388        let mut outputs = Vec::new();
389
390        for arg in args {
391            if arg == "only" {
392                only = true;
393            } else {
394                outputs.push(arg);
395            }
396        }
397
398        if only {
399            // first disable all outputs
400            for output in self.client.outputs()? {
401                self.client.output(output, enable)?;
402            }
403        }
404
405        for name_or_id in outputs {
406            let id = self.output_for(&name_or_id)?;
407
408            self.client.output(id, enable)?;
409        }
410
411        self.outputs()
412    }
413
414    pub fn enable(
415        &mut self,
416        args: Vec<String>,
417    ) -> eyre::Result<Option<String>> {
418        self.enable_or_disable(true, args)
419    }
420
421    pub fn disable(
422        &mut self,
423        args: Vec<String>,
424    ) -> eyre::Result<Option<String>> {
425        self.enable_or_disable(false, args)
426    }
427
428    pub fn toggle_output(
429        &mut self,
430        args: Vec<String>,
431    ) -> eyre::Result<Option<String>> {
432        if args.is_empty() {
433            return Err(eyre::eyre!("no outputs given"));
434        }
435
436        for name_or_id in args {
437            let id = self.output_for(&name_or_id)?;
438
439            self.client.out_toggle(id)?;
440        }
441
442        self.outputs()
443    }
444
445    pub fn queued(&mut self) -> eyre::Result<Option<String>> {
446        if let Some(song) =
447            self.client.queue().map_err(|e| eyre::eyre!(e))?.first()
448        {
449            // safe to unwrap because we know we have a song
450            let current = Current::from(Song {
451                inner: song.clone(),
452            });
453
454            let response = match self.format {
455                OutputFormat::Json => serde_json::to_string(&current)?,
456                OutputFormat::Text => current.to_string(),
457                OutputFormat::None => NO_OUT,
458            };
459
460            Ok(Some(response))
461        } else {
462            Ok(None)
463        }
464    }
465
466    pub fn shuffle(&mut self) -> eyre::Result<Option<String>> {
467        self.client.shuffle(..)?;
468
469        self.current_status()
470    }
471
472    pub fn lsplaylists(&mut self) -> eyre::Result<Option<String>> {
473        let playlists = self.client.playlists()?;
474        let playlists: Vec<Playlist> = playlists
475            .into_iter()
476            .map(|p| Playlist::from(p.name))
477            .collect();
478        let playlists = Playlists { playlists };
479
480        let response = match self.format {
481            OutputFormat::Json => serde_json::to_string(&playlists)?,
482            OutputFormat::Text => playlists.to_string(),
483            OutputFormat::None => NO_OUT,
484        };
485
486        Ok(Some(response))
487    }
488
489    pub fn load(
490        &mut self,
491        name: &String,
492        range: Option<String>,
493    ) -> eyre::Result<Option<String>> {
494        match range {
495            Some(range_str) => {
496                let range_or_index = range::Parser::new(&range_str)?;
497
498                if !range_or_index.is_range {
499                    return Err(eyre::eyre!(INVALID_RANGE));
500                }
501
502                self.client.load(name, range_or_index.range)?;
503            }
504            None => {
505                self.client.load(name, ..)?;
506            }
507        }
508
509        Ok(Some(format!("loading: {name}")))
510    }
511
512    /// Retrieves a list of song files from a given directory
513    fn files_for(
514        &mut self,
515        file: Option<&str>,
516    ) -> Result<Vec<String>, eyre::Error> {
517        let all_files = Listing::from(self.client.listall()?);
518
519        let files = if let Some(ref file) = file {
520            // TODO: this is inefficient but it's the only way I see at the moment
521            all_files
522                .listing
523                .iter()
524                .filter(|song| song.starts_with(file))
525                .cloned()
526                .collect::<Vec<_>>()
527        } else {
528            all_files.listing.clone()
529        };
530
531        Ok(files)
532    }
533
534    pub fn insert(&mut self, uri: &str) -> eyre::Result<Option<String>> {
535        let files = self.files_for(Some(uri))?;
536
537        for file in &files {
538            let song = mpd::song::Song {
539                file: file.to_string(),
540                ..Default::default()
541            };
542
543            self.client.insert(song, 0)?;
544        }
545
546        Ok(None)
547    }
548
549    pub fn prio(
550        &mut self,
551        priority: &str,
552        position_or_range: &str,
553    ) -> eyre::Result<Option<String>> {
554        let priority = u8::try_from(priority.parse::<u32>()?).wrap_err(
555            format!("\"{priority}\" must be a value between 0 and 255"),
556        )?;
557
558        let queue_size = u32::try_from(self.client.queue()?.len())?;
559        let position_or_range = range::Parser::new(position_or_range)?;
560
561        if position_or_range.index > queue_size {
562            return Err(eyre::eyre!(
563                "position ({}) must be less than or equal to the queue length {}",
564                position_or_range.index,
565                queue_size,
566            ));
567        }
568
569        if position_or_range.is_range {
570            self.client.priority(position_or_range.range, priority)?;
571        } else {
572            self.client.priority(position_or_range.index, priority)?;
573        };
574
575        Ok(None)
576    }
577
578    pub fn playlist(
579        &mut self,
580        name: Option<String>,
581    ) -> eyre::Result<Option<String>> {
582        // if given a name list songs in that playlist
583        // if `None` list songs in current playlist
584        let songs = match name {
585            Some(name) => self.client.playlist(&name)?,
586            None => self.client.queue()?,
587        };
588
589        let songs: Vec<Current> = songs
590            .into_iter()
591            .map(|s| Current::from(Song { inner: s }))
592            .collect();
593        let track_list = TrackList { songs };
594
595        let response = match self.format {
596            OutputFormat::Json => serde_json::to_string(&track_list)?,
597            OutputFormat::Text => track_list.to_string(),
598            OutputFormat::None => NO_OUT,
599        };
600
601        Ok(Some(response))
602    }
603
604    pub fn listall(
605        &mut self,
606        file: Option<&str>,
607    ) -> eyre::Result<Option<String>> {
608        let files = Listing::from(self.files_for(file)?);
609
610        let response = match self.format {
611            OutputFormat::Json => serde_json::to_string(&files)?,
612            OutputFormat::Text => files.to_string(),
613            OutputFormat::None => NO_OUT,
614        };
615
616        Ok(Some(response))
617    }
618
619    pub fn ls(
620        &mut self,
621        directory: Option<&str>,
622    ) -> eyre::Result<Option<String>> {
623        let directory = directory.unwrap_or("");
624        let listing = self.client.listfiles(directory)?;
625        let filter_for = if let Some(entry) = listing.first() {
626            entry.0.as_str()
627        } else {
628            "directory"
629        };
630
631        let results = Listing::from(
632            listing
633                .clone()
634                .into_iter()
635                .filter(|(key, _)| key == filter_for)
636                .map(|(_, value)| {
637                    PathBuf::from(&directory)
638                        .join(value)
639                        .to_str()
640                        .unwrap()
641                        .to_string()
642                })
643                .collect::<Vec<String>>(),
644        );
645
646        let response = match self.format {
647            OutputFormat::Json => serde_json::to_string(&results)?,
648            OutputFormat::Text => results.to_string(),
649            OutputFormat::None => NO_OUT,
650        };
651
652        Ok(Some(response))
653    }
654
655    pub fn repeat(
656        &mut self,
657        state: Option<OnOff>,
658    ) -> eyre::Result<Option<String>> {
659        let state = match state {
660            Some(state) => state == OnOff::On,
661            None => !self.client.status()?.repeat,
662        };
663
664        self.client.repeat(state)?;
665
666        self.current_status()
667    }
668
669    pub fn random(
670        &mut self,
671        state: Option<OnOff>,
672    ) -> eyre::Result<Option<String>> {
673        let state = match state {
674            Some(state) => state == OnOff::On,
675            None => !self.client.status()?.random,
676        };
677
678        self.client.random(state)?;
679
680        self.current_status()
681    }
682
683    pub fn single(
684        &mut self,
685        state: Option<OnOff>,
686    ) -> eyre::Result<Option<String>> {
687        let state = match state {
688            Some(state) => state == OnOff::On,
689            None => !self.client.status()?.single,
690        };
691
692        self.client.single(state)?;
693
694        self.current_status()
695    }
696
697    // TODO: better name or abstraction?
698    pub fn _search(
699        &mut self,
700        tag: &str,
701        query: &str,
702    ) -> eyre::Result<Vec<mpd::Song>> {
703        let term = mpd::Term::Tag(tag.into());
704        let mut binding = mpd::Query::new();
705        let songs = binding.and(term, query);
706
707        Ok(self.client.search(songs, None)?)
708    }
709
710    // TODO: better name or abstraction?
711    pub fn _find(
712        &mut self,
713        tag: &str,
714        query: &str,
715    ) -> eyre::Result<Vec<mpd::Song>> {
716        let term = mpd::Term::Tag(tag.into());
717        let mut binding = mpd::Query::new();
718        let songs = binding.and(term, query);
719
720        Ok(self.client.search(songs, None)?)
721    }
722
723    pub fn search(
724        &mut self,
725        tag: &str,
726        query: &str,
727    ) -> eyre::Result<Option<String>> {
728        let songs = self._search(tag, query)?;
729        let files = Listing::from(songs);
730
731        let response = match self.format {
732            OutputFormat::Json => serde_json::to_string(&files)?,
733            OutputFormat::Text => files.to_string(),
734            OutputFormat::None => NO_OUT,
735        };
736
737        Ok(Some(response))
738    }
739
740    pub fn search_add(
741        &mut self,
742        tag: &str,
743        query: &str,
744    ) -> eyre::Result<Option<String>> {
745        let songs = self._search(tag, query)?;
746
747        for song in songs {
748            self.client
749                .push(song.clone())
750                .wrap_err(format!("unkown or inalid path: {}", song.file))?;
751        }
752
753        Ok(None)
754    }
755
756    pub fn find(
757        &mut self,
758        tag: &str,
759        query: &str,
760    ) -> eyre::Result<Option<String>> {
761        let songs = self._find(tag, query)?;
762        let files = Listing::from(songs);
763
764        let response = match self.format {
765            OutputFormat::Json => serde_json::to_string(&files)?,
766            OutputFormat::Text => files.to_string(),
767            OutputFormat::None => NO_OUT,
768        };
769
770        Ok(Some(response))
771    }
772
773    pub fn find_add(
774        &mut self,
775        tag: &str,
776        query: &str,
777    ) -> eyre::Result<Option<String>> {
778        let songs = self._find(tag, query)?;
779
780        for song in songs {
781            self.client
782                .push(song.clone())
783                .wrap_err(format!("unkown or inalid path: {}", song.file))?;
784        }
785
786        Ok(None)
787    }
788
789    pub fn list(&mut self, tag: &str) -> eyre::Result<Option<String>> {
790        let term = mpd::Term::Tag(tag.into());
791        let query = mpd::Query::new();
792
793        let results = self.client.list(&term, &query)?;
794        let files = Listing::from(results);
795
796        let response = match self.format {
797            OutputFormat::Json => serde_json::to_string(&files)?,
798            OutputFormat::Text => files.to_string(),
799            OutputFormat::None => NO_OUT,
800        };
801
802        Ok(Some(response))
803    }
804
805    pub fn consume(
806        &mut self,
807        state: Option<OnOff>,
808    ) -> eyre::Result<Option<String>> {
809        let state = match state {
810            Some(state) => state == OnOff::On,
811            None => !self.client.status()?.consume,
812        };
813
814        self.client.consume(state)?;
815
816        self.current_status()
817    }
818
819    pub fn crossfade(
820        &mut self,
821        seconds: Option<String>,
822    ) -> eyre::Result<Option<String>> {
823        let crossfade = match seconds {
824            Some(secs) => secs.parse::<i64>().wrap_err(format!(
825                "\"{secs}\" is not 0 or a positive number"
826            ))?,
827            None => 0,
828        };
829
830        self.client
831            .crossfade(crossfade)
832            .wrap_err(format!("\"{crossfade}\" is too large"))?;
833
834        Ok(Some(format!("crossfade: {crossfade}")))
835    }
836
837    pub fn version(&mut self) -> eyre::Result<Option<String>> {
838        let mpd = format!(
839            "{}.{}.{}",
840            self.client.version.0, self.client.version.1, self.client.version.2
841        );
842        let mp_cli = env!("CARGO_PKG_VERSION").to_string();
843
844        let versions = Versions { mpd, mp_cli };
845
846        let response = match self.format {
847            OutputFormat::Json => serde_json::to_string(&versions)?,
848            OutputFormat::Text => versions.to_string(),
849            OutputFormat::None => NO_OUT,
850        };
851
852        Ok(Some(response))
853    }
854
855    pub fn stats(&mut self) -> eyre::Result<Option<String>> {
856        let stats = Stats::new(self.client.stats()?);
857
858        let response = match self.format {
859            OutputFormat::Json => serde_json::to_string(&stats)?,
860            OutputFormat::Text => stats.to_string(),
861            OutputFormat::None => NO_OUT,
862        };
863
864        Ok(Some(response))
865    }
866
867    pub fn save(&mut self, name: &str) -> eyre::Result<Option<String>> {
868        self.client
869            .save(name)
870            .wrap_err(format!("Playlist already exists: {name}"))?;
871
872        Ok(None)
873    }
874
875    pub fn rm(&mut self, name: &str) -> eyre::Result<Option<String>> {
876        self.client
877            .pl_remove(name)
878            .wrap_err(format!("Unknown playlist: {name}"))?;
879
880        Ok(None)
881    }
882
883    //
884    // volume related commands
885    //
886
887    pub fn set_volume(&mut self, input: &str) -> eyre::Result<Option<String>> {
888        let current = self.client.status()?.volume;
889
890        let target = match input {
891            matched if matched.starts_with('+') => {
892                if let Ok(volume) = matched[1..].parse::<i8>() {
893                    current.checked_add(volume).unwrap_or(100).min(100)
894                } else {
895                    panic!("Invalid volume increment, must be between 1-100")
896                }
897            }
898            matched if matched.starts_with('-') => {
899                if let Ok(volume) = matched[1..].parse::<i8>() {
900                    current.checked_sub(volume).unwrap_or(100).max(0)
901                } else {
902                    panic!("Invalid volume increment, must be between 1-100")
903                }
904            }
905            _ => input.parse::<i8>().unwrap_or(0),
906        };
907
908        self.client
909            .volume(target)
910            .map(|()| None)
911            .map_err(eyre::Report::from)
912    }
913
914    //
915    // output related commands
916    //
917
918    pub fn status(&mut self) -> eyre::Result<Status> {
919        let status = self.client.status()?;
920
921        let volume = status.volume.to_string();
922
923        let current_song = self.client.currentsong()?;
924
925        let artist = current_song
926            .as_ref()
927            .and_then(|song| song.artist.as_ref())
928            .map_or(String::new(), ToString::to_string);
929
930        let album = current_song
931            .as_ref()
932            .and_then(|song| {
933                song.tags
934                    .iter()
935                    .find(|&(key, _)| key.to_lowercase() == "album")
936            })
937            .map_or_else(String::new, |(_, value)| value.clone());
938
939        let title = current_song
940            .as_ref()
941            .and_then(|song| song.title.as_ref())
942            .map_or(String::new(), ToString::to_string);
943
944        let position = match status.song {
945            Some(song) => song.pos,
946            None => 0,
947        };
948
949        let time = crate::time::Track::from(status.time);
950
951        let file_path =
952            self.client.currentsong()?.map(|song| song.file.clone());
953
954        Ok(Status {
955            volume,
956            state: crate::status::State::from(status.state),
957            artist,
958            album,
959            title,
960            position,
961            queue_count: status.queue_len,
962            elapsed: time.elapsed,
963            track_length: time.total,
964            repeat: OnOff::from(status.repeat),
965            random: OnOff::from(status.random),
966            single: OnOff::from(status.single),
967            consume: OnOff::from(status.consume),
968            file_path,
969        })
970    }
971
972    pub fn current_status(&mut self) -> eyre::Result<Option<String>> {
973        let status = self.status()?;
974        let response = match self.format {
975            OutputFormat::Json => serde_json::to_string(&status)?,
976            OutputFormat::Text => format!("{status}"),
977            OutputFormat::None => NO_OUT,
978        };
979
980        Ok(Some(response))
981    }
982}