root/mpd/src/lib.rs

// Mozilla Public License Version 2.0
// copied from https://github.com/figsoda/mmtc/blob/main/src/mpd.rs

use anyhow::{bail, Context, Result};
use expand::expand;
use futures_lite::{
    io::{split, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf},
    StreamExt,
};

use async_net::{AsyncToSocketAddrs, TcpStream};

// #[derive(Deserialize)]
pub struct SearchFields {
    // #[serde(default)]
    pub file: bool,
    // #[serde(default = "yes")]
    pub title: bool,
    // #[serde(default = "yes")]
    pub artist: bool,
    // #[serde(default = "yes")]
    pub album: bool,
}

#[derive(Debug)]
pub struct Client {
    r: BufReader<ReadHalf<TcpStream>>,
    w: WriteHalf<TcpStream>,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Playback {
    Playing,
    Paused,
    Stopped,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Tag {
    Artist,
    Artistsort, // not on pre 0.16
    Album,
    AlbumSort,
    AlbumArtist,
    AlbumArtistSort,
    Title,
    Track,
    Name,
    Genre,
    Date,
    Composer,
    Performer,
    Conductor, // unknown in pre 0.16
    Work,      // unknown in pre 0.16
    Grouping,  // unknown in pre 0.16
    Comment,
    Disc,
    Label,
    MusicbrainzArtistid,
    MusicbrainzAlbumid,
    MusicbrainzAlbumartistid,
    MusicbrainzTrackid,
    MusicbrainzReleasetrackid,
    MusicbrainzWorkid,
}
impl std::fmt::Display for Tag {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Tag::Artist => write!(f, "artist"),
            Tag::Artistsort => write!(f, "artistsort"),
            Tag::Album => write!(f, "album"),
            Tag::AlbumSort => write!(f, "albumsort"),
            Tag::AlbumArtist => write!(f, "albumartist"),
            Tag::AlbumArtistSort => write!(f, "albumartistsort"),
            Tag::Title => write!(f, "title"),
            Tag::Track => write!(f, "track"),
            Tag::Name => write!(f, "name"),
            Tag::Genre => write!(f, "genre"),
            Tag::Date => write!(f, "date"),
            Tag::Composer => write!(f, "composer"),
            Tag::Performer => write!(f, "performer"),
            Tag::Conductor => write!(f, "conductor"),
            Tag::Work => write!(f, "work"),
            Tag::Grouping => write!(f, "grouping"),
            Tag::Comment => write!(f, "comment"),
            Tag::Disc => write!(f, "disc"),
            Tag::Label => write!(f, "label"),
            Tag::MusicbrainzArtistid => write!(f, "musicbrainz_artistid"),
            Tag::MusicbrainzAlbumid => write!(f, "musicbrainz_albumid"),
            Tag::MusicbrainzAlbumartistid => write!(f, "musicbrainz_albumartistid"),
            Tag::MusicbrainzTrackid => write!(f, "musicbrainz_trackid"),
            Tag::MusicbrainzReleasetrackid => write!(f, "musicbrainz_releasetrackid"),
            Tag::MusicbrainzWorkid => write!(f, "musicbrainz_workid"),
        }
    }
}

#[allow(dead_code)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SingleMode {
    On,
    Off,
    Oneshot,
}

#[derive(Debug)]
pub struct Status {
    pub repeat: bool,
    pub random: bool,
    pub single: SingleMode, // None: oneshot
    pub consume: bool,
    pub queue_len: usize,
    pub state: Playback,
    pub song: Option<Song>,
    pub volume: u16,
}

#[derive(Debug, Clone)]
pub struct Song {
    pub pos: usize,
    pub elapsed: u16,
}

#[derive(Debug)]
pub struct Track {
    pub file: String,
    pub artist: Option<String>,
    pub album: Option<String>,
    pub title: Option<String>,
    pub time: u16,
}
impl PartialEq for Track {
    fn eq(&self, other: &Self) -> bool {
        self.file == other.file
    }
}

fn track_string(track: &Track, search_fields: &SearchFields) -> String {
    let mut track_string = String::with_capacity(64);

    if search_fields.file {
        track_string.push_str(&track.file.to_lowercase());
        track_string.push('\n');
    }

    if search_fields.title {
        if let Some(title) = &track.title {
            track_string.push_str(&title.to_lowercase());
            track_string.push('\n');
        }
    }

    if search_fields.artist {
        if let Some(artist) = &track.artist {
            track_string.push_str(&artist.to_lowercase());
            track_string.push('\n');
        }
    }

    if search_fields.album {
        if let Some(album) = &track.album {
            track_string.push_str(&album.to_lowercase());
        }
    }

    track_string
}

