mp3rgain 3.7.0

Lossless MP3 volume adjustment - a modern mp3gain replacement written in Rust
Documentation
//! Custom error types for mp3rgain.

use std::path::{Path, PathBuf};

pub type Result<T> = std::result::Result<T, Error>;

/// All errors that can occur in mp3rgain operations.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    // I/O
    #[error("Failed to read '{path}': {source}")]
    IoRead {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to write '{path}': {source}")]
    IoWrite {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to open '{path}': {source}")]
    IoOpen {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },

    // MP3
    #[error("No valid MP3 frames found")]
    NoMp3Frames,

    #[error("Cannot apply channel-specific gain to mono file. Use -g for mono files.")]
    ChannelGainOnMono,

    #[error("Channel-specific gain is not supported for AAC/M4A files")]
    ChannelGainOnAac,

    #[error("No APE tag found - cannot undo")]
    NoApeTag,

    #[error("No MP3GAIN_UNDO tag found - cannot undo")]
    NoUndoTag,

    // ReplayGain / decoder
    #[error("No audio track found")]
    NoAudioTrack,

    #[error("Track index {index} out of range (file has {count} audio track(s))")]
    TrackIndexOutOfRange { index: u32, count: usize },

    #[error("Unsupported sample rate: {0} Hz")]
    UnsupportedSampleRate(u32),

    #[error("The '{feature}' feature is not available. Rebuild with --features {feature_flag}.")]
    FeatureNotAvailable {
        feature: &'static str,
        feature_flag: &'static str,
    },

    #[error("Failed to probe audio format in '{path}': {source}")]
    ProbeFailed {
        path: PathBuf,
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },

    #[error("Audio decode error: {0}")]
    Decode(#[source] Box<dyn std::error::Error + Send + Sync>),

    // MP4 / AAC
    #[error("No moov box found in MP4 file")]
    NoMoovBox,

    #[error("Not an MP4 file: {path}")]
    NotMp4File { path: PathBuf },

    #[error("AAC bitstream parse error: {message}")]
    AacParse { message: String },

    #[error("No AAC audio track found")]
    NoAacTrack,

    // Format support
    #[error("{format} is not supported for gain adjustment")]
    UnsupportedFormat { format: &'static str },

    #[error("Failed to parse any AAC samples ({warnings} errors)")]
    AacParseFailure { warnings: u32 },

    #[error("All {count} file(s) failed to analyze")]
    AllFilesFailed { count: usize },

    #[error("Operation cancelled")]
    Cancelled,

    // ID3v2
    #[error("ID3v2 tag error: {message}")]
    Id3v2Error { message: String },

    #[error("No ID3v2 undo tag found - cannot undo")]
    NoId3v2UndoTag,
}

impl Error {
    /// Re-label a "cannot read this file's audio" failure as
    /// [`Self::UnsupportedFormat`] when `path` turns out to be a container
    /// mp3rgain recognizes but cannot process — ALAC or DRM-protected M4P
    /// (issue #330).
    ///
    /// Without this, an ALAC file fails analysis with symphonia's
    /// "unsupported audio codec" and the MP3 apply path reports
    /// [`Self::NoMp3Frames`] for a file that was never an MP3, both of which
    /// read as genuine failures. Costs a 128-byte header read, and only on the
    /// failure path.
    pub fn refine_format(self, path: &Path) -> Self {
        if !matches!(
            self,
            Self::NoMp3Frames | Self::Decode(_) | Self::ProbeFailed { .. }
        ) {
            return self;
        }
        match crate::mp4meta::unsupported_audio_format(path) {
            Some(format) => Self::UnsupportedFormat { format },
            None => self,
        }
    }

    /// True for [`Self::UnsupportedFormat`]: the file's *format* is the
    /// problem, not the file or the run, so frontends report it as a skipped
    /// file rather than a failure that sets the exit code.
    pub fn is_unsupported_format(&self) -> bool {
        matches!(self, Self::UnsupportedFormat { .. })
    }

    pub fn io_read(path: &Path, source: std::io::Error) -> Self {
        Self::IoRead {
            path: path.to_path_buf(),
            source,
        }
    }

    pub fn io_write(path: &Path, source: std::io::Error) -> Self {
        Self::IoWrite {
            path: path.to_path_buf(),
            source,
        }
    }

    pub fn io_open(path: &Path, source: std::io::Error) -> Self {
        Self::IoOpen {
            path: path.to_path_buf(),
            source,
        }
    }
}