mpdclient 0.2.0

Rust interface to MPD using libmpdclient
Documentation
use std::{
    fs::{File, copy, create_dir, create_dir_all, remove_dir_all},
    io::Write,
    os::unix::fs::symlink,
    path::PathBuf,
    process::{Command, Stdio},
};

use mpdclient::{Connection, connection::idle::IdleReturn};
use rand::Rng;

pub fn setup_mpd(
    setup: impl Fn(TestPath) -> eyre::Result<()>,
    conn: impl Fn(Connection) -> eyre::Result<()>,
) -> eyre::Result<()> {
    // setup id
    let id: String = format!("{:05}", rand::rng().random::<u16>());
    println!("==== Test: {id} ====");

    // setup path
    let tmp = get_tmp();
    let test_path = PathBuf::from(tmp).join("mpdclient-tests").join(id);
    if test_path.exists() {
        remove_dir_all(&test_path)?;
    }
    create_dir_all(&test_path)?;
    println!("Path: {}", test_path.to_string_lossy());

    // setup directories
    create_dir(test_path.join("music"))?;
    create_dir(test_path.join("playlists"))?;
    setup(TestPath {
        path: test_path.clone(),
    })?;

    // setup mpd
    let mpd_conf = test_path.join("mpd.conf");
    File::create(&mpd_conf)?.write_all(
        include_str!("../assets/mpd.conf")
            .replace("%pwd%", &test_path.to_string_lossy())
            .as_bytes(),
    )?;

    let mut mpd = Command::new("mpd")
        .arg("--no-daemon")
        .arg(mpd_conf)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;
    println!("MPD pid: {}", mpd.id());

    let mpd_sock = test_path.join("mpd.sock");
    while !mpd_sock.exists() {
        std::thread::sleep(std::time::Duration::from_millis(10));
    }

    let connection = Connection::connect(Some(&mpd_sock.to_string_lossy()), 0, 0)?;
    if connection.status()?.update() {
        connection.idle().idle_mask(&[IdleReturn::Update])?;
    }

    let conn_res = conn(connection);

    mpd.kill()?;

    conn_res?;

    remove_dir_all(test_path)?;

    Ok(())
}

pub fn get_tmp() -> String {
    std::env::var("TMPDIR").unwrap_or("/tmp".to_string())
}

pub struct TestPath {
    pub path: PathBuf,
}

#[allow(unused)]
impl TestPath {
    fn music_dir(&self) -> PathBuf {
        self.path.join("music")
    }

    fn playlists_dir(&self) -> PathBuf {
        self.path.join("playlists")
    }

    pub fn song(&self, target: &str) -> eyre::Result<()> {
        symlink(
            PathBuf::from("tests/assets/silence.mp3").canonicalize()?,
            self.music_dir().join(format!("{target}.mp3")),
        )?;
        Ok(())
    }

    pub fn song_copy(&self, target: &str) -> eyre::Result<()> {
        copy(
            PathBuf::from("tests/assets/silence.mp3").canonicalize()?,
            self.music_dir().join(format!("{target}.mp3")),
        )?;
        Ok(())
    }

    pub fn playlist(&self) -> eyre::Result<()> {
        copy(
            PathBuf::from("tests/assets/playlist.m3u").canonicalize()?,
            self.playlists_dir().join("playlist.m3u"),
        )?;
        Ok(())
    }

    pub fn directory(&self) -> eyre::Result<()> {
        create_dir(self.music_dir().join("dir"))?;
        Ok(())
    }

    pub fn file(&self, target: &str) -> eyre::Result<()> {
        File::create(self.music_dir().join(target))?;
        Ok(())
    }
}