impl Client {
    pub async fn init(addr: impl AsyncToSocketAddrs, password: &str) -> Result<Client> {
        async move {
            let (r, w) = split(TcpStream::connect(addr).await?);
            let mut cl = Client {
                r: BufReader::new(r),
                w,
            };

            let buf = &mut [0; 7];
            cl.r.read(buf).await?;

            if buf != b"OK MPD " {
                bail!("server did not greet with a success");
            }
            let version = cl.r.read_line(&mut String::with_capacity(8)).await?;
            println!("mpd version {}", version);
            if password.len() > 0 {
                cl.w.write_all(vec!["password ", password, "\n"].join("").as_bytes())
                    .await?;
                let _pw_ok = cl.r.read_line(&mut String::with_capacity(8)).await?;
            }
            {
                // let kb32 = "32768"; // setting like 1mb really fucks up the server ;_;
                // cl.w.write_all(vec!["binarylimit ", kb32, "\n"].join("").as_bytes())
                //     .await?;
                // let _binarylimit_ok = cl.r.read_line(&mut String::with_capacity(8)).await?;
            }

            Ok(cl)
        }
        .await
        .context("Failed to init client")
    }

    pub async fn idle(&mut self) -> Result<(bool, bool)> {
        // TODO only block for a short time
        async move {
            self.w.write_all(b"idle options player playlist\n").await?;
            let mut lines = (&mut self.r).lines();
            let mut status = false;
            let mut queue = false;

            while let Some(line) = lines.next().await {
                match line?.as_bytes() {
                    b"changed: options" => status = true,
                    b"changed: player" => status = true,
                    b"changed: playlist" => queue = true,
                    b"OK" => break,
                    _ => continue,
                }
            }

            Result::<_>::Ok((status, queue))
        }
        .await
        .context("Failed to idle")
    }

    pub async fn queue(
        &mut self,
        len: usize,
        search_fields: &SearchFields,
    ) -> Result<(Vec<Track>, Vec<String>)> {
        async move {
            self.w.write_all(b"playlistinfo\n").await?;
            let mut lines = (&mut self.r).lines();

            Client::parse_track_lines(&mut lines, len, search_fields).await
        }
        .await
        .context("Failed to query queue")
    }

    pub async fn search(
        &mut self,
        tag: Tag,
        search: String,
        search_fields: &SearchFields,
    ) -> Result<(Vec<Track>, Vec<String>)> {
        async move {
            self.w
                .write_all(format!("search {} \"{}\"\n", tag, search).as_bytes())
                .await?;
            let mut lines = (&mut self.r).lines();

            Client::parse_track_lines(&mut lines, 0, search_fields).await
        }
        .await
        .context("Failed to query queue")
    }

    pub async fn find(
        &mut self,
        tag: Tag,
        search: String,
        search_fields: &SearchFields,
    ) -> Result<(Vec<Track>, Vec<String>)> {
        async move {
            println!("find {} \"{}\"\n", &tag, &search);
            self.w
                .write_all(format!("find {} \"{}\"\n", tag, search).as_bytes())
                .await?;
            let mut lines = (&mut self.r).lines();

            Client::parse_track_lines(&mut lines, 0, search_fields).await
        }
        .await
        .context("Failed to query queue")
    }

    async fn parse_track_lines(
        lines: &mut futures_lite::io::Lines<&mut BufReader<ReadHalf<TcpStream>>>,
        len: usize,
        search_fields: &SearchFields,
    ) -> Result<(Vec<Track>, Vec<String>)> {
        let mut first = true;
        let mut tracks = Vec::with_capacity(len);
        let mut track_strings = Vec::with_capacity(len);

        let mut file: Option<String> = None;
        let mut artist: Option<String> = None;
        let mut album: Option<String> = None;
        let mut title: Option<String> = None;
        let mut time = None;

        while let Some(line) = lines.next().await {
            let line = line?;
            match line.as_bytes() {
                b"OK" => break,
                expand!([@b"file: ", ..]) => {
                    if first {
                        first = false;
                    } else if let Some(file) = file {
                        let track = Track {
                            file,
                            artist,
                            album,
                            title,
                            time: time.unwrap_or_default(),
                        };
                        track_strings.push(track_string(&track, search_fields));
                        tracks.push(track);
                    } else {
                        bail!("incomplete playlist response");
                    }

                    file = Some(line[6..].into());
                    artist = None;
                    album = None;
                    title = None;
                    time = None;
                }
                expand!([@b"Artist: ", ..]) => artist = Some(line[8..].into()),
                expand!([@b"Album: ", ..]) => album = Some(line[7..].into()),
                expand!([@b"Title: ", ..]) => title = Some(line[7..].into()),
                expand!([@b"Time: ", ..]) => time = Some(line[6..].parse()?),
                _ => continue,
            }
        }

        if let Some(file) = file {
            let track = Track {
                file,
                artist,
                album,
                title,
                time: time.unwrap_or_default(),
            };
            track_strings.push(track_string(&track, search_fields));
            tracks.push(track);
        }

        Ok((tracks, track_strings))
    }

