autorip 0.1.1

Composes other programs to automatically rip optical media
Documentation
use std::{
    borrow::Cow,
    fmt, fs,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{anyhow, Result};

pub(crate) mod metadata;

use crate::{
    capture_group_as_str, get_stderr, get_stdout, MetaData, Rip, RipConfig,
};

pub(crate) struct Preset {
    quality: Quality,
    resolution: Resolution,
}

const GIGABYTE: u64 = 1_073_741_824;

impl Preset {
    fn guess_from_source(s: &VideoMediaSource) -> Self {
        use VideoMediaSource as VMS;
        match s {
            VMS::Dvd(_) => Preset {
                quality: Quality::Default,
                resolution: Resolution::Default,
            },
            VMS::Bluray(_) => Preset {
                quality: Quality::High,
                resolution: Resolution::High,
            },
            VMS::File(File { path, .. }) => {
                if let Ok(md) = fs::metadata(path) {
                    const TWO_GIGS: u64 = 2 * GIGABYTE;

                    // https://users.rust-lang.org/t/better-way-to-use-constants-in-match-statements/
                    #[allow(clippy::match_overlapping_arm)]
                    match md.len() {
                        0..=GIGABYTE => Preset {
                            quality: Quality::Default,
                            resolution: Resolution::Default,
                        },
                        0..=TWO_GIGS => Preset {
                            quality: Quality::High,
                            resolution: Resolution::Default,
                        },
                        _ => Preset {
                            quality: Quality::High,
                            resolution: Resolution::High,
                        },
                    }
                } else {
                    Preset {
                        quality: Quality::Default,
                        resolution: Resolution::Default,
                    }
                }
            }
        }
    }
}

impl fmt::Display for Preset {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.quality.as_ref(), self.resolution.as_ref())
    }
}

enum Quality {
    Default,
    High,
}

impl AsRef<str> for Quality {
    fn as_ref(&self) -> &str {
        use Quality::{Default, High};
        match self {
            Default => "Devices/Apple",
            High => "General/Super HQ",
        }
    }
}

enum Resolution {
    Default,
    High,
}

impl AsRef<str> for Resolution {
    fn as_ref(&self) -> &str {
        use Resolution::{Default, High};
        match self {
            Default => "720p30",
            High => "1080p30",
        }
    }
}

mod dvd;
use dvd::Dvd;

pub(crate) mod file;
use file::File;

mod bluray;
use bluray::Bluray;

pub(crate) enum VideoMediaSource {
    Dvd(Dvd),
    Bluray(Bluray),
    File(File),
}

impl VideoMediaSource {
    pub(crate) fn new(path: impl Into<PathBuf>) -> Result<Self> {
        match path.into() {
            p if p.starts_with("/dev/dvd") => {
                Ok(VideoMediaSource::Dvd(Dvd::new(p)?))
            }
            p if p.starts_with("/dev/bluray") => {
                Ok(VideoMediaSource::Bluray(Bluray::new(p)?))
            }
            p => Ok(VideoMediaSource::File(File::new(p)?)),
        }
    }

    pub(crate) fn duration(&self) -> Option<u32> {
        match self {
            VideoMediaSource::Dvd(dvd) => dvd.duration,
            VideoMediaSource::Bluray(bluray) => bluray.duration,
            VideoMediaSource::File(file) => file.duration,
        }
    }

    pub(crate) fn title_hint(&self) -> &str {
        match self {
            VideoMediaSource::Dvd(dvd) => &dvd.title_hint,
            VideoMediaSource::Bluray(bluray) => &bluray.title_hint,
            VideoMediaSource::File(file) => &file.title_hint,
        }
    }

    pub(crate) fn rip(&self, preset: Option<Preset>) -> Result<PathBuf> {
        let preset = preset.unwrap_or_else(|| Preset::guess_from_source(self));
        match self {
            VideoMediaSource::Dvd(dvd) => dvd.rip(&preset),
            VideoMediaSource::Bluray(bluray) => bluray.rip(&preset),
            VideoMediaSource::File(file) => file.rip(&preset),
        }
    }
}

fn title_from_label(path: &Path) -> Result<String> {
    let output = Command::new("blkid")
        .args("-o value -s LABEL".split_whitespace())
        .arg(path)
        .output()?;
    get_stdout(output).map(String::from)
}

impl Rip for VideoMediaSource {
    fn rip(&self, config: &RipConfig) -> Result<PathBuf> {
        let title_hint = self.title_hint();

        let metadata = config
            .tmdb_id
            .and_then(|id| MetaData::from_id(id).ok())
            .or_else(|| MetaData::guess_from_title(title_hint).ok());

        let outfile: PathBuf = metadata
            .as_ref()
            .map(|md| Cow::Owned(md.title.trim().to_string()) + ".mp4")
            .unwrap_or(Cow::Borrowed(title_hint))
            .as_ref()
            .into();

        let ripped = self.rip(None)?;

        if let Some(duration) = self.duration() {
            let ripped_duration =
                File::new(&ripped)?.duration.ok_or_else(|| {
                    anyhow!("Ripped file should have a duration")
                })?;
            let abs_diff =
                duration.max(ripped_duration) - duration.min(ripped_duration);
            if abs_diff > 1 {
                log::warn!(
                "Ripped file {} has duration {} seconds, expected duration \
                 {} seconds",
                &ripped.display(),
                &ripped_duration,
                &duration
            );
            }
        };

        log::info!(
            "File successfully converted and written to {}",
            ripped.display(),
        );

        fs::rename(ripped, &*outfile)?;

        log::info!(
            "File successfully renamed to {}. Setting metadata:\n{:?}",
            outfile.display(),
            &metadata
        );
        if let Some(md) = metadata {
            metadata::set_metadata(&*outfile, &md)?;
        }
        log::debug!("Done setting metadata");
        Ok(outfile)
    }
}