avatarr-parser 0.1.0

Release-name parser ported from Sonarr v4.0.17.2952
Documentation
// Ported from Sonarr v4.0.17.2952 (97e85a90):
//   src/NzbDrone.Core/Qualities/QualityFinder.cs
//   src/NzbDrone.Core/Qualities/Quality.cs (for the (source, resolution) catalogue)
//
// The C# `QualityFinder.FindBySourceAndResolution` is a 3-step algorithm:
//   1. Try an exact `(Source, Resolution)` match in `Quality.All`.
//   2. If `resolution < 720`, fall back to the SD-class default for the three
//      "soft" sources (Television, Web, WebRip) only. Other sources skip this
//      branch and fall through to step 3.
//   3. Otherwise, take every quality whose Resolution matches, sort by Source
//      enum value ascending, and return the first one whose Source is greater
//      than or equal to the requested source. If none qualify, return
//      `Quality.Unknown`.
//
// The plan-provided match table only modelled step 1 plus a subset of step 2.
// The m50 standing rule says "when the plan disagrees with the C# source, the
// C# wins", so this port mirrors the full algorithm. The 3 plan-baseline tests
// still pass; additional tests below pin the step-2 / step-3 corner cases that
// the plan's flat-match version would have silently regressed.

use crate::quality::{Quality, QualitySource};

/// Catalogue mirroring `Quality.All` in the C# source. Order matches the C#
/// `static Quality()` initializer for narrative-debug parity. Today every
/// `(Source, Resolution)` pair is unique, so step 3's scan has no ties to
/// resolve; if a future catalogue addition ever introduces a tie, `min_by_key`
/// returns the first encountered minimum, mirroring C# `OrderBy`'s
/// declaration-order tie-break.
const ALL: &[(Quality, QualitySource, u32)] = &[
    (Quality::Unknown, QualitySource::Unknown, 0),
    (Quality::Sdtv, QualitySource::Television, 480),
    (Quality::Dvd, QualitySource::Dvd, 480),
    (Quality::Webrip480p, QualitySource::WebRip, 480),
    (Quality::Webdl480p, QualitySource::Web, 480),
    (Quality::Bluray480p, QualitySource::Bluray, 480),
    (Quality::Bluray576p, QualitySource::Bluray, 576),
    (Quality::Hdtv720p, QualitySource::Television, 720),
    (Quality::Webrip720p, QualitySource::WebRip, 720),
    (Quality::Webdl720p, QualitySource::Web, 720),
    (Quality::Bluray720p, QualitySource::Bluray, 720),
    (Quality::Bluray1080p, QualitySource::Bluray, 1080),
    (Quality::Hdtv1080p, QualitySource::Television, 1080),
    (Quality::Webrip1080p, QualitySource::WebRip, 1080),
    (Quality::Webdl1080p, QualitySource::Web, 1080),
    (Quality::RawHd, QualitySource::TelevisionRaw, 1080),
    (Quality::Hdtv2160p, QualitySource::Television, 2160),
    (Quality::Webrip2160p, QualitySource::WebRip, 2160),
    (Quality::Webdl2160p, QualitySource::Web, 2160),
    (Quality::Bluray2160p, QualitySource::Bluray, 2160),
    (Quality::Bluray1080pRemux, QualitySource::BlurayRaw, 1080),
    (Quality::Bluray2160pRemux, QualitySource::BlurayRaw, 2160),
];

/// `QualitySource` ordinal, matching the C# enum's int value. Used to mirror
/// `OrderBy(q => q.Source)` and the `quality.Source >= source` comparison in
/// step 3.
fn source_rank(s: QualitySource) -> u8 {
    match s {
        QualitySource::Unknown => 0,
        QualitySource::Television => 1,
        QualitySource::TelevisionRaw => 2,
        QualitySource::Web => 3,
        QualitySource::WebRip => 4,
        QualitySource::Dvd => 5,
        QualitySource::Bluray => 6,
        QualitySource::BlurayRaw => 7,
    }
}

