autorip 0.1.1

Composes other programs to automatically rip optical media
Documentation
#![warn(clippy::pedantic)]
#![allow(dead_code)]
use std::{
    io::{self, Write},
    path::PathBuf,
};

use anyhow::{anyhow, bail, Result};

mod cdrom;
mod video;
use cdrom::AudioMediaSource;
use structopt::StructOpt;
use video::{metadata::MetaData, VideoMediaSource};

#[derive(Debug, StructOpt)]
#[structopt(author)]
pub enum Config {
    Rip(RipConfig),
    Search(SearchConfig),
}

#[derive(Debug, StructOpt)]
pub struct RipConfig {
    #[structopt(parse(from_os_str))]
    /// Sets the input device or file, e.g. `/dev/dvd`
    path: PathBuf,

    #[structopt(short, long, parse(from_occurrences))]
    /// Sets the verbosity of logging output
    pub verbosity: u8,

    #[structopt(long)]
    /// API key for themoviedb.org for fetching metadata
    tmdb_api_key: Option<String>,

    #[structopt(short = "i", long)]
    /// Movie ID from themoviedb.org, for metadata
    tmdb_id: Option<u32>,

    /// Configuration from file #TODO
    #[structopt(short, long, parse(from_os_str))]
    config: Option<PathBuf>,
}

#[derive(Debug, StructOpt)]
pub struct SearchConfig {
    #[structopt(short, long, parse(from_occurrences))]
    /// Sets the verbosity of logging output
    pub verbosity: u8,

    #[structopt(long)]
    /// API key for themoviedb.org for fetching metadata
    tmdb_api_key: Option<String>,

    /// Configuration from file #TODO
    #[structopt(short, long, parse(from_os_str))]
    config: Option<PathBuf>,

    // themoviedb.org id for which to show metad
    #[structopt(short, long)]
    id: Option<u32>,

    #[structopt(short, long)]
    // Movie title to search for
    title: Option<String>,
}

fn get_stdout(output: std::process::Output) -> Result<String> {
    if !output.status.success() {
        let stderr = String::from_utf8(output.stderr)?;
        return Err(anyhow!(stderr));
    }
    Ok(String::from_utf8(output.stdout)?)
}

fn get_stderr(output: std::process::Output) -> Result<String> {
    let stderr = String::from_utf8(output.stderr)?;
    if !output.status.success() {
        return Err(anyhow!(stderr));
    }
    Ok(stderr)
}

fn capture_group_as_str<'a>(
    cap: &regex::Captures<'a>,
    groupname: &'static str,
) -> Result<&'a str> {
    cap.name(groupname)
        .map(|v| v.as_str())
        .ok_or_else(|| anyhow!("could not find capture group `{}`", groupname))
}

trait Rip {
    fn rip(&self, config: &RipConfig) -> Result<PathBuf>;
}

/// # Errors
pub fn rip(config: &RipConfig) -> Result<PathBuf> {
    if !config.path.starts_with("/dev/") {
        return Err(anyhow!(
            "Unsure how deal with path {}",
            config.path.display()
        ));
    }
    match config.path.to_str() {
        Some("/dev/cdrom") => {
            AudioMediaSource::new(&config.path).and_then(|src| src.rip(config))
        }
        Some(path) => {
            VideoMediaSource::new(path).and_then(|src| src.rip(None))
        }
        _ => Err(anyhow!("Unknown audio source")),
    }
}

/// # Errors
pub fn search(config: &SearchConfig) -> Result<()> {
    let md = match config {
        SearchConfig { id: Some(id), .. } => {
            MetaData::from_id(*id)?.to_string()
        }
        SearchConfig {
            title: Some(title), ..
        } => MetaData::search_by_title(title, 2)?
            .map(|v| v.to_string())
            .collect::<Vec<_>>()
            .join("\n"),
        _ => bail!("not sure how to search for item without title or id"),
    };

    match config.verbosity {
        0 => write!(io::stdout(), "{md}")?,
        _ => write!(io::stdout(), "{md:?}")?,
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::process::Command;

    use super::*;
    #[test]
    fn test_get_stdout() -> Result<()> {
        let output_works =
            get_stdout(Command::new("echo").arg("foo bar").output()?)?;
        assert_eq!(output_works.trim(), "foo bar");

        let command_fails = get_stdout(
            Command::new("ls").arg("file that doesn't.exist").output()?,
        );
        assert!(command_fails.is_err());
        assert!(command_fails.unwrap_err().to_string().starts_with("ls:"));
        Ok(())
    }

    #[test]
    fn test_get_stderr() -> Result<()> {
        let output_works = get_stderr(
            Command::new("bash")
                .arg("-c")
                .arg("echo foo bar 1>&2")
                .output()?,
        )?;
        assert_eq!(output_works.trim(), "foo bar");

        let command_fails = get_stderr(
            Command::new("ls").arg("file that doesn't.exist").output()?,
        );
        assert!(command_fails.is_err());
        assert!(command_fails.unwrap_err().to_string().starts_with("ls:"));
        Ok(())
    }
}