@@ -374,6 +374,10 @@ impl TvGrabAction {
|
||||
original_language: &Language,
|
||||
blacklist: &Blacklist,
|
||||
) -> Result<Vec<TvCandidate>, GrabError> {
|
||||
// #182: a season-scoped sweep is the pack lane's own search, and the
|
||||
// release deck needs to know it completed to tell "nothing found"
|
||||
// from "nothing has run yet".
|
||||
let pack_sweep = matches!(selector, TvSelector::Season { .. });
|
||||
let target = TvTarget {
|
||||
tvdb_id: season.series_tvdb_id.and_then(|id| u64::try_from(id).ok()),
|
||||
title: season.series_title.clone(),
|
||||
@@ -451,6 +455,9 @@ impl TvGrabAction {
|
||||
eligible: stored,
|
||||
});
|
||||
}
|
||||
if pack_sweep && any_searchable {
|
||||
record_pack_search(database, season.season_id).await?;
|
||||
}
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
@@ -1113,6 +1120,22 @@ async fn load_season_release(
|
||||
}))
|
||||
}
|
||||
|
||||
/// When the season's own pack search last completed (#182). Written only by
|
||||
/// a season-scoped sweep, so it answers the deck's question — did a pack
|
||||
/// search run — and nothing else.
|
||||
async fn record_pack_search(database: &Db, season_id: i64) -> Result<(), GrabError> {
|
||||
sqlx::query!(
|
||||
"UPDATE seasons
|
||||
SET last_pack_search_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||
WHERE id = ?",
|
||||
season_id
|
||||
)
|
||||
.execute(database.pool())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether failed season-pack grabs still hold this season off the pack
|
||||
/// lane. §6.2: a failure quiets the pack search on the shared backoff curve
|
||||
/// (each failed grab is one attempt), it never disables it. Anchored on the
|
||||
@@ -1648,6 +1671,49 @@ mod tests {
|
||||
assert!(sources[1].ends_with("e03.torrent"));
|
||||
}
|
||||
|
||||
/// #182: the season deck cannot tell a queued pack sweep from one that
|
||||
/// ran and found nothing unless the pack search leaves a mark. Episode
|
||||
/// searches must not leave it — a season on the per-episode lane never
|
||||
/// asked for a pack, and pretending otherwise is the lie the deck told.
|
||||
#[tokio::test]
|
||||
async fn only_a_pack_sweep_stamps_the_season() {
|
||||
let (_dir, database, season_id) =
|
||||
wanted_season(&["2024-04-11", "2024-04-11", "2024-04-11"]).await;
|
||||
let indexer = prowlarr().await;
|
||||
let (downloader, fake) = transmission().await;
|
||||
|
||||
let stamp = |database: Db| async move {
|
||||
sqlx::query_scalar::<_, Option<String>>(
|
||||
"SELECT last_pack_search_at FROM seasons WHERE id = ?",
|
||||
)
|
||||
.bind(season_id)
|
||||
.fetch_one(database.pool())
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
assert!(stamp(database.clone()).await.is_none());
|
||||
|
||||
action(&indexer, &downloader).tick(&database).await.unwrap();
|
||||
let after_pack = stamp(database.clone()).await;
|
||||
assert!(
|
||||
after_pack.is_some(),
|
||||
"a fully released season takes the pack lane and its sweep is the deck's evidence"
|
||||
);
|
||||
assert_eq!(fake.torrents().len(), 1);
|
||||
|
||||
// An airing season is on the per-episode lane: its episode searches
|
||||
// say nothing about whether a pack was ever looked for.
|
||||
let (_other_dir, airing, airing_id) = wanted_season(&["2024-04-11", "2999-01-01"]).await;
|
||||
action(&indexer, &downloader).tick(&airing).await.unwrap();
|
||||
let never: Option<String> =
|
||||
sqlx::query_scalar("SELECT last_pack_search_at FROM seasons WHERE id = ?")
|
||||
.bind(airing_id)
|
||||
.fetch_one(airing.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(never.is_none());
|
||||
}
|
||||
|
||||
/// An empty result backs the whole season off together (§6.2) instead of
|
||||
/// hammering the tracker every 30 s.
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user