    pub async fn status(&mut self) -> Result<Status> {
        async move {
            let mut repeat = None;
            let mut random = None;
            let mut single = None;
            let mut consume = None;
            let mut queue_len = None;
            let mut state = Playback::Stopped;
            let mut pos = None;
            let mut elapsed = None;
            let mut volume = None;

            self.w.write_all(b"status\n").await?;
            let mut lines = (&mut self.r).lines();

            while let Some(line) = lines.next().await {
                let line = line?;
                match line.as_bytes() {
                    b"OK" => break,
                    b"repeat: 0" => repeat = Some(false),
                    b"repeat: 1" => repeat = Some(true),
                    b"random: 0" => random = Some(false),
                    b"random: 1" => random = Some(true),
                    b"single: 0" => single = Some(SingleMode::Off),
                    b"single: 1" => single = Some(SingleMode::On),
                    b"single: oneshot" => single = Some(SingleMode::Oneshot),
                    b"consume: 0" => consume = Some(false),
                    b"consume: 1" => consume = Some(true),
                    expand!([@b"playlistlength: ", ..]) => queue_len = Some(line[16..].parse()?),
                    b"state: play" => state = Playback::Playing,
                    b"state: pause" => state = Playback::Paused,
                    expand!([@b"song: ", ..]) => pos = Some(line[6..].parse()?),
                    expand!([@b"elapsed: ", ..]) => {
                        elapsed = Some(line[9..].parse::<f32>()?.round() as u16)
                    }
                    expand!([@b"volume: ", ..]) => volume = Some(line[8..].parse()?),
                    _ => {
                        // println!("unknown status, {:#?}", line);
                        continue;
                    }
                }
            }

            if let (Some(repeat), Some(random), Some(single), Some(consume), Some(queue_len)) =
                (repeat, random, single, consume, queue_len)
            {
                Ok(Status {
                    repeat,
                    random,
                    single,
                    consume,
                    queue_len,
                    state,
                    volume: volume.unwrap_or(0), // TODO add N/A volume
                    song: if let (Some(pos), Some(elapsed)) = (pos, elapsed) {
                        Some(Song { pos, elapsed })
                    } else {
                        None
                    },
                })
            } else {
                println!(
                    "incomplete status {:#?}",
                    (repeat, random, single, consume, queue_len, volume, state,)
                );
                bail!("incomplete status response");
            }
        }
        .await
        .context("Failed to query status")
    }

    pub async fn play(&mut self, pos: usize) -> Result<()> {
        self.w.write_all(b"play ").await?;
        self.w.write_all(pos.to_string().as_bytes()).await?;
        self.w.write_all(b"\n").await?;
        let mut lines = (&mut self.r).lines();

        while let Some(line) = lines.next().await {
            match line?.as_bytes() {
                b"OK" | expand!([@b"ACK ", ..]) => break,
                _ => continue,
            }
        }

        Ok(())
    }

    pub async fn list(&mut self, tag: &Tag) -> Result<Vec<String>> {
        self.w.write_all(b"list ").await?;
        self.w.write_all(tag.to_string().as_bytes()).await?;
        self.w.write_all(b"\n").await?;
        let mut lines = (&mut self.r).lines();
        let mut result = Vec::new();
        while let Some(line) = lines.next().await {
            let line = line?;
            match line.as_bytes() {
                b"OK" => break,
                // only one type that is made obvious by the tag can be listed,
                // so we don't need to wrap each value I guess.
                expand!([@b"Artist: ", ..]) => result.push(line[8..].to_owned()),
                expand!([@b"Album: ", ..]) => result.push(line[7..].to_owned()),
                expand!([@b"Title: ", ..]) => result.push(line[7..].to_owned()),
                expand!([@b"Track: ", ..]) => result.push(line[7..].to_owned()),
                expand!([@b"Name: ", ..]) => result.push(line[6..].to_owned()),
                expand!([@b"Genre: ", ..]) => result.push(line[7..].to_owned()),
                expand!([@b"Date: ", ..]) => result.push(line[6..].to_owned()),
                expand!([@b"Composer: ", ..]) => result.push(line[10..].to_owned()),
                expand!([@b"Performer: ", ..]) => result.push(line[11..].to_owned()),
                expand!([@b"Conductor: ", ..]) => result.push(line[11..].to_owned()), // untested
                expand!([@b"Comment: ", ..]) => result.push(line[9..].to_owned()),
                // TODO the rest of them
                l => {
                    println!("command line: {}", String::from_utf8(l.to_vec())?);
                    if l.starts_with(b"ACK ") {
                        break;
                    } else {
                        continue;
                    }
                }
            }
        }

        Ok(result)
    }

