feat(core): rank resolutions in the score
`resolution_pref` only gated eligibility, so every release was scored against its own resolution's size band and a 23 GB 4K lost to an at-target 1080p. Each step up the list is now worth `resolution_step` points, seeded at 300: five gibibytes of 4K overshoot, so a 4K up to 27 GB wins and a bloated one still does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
-- §5.5 cross-resolution ranking, issue #113. The size term scores a release
|
||||
-- against the band for its own resolution, so it says nothing about how
|
||||
-- resolutions rank against each other: an at-target 1080p and an at-target
|
||||
-- 4K both score `size_at_target`, and a 4K a couple of gibibytes over target
|
||||
-- lost to a 1080p that was merely on target. `resolution_pref` being an
|
||||
-- ordered list already implies a preference; this is the weight that makes
|
||||
-- the scorer honour it.
|
||||
--
|
||||
-- The last entry of `resolution_pref` is worth nothing and each earlier one
|
||||
-- `resolution_step` points more. 300 is chosen against the seeded 4K band
|
||||
-- (target 22 GiB, 60 points per gibibyte over): five gibibytes of overshoot,
|
||||
-- so a 4K up to 27 GiB beats an at-target 1080p and a bloated 4K past that
|
||||
-- does not. Below target the same arithmetic asks a 4K to be within about
|
||||
-- four gibibytes of its target to win, which keeps an 8 GiB 4K that looks
|
||||
-- like mud from beating a good 1080p.
|
||||
UPDATE policies
|
||||
SET score_weights = json_set(score_weights, '$.resolution_step', 300),
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');
|
||||
@@ -223,6 +223,8 @@ mod tests {
|
||||
assert!(score_weights.contains("size_at_target"), "{score_weights}");
|
||||
assert!(score_weights.contains("source_tier"), "{score_weights}");
|
||||
assert!(score_weights.contains("seeder_doubling"), "{score_weights}");
|
||||
// §5.5: and the step that ranks resolutions against each other.
|
||||
assert!(score_weights.contains("resolution_step"), "{score_weights}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ impl PolicyColumns {
|
||||
size_at_target: score_weights.size_at_target,
|
||||
source_tier: score_weights.source_tier,
|
||||
seeder_doubling: score_weights.seeder_doubling,
|
||||
resolution_step: score_weights.resolution_step,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -340,6 +341,16 @@ struct ScoreWeightsJson {
|
||||
size_at_target: i32,
|
||||
source_tier: i32,
|
||||
seeder_doubling: i32,
|
||||
/// Added in 0015. The column default written by 0004 predates it, so a
|
||||
/// row inserted without the column would carry no resolution rank at
|
||||
/// all; fall back to the engine's own default rather than silently
|
||||
/// scoring every resolution the same.
|
||||
#[serde(default = "default_resolution_step")]
|
||||
resolution_step: i32,
|
||||
}
|
||||
|
||||
fn default_resolution_step() -> i32 {
|
||||
ScoreWeights::default().resolution_step
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -427,6 +438,36 @@ mod tests {
|
||||
assert_eq!(loaded.policy.score_weights.seeder_doubling, 1);
|
||||
}
|
||||
|
||||
/// §5.5, issue #113: the seeded rows carry a resolution rank, and a row
|
||||
/// written before 0015 falls back to the engine's default rather than
|
||||
/// ranking every resolution the same.
|
||||
#[tokio::test]
|
||||
async fn the_resolution_step_is_seeded_and_defaults_when_absent() {
|
||||
let (_dir, db) = seeded().await;
|
||||
let seeded_step = db
|
||||
.movie_policy(1)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("movie 1")
|
||||
.policy
|
||||
.score_weights
|
||||
.resolution_step;
|
||||
assert_eq!(seeded_step, 300);
|
||||
|
||||
sqlx::query(
|
||||
r#"UPDATE policies SET score_weights = '{"size_at_target":500,"source_tier":10,"seeder_doubling":1}'"#,
|
||||
)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
let loaded = db.movie_policy(1).await.unwrap().expect("movie 1");
|
||||
|
||||
assert_eq!(
|
||||
loaded.policy.score_weights.resolution_step,
|
||||
ScoreWeights::default().resolution_step
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn per_title_overrides_ride_along() {
|
||||
let (_dir, db) = seeded().await;
|
||||
@@ -456,7 +497,9 @@ mod tests {
|
||||
.into(),
|
||||
resolution_pref: r#"["2160p"]"#.into(),
|
||||
source_weights: r#"{"WEB-DL":2}"#.into(),
|
||||
score_weights: r#"{"size_at_target":2000,"source_tier":7,"seeder_doubling":11}"#.into(),
|
||||
score_weights:
|
||||
r#"{"size_at_target":2000,"source_tier":7,"seeder_doubling":11,"resolution_step":42}"#
|
||||
.into(),
|
||||
}
|
||||
.to_policy()
|
||||
.unwrap();
|
||||
@@ -467,6 +510,7 @@ mod tests {
|
||||
size_at_target: 2000,
|
||||
source_tier: 7,
|
||||
seeder_doubling: 11,
|
||||
resolution_step: 42,
|
||||
}
|
||||
);
|
||||
assert_eq!(policy.source_weights[&Source::WebDl], 2);
|
||||
|
||||
Reference in New Issue
Block a user