Merge #131: derive status from the episode's own season number
Closes #131
This commit is contained in:
+20
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?",
|
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", se.number AS \"season_number!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -19,43 +19,48 @@
|
|||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "number!: i64",
|
"name": "season_number!: i64",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "title!: String",
|
"name": "number!: i64",
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"type_info": "Text"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "air_date",
|
"name": "title!: String",
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "wanted!: bool",
|
"name": "air_date",
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wanted!: bool",
|
||||||
|
"ordinal": 7,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "state!: String",
|
"name": "state!: String",
|
||||||
"ordinal": 7,
|
"ordinal": 8,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vanished!: bool",
|
"name": "vanished!: bool",
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search_attempts!: i64",
|
|
||||||
"ordinal": 9,
|
"ordinal": 9,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "last_searched_at",
|
"name": "search_attempts!: i64",
|
||||||
"ordinal": 10,
|
"ordinal": 10,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_searched_at",
|
||||||
|
"ordinal": 11,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -68,6 +73,7 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -76,5 +82,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "633d5ff860d9c677eae5d06324d3098a0bbad30b46b23287131de72e64751256"
|
"hash": "3c4fdf30427dc972e695940a7a98cbc7c1da617502c818e5dbe0f35c87b175d5"
|
||||||
}
|
}
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT id AS \"id!: i64\", series_id AS \"series_id!: i64\", number AS \"number!: i64\" FROM seasons",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id!: i64",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "series_id!: i64",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "number!: i64",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Integer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "3e3a42524f3e481eef0b896c20aac3cf11d578ee864e36412a6f70477fff8325"
|
|
||||||
}
|
|
||||||
+20
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number",
|
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", se.number AS \"season_number!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -19,43 +19,48 @@
|
|||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "number!: i64",
|
"name": "season_number!: i64",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "title!: String",
|
"name": "number!: i64",
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"type_info": "Text"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "air_date",
|
"name": "title!: String",
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "wanted!: bool",
|
"name": "air_date",
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wanted!: bool",
|
||||||
|
"ordinal": 7,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "state!: String",
|
"name": "state!: String",
|
||||||
"ordinal": 7,
|
"ordinal": 8,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vanished!: bool",
|
"name": "vanished!: bool",
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search_attempts!: i64",
|
|
||||||
"ordinal": 9,
|
"ordinal": 9,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "last_searched_at",
|
"name": "search_attempts!: i64",
|
||||||
"ordinal": 10,
|
"ordinal": 10,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_searched_at",
|
||||||
|
"ordinal": 11,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -68,6 +73,7 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -76,5 +82,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "3ab4e736a4fc73625fd66e38dc3f510193b9c97788503f06c5c23f859780349a"
|
"hash": "3ee558a18edee423eab6fe572f2a6f5bad8db69e0827e620264700f4affb3652"
|
||||||
}
|
}
|
||||||
+20
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id",
|
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", se.number AS \"season_number!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -19,43 +19,48 @@
|
|||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "number!: i64",
|
"name": "season_number!: i64",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "title!: String",
|
"name": "number!: i64",
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"type_info": "Text"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "air_date",
|
"name": "title!: String",
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "wanted!: bool",
|
"name": "air_date",
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wanted!: bool",
|
||||||
|
"ordinal": 7,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "state!: String",
|
"name": "state!: String",
|
||||||
"ordinal": 7,
|
"ordinal": 8,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vanished!: bool",
|
"name": "vanished!: bool",
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search_attempts!: i64",
|
|
||||||
"ordinal": 9,
|
"ordinal": 9,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "last_searched_at",
|
"name": "search_attempts!: i64",
|
||||||
"ordinal": 10,
|
"ordinal": 10,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_searched_at",
|
||||||
|
"ordinal": 11,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -68,6 +73,7 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -76,5 +82,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "c77c9cac0129fb4da57bef8ef2fb149c57293c76bad04f01603f11635789222d"
|
"hash": "6bda2cef202e94b64f7bbcf88bbed823cca790c1439d94fba1c7dcea024d9467"
|
||||||
}
|
}
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT id AS \"id!: i64\", series_id AS \"series_id!: i64\", number AS \"number!: i64\" FROM seasons WHERE series_id = ?",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id!: i64",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "series_id!: i64",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "number!: i64",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Integer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "808c20cad25bdb0d81ac502379dcf72c77bb581bc0fdff7b2728cfa13712eb10"
|
|
||||||
}
|
|
||||||
+20
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?",
|
"query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", se.number AS \"season_number!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -19,43 +19,48 @@
|
|||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "number!: i64",
|
"name": "season_number!: i64",
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "title!: String",
|
"name": "number!: i64",
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"type_info": "Text"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "air_date",
|
"name": "title!: String",
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "wanted!: bool",
|
"name": "air_date",
|
||||||
"ordinal": 6,
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wanted!: bool",
|
||||||
|
"ordinal": 7,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "state!: String",
|
"name": "state!: String",
|
||||||
"ordinal": 7,
|
"ordinal": 8,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vanished!: bool",
|
"name": "vanished!: bool",
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search_attempts!: i64",
|
|
||||||
"ordinal": 9,
|
"ordinal": 9,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "last_searched_at",
|
"name": "search_attempts!: i64",
|
||||||
"ordinal": 10,
|
"ordinal": 10,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_searched_at",
|
||||||
|
"ordinal": 11,
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -68,6 +73,7 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -76,5 +82,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "448bdb6b59938fe6c8221092fd30c0e9b278dd4df85288aa165753c0efb1d2a8"
|
"hash": "e6f5f116e453756eb57d58da32dd32a53984f9884b179346b1faf07032b08187"
|
||||||
}
|
}
|
||||||
@@ -196,6 +196,7 @@ struct EpisodeRow {
|
|||||||
series_id: i64,
|
series_id: i64,
|
||||||
id: i64,
|
id: i64,
|
||||||
season_id: i64,
|
season_id: i64,
|
||||||
|
season_number: i64,
|
||||||
number: i64,
|
number: i64,
|
||||||
title: String,
|
title: String,
|
||||||
air_date: Option<String>,
|
air_date: Option<String>,
|
||||||
@@ -257,6 +258,7 @@ fn core_episode(row: &EpisodeRow) -> arr_core::Episode {
|
|||||||
arr_core::Episode {
|
arr_core::Episode {
|
||||||
id: EpisodeId(row.id),
|
id: EpisodeId(row.id),
|
||||||
season_id: SeasonId(row.season_id),
|
season_id: SeasonId(row.season_id),
|
||||||
|
season_number: u16::try_from(row.season_number).unwrap_or_default(),
|
||||||
number: u16::try_from(row.number).unwrap_or_default(),
|
number: u16::try_from(row.number).unwrap_or_default(),
|
||||||
title: row.title.clone(),
|
title: row.title.clone(),
|
||||||
air_date: air_date(row.air_date.as_deref()),
|
air_date: air_date(row.air_date.as_deref()),
|
||||||
@@ -277,31 +279,14 @@ fn status_name(status: SeriesStatus) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn core_season(id: i64, series_id: i64, number: i64) -> arr_core::Season {
|
fn with_status(row: &SeriesRow, episodes: &[arr_core::Episode], now: SystemTime) -> Series {
|
||||||
arr_core::Season {
|
|
||||||
id: SeasonId(id),
|
|
||||||
series_id: SeriesId(series_id),
|
|
||||||
number: u16::try_from(number).unwrap_or_default(),
|
|
||||||
tracked: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_status(
|
|
||||||
row: &SeriesRow,
|
|
||||||
seasons: &[arr_core::Season],
|
|
||||||
episodes: &[arr_core::Episode],
|
|
||||||
now: SystemTime,
|
|
||||||
) -> Series {
|
|
||||||
// §4.2: season 0 is invisible to status, so it stays out of the counters
|
// §4.2: season 0 is invisible to status, so it stays out of the counters
|
||||||
// too. A series reading `complete` next to `42/52 eps` is the confusion
|
// too. A series reading `complete` next to `42/52 eps` is the confusion
|
||||||
// this avoids. `UNIQUE (series_id, number)` means there is at most one.
|
// this avoids. The season number rides on each episode (#131), so no
|
||||||
let specials = seasons
|
// parallel slice can be forgotten.
|
||||||
.iter()
|
|
||||||
.find(|season| season.number == 0)
|
|
||||||
.map(|season| season.id);
|
|
||||||
let wanted = episodes
|
let wanted = episodes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|episode| episode.wanted && specials != Some(episode.season_id));
|
.filter(|episode| episode.wanted && episode.season_number != 0);
|
||||||
let available = wanted
|
let available = wanted
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|episode| episode.state == MediaState::Available);
|
.filter(|episode| episode.state == MediaState::Available);
|
||||||
@@ -317,53 +302,26 @@ fn with_status(
|
|||||||
overrides: row.overrides.clone(),
|
overrides: row.overrides.clone(),
|
||||||
upstream_ended: row.upstream_ended,
|
upstream_ended: row.upstream_ended,
|
||||||
blocked: row.blocked,
|
blocked: row.blocked,
|
||||||
status: status_name(derive_series_status(
|
status: status_name(derive_series_status(&core_series(row), episodes, now)).to_owned(),
|
||||||
&core_series(row),
|
|
||||||
seasons,
|
|
||||||
episodes,
|
|
||||||
now,
|
|
||||||
))
|
|
||||||
.to_owned(),
|
|
||||||
wanted_episodes: i64::try_from(wanted.count()).unwrap_or(i64::MAX),
|
wanted_episodes: i64::try_from(wanted.count()).unwrap_or(i64::MAX),
|
||||||
available_episodes: i64::try_from(available.count()).unwrap_or(i64::MAX),
|
available_episodes: i64::try_from(available.count()).unwrap_or(i64::MAX),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every season and episode in the library, keyed by the series they belong to.
|
/// Every episode in the library, keyed by the series it belongs to.
|
||||||
///
|
///
|
||||||
/// One query each rather than one per series: the whole table is a few thousand
|
/// One query rather than one per series: the whole table is a few thousand
|
||||||
/// rows for a single household (§10), and the status of every listed series
|
/// rows for a single household (§10), and the status of every listed series
|
||||||
/// needs all of them anyway.
|
/// needs all of them anyway.
|
||||||
async fn tv_by_series(
|
async fn tv_by_series(state: &AppState) -> Result<HashMap<i64, Vec<arr_core::Episode>>, ApiError> {
|
||||||
state: &AppState,
|
|
||||||
) -> Result<
|
|
||||||
(
|
|
||||||
HashMap<i64, Vec<arr_core::Season>>,
|
|
||||||
HashMap<i64, Vec<arr_core::Episode>>,
|
|
||||||
),
|
|
||||||
ApiError,
|
|
||||||
> {
|
|
||||||
let season_rows = sqlx::query!(
|
|
||||||
r#"SELECT id AS "id!: i64", series_id AS "series_id!: i64", number AS "number!: i64" FROM seasons"#
|
|
||||||
)
|
|
||||||
.fetch_all(pool(state)?)
|
|
||||||
.await?;
|
|
||||||
let rows = sqlx::query_as!(
|
let rows = sqlx::query_as!(
|
||||||
EpisodeRow,
|
EpisodeRow,
|
||||||
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", se.number AS "season_number!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
||||||
FROM episodes e JOIN seasons se ON se.id = e.season_id"#
|
FROM episodes e JOIN seasons se ON se.id = e.season_id"#
|
||||||
)
|
)
|
||||||
.fetch_all(pool(state)?)
|
.fetch_all(pool(state)?)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut seasons: HashMap<i64, Vec<arr_core::Season>> = HashMap::new();
|
|
||||||
for row in &season_rows {
|
|
||||||
seasons.entry(row.series_id).or_default().push(core_season(
|
|
||||||
row.id,
|
|
||||||
row.series_id,
|
|
||||||
row.number,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut episodes: HashMap<i64, Vec<arr_core::Episode>> = HashMap::new();
|
let mut episodes: HashMap<i64, Vec<arr_core::Episode>> = HashMap::new();
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
episodes
|
episodes
|
||||||
@@ -371,7 +329,7 @@ async fn tv_by_series(
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(core_episode(row));
|
.push(core_episode(row));
|
||||||
}
|
}
|
||||||
Ok((seasons, episodes))
|
Ok(episodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiError> {
|
async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiError> {
|
||||||
@@ -383,26 +341,16 @@ async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiErro
|
|||||||
|
|
||||||
async fn load_series(state: &AppState, id: i64) -> Result<Series, ApiError> {
|
async fn load_series(state: &AppState, id: i64) -> Result<Series, ApiError> {
|
||||||
let row = load_series_row(state, id).await?;
|
let row = load_series_row(state, id).await?;
|
||||||
let season_rows = sqlx::query!(
|
|
||||||
r#"SELECT id AS "id!: i64", series_id AS "series_id!: i64", number AS "number!: i64" FROM seasons WHERE series_id = ?"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_all(pool(state)?)
|
|
||||||
.await?;
|
|
||||||
let seasons: Vec<_> = season_rows
|
|
||||||
.iter()
|
|
||||||
.map(|row| core_season(row.id, row.series_id, row.number))
|
|
||||||
.collect();
|
|
||||||
let episodes = sqlx::query_as!(
|
let episodes = sqlx::query_as!(
|
||||||
EpisodeRow,
|
EpisodeRow,
|
||||||
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", se.number AS "season_number!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
||||||
FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?"#,
|
FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.fetch_all(pool(state)?)
|
.fetch_all(pool(state)?)
|
||||||
.await?;
|
.await?;
|
||||||
let episodes: Vec<_> = episodes.iter().map(core_episode).collect();
|
let episodes: Vec<_> = episodes.iter().map(core_episode).collect();
|
||||||
Ok(with_status(&row, &seasons, &episodes, SystemTime::now()))
|
Ok(with_status(&row, &episodes, SystemTime::now()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn require_tv_root(state: &AppState, root_id: i64) -> Result<(), ApiError> {
|
async fn require_tv_root(state: &AppState, root_id: i64) -> Result<(), ApiError> {
|
||||||
@@ -448,20 +396,12 @@ pub async fn list(
|
|||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let (seasons, episodes) = tv_by_series(&state).await?;
|
let episodes = tv_by_series(&state).await?;
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
let no_seasons: Vec<arr_core::Season> = Vec::new();
|
|
||||||
let no_episodes: Vec<arr_core::Episode> = Vec::new();
|
let no_episodes: Vec<arr_core::Episode> = Vec::new();
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
rows.iter()
|
rows.iter()
|
||||||
.map(|row| {
|
.map(|row| with_status(row, episodes.get(&row.id).unwrap_or(&no_episodes), now))
|
||||||
with_status(
|
|
||||||
row,
|
|
||||||
seasons.get(&row.id).unwrap_or(&no_seasons),
|
|
||||||
episodes.get(&row.id).unwrap_or(&no_episodes),
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -607,7 +547,7 @@ async fn load_seasons(state: &AppState, series_id: i64) -> Result<Vec<Season>, A
|
|||||||
.await?;
|
.await?;
|
||||||
let episodes = sqlx::query_as!(
|
let episodes = sqlx::query_as!(
|
||||||
EpisodeRow,
|
EpisodeRow,
|
||||||
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", se.number AS "season_number!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
||||||
FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number"#,
|
FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number"#,
|
||||||
series_id
|
series_id
|
||||||
)
|
)
|
||||||
@@ -714,6 +654,7 @@ pub async fn create_season(
|
|||||||
.map(|episode| arr_core::Episode {
|
.map(|episode| arr_core::Episode {
|
||||||
id: EpisodeId(0),
|
id: EpisodeId(0),
|
||||||
season_id: SeasonId(0),
|
season_id: SeasonId(0),
|
||||||
|
season_number: u16::try_from(input.number).unwrap_or_default(),
|
||||||
number: u16::try_from(episode.number).unwrap_or_default(),
|
number: u16::try_from(episode.number).unwrap_or_default(),
|
||||||
title: episode.title.clone(),
|
title: episode.title.clone(),
|
||||||
air_date: air_date(episode.air_date.as_deref()),
|
air_date: air_date(episode.air_date.as_deref()),
|
||||||
@@ -837,7 +778,7 @@ pub async fn update_season(
|
|||||||
async fn load_episode(state: &AppState, id: i64) -> Result<Episode, ApiError> {
|
async fn load_episode(state: &AppState, id: i64) -> Result<Episode, ApiError> {
|
||||||
let row = sqlx::query_as!(
|
let row = sqlx::query_as!(
|
||||||
EpisodeRow,
|
EpisodeRow,
|
||||||
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", se.number AS "season_number!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at
|
||||||
FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?"#,
|
FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?"#,
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -288,6 +288,9 @@ pub struct Season {
|
|||||||
pub struct Episode {
|
pub struct Episode {
|
||||||
pub id: EpisodeId,
|
pub id: EpisodeId,
|
||||||
pub season_id: SeasonId,
|
pub season_id: SeasonId,
|
||||||
|
/// The owning season's number, carried so status derivation never needs
|
||||||
|
/// a second slice a caller can forget (#131).
|
||||||
|
pub season_number: u16,
|
||||||
pub number: u16,
|
pub number: u16,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub air_date: Option<SystemTime>,
|
pub air_date: Option<SystemTime>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use crate::{Episode, MediaState, Season, Series};
|
use crate::{Episode, MediaState, Series};
|
||||||
|
|
||||||
const AIRING_WINDOW: Duration = Duration::from_hours(14 * 24);
|
const AIRING_WINDOW: Duration = Duration::from_hours(14 * 24);
|
||||||
|
|
||||||
@@ -16,26 +16,19 @@ pub enum SeriesStatus {
|
|||||||
|
|
||||||
/// Derives a series' status at `now` without persisting lifecycle intent.
|
/// Derives a series' status at `now` without persisting lifecycle intent.
|
||||||
///
|
///
|
||||||
/// Season 0 is invisible to all of it (`DESIGN.md` §4.2): episodes in a
|
/// Season 0 is invisible to all of it (`DESIGN.md` §4.2): episodes with
|
||||||
/// season 0 listed here are ignored, so a manually wanted special cannot pin
|
/// `season_number` 0 are ignored, so a manually wanted special cannot pin
|
||||||
/// the series at `incomplete` or hold back `ended`. The seasons must be the
|
/// the series at `incomplete` or hold back `ended`. The season number rides
|
||||||
/// series' own; an episode whose season is not listed counts like any other.
|
/// on each episode, so there is no second slice a caller can forget (#131).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn derive_series_status(
|
pub fn derive_series_status(
|
||||||
series: &Series,
|
series: &Series,
|
||||||
seasons: &[Season],
|
|
||||||
episodes: &[Episode],
|
episodes: &[Episode],
|
||||||
now: SystemTime,
|
now: SystemTime,
|
||||||
) -> SeriesStatus {
|
) -> SeriesStatus {
|
||||||
// A series has at most one season 0, so this is a comparison rather than
|
|
||||||
// a set: no allocation on a function the list view calls per series.
|
|
||||||
let specials = seasons
|
|
||||||
.iter()
|
|
||||||
.find(|season| season.number == 0)
|
|
||||||
.map(|season| season.id);
|
|
||||||
let wanted = episodes
|
let wanted = episodes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|episode| episode.wanted && specials != Some(episode.season_id));
|
.filter(|episode| episode.wanted && episode.season_number != 0);
|
||||||
let complete = wanted
|
let complete = wanted
|
||||||
.clone()
|
.clone()
|
||||||
.all(|episode| episode.state == MediaState::Available);
|
.all(|episode| episode.state == MediaState::Available);
|
||||||
@@ -107,6 +100,7 @@ mod tests {
|
|||||||
Episode {
|
Episode {
|
||||||
id: EpisodeId(i64::from(number)),
|
id: EpisodeId(i64::from(number)),
|
||||||
season_id: SeasonId(1),
|
season_id: SeasonId(1),
|
||||||
|
season_number: 1,
|
||||||
number,
|
number,
|
||||||
title: format!("Episode {number}"),
|
title: format!("Episode {number}"),
|
||||||
air_date,
|
air_date,
|
||||||
@@ -117,23 +111,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The season every default-test episode sits in.
|
|
||||||
fn seasons() -> [Season; 1] {
|
|
||||||
[Season {
|
|
||||||
id: SeasonId(1),
|
|
||||||
series_id: SeriesId(1),
|
|
||||||
number: 1,
|
|
||||||
tracked: true,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn airing_when_a_wanted_episode_is_in_the_window() {
|
fn airing_when_a_wanted_episode_is_in_the_window() {
|
||||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||||
for air_date in [now - 14 * DAY, now + 14 * DAY] {
|
for air_date in [now - 14 * DAY, now + 14 * DAY] {
|
||||||
let episodes = [episode(1, Some(air_date), MediaState::Missing)];
|
let episodes = [episode(1, Some(air_date), MediaState::Missing)];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Airing
|
SeriesStatus::Airing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -145,7 +129,7 @@ mod tests {
|
|||||||
MediaState::Missing,
|
MediaState::Missing,
|
||||||
)];
|
)];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Waiting
|
SeriesStatus::Waiting
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -156,7 +140,7 @@ mod tests {
|
|||||||
let episodes = [episode(1, Some(now - 20 * DAY), MediaState::Missing)];
|
let episodes = [episode(1, Some(now - 20 * DAY), MediaState::Missing)];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Incomplete
|
SeriesStatus::Incomplete
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -167,7 +151,7 @@ mod tests {
|
|||||||
let episodes = [episode(1, Some(now + 30 * DAY), MediaState::Missing)];
|
let episodes = [episode(1, Some(now + 30 * DAY), MediaState::Missing)];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Waiting
|
SeriesStatus::Waiting
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -178,7 +162,7 @@ mod tests {
|
|||||||
let episodes = [episode(1, Some(now), MediaState::Available)];
|
let episodes = [episode(1, Some(now), MediaState::Available)];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(false, false), &seasons(), &episodes, now),
|
derive_series_status(&series(false, false), &episodes, now),
|
||||||
SeriesStatus::Complete
|
SeriesStatus::Complete
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -189,7 +173,7 @@ mod tests {
|
|||||||
let episodes = [episode(1, Some(now), MediaState::Available)];
|
let episodes = [episode(1, Some(now), MediaState::Available)];
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, true), &seasons(), &episodes, now),
|
derive_series_status(&series(true, true), &episodes, now),
|
||||||
SeriesStatus::Ended
|
SeriesStatus::Ended
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -199,14 +183,14 @@ mod tests {
|
|||||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||||
let mut episodes = [episode(1, Some(now - 20 * DAY), MediaState::Missing)];
|
let mut episodes = [episode(1, Some(now - 20 * DAY), MediaState::Missing)];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(false, false), &seasons(), &episodes, now),
|
derive_series_status(&series(false, false), &episodes, now),
|
||||||
SeriesStatus::Incomplete
|
SeriesStatus::Incomplete
|
||||||
);
|
);
|
||||||
|
|
||||||
episodes[0].state = MediaState::Available;
|
episodes[0].state = MediaState::Available;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(false, false), &seasons(), &episodes, now),
|
derive_series_status(&series(false, false), &episodes, now),
|
||||||
SeriesStatus::Complete
|
SeriesStatus::Complete
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,14 +200,14 @@ mod tests {
|
|||||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||||
let mut episodes = vec![episode(1, Some(now - 30 * DAY), MediaState::Available)];
|
let mut episodes = vec![episode(1, Some(now - 30 * DAY), MediaState::Available)];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Waiting
|
SeriesStatus::Waiting
|
||||||
);
|
);
|
||||||
|
|
||||||
episodes.push(episode(2, Some(now + 7 * DAY), MediaState::Missing));
|
episodes.push(episode(2, Some(now + 7 * DAY), MediaState::Missing));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Airing
|
SeriesStatus::Airing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -231,34 +215,30 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn missing_specials_do_not_hold_back_complete_or_ended() {
|
fn missing_specials_do_not_hold_back_complete_or_ended() {
|
||||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||||
let specials = [Season {
|
|
||||||
id: SeasonId(9),
|
|
||||||
series_id: SeriesId(1),
|
|
||||||
number: 0,
|
|
||||||
tracked: false,
|
|
||||||
}];
|
|
||||||
let mut episodes = vec![episode(1, Some(now - 30 * DAY), MediaState::Available)];
|
let mut episodes = vec![episode(1, Some(now - 30 * DAY), MediaState::Available)];
|
||||||
let mut special = episode(1, Some(now - 90 * DAY), MediaState::Missing);
|
let mut special = episode(1, Some(now - 90 * DAY), MediaState::Missing);
|
||||||
special.season_id = SeasonId(9);
|
special.season_id = SeasonId(9);
|
||||||
|
special.season_number = 0;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &specials, &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Waiting
|
SeriesStatus::Waiting
|
||||||
);
|
);
|
||||||
|
|
||||||
episodes.push(special);
|
episodes.push(special);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(true, false), &[], &episodes, now),
|
derive_series_status(&series(true, false), &episodes, now),
|
||||||
SeriesStatus::Incomplete,
|
SeriesStatus::Waiting,
|
||||||
"without knowing the seasons, the special pins the series"
|
"#131: the season number rides on the episode, so the special is \
|
||||||
|
invisible however the caller builds the slice"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(false, false), &specials, &episodes, now),
|
derive_series_status(&series(false, false), &episodes, now),
|
||||||
SeriesStatus::Complete,
|
SeriesStatus::Complete,
|
||||||
"§4.2: the only gap is a special"
|
"§4.2: the only gap is a special"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
derive_series_status(&series(false, true), &specials, &episodes, now),
|
derive_series_status(&series(false, true), &episodes, now),
|
||||||
SeriesStatus::Ended
|
SeriesStatus::Ended
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ mod tests {
|
|||||||
Episode {
|
Episode {
|
||||||
id: EpisodeId(season * 100 + i64::from(number)),
|
id: EpisodeId(season * 100 + i64::from(number)),
|
||||||
season_id: SeasonId(season),
|
season_id: SeasonId(season),
|
||||||
|
season_number: u16::try_from(season).unwrap_or_default(),
|
||||||
number,
|
number,
|
||||||
title: format!("Episode {number}"),
|
title: format!("Episode {number}"),
|
||||||
air_date: Some(SystemTime::UNIX_EPOCH),
|
air_date: Some(SystemTime::UNIX_EPOCH),
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ impl SeriesRefreshAction {
|
|||||||
|
|
||||||
let mut fresh = Vec::new();
|
let mut fresh = Vec::new();
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
let season = season_number(number);
|
||||||
for source in &detail.episodes {
|
for source in &detail.episodes {
|
||||||
let number = i64::from(source.number);
|
let number = i64::from(source.number);
|
||||||
if let Some(&(id, ref title, ref air_date, vanished)) = known.get(&number) {
|
if let Some(&(id, ref title, ref air_date, vanished)) = known.get(&number) {
|
||||||
@@ -311,6 +312,7 @@ impl SeriesRefreshAction {
|
|||||||
fresh.push(CoreEpisode {
|
fresh.push(CoreEpisode {
|
||||||
id: EpisodeId(0),
|
id: EpisodeId(0),
|
||||||
season_id: SeasonId(season_id),
|
season_id: SeasonId(season_id),
|
||||||
|
season_number: season,
|
||||||
number: season_number(source.number),
|
number: season_number(source.number),
|
||||||
title: source.title.clone(),
|
title: source.title.clone(),
|
||||||
air_date: source.air_date.map(system_time),
|
air_date: source.air_date.map(system_time),
|
||||||
@@ -449,6 +451,7 @@ fn core_episodes(series_id: i64, detail: &TmdbSeasonDetail) -> Vec<CoreEpisode>
|
|||||||
.map(|source| CoreEpisode {
|
.map(|source| CoreEpisode {
|
||||||
id: EpisodeId(0),
|
id: EpisodeId(0),
|
||||||
season_id: SeasonId(series_id),
|
season_id: SeasonId(series_id),
|
||||||
|
season_number: season_number(detail.number),
|
||||||
number: season_number(source.number),
|
number: season_number(source.number),
|
||||||
title: source.title.clone(),
|
title: source.title.clone(),
|
||||||
air_date: source.air_date.map(system_time),
|
air_date: source.air_date.map(system_time),
|
||||||
|
|||||||
Reference in New Issue
Block a user