Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f7373d73d |
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", tvdb_id, title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", auto_track AS \"auto_track!: bool\", overrides AS \"overrides!: serde_json::Value\", upstream_ended AS \"upstream_ended!: bool\", blocked AS \"blocked!: bool\", poster_path, vote_average FROM series ORDER BY title, year, id",
|
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", tvdb_id, title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", auto_track AS \"auto_track!: bool\", overrides AS \"overrides!: serde_json::Value\", upstream_ended AS \"upstream_ended!: bool\", blocked AS \"blocked!: bool\", poster_path, vote_average, metadata_refreshed_at FROM series ORDER BY title, year, id",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -67,6 +67,11 @@
|
|||||||
"name": "vote_average",
|
"name": "vote_average",
|
||||||
"ordinal": 12,
|
"ordinal": 12,
|
||||||
"type_info": "Float"
|
"type_info": "Float"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_refreshed_at",
|
||||||
|
"ordinal": 13,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -85,8 +90,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "cf040af066e35e9bcb969c84ac180784d5bc9a2bf0630cd6fef2bb8f4cb467b5"
|
"hash": "3b518839d694b872f83005ca059d45d048a8c44efd78b75e08e74299fed072ea"
|
||||||
}
|
}
|
||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.tvdb_id, s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.auto_track AS \"auto_track!: bool\", s.overrides AS \"overrides!: serde_json::Value\", s.upstream_ended AS \"upstream_ended!: bool\", s.blocked AS \"blocked!: bool\", poster_path, vote_average FROM series s JOIN title_owners t ON t.title_kind = 'series' AND t.title_id = s.id WHERE t.owner_id = ? ORDER BY s.title, s.year, s.id",
|
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.tvdb_id, s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.auto_track AS \"auto_track!: bool\", s.overrides AS \"overrides!: serde_json::Value\", s.upstream_ended AS \"upstream_ended!: bool\", s.blocked AS \"blocked!: bool\", poster_path, vote_average, s.metadata_refreshed_at FROM series s JOIN title_owners t ON t.title_kind = 'series' AND t.title_id = s.id WHERE t.owner_id = ? ORDER BY s.title, s.year, s.id",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -67,6 +67,11 @@
|
|||||||
"name": "vote_average",
|
"name": "vote_average",
|
||||||
"ordinal": 12,
|
"ordinal": 12,
|
||||||
"type_info": "Float"
|
"type_info": "Float"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_refreshed_at",
|
||||||
|
"ordinal": 13,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -85,8 +90,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "9f0164d699ffd66f31e7916827c81e01d283f35bb91ec2f34f63240792c01152"
|
"hash": "9755322057875ad786326cf533bff5d6b78e7844476731b309a3859287ff8c2f"
|
||||||
}
|
}
|
||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", tvdb_id, title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", auto_track AS \"auto_track!: bool\", overrides AS \"overrides!: serde_json::Value\", upstream_ended AS \"upstream_ended!: bool\", blocked AS \"blocked!: bool\", poster_path, vote_average FROM series WHERE id = ?",
|
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", tvdb_id, title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", auto_track AS \"auto_track!: bool\", overrides AS \"overrides!: serde_json::Value\", upstream_ended AS \"upstream_ended!: bool\", blocked AS \"blocked!: bool\", poster_path, vote_average, metadata_refreshed_at FROM series WHERE id = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -67,6 +67,11 @@
|
|||||||
"name": "vote_average",
|
"name": "vote_average",
|
||||||
"ordinal": 12,
|
"ordinal": 12,
|
||||||
"type_info": "Float"
|
"type_info": "Float"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "metadata_refreshed_at",
|
||||||
|
"ordinal": 13,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -85,8 +90,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "ab536da7864e712754f1742e1f1f67d1d5cef884dafcaae1ae9ce498fc852e32"
|
"hash": "fd340665e0593bf692f0ffbe60ba4bebbc1a3d80c1304ed1fb790ccbfc6cc420"
|
||||||
}
|
}
|
||||||
@@ -208,6 +208,9 @@ struct SeriesRow {
|
|||||||
poster_path: Option<String>,
|
poster_path: Option<String>,
|
||||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||||
vote_average: Option<f64>,
|
vote_average: Option<f64>,
|
||||||
|
/// NULL until a metadata refresh has stamped it (#160): the fact the
|
||||||
|
/// auto-track rule needs to tell a seeding refresh from a later one.
|
||||||
|
metadata_refreshed_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EpisodeRow {
|
struct EpisodeRow {
|
||||||
@@ -353,7 +356,7 @@ async fn tv_by_series(state: &AppState) -> Result<HashMap<i64, Vec<arr_core::Epi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiError> {
|
async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiError> {
|
||||||
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average FROM series WHERE id = ?"#, id)
|
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average, metadata_refreshed_at FROM series WHERE id = ?"#, id)
|
||||||
.fetch_optional(pool(state)?)
|
.fetch_optional(pool(state)?)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::SeriesNotFound)
|
.ok_or(ApiError::SeriesNotFound)
|
||||||
@@ -407,11 +410,11 @@ pub async fn list(
|
|||||||
Query(query): Query<ListSeriesQuery>,
|
Query(query): Query<ListSeriesQuery>,
|
||||||
) -> Result<Json<Vec<Series>>, ApiError> {
|
) -> Result<Json<Vec<Series>>, ApiError> {
|
||||||
let rows = if let Some(owner_id) = query.owner_id {
|
let rows = if let Some(owner_id) = query.owner_id {
|
||||||
sqlx::query_as!(SeriesRow, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.tvdb_id, s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.auto_track AS "auto_track!: bool", s.overrides AS "overrides!: serde_json::Value", s.upstream_ended AS "upstream_ended!: bool", s.blocked AS "blocked!: bool", poster_path, vote_average FROM series s JOIN title_owners t ON t.title_kind = 'series' AND t.title_id = s.id WHERE t.owner_id = ? ORDER BY s.title, s.year, s.id"#, owner_id)
|
sqlx::query_as!(SeriesRow, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.tvdb_id, s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.auto_track AS "auto_track!: bool", s.overrides AS "overrides!: serde_json::Value", s.upstream_ended AS "upstream_ended!: bool", s.blocked AS "blocked!: bool", poster_path, vote_average, s.metadata_refreshed_at FROM series s JOIN title_owners t ON t.title_kind = 'series' AND t.title_id = s.id WHERE t.owner_id = ? ORDER BY s.title, s.year, s.id"#, owner_id)
|
||||||
.fetch_all(pool(&state)?)
|
.fetch_all(pool(&state)?)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average FROM series ORDER BY title, year, id"#)
|
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average, metadata_refreshed_at FROM series ORDER BY title, year, id"#)
|
||||||
.fetch_all(pool(&state)?)
|
.fetch_all(pool(&state)?)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
@@ -765,6 +768,9 @@ pub async fn create_season(
|
|||||||
|
|
||||||
// §4.1. The request does not say whether the episodes are wanted; the
|
// §4.1. The request does not say whether the episodes are wanted; the
|
||||||
// series' auto_track rule does, through the one function that owns it.
|
// series' auto_track rule does, through the one function that owns it.
|
||||||
|
// #160: whether a refresh has happened yet rides in from the row — a
|
||||||
|
// hand-added season on a never-refreshed series is still seeding, and
|
||||||
|
// §4.1 does not track what was there at add time.
|
||||||
let mut revealed = [RefreshedSeason {
|
let mut revealed = [RefreshedSeason {
|
||||||
season: arr_core::Season {
|
season: arr_core::Season {
|
||||||
id: SeasonId(0),
|
id: SeasonId(0),
|
||||||
@@ -790,7 +796,11 @@ pub async fn create_season(
|
|||||||
.collect(),
|
.collect(),
|
||||||
is_new: true,
|
is_new: true,
|
||||||
}];
|
}];
|
||||||
apply_auto_track(&core_series(&series), &mut revealed);
|
apply_auto_track(
|
||||||
|
&core_series(&series),
|
||||||
|
series.metadata_refreshed_at.is_some(),
|
||||||
|
&mut revealed,
|
||||||
|
);
|
||||||
let [revealed] = revealed;
|
let [revealed] = revealed;
|
||||||
|
|
||||||
// One transaction: a rejected episode must not leave the season behind,
|
// One transaction: a rejected episode must not leave the season behind,
|
||||||
@@ -1457,15 +1467,21 @@ mod tests {
|
|||||||
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #160. A season added before the series' first metadata refresh is part
|
||||||
|
/// of the back catalogue at add time, so the rule does not track it. Once
|
||||||
|
/// a refresh has happened, a hand-revealed season is genuinely new and
|
||||||
|
/// arrives tracked with its episodes wanted.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn auto_track_decides_whether_a_new_season_arrives_wanted() {
|
async fn auto_track_decides_whether_a_new_season_arrives_wanted() {
|
||||||
let (_dir, state, base) = application().await;
|
let (_dir, state, base) = application().await;
|
||||||
let root_id = tv_root(&state, "kids").await;
|
let root_id = tv_root(&state, "kids").await;
|
||||||
|
|
||||||
let tracked = add_series(&base, root_id, true).await;
|
let tracked = add_series(&base, root_id, true).await;
|
||||||
|
let tracked_id = tracked["id"].as_i64().expect("id");
|
||||||
|
// Seeding: no refresh has ever stamped this series.
|
||||||
let season = add_season(
|
let season = add_season(
|
||||||
&base,
|
&base,
|
||||||
tracked["id"].as_i64().expect("id"),
|
tracked_id,
|
||||||
1,
|
1,
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
{"number": 1, "title": "The Magic Xylophone", "air_date": "2018-10-01"},
|
{"number": 1, "title": "The Magic Xylophone", "air_date": "2018-10-01"},
|
||||||
@@ -1473,6 +1489,33 @@ mod tests {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
assert_eq!(season["tracked"], false);
|
||||||
|
assert!(
|
||||||
|
season["episodes"]
|
||||||
|
.as_array()
|
||||||
|
.expect("episodes")
|
||||||
|
.iter()
|
||||||
|
.all(|episode| episode["wanted"] == false),
|
||||||
|
"§4.1: the back catalogue at add time is never auto-tracked"
|
||||||
|
);
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE series SET metadata_refreshed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||||
|
WHERE id = ?",
|
||||||
|
)
|
||||||
|
.bind(tracked_id)
|
||||||
|
.execute(state.database().expect("database").pool())
|
||||||
|
.await
|
||||||
|
.expect("stamp refreshed");
|
||||||
|
let season = add_season(
|
||||||
|
&base,
|
||||||
|
tracked_id,
|
||||||
|
2,
|
||||||
|
serde_json::json!([
|
||||||
|
{"number": 1, "title": "Dance Mode", "air_date": "2019-04-01"}
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
assert_eq!(season["tracked"], true);
|
assert_eq!(season["tracked"], true);
|
||||||
assert!(
|
assert!(
|
||||||
season["episodes"]
|
season["episodes"]
|
||||||
@@ -1480,7 +1523,7 @@ mod tests {
|
|||||||
.expect("episodes")
|
.expect("episodes")
|
||||||
.iter()
|
.iter()
|
||||||
.all(|episode| episode["wanted"] == true),
|
.all(|episode| episode["wanted"] == true),
|
||||||
"§4.1: auto_track marks the episodes of a revealed season wanted"
|
"§4.1: from the second refresh onward the rule tracks what is new"
|
||||||
);
|
);
|
||||||
|
|
||||||
let untracked_id = sqlx::query_scalar::<_, i64>(
|
let untracked_id = sqlx::query_scalar::<_, i64>(
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ pub struct RefreshedSeason {
|
|||||||
/// Existing seasons keep their tracking rule and leaf-level intent unchanged.
|
/// Existing seasons keep their tracking rule and leaf-level intent unchanged.
|
||||||
/// Season 0 is never tracked by the rule (`DESIGN.md` §4.1); an operator may
|
/// Season 0 is never tracked by the rule (`DESIGN.md` §4.1); an operator may
|
||||||
/// still track it by hand.
|
/// still track it by hand.
|
||||||
pub fn apply_auto_track(series: &Series, seasons: &mut [RefreshedSeason]) {
|
///
|
||||||
if !series.auto_track {
|
/// `ever_refreshed` says whether the series has had a metadata refresh yet
|
||||||
|
/// (`metadata_refreshed_at IS NOT NULL`). The caller supplies the fact because
|
||||||
|
/// `arr-core` has no IO; seasons revealed by a series' first refresh are its
|
||||||
|
/// back catalogue as it stood at add time, and §4.1 never tracks those — the
|
||||||
|
/// rule applies from the second refresh onward.
|
||||||
|
pub fn apply_auto_track(series: &Series, ever_refreshed: bool, seasons: &mut [RefreshedSeason]) {
|
||||||
|
if !series.auto_track || !ever_refreshed {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +120,7 @@ mod tests {
|
|||||||
refreshed_season(3, true, &[false, false]),
|
refreshed_season(3, true, &[false, false]),
|
||||||
];
|
];
|
||||||
|
|
||||||
apply_auto_track(&series(true), &mut refresh);
|
apply_auto_track(&series(true), true, &mut refresh);
|
||||||
|
|
||||||
assert!(!refresh[0].season.tracked);
|
assert!(!refresh[0].season.tracked);
|
||||||
assert!(refresh[0].episodes.iter().all(|episode| !episode.wanted));
|
assert!(refresh[0].episodes.iter().all(|episode| !episode.wanted));
|
||||||
@@ -122,6 +128,39 @@ mod tests {
|
|||||||
assert!(refresh[1].episodes.iter().all(|episode| episode.wanted));
|
assert!(refresh[1].episodes.iter().all(|episode| episode.wanted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #160. The first refresh of a newly added series reveals the whole back
|
||||||
|
/// catalogue at once; §4.1 never tracks those.
|
||||||
|
#[test]
|
||||||
|
fn the_seeding_refresh_tracks_nothing() {
|
||||||
|
let mut refresh = [
|
||||||
|
refreshed_season(1, true, &[false, false]),
|
||||||
|
refreshed_season(2, true, &[false]),
|
||||||
|
];
|
||||||
|
|
||||||
|
apply_auto_track(&series(true), false, &mut refresh);
|
||||||
|
|
||||||
|
assert!(refresh.iter().all(|refreshed| !refreshed.season.tracked));
|
||||||
|
assert!(refresh
|
||||||
|
.iter()
|
||||||
|
.all(|refreshed| refreshed.episodes.iter().all(|episode| !episode.wanted)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #160. From the second refresh onward a season that genuinely did not
|
||||||
|
/// exist when the series was added is tracked, and its episodes arrive
|
||||||
|
/// wanted — while the seasons already on record stay as they were.
|
||||||
|
#[test]
|
||||||
|
fn a_later_refresh_tracks_a_genuinely_new_season() {
|
||||||
|
let mut refresh = [
|
||||||
|
refreshed_season(1, false, &[true, true]),
|
||||||
|
refreshed_season(3, true, &[false]),
|
||||||
|
];
|
||||||
|
|
||||||
|
apply_auto_track(&series(true), true, &mut refresh);
|
||||||
|
|
||||||
|
assert!(refresh[1].season.tracked);
|
||||||
|
assert!(refresh[1].episodes.iter().all(|episode| episode.wanted));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn untracked_series_keeps_manual_leaf_intent_only() {
|
fn untracked_series_keeps_manual_leaf_intent_only() {
|
||||||
let mut refresh = [
|
let mut refresh = [
|
||||||
@@ -129,7 +168,7 @@ mod tests {
|
|||||||
refreshed_season(3, true, &[false, false]),
|
refreshed_season(3, true, &[false, false]),
|
||||||
];
|
];
|
||||||
|
|
||||||
apply_auto_track(&series(false), &mut refresh);
|
apply_auto_track(&series(false), true, &mut refresh);
|
||||||
|
|
||||||
assert!(refresh[0].episodes.iter().all(|episode| episode.wanted));
|
assert!(refresh[0].episodes.iter().all(|episode| episode.wanted));
|
||||||
assert!(!refresh[0].season.tracked);
|
assert!(!refresh[0].season.tracked);
|
||||||
@@ -141,7 +180,7 @@ mod tests {
|
|||||||
fn auto_track_skips_season_zero() {
|
fn auto_track_skips_season_zero() {
|
||||||
let mut refresh = [refreshed_season(0, true, &[false])];
|
let mut refresh = [refreshed_season(0, true, &[false])];
|
||||||
|
|
||||||
apply_auto_track(&series(true), &mut refresh);
|
apply_auto_track(&series(true), true, &mut refresh);
|
||||||
|
|
||||||
assert!(!refresh[0].season.tracked);
|
assert!(!refresh[0].season.tracked);
|
||||||
assert!(refresh[0].episodes.iter().all(|episode| !episode.wanted));
|
assert!(refresh[0].episodes.iter().all(|episode| !episode.wanted));
|
||||||
|
|||||||
@@ -168,7 +168,13 @@ impl SeriesRefreshAction {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apply_auto_track(&core_series(stale), &mut revealed);
|
// #160: the fact of a previous refresh rides in from the row, so the
|
||||||
|
// seeding refresh never tracks the back catalogue it reveals.
|
||||||
|
apply_auto_track(
|
||||||
|
&core_series(stale),
|
||||||
|
stale.metadata_refreshed_at.is_some(),
|
||||||
|
&mut revealed,
|
||||||
|
);
|
||||||
for refreshed in revealed {
|
for refreshed in revealed {
|
||||||
self.insert_revealed_season(&mut transaction, stale.id, refreshed)
|
self.insert_revealed_season(&mut transaction, stale.id, refreshed)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -779,8 +785,11 @@ mod tests {
|
|||||||
assert_eq!(vote, Some(8.417));
|
assert_eq!(vote, Some(8.417));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #160. A series' first refresh reveals its back catalogue, but §4.1
|
||||||
|
/// never tracks what was already there at add time: nothing is tracked,
|
||||||
|
/// nothing arrives wanted.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn first_refresh_reveals_seasons_and_applies_auto_track() {
|
async fn the_seeding_refresh_reveals_but_tracks_nothing() {
|
||||||
let (_dir, database) = seeded_series(true).await;
|
let (_dir, database) = seeded_series(true).await;
|
||||||
let server = tmdb("Returning Series", season_one_body(&two_episodes())).await;
|
let server = tmdb("Returning Series", season_one_body(&two_episodes())).await;
|
||||||
|
|
||||||
@@ -792,14 +801,14 @@ mod tests {
|
|||||||
.fetch_all(database.pool())
|
.fetch_all(database.pool())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(rows, vec![(1, true), (2, true)]);
|
assert_eq!(rows, vec![(1, false), (2, false)]);
|
||||||
let wanted: i64 = sqlx::query_scalar(
|
let wanted: i64 = sqlx::query_scalar(
|
||||||
"SELECT count(*) FROM episodes WHERE wanted = 1 AND state = 'missing'",
|
"SELECT count(*) FROM episodes WHERE wanted = 1 AND state = 'missing'",
|
||||||
)
|
)
|
||||||
.fetch_one(database.pool())
|
.fetch_one(database.pool())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(wanted, 3);
|
assert_eq!(wanted, 0);
|
||||||
// #120 backfill: the series was added before it had an id on file.
|
// #120 backfill: the series was added before it had an id on file.
|
||||||
let tvdb_id: Option<i64> =
|
let tvdb_id: Option<i64> =
|
||||||
sqlx::query_scalar("SELECT tvdb_id FROM series WHERE tmdb_id = 82728")
|
sqlx::query_scalar("SELECT tvdb_id FROM series WHERE tmdb_id = 82728")
|
||||||
@@ -816,6 +825,50 @@ mod tests {
|
|||||||
assert_eq!(stamped, 1);
|
assert_eq!(stamped, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #160. Once the series has been refreshed before, a season TMDB reveals
|
||||||
|
/// that the library has never seen genuinely appeared after add time, so
|
||||||
|
/// it is tracked and its episodes arrive wanted.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_later_refresh_tracks_a_genuinely_new_season() {
|
||||||
|
let (_dir, database) = seeded_series(true).await;
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
mount_with(&server, "Returning Series", &[1]).await;
|
||||||
|
|
||||||
|
action(&server).tick(&database).await.unwrap();
|
||||||
|
let tracked: i64 = sqlx::query_scalar("SELECT count(*) FROM seasons WHERE tracked = 1")
|
||||||
|
.fetch_one(database.pool())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(tracked, 0, "the seeding refresh tracks nothing");
|
||||||
|
|
||||||
|
expire_refresh(&database).await;
|
||||||
|
server.reset().await;
|
||||||
|
mount(
|
||||||
|
&server,
|
||||||
|
"Returning Series",
|
||||||
|
season_one_body(&two_episodes()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let outcomes = action(&server).tick(&database).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(outcomes.len(), 1);
|
||||||
|
let rows: Vec<(i64, bool)> =
|
||||||
|
sqlx::query_as("SELECT number, tracked FROM seasons ORDER BY number")
|
||||||
|
.fetch_all(database.pool())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(rows, vec![(1, false), (2, true)]);
|
||||||
|
let wanted: Vec<i64> = sqlx::query_scalar(
|
||||||
|
"SELECT e.number FROM episodes e JOIN seasons s ON s.id = e.season_id
|
||||||
|
WHERE s.number = 2 AND e.wanted = 1",
|
||||||
|
)
|
||||||
|
.fetch_all(database.pool())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(wanted, vec![1]);
|
||||||
|
}
|
||||||
|
|
||||||
/// `auto_track` is a rule about reveals, not intent: without it nothing
|
/// `auto_track` is a rule about reveals, not intent: without it nothing
|
||||||
/// arrives wanted, even though everything is revealed.
|
/// arrives wanted, even though everything is revealed.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -877,13 +930,13 @@ mod tests {
|
|||||||
.fetch_all(database.pool())
|
.fetch_all(database.pool())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(rows, vec![(1, true), (2, true)]);
|
assert_eq!(rows, vec![(1, false), (2, false)]);
|
||||||
let counts: (i64, i64) =
|
let counts: (i64, i64) =
|
||||||
sqlx::query_as("SELECT count(*), coalesce(sum(wanted), 0) FROM episodes")
|
sqlx::query_as("SELECT count(*), coalesce(sum(wanted), 0) FROM episodes")
|
||||||
.fetch_one(database.pool())
|
.fetch_one(database.pool())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(counts, (3, 3));
|
assert_eq!(counts, (3, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Episodes revealed into an existing season follow that season's rule:
|
/// Episodes revealed into an existing season follow that season's rule:
|
||||||
|
|||||||
Reference in New Issue
Block a user