avatarr-parser 0.1.0

Release-name parser ported from Sonarr v4.0.17.2952
Documentation
// Ported from Sonarr v4.0.17.2952 (97e85a90).
// Sources:
//   src/NzbDrone.Core/Qualities/Quality.cs
//   src/NzbDrone.Core/Qualities/QualityModel.cs
//   src/NzbDrone.Core/Qualities/QualitySource.cs
//   src/NzbDrone.Core/Qualities/QualityDetectionSource.cs

pub mod finder;
pub mod parser;

pub use parser::{parse_quality, parse_quality_name};

/// Quality identity: a stable integer ID per release class.
///
/// IDs match Sonarr's `Quality.cs` static initializers verbatim. ID 11 is
/// intentionally skipped. It was reserved for `HDTV-480p`, which Sonarr
/// commented out and never shipped. Bluray576p lands at ID 22 because it was
/// added after the 2160p suite.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Quality {
    #[default]
    Unknown = 0,
    Sdtv = 1,
    Dvd = 2,
    Webdl1080p = 3,
    Hdtv720p = 4,
    Webdl720p = 5,
    Bluray720p = 6,
    Bluray1080p = 7,
    Webdl480p = 8,
    Hdtv1080p = 9,
    RawHd = 10,
    Webrip480p = 12,
    Bluray480p = 13,
    Webrip720p = 14,
    Webrip1080p = 15,
    Hdtv2160p = 16,
    Webrip2160p = 17,
    Webdl2160p = 18,
    Bluray2160p = 19,
    Bluray1080pRemux = 20,
    Bluray2160pRemux = 21,
    Bluray576p = 22,
}

/// Source-class metadata attached to each `Quality` (Television, Web, etc.).
///
/// Mirrors `QualitySource.cs` (8 variants). `Dvd` and `BlurayRaw` are spelled
/// `DVD` and `BlurayRaw` in the C# enum; we lowercase per Rust convention.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum QualitySource {
    #[default]
    Unknown,
    Television,
    TelevisionRaw,
    Web,
    WebRip,
    Dvd,
    Bluray,
    BlurayRaw,
}

/// Vertical-resolution buckets a release falls into.
///
/// Sonarr stores resolution as a raw `int` on each `Quality` instance; the
/// Rust port models it as a typed enum so the parser can return well-formed
/// values without a magic-number lookup. `pixels()` returns the numeric height
/// for any consumer that needs to compare against a raw integer (e.g. minimum
/// quality thresholds).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Resolution {
    #[default]
    Unknown,
    R360p,
    R480p,
    R540p,
    R576p,
    R720p,
    R1080p,
    R2160p,
}

impl Resolution {
    /// Vertical pixel count for this resolution; `0` for `Unknown`.
    pub fn pixels(self) -> u32 {
        match self {
            Resolution::R360p => 360,
            Resolution::R480p => 480,
            Resolution::R540p => 540,
            Resolution::R576p => 576,
            Resolution::R720p => 720,
            Resolution::R1080p => 1080,
            Resolution::R2160p => 2160,
            Resolution::Unknown => 0,
        }
    }
}

/// Where in a release a particular field (source, resolution, revision) was
/// detected.
///
/// Mirrors `QualityDetectionSource.cs` (4 variants: `Unknown`, `Name`,
/// `Extension`, `MediaInfo`). `MediaInfo` is set when the field came from
/// MediaInfo probing rather than the release name; the parser layer only
/// emits `Name` / `Extension` / `Unknown`, but the variant exists so consumers
/// can model the full Sonarr surface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum QualityDetectionSource {
    #[default]
    Unknown,
    Name,
    Extension,
    MediaInfo,
}

/// Repack / proper revision metadata.
///
/// `version` defaults to `1` (the original release); `2+` indicates a
/// PROPER, REPACK, or explicit `vN` marker. `real` is a count of `REAL` tags
/// (Sonarr stores it as `int`, not `bool`, because the modifier cascade
/// records every uppercase REAL match via `RealRegex.Matches(name).Count`).
/// `is_repack` flags REPACK; the version bump from REPACK is kept on
/// `version` separately, and the two coexist for the comparison ordering in
/// `CompareTo`.
///
/// `Default` is implemented manually because we need a non-zero `version`.
///
/// Verified at T6 (Sonarr v4.0.17.2952, `Qualities/Revision.cs:19-21` and
/// `Parser/QualityParser.cs:702-708`): `Real` is `int`, set from
/// `realRegexResult.Count`. The plan-spec drafted `bool`, but the multi-real
/// case (`Show.REAL.REAL.1080p`) would lose information at the cast boundary.
/// Ported as `u32` to mirror the C# signed-int counter; values are always
/// non-negative because `Matches.Count` is non-negative.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Revision {
    pub version: u32,
    pub real: u32,
    pub is_repack: bool,
}

impl Default for Revision {
    fn default() -> Self {
        Self {
            version: 1,
            real: 0,
            is_repack: false,
        }
    }
}

/// A parsed `Quality` plus its `Revision` and per-field detection-source
/// trail.
///
/// Mirrors `QualityModel.cs`: `Quality` + `Revision` are the user-visible
/// payload; the three `*DetectionSource` fields record provenance for the
/// Quality, Resolution, and Revision components respectively. C# carries
/// these as `[JsonIgnore]` properties (they never serialise), so the Rust
/// port keeps them as plain fields without serde wiring.
///
/// `Default` derives because every constituent field is `Default`; the
/// resulting model is `Quality::Unknown` + version-1 revision + all
/// detection sources unknown.
// TODO(m51): disambiguate from `crates/core::domain::embedded::QualityModel`
// (Radarr-shape, flat fields). Both will co-exist when m51 wires the parser
// into the user-visible path; resolve via rename or `From<>` converter.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct QualityModel {
    pub quality: Quality,
    pub revision: Revision,
    pub source_detection_source: QualityDetectionSource,
    pub resolution_detection_source: QualityDetectionSource,
    pub revision_detection_source: QualityDetectionSource,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn quality_constants_have_expected_ids() {
        assert_eq!(Quality::Unknown as i32, 0);
        assert_eq!(Quality::Sdtv as i32, 1);
        assert_eq!(Quality::Webdl1080p as i32, 3);
        assert_eq!(Quality::Bluray2160pRemux as i32, 21);
        assert_eq!(Quality::Bluray576p as i32, 22);
    }

    #[test]
    fn quality_model_default_is_unknown() {
        let model = QualityModel::default();
        assert_eq!(model.quality, Quality::Unknown);
        assert_eq!(model.revision.version, 1);
        assert!(!model.revision.is_repack);
        assert_eq!(model.revision.real, 0);
    }
}