    pub async fn lyrics(&mut self, uri: String) -> Result<Vec<u8>> {
        let uri = uri.replace('\\', "\\\\").replace('"', "\\\"");
        self.offset_command(&format!("readlyrics \"{}\"", uri))
            .await
    }

    pub async fn cover(&mut self, uri: String) -> Result<Vec<u8>> {
        let uri = uri.replace('\\', "\\\\").replace('"', "\\\"");
        if let Ok(v) = self
            .offset_command(&format!("readpicture \"{}\"", uri))
            .await
        {
            println!("OK DONE ATRT");
            Ok(v)
        } else {
            println!("ART BACKUP");
            self.offset_command(&format!("albumart \"{}\"", uri)).await
        }
    }

    async fn offset_command(&mut self, command: &str) -> Result<Vec<u8>> {
        let mut result = Vec::<u8>::new();
        let mut total_size;
        let mut read_size = 0;
        loop {
            let offset = read_size;
            let command = format!("{} {}\n", command, offset);
            println!("COMMAND {}", command);
            self.w.write_all(command.as_bytes()).await?;

            // let mut tmp = vec![0u8; 256];
            // let line_size = (&mut self.r).read_until(b'\n', &mut tmp).await;
            // println!("READING albumart HEADER {:#?}", String::from_utf8(tmp[(256 as usize) - line_size? + 13..].to_vec()));

            // let mut tmp = vec![0u8; 256];
            // let line_size = (&mut self.r).read_until(b'\n', &mut tmp).await;
            // println!("READING albumart HEADER {:#?}", String::from_utf8(tmp[(256 as usize) - line_size? + 13..].to_vec()));
            let (total, mut buf) = self.read_binary().await?;
            total_size = total;
            let chunk_size = buf.len();

            println!("APPEND");
            result.append(&mut buf);
            println!("APPEND DONE");

            read_size = read_size + chunk_size;
            if read_size >= total_size {
                break;
            }
        }

        Ok(result)
    }

    async fn read_binary(&mut self) -> Result<(usize, Vec<u8>)> {
        let mut total_size = 0;
        let mut chunk_size = 0;
        let mut line = String::new();
        while let Ok(_len) = (&mut self.r).read_line(&mut line).await {
            println!("LINE {}", line);
            match line.as_bytes() {
                b"OK\n" => bail!("Early ok in binary response"),
                expand!([@b"ACK [", ..]) => bail!("MPD ERROR {}", line),
                // only one type that is made obvious by the tag can be listed,
                // so we don't need to wrap each value I guess.
                expand!([@b"size: ", ..]) => total_size = line[6..line.len() - 1].parse()?,
                expand!([@b"binary: ", ..]) => {
                    chunk_size = line[8..line.len() - 1].parse()?;
                    break;
                }
                _ => (),
            }
            line = String::new();
        }
        println!("DONE WITH LINES {}", chunk_size);

        if chunk_size == 0 {
            bail!("Chunk size 0");
        }

        let mut buf = vec![0u8; chunk_size];
        println!("read buf");
        (&mut self.r).read_exact(&mut buf).await?;
        println!("read line");
        line = String::new();
        (&mut self.r).read_line(&mut line).await?;
        println!("LINE {}", line);
        if line != "\n" {
            bail!("No nl after binary response");
        }
        line = String::new();
        (&mut self.r).read_line(&mut line).await?;
        println!("LINE {}", line);
        if line != "OK\n" {
            bail!("No OK after binary response");
        }

        if buf.is_empty() {
            bail!("did not read any bytes");
        }
        Ok((total_size, buf))
    }

    //     command line: file: music2/Nekrock/Nekrock - ...To Mend The Heart/NekRock - ...To Mend The Heart - 01 Before The Bed Of Flowers (Hanako).flac
    // command line: Last-Modified: 2017-04-08T02:10:41Z
    // command line: Time: 945
    // command line: Title: Before The Bed Of Flowers (Hanako)
    // command line: Artist: NekRock
    // command line: Date: 2016
    // command line: Album: ...To Mend The Heart
    // command line: Track: 1
    // command line: AlbumArtist: NekRock

    pub async fn command(&mut self, cmd: &[u8]) -> Result<()> {
        self.w.write_all(cmd).await?;
        let mut lines = (&mut self.r).lines();

        while let Some(line) = lines.next().await {
            match line?.as_bytes() {
                b"OK" => break,
                l => {
                    println!("command line: {}", String::from_utf8(l.to_vec())?);
                    if l.starts_with(b"ACK ") {
                        break;
                    } else {
                        continue;
                    }
                }
            }
        }

        Ok(())
    }
}