pub(crate) fn find_by_source_and_resolution(source: QualitySource, pixels: u32) -> Quality {
    // Step 1: exact (Source, Resolution) match.
    for (q, s, r) in ALL {
        if *s == source && *r == pixels {
            return *q;
        }
    }

    // Step 2: sub-720p fallback for the three "soft" sources. Other sources
    // (Bluray, BlurayRaw, Dvd, TelevisionRaw, Unknown) skip this branch and
    // fall through to step 3.
    if pixels < 720 {
        match source {
            QualitySource::Television => return Quality::Sdtv,
            QualitySource::Web => return Quality::Webdl480p,
            QualitySource::WebRip => return Quality::Webrip480p,
            _ => {}
        }
    }

    // Step 3: nearest-quality at this resolution. Find the entry with the
    // smallest source rank that is >= the requested source rank, walking ALL
    // once with no heap allocation. min_by_key returns the first encountered
    // minimum, so ties resolve in ALL declaration order, same tie-resolution
    // as C#'s LINQ OrderBy + foreach + first-match.
    let target_rank = source_rank(source);
    ALL.iter()
        .filter(|(_, _, r)| *r == pixels)
        .filter(|(_, s, _)| source_rank(*s) >= target_rank)
        .min_by_key(|(_, s, _)| source_rank(*s))
        .map(|(q, _, _)| *q)
        .unwrap_or(Quality::Unknown)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::quality::{Quality, QualitySource};

    // --- plan baseline (must stay) ---

    #[test]
    fn finds_webdl_1080p_by_source_and_resolution() {
        let q = find_by_source_and_resolution(QualitySource::Web, 1080);
        assert_eq!(q, Quality::Webdl1080p);
    }

    #[test]
    fn finds_hdtv_720p_by_source_and_resolution() {
        let q = find_by_source_and_resolution(QualitySource::Television, 720);
        assert_eq!(q, Quality::Hdtv720p);
    }

    #[test]
    fn unknown_for_unmapped_combo() {
        let q = find_by_source_and_resolution(QualitySource::Unknown, 9999);
        assert_eq!(q, Quality::Unknown);
    }

    // --- C# fidelity: step 2 (sub-720p soft-source fallback) ---

    #[test]
    fn television_zero_pixels_falls_back_to_sdtv() {
        // C# step 2: resolution<720 + Television => SDTV.
        assert_eq!(
            find_by_source_and_resolution(QualitySource::Television, 0),
            Quality::Sdtv
        );
    }

    #[test]
    fn television_576_falls_back_to_sdtv() {
        // No exact match (no Television@576). Step 2 catches it.
        assert_eq!(
            find_by_source_and_resolution(QualitySource::Television, 576),
            Quality::Sdtv
        );
    }

    #[test]
    fn web_576_falls_back_to_webdl480p() {
        assert_eq!(
            find_by_source_and_resolution(QualitySource::Web, 576),
            Quality::Webdl480p
        );
    }

    #[test]
    fn webrip_zero_pixels_falls_back_to_webrip480p() {
        assert_eq!(
            find_by_source_and_resolution(QualitySource::WebRip, 0),
            Quality::Webrip480p
        );
    }

    // --- C# fidelity: step 3 (nearest-quality scan) ---

    #[test]
    fn television_raw_at_1080_is_rawhd() {
        // Exact match: RAWHD == (TelevisionRaw, 1080).
        assert_eq!(
            find_by_source_and_resolution(QualitySource::TelevisionRaw, 1080),
            Quality::RawHd
        );
    }

    #[test]
    fn television_raw_at_720_promotes_to_webdl720p() {
        // No exact match; step 3 lists the four 720p qualities ordered by
        // source rank: HDTV720p(1), WEBDL720p(3), WEBRip720p(4), Bluray720p(6).
        // First with rank >= TelevisionRaw(2) is WEBDL720p.
        assert_eq!(
            find_by_source_and_resolution(QualitySource::TelevisionRaw, 720),
            Quality::Webdl720p
        );
    }

    #[test]
    fn bluray_raw_at_720_is_unknown() {
        // 720p qualities top out at Bluray(6); BlurayRaw(7) has no match.
        assert_eq!(
            find_by_source_and_resolution(QualitySource::BlurayRaw, 720),
            Quality::Unknown
        );
    }

    #[test]
    fn bluray_576_exact_match() {
        // Bluray576p is the only 576p quality in C# Quality.All.
        assert_eq!(
            find_by_source_and_resolution(QualitySource::Bluray, 576),
            Quality::Bluray576p
        );
    }

    #[test]
    fn dvd_at_720_promotes_to_bluray720p() {
        // No DVD@720 exists; step 3 at 720 with rank>=DVD(5) yields Bluray720p(6).
        assert_eq!(
            find_by_source_and_resolution(QualitySource::Dvd, 720),
            Quality::Bluray720p
        );
    }

    #[test]
    fn bluray_540_is_unknown() {
        // No 540p qualities exist; step 2 doesn't apply (Bluray is not a soft
        // source); step 3 has no candidates -> Unknown.
        assert_eq!(
            find_by_source_and_resolution(QualitySource::Bluray, 540),
            Quality::Unknown
        );
    }

    #[test]
    fn source_rank_matches_csharp_ordinals() {
        // Pinned to QualitySource.cs ordinals at v4.0.17.2952 (97e85a90).
        // Drift here would silently corrupt step 3's nearest-match scan.
        assert_eq!(source_rank(QualitySource::Unknown), 0);
        assert_eq!(source_rank(QualitySource::Television), 1);
        assert_eq!(source_rank(QualitySource::TelevisionRaw), 2);
        assert_eq!(source_rank(QualitySource::Web), 3);
        assert_eq!(source_rank(QualitySource::WebRip), 4);
        assert_eq!(source_rank(QualitySource::Dvd), 5);
        assert_eq!(source_rank(QualitySource::Bluray), 6);
        assert_eq!(source_rank(QualitySource::BlurayRaw), 7);
    }
}