feat(api): carry poster and rating through search and lists
Widen the unified search so both §9.2 halves can render a thumbnail and a rating: TMDB results gain vote_average/vote_count from the search bodies, library results read the #145 artwork columns, and episodes take their series' art. GET /api/movies and /api/series gain the same two fields for the #151 grid. Mirrors in web updated.
This commit is contained in:
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE tmdb_id = ?",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE tmdb_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6a25900e3815470be997f96bfee10f2079242e8d755da6387dbb8a0f40fe6ea1"
|
||||
"hash": "16778fe075b61babcb7f5436113e91bf5f86e9a968d7b61f12daa022cc27f4d1"
|
||||
}
|
||||
+16
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.blocked AS \"blocked!: bool\" FROM series s WHERE s.tmdb_id = ?",
|
||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.blocked AS \"blocked!: bool\", s.poster_path, s.vote_average FROM series s WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY s.title, s.year, s.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -37,20 +37,32 @@
|
||||
"name": "blocked!: bool",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ccf61e6cd96c0d048e054fdfa06af5a2551a87ccb172c02795e75129e35bfc63"
|
||||
"hash": "268dc95276f57bf3c457c98d172ef5e6f9acf430ae419ca68dc3800b0390b5b7"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY title, year, id",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7b6b0d6bbfcd752bd3a9d97f8d64529bd7aab96ad817fdb9ac9829a525a9f1d7"
|
||||
"hash": "53a2b5bcc6a753b8ef0a5f4110a69cc8ece28966cfa003f4bfe1190c55831a23"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT m.id AS \"id!: i64\", m.tmdb_id AS \"tmdb_id!: i64\", m.title AS \"title!: String\", m.year, m.original_language, m.root_id AS \"root_id!: i64\", m.wanted AS \"wanted!: bool\", m.overrides AS \"overrides!: serde_json::Value\", m.state AS \"state!: String\", m.blocked AS \"blocked!: bool\", m.search_attempts AS \"search_attempts!: i64\", m.last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = m.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies m JOIN title_owners t ON t.title_kind = 'movie' AND t.title_id = m.id WHERE t.owner_id = ? ORDER BY m.title, m.year, m.id",
|
||||
"query": "SELECT m.id AS \"id!: i64\", m.tmdb_id AS \"tmdb_id!: i64\", m.title AS \"title!: String\", m.year, m.original_language, m.root_id AS \"root_id!: i64\", m.wanted AS \"wanted!: bool\", m.overrides AS \"overrides!: serde_json::Value\", m.state AS \"state!: String\", m.blocked AS \"blocked!: bool\", m.search_attempts AS \"search_attempts!: i64\", m.last_searched_at, m.poster_path, m.vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = m.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies m JOIN title_owners t ON t.title_kind = 'movie' AND t.title_id = m.id WHERE t.owner_id = ? ORDER BY m.title, m.year, m.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5fc9aa28b8329895c03c7be43024069d8a065ddd37ea77bb3058aa8ec1cab52e"
|
||||
"hash": "6394f95e9e437ed3c46bb325c2d946ce75f94e00bf0d3aa6ac54811080725cc2"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.blocked AS \"blocked!: bool\" FROM series s WHERE s.tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY s.title, s.year, s.id",
|
||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.blocked AS \"blocked!: bool\", s.poster_path, s.vote_average FROM series s WHERE s.tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY s.title, s.year, s.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -37,6 +37,16 @@
|
||||
"name": "blocked!: bool",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -49,8 +59,10 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9fa86070879694b17a8b42e0717f70bd071b8169e6469f0d568754fc6b26c863"
|
||||
"hash": "6f41ffaa0679bbbe51ea5d54fbdbf757e0f85d1d86ee6322253f223fae49265f"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT e.id AS \"episode_id!: i64\", s.id AS \"series_id!: i64\", s.title AS \"series_title!: String\", printf('S%02dE%02d', se.number, e.number) AS \"tag!: String\", e.title AS \"title!: String\" FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN series s ON s.id = se.series_id WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE (s.title || ' ' || e.title) NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY s.title, se.number, e.number, e.id",
|
||||
"query": "SELECT e.id AS \"episode_id!: i64\", s.id AS \"series_id!: i64\", s.title AS \"series_title!: String\", printf('S%02dE%02d', se.number, e.number) AS \"tag!: String\", e.title AS \"title!: String\", s.poster_path, s.vote_average FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN series s ON s.id = se.series_id WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE (s.title || ' ' || e.title) NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY s.title, se.number, e.number, e.id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -27,6 +27,16 @@
|
||||
"name": "title!: String",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 6,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -37,8 +47,10 @@
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b4074ba8e74383de4a351f1fceae938fa952b522db9798039339b01e284636c2"
|
||||
"hash": "833f1514dad9e77075cd8c1b38d483de6a9ba5be46a8e7d89a05fe33f3f20044"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE (SELECT count(DISTINCT g.release_id) FROM grabs g WHERE g.target_kind = 'movie' AND g.target_id = movies.id AND g.state = 'failed') >= 2 ORDER BY title",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies ORDER BY title, year, id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "af1b98d2aa84eac69323823e232b61897d4f6bef978e3bf46ba0d999f90c4acd"
|
||||
"hash": "8c61a4e4f6b3b8cc02cd4b6289f6889889ab06d9a77f26c4d745659c8e1f4a07"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies ORDER BY title, year, id",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE (SELECT count(DISTINCT g.release_id) FROM grabs g WHERE g.target_kind = 'movie' AND g.target_id = movies.id AND g.state = 'failed') >= 2 ORDER BY title",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "61e9081eaedc7a69ada44b057c59553511e64634b75386d523f82c2d03d78f1a"
|
||||
"hash": "9e5df0da99c02d3bd2f9235bb53f1caac0b1b104ed78f34799f494d85d1eccc2"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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\" 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 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": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -57,6 +57,16 @@
|
||||
"name": "blocked!: bool",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 12,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -73,8 +83,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "45d5b3f5e133289014b2f6e10fc14070b4b4ec2f18d97f7546f3b866004c404b"
|
||||
"hash": "9f0164d699ffd66f31e7916827c81e01d283f35bb91ec2f34f63240792c01152"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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\" 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 FROM series WHERE id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -57,6 +57,16 @@
|
||||
"name": "blocked!: bool",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 12,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -73,8 +83,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "71534a7c100984bfe082a1348474263fa67c980e532fc037cc77b82c1109ecaa"
|
||||
"hash": "ab536da7864e712754f1742e1f1f67d1d5cef884dafcaae1ae9ce498fc852e32"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY title, year, id",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY title, year, id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0e1cffb9b344a41af0cc64bbe79b2bf303aae47593eaf8df5908a1df1480e4f6"
|
||||
"hash": "ad1a3b886179c638051ed9bf34ddbc153097fd7626f68b316d810ccc8305222d"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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\" 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 FROM series ORDER BY title, year, id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -57,6 +57,16 @@
|
||||
"name": "blocked!: bool",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 12,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -73,8 +83,10 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7c76ab54e1c32a1e98cfc04ffb709382d09fc1eea9f1ddf3b613446a4c1bdb31"
|
||||
"hash": "cf040af066e35e9bcb969c84ac180784d5bc9a2bf0630cd6fef2bb8f4cb467b5"
|
||||
}
|
||||
+16
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.blocked AS \"blocked!: bool\" FROM series s WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY s.title, s.year, s.id",
|
||||
"query": "SELECT s.id AS \"id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\", s.title AS \"title!: String\", s.year, s.original_language, s.root_id AS \"root_id!: i64\", s.blocked AS \"blocked!: bool\", s.poster_path, s.vote_average FROM series s WHERE s.tmdb_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -37,20 +37,32 @@
|
||||
"name": "blocked!: bool",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "poster_path",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 8,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c4bf148c4a953467fb16d7fc726e06d96575ba9ff2ef75bc75c32062430a50d7"
|
||||
"hash": "da5d4fba049300621dc1c3d44ce008835310ae18c9904cea0080267186d64a34"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.blocked = 0 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.blocked = 0 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "af0b8b088dbcbe51203888836bb844d004d18baa022c6d77ae5e1e551f8cb096"
|
||||
"hash": "f2ae7a2dec9da2a2bfebb2a4dbd8e4e1db0fc5f15cee6a9c78af077ee1277153"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE id = ?",
|
||||
"query": "SELECT id AS \"id!: i64\", tmdb_id AS \"tmdb_id!: i64\", title AS \"title!: String\", year, original_language, root_id AS \"root_id!: i64\", wanted AS \"wanted!: bool\", overrides AS \"overrides!: serde_json::Value\", state AS \"state!: String\", blocked AS \"blocked!: bool\", search_attempts AS \"search_attempts!: i64\", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS \"waiver?: serde_json::Value\" FROM movies WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\\') ORDER BY title, year, id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,9 +64,19 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"name": "poster_path",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "vote_average",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "waiver?: serde_json::Value",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -85,8 +95,10 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "44886371d0d5966ce83ca4b10f6e4d424b5eb3598a8c9940e59ab23b4fae95f5"
|
||||
"hash": "f986006f35b44d5e0ba9ad7ff9aa6aa695ee3b54e15b7259df2cddaba4ce2644"
|
||||
}
|
||||
@@ -25,6 +25,11 @@ pub struct Movie {
|
||||
pub blocked: bool,
|
||||
pub search_attempts: i64,
|
||||
pub last_searched_at: Option<String>,
|
||||
/// Path fragment, not a URL; stored at add time and refreshed with
|
||||
/// metadata (#145), so pure-SQL views never need TMDB (§9.6).
|
||||
pub poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
pub vote_average: Option<f64>,
|
||||
/// The relaxed rule recorded when a file was imported under a waiver
|
||||
/// (§5.7), so the UI can show "English, no dub" instead of a clean match.
|
||||
pub waiver: Option<serde_json::Value>,
|
||||
@@ -244,7 +249,7 @@ pub(crate) fn pool(state: &AppState) -> Result<&sqlx::SqlitePool, ApiError> {
|
||||
}
|
||||
|
||||
async fn load_movie(state: &AppState, id: i64) -> Result<Movie, ApiError> {
|
||||
Ok(sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE id = ?"#, id)
|
||||
Ok(sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE id = ?"#, id)
|
||||
.fetch_one(pool(state)?)
|
||||
.await?)
|
||||
}
|
||||
@@ -269,11 +274,11 @@ pub async fn list(
|
||||
Query(query): Query<ListMoviesQuery>,
|
||||
) -> Result<Json<Vec<Movie>>, ApiError> {
|
||||
let movies = if let Some(owner_id) = query.owner_id {
|
||||
sqlx::query_as!(Movie, r#"SELECT m.id AS "id!: i64", m.tmdb_id AS "tmdb_id!: i64", m.title AS "title!: String", m.year, m.original_language, m.root_id AS "root_id!: i64", m.wanted AS "wanted!: bool", m.overrides AS "overrides!: serde_json::Value", m.state AS "state!: String", m.blocked AS "blocked!: bool", m.search_attempts AS "search_attempts!: i64", m.last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = m.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies m JOIN title_owners t ON t.title_kind = 'movie' AND t.title_id = m.id WHERE t.owner_id = ? ORDER BY m.title, m.year, m.id"#, owner_id)
|
||||
sqlx::query_as!(Movie, r#"SELECT m.id AS "id!: i64", m.tmdb_id AS "tmdb_id!: i64", m.title AS "title!: String", m.year, m.original_language, m.root_id AS "root_id!: i64", m.wanted AS "wanted!: bool", m.overrides AS "overrides!: serde_json::Value", m.state AS "state!: String", m.blocked AS "blocked!: bool", m.search_attempts AS "search_attempts!: i64", m.last_searched_at, m.poster_path, m.vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = m.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies m JOIN title_owners t ON t.title_kind = 'movie' AND t.title_id = m.id WHERE t.owner_id = ? ORDER BY m.title, m.year, m.id"#, owner_id)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies ORDER BY title, year, id"#)
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies ORDER BY title, year, id"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
};
|
||||
@@ -679,10 +684,10 @@ pub async fn grab(
|
||||
)
|
||||
)]
|
||||
pub async fn attention(State(state): State<AppState>) -> Result<Json<AttentionQueues>, ApiError> {
|
||||
let no_pt_source = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.blocked = 0 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title"#)
|
||||
let no_pt_source = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.blocked = 0 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?;
|
||||
let needs_decision = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE (SELECT count(DISTINCT g.release_id) FROM grabs g WHERE g.target_kind = 'movie' AND g.target_id = movies.id AND g.state = 'failed') >= 2 ORDER BY title"#)
|
||||
let needs_decision = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE (SELECT count(DISTINCT g.release_id) FROM grabs g WHERE g.target_kind = 'movie' AND g.target_id = movies.id AND g.state = 'failed') >= 2 ORDER BY title"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?;
|
||||
let (tv_no_pt_source, tv_needs_decision) = tv_attention(&state).await?;
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct LibrarySeries {
|
||||
pub original_language: Option<String>,
|
||||
pub root_id: i64,
|
||||
pub blocked: bool,
|
||||
/// Stored artwork (§9.6), so a row renders without a TMDB call.
|
||||
pub poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
pub vote_average: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
@@ -70,6 +74,11 @@ pub struct LibraryEpisode {
|
||||
pub tag: String,
|
||||
/// The episode title — what the search matched on.
|
||||
pub title: String,
|
||||
/// The series' poster — an episode has no artwork of its own worth
|
||||
/// showing at row size.
|
||||
pub poster_path: Option<String>,
|
||||
/// The series' rating, out of 10; `null` when TMDB has no votes for it.
|
||||
pub vote_average: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
|
||||
@@ -91,6 +100,10 @@ pub struct TmdbMovie {
|
||||
pub year: Option<i32>,
|
||||
pub overview: Option<String>,
|
||||
pub poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
pub vote_average: Option<f64>,
|
||||
/// How many votes the rating rests on.
|
||||
pub vote_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
@@ -108,6 +121,10 @@ pub struct TmdbSeries {
|
||||
pub year: Option<i32>,
|
||||
pub overview: Option<String>,
|
||||
pub poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
pub vote_average: Option<f64>,
|
||||
/// How many votes the rating rests on.
|
||||
pub vote_count: u32,
|
||||
}
|
||||
|
||||
/// One release's score, kept as its terms so the UI can explain a ranking
|
||||
@@ -184,13 +201,13 @@ pub async fn search(
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.map_err(|_| ApiError::Invalid("invalid TMDB id".into()))?;
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY title, year, id"#, tmdb_id, tokens)
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY title, year, id"#, tmdb_id, tokens)
|
||||
.fetch_all(database.pool()).await?
|
||||
.into_iter()
|
||||
.map(LibraryResult::Movie)
|
||||
.collect()
|
||||
} else {
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY title, year, id"#, tokens)
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY title, year, id"#, tokens)
|
||||
.fetch_all(database.pool()).await?
|
||||
.into_iter()
|
||||
.map(LibraryResult::Movie)
|
||||
@@ -204,10 +221,10 @@ pub async fn search(
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.map_err(|_| ApiError::Invalid("invalid TMDB id".into()))?;
|
||||
sqlx::query_as!(LibrarySeries, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.blocked AS "blocked!: bool" FROM series s WHERE s.tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY s.title, s.year, s.id"#, tmdb_id, tokens)
|
||||
sqlx::query_as!(LibrarySeries, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.blocked AS "blocked!: bool", s.poster_path, s.vote_average FROM series s WHERE s.tmdb_id = ? OR NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY s.title, s.year, s.id"#, tmdb_id, tokens)
|
||||
.fetch_all(database.pool()).await?
|
||||
} else {
|
||||
sqlx::query_as!(LibrarySeries, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.blocked AS "blocked!: bool" FROM series s WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY s.title, s.year, s.id"#, tokens)
|
||||
sqlx::query_as!(LibrarySeries, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.blocked AS "blocked!: bool", s.poster_path, s.vote_average FROM series s WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE s.title NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY s.title, s.year, s.id"#, tokens)
|
||||
.fetch_all(database.pool()).await?
|
||||
};
|
||||
library.extend(series_rows.into_iter().map(LibraryResult::Series));
|
||||
@@ -215,7 +232,7 @@ pub async fn search(
|
||||
// §9.2 names the TV case explicitly: `bluey hospital` finds the episode.
|
||||
// Tokens may split across the series and episode titles, so the haystack
|
||||
// is both joined and every token must land somewhere in it.
|
||||
let episode_rows = sqlx::query_as!(LibraryEpisode, r#"SELECT e.id AS "episode_id!: i64", s.id AS "series_id!: i64", s.title AS "series_title!: String", printf('S%02dE%02d', se.number, e.number) AS "tag!: String", e.title AS "title!: String" FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN series s ON s.id = se.series_id WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE (s.title || ' ' || e.title) NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY s.title, se.number, e.number, e.id"#, tokens)
|
||||
let episode_rows = sqlx::query_as!(LibraryEpisode, r#"SELECT e.id AS "episode_id!: i64", s.id AS "series_id!: i64", s.title AS "series_title!: String", printf('S%02dE%02d', se.number, e.number) AS "tag!: String", e.title AS "title!: String", s.poster_path, s.vote_average FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN series s ON s.id = se.series_id WHERE NOT EXISTS (SELECT 1 FROM json_each(?) token WHERE (s.title || ' ' || e.title) NOT LIKE '%' || token.value || '%' ESCAPE '\') ORDER BY s.title, se.number, e.number, e.id"#, tokens)
|
||||
.fetch_all(database.pool())
|
||||
.await?;
|
||||
library.extend(episode_rows.into_iter().map(LibraryResult::Episode));
|
||||
@@ -227,7 +244,7 @@ pub async fn search(
|
||||
match result {
|
||||
TmdbResult::Movie(movie) => {
|
||||
let tmdb_id = i64::from(movie.tmdb_id);
|
||||
if let Some(found) = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE tmdb_id = ?"#, tmdb_id)
|
||||
if let Some(found) = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, poster_path, vote_average, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE tmdb_id = ?"#, tmdb_id)
|
||||
.fetch_optional(database.pool()).await?
|
||||
{
|
||||
library.push(LibraryResult::Movie(found));
|
||||
@@ -235,7 +252,7 @@ pub async fn search(
|
||||
}
|
||||
TmdbResult::Series(series) => {
|
||||
let tmdb_id = i64::from(series.tmdb_id);
|
||||
if let Some(found) = sqlx::query_as!(LibrarySeries, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.blocked AS "blocked!: bool" FROM series s WHERE s.tmdb_id = ?"#, tmdb_id)
|
||||
if let Some(found) = sqlx::query_as!(LibrarySeries, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.blocked AS "blocked!: bool", s.poster_path, s.vote_average FROM series s WHERE s.tmdb_id = ?"#, tmdb_id)
|
||||
.fetch_optional(database.pool()).await?
|
||||
{
|
||||
library.push(LibraryResult::Series(found));
|
||||
@@ -291,6 +308,8 @@ async fn search_tmdb(
|
||||
year,
|
||||
overview: movie.overview,
|
||||
poster_path: movie.poster_path,
|
||||
vote_average: movie.vote_average,
|
||||
vote_count: movie.vote_count,
|
||||
})]
|
||||
}
|
||||
Err(arr_meta::Error::NotFound { .. }) => Vec::new(),
|
||||
@@ -558,6 +577,8 @@ impl From<arr_meta::MovieSearchResult> for TmdbMovie {
|
||||
year,
|
||||
overview: movie.overview,
|
||||
poster_path: movie.poster_path,
|
||||
vote_average: movie.vote_average,
|
||||
vote_count: movie.vote_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,6 +593,8 @@ impl From<arr_meta::SeriesSearchResult> for TmdbSeries {
|
||||
year,
|
||||
overview: series.overview,
|
||||
poster_path: series.poster_path,
|
||||
vote_average: series.vote_average,
|
||||
vote_count: series.vote_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,6 +609,8 @@ impl From<arr_meta::Series> for TmdbSeries {
|
||||
year,
|
||||
overview: series.overview,
|
||||
poster_path: series.poster_path,
|
||||
vote_average: series.vote_average,
|
||||
vote_count: series.vote_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -816,7 +841,8 @@ mod tests {
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(serde_json::json!({"results":[{
|
||||
"id": 693_134, "title": "Dune: Part Two", "original_title": "Dune: Part Two",
|
||||
"original_language": "en", "release_date": "2024-02-27"
|
||||
"original_language": "en", "release_date": "2024-02-27",
|
||||
"poster_path": "/dune-two.jpg", "vote_average": 8.1, "vote_count": 5000
|
||||
}]})),
|
||||
)
|
||||
.mount(&tmdb)
|
||||
@@ -843,6 +869,19 @@ mod tests {
|
||||
assert_eq!(response["library"][0]["title"], "Dune");
|
||||
assert_eq!(response["tmdb"][0]["kind"], "movie");
|
||||
assert_eq!(response["tmdb"][0]["tmdb_id"], 693_134);
|
||||
// §9.6: both halves carry artwork and a rating, read from stored
|
||||
// columns and TMDB's search body — no upstream call per row.
|
||||
assert_eq!(
|
||||
response["library"][0]["poster_path"],
|
||||
serde_json::Value::Null
|
||||
);
|
||||
assert_eq!(
|
||||
response["library"][0]["vote_average"],
|
||||
serde_json::Value::Null
|
||||
);
|
||||
assert_eq!(response["tmdb"][0]["poster_path"], "/dune-two.jpg");
|
||||
assert_eq!(response["tmdb"][0]["vote_average"], 8.1);
|
||||
assert_eq!(response["tmdb"][0]["vote_count"], 5000);
|
||||
}
|
||||
|
||||
/// §9.2: the in-library set matches series titles and episode titles, so
|
||||
|
||||
@@ -52,6 +52,11 @@ pub struct Series {
|
||||
/// Whether the show finished upstream, which `ended` is derived from.
|
||||
pub upstream_ended: bool,
|
||||
pub blocked: bool,
|
||||
/// Path fragment, not a URL; stored at add time and refreshed with
|
||||
/// metadata (#145), so pure-SQL views never need TMDB (§9.6).
|
||||
pub poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
pub vote_average: Option<f64>,
|
||||
/// `airing`, `incomplete`, `waiting`, `complete` or `ended` (§4.2).
|
||||
pub status: String,
|
||||
/// Episodes currently marked wanted (§4.1 — the only intent).
|
||||
@@ -198,6 +203,11 @@ struct SeriesRow {
|
||||
overrides: serde_json::Value,
|
||||
upstream_ended: bool,
|
||||
blocked: bool,
|
||||
/// Stored at add time and refreshed with metadata (#145), so pure-SQL
|
||||
/// views never need TMDB (§9.6).
|
||||
poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
vote_average: Option<f64>,
|
||||
}
|
||||
|
||||
struct EpisodeRow {
|
||||
@@ -310,6 +320,8 @@ fn with_status(row: &SeriesRow, episodes: &[arr_core::Episode], now: SystemTime)
|
||||
overrides: row.overrides.clone(),
|
||||
upstream_ended: row.upstream_ended,
|
||||
blocked: row.blocked,
|
||||
poster_path: row.poster_path.clone(),
|
||||
vote_average: row.vote_average,
|
||||
status: status_name(derive_series_status(&core_series(row), episodes, now)).to_owned(),
|
||||
wanted_episodes: i64::try_from(wanted.count()).unwrap_or(i64::MAX),
|
||||
available_episodes: i64::try_from(available.count()).unwrap_or(i64::MAX),
|
||||
@@ -341,7 +353,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> {
|
||||
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" 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 FROM series WHERE id = ?"#, id)
|
||||
.fetch_optional(pool(state)?)
|
||||
.await?
|
||||
.ok_or(ApiError::SeriesNotFound)
|
||||
@@ -395,11 +407,11 @@ pub async fn list(
|
||||
Query(query): Query<ListSeriesQuery>,
|
||||
) -> Result<Json<Vec<Series>>, ApiError> {
|
||||
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" 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 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)?)
|
||||
.await?
|
||||
} 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" 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 FROM series ORDER BY title, year, id"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
};
|
||||
|
||||
@@ -12,7 +12,9 @@ use serde::{Deserialize, Serialize};
|
||||
const RELEASE_TYPE_DIGITAL: u8 = 4;
|
||||
|
||||
/// One row of a TMDB search.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
///
|
||||
/// `vote_average` is the one float, so this is `PartialEq` only.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MovieSearchResult {
|
||||
/// TMDB's own id, the key everything else hangs off.
|
||||
pub tmdb_id: u32,
|
||||
@@ -30,6 +32,11 @@ pub struct MovieSearchResult {
|
||||
pub overview: Option<String>,
|
||||
/// Path fragment, not a URL. TMDB's image base is a separate concern.
|
||||
pub poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10, when TMDB has votes for it; zero normalised
|
||||
/// away like [`Movie::vote_average`].
|
||||
pub vote_average: Option<f64>,
|
||||
/// How many votes the rating rests on.
|
||||
pub vote_count: u32,
|
||||
}
|
||||
|
||||
impl MovieSearchResult {
|
||||
@@ -86,6 +93,8 @@ pub struct Movie {
|
||||
/// artwork (§9.6). TMDB sends `0` where "no rating yet" is meant, so a
|
||||
/// zero rating is normalised away here rather than stored as one.
|
||||
pub vote_average: Option<f64>,
|
||||
/// How many votes the rating rests on.
|
||||
pub vote_count: u32,
|
||||
}
|
||||
|
||||
impl Movie {
|
||||
@@ -106,7 +115,7 @@ impl Movie {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SeriesSearchResult {
|
||||
pub tmdb_id: u32,
|
||||
pub title: String,
|
||||
@@ -114,6 +123,10 @@ pub struct SeriesSearchResult {
|
||||
pub first_air_date: Option<NaiveDate>,
|
||||
pub overview: Option<String>,
|
||||
pub poster_path: Option<String>,
|
||||
/// Zero normalised away, like [`Movie::vote_average`].
|
||||
pub vote_average: Option<f64>,
|
||||
/// How many votes the rating rests on.
|
||||
pub vote_count: u32,
|
||||
}
|
||||
|
||||
impl SeriesSearchResult {
|
||||
@@ -141,6 +154,8 @@ pub struct Series {
|
||||
/// TMDB's rating, out of 10, when TMDB has votes for it. Stored with the
|
||||
/// artwork (§9.6); zero is normalised away like [`Movie::vote_average`].
|
||||
pub vote_average: Option<f64>,
|
||||
/// How many votes the rating rests on.
|
||||
pub vote_count: u32,
|
||||
pub seasons: Vec<SeasonSummary>,
|
||||
}
|
||||
|
||||
@@ -310,6 +325,10 @@ pub(crate) struct RawSeriesSearchResult {
|
||||
overview: Option<String>,
|
||||
#[serde(default)]
|
||||
poster_path: Option<String>,
|
||||
#[serde(default)]
|
||||
vote_average: f64,
|
||||
#[serde(default)]
|
||||
vote_count: u32,
|
||||
}
|
||||
|
||||
impl From<RawSeriesSearchResult> for SeriesSearchResult {
|
||||
@@ -321,6 +340,8 @@ impl From<RawSeriesSearchResult> for SeriesSearchResult {
|
||||
first_air_date: raw.first_air_date.as_deref().and_then(parse_date),
|
||||
overview: non_empty(raw.overview),
|
||||
poster_path: non_empty(raw.poster_path),
|
||||
vote_average: rating(raw.vote_average),
|
||||
vote_count: raw.vote_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,6 +366,8 @@ pub(crate) struct RawSeries {
|
||||
#[serde(default)]
|
||||
vote_average: f64,
|
||||
#[serde(default)]
|
||||
vote_count: u32,
|
||||
#[serde(default)]
|
||||
seasons: Vec<RawSeasonSummary>,
|
||||
#[serde(default)]
|
||||
external_ids: Option<RawExternalIds>,
|
||||
@@ -382,6 +405,7 @@ impl From<RawSeries> for Series {
|
||||
poster_path: non_empty(raw.poster_path),
|
||||
backdrop_path: non_empty(raw.backdrop_path),
|
||||
vote_average: rating(raw.vote_average),
|
||||
vote_count: raw.vote_count,
|
||||
seasons: raw
|
||||
.seasons
|
||||
.into_iter()
|
||||
@@ -436,7 +460,7 @@ impl From<RawSeason> for Season {
|
||||
/// What one `IMDb` id resolved to. An id names one title, so at most one of
|
||||
/// the two lists is non-empty — but TMDB answers both kinds in the same
|
||||
/// response, and both are surfaced rather than filtered here.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct FindResults {
|
||||
pub movies: Vec<MovieSearchResult>,
|
||||
pub series: Vec<SeriesSearchResult>,
|
||||
@@ -465,6 +489,10 @@ pub(crate) struct RawSearchResult {
|
||||
overview: Option<String>,
|
||||
#[serde(default)]
|
||||
poster_path: Option<String>,
|
||||
#[serde(default)]
|
||||
vote_average: f64,
|
||||
#[serde(default)]
|
||||
vote_count: u32,
|
||||
}
|
||||
|
||||
impl From<RawSearchResult> for MovieSearchResult {
|
||||
@@ -477,6 +505,8 @@ impl From<RawSearchResult> for MovieSearchResult {
|
||||
release_date: raw.release_date.as_deref().and_then(parse_date),
|
||||
overview: non_empty(raw.overview),
|
||||
poster_path: non_empty(raw.poster_path),
|
||||
vote_average: rating(raw.vote_average),
|
||||
vote_count: raw.vote_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,6 +541,8 @@ pub(crate) struct RawMovie {
|
||||
#[serde(default)]
|
||||
vote_average: f64,
|
||||
#[serde(default)]
|
||||
vote_count: u32,
|
||||
#[serde(default)]
|
||||
release_dates: Option<RawReleaseDates>,
|
||||
}
|
||||
|
||||
@@ -572,6 +604,7 @@ impl From<RawMovie> for Movie {
|
||||
poster_path: non_empty(raw.poster_path),
|
||||
backdrop_path: non_empty(raw.backdrop_path),
|
||||
vote_average: rating(raw.vote_average),
|
||||
vote_count: raw.vote_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ export interface LibrarySeries {
|
||||
auto_track: boolean;
|
||||
upstream_ended: boolean;
|
||||
blocked: boolean;
|
||||
/** Stored artwork (§9.6) — a grid tile renders without a TMDB call. */
|
||||
poster_path: string | null;
|
||||
/** TMDB's rating, out of 10; `null` when TMDB has no votes for it. */
|
||||
vote_average: number | null;
|
||||
status: SeriesStatus;
|
||||
wanted_episodes: number;
|
||||
available_episodes: number;
|
||||
|
||||
@@ -15,6 +15,10 @@ export interface LibraryMovie {
|
||||
wanted: boolean;
|
||||
state: "missing" | "downloading" | "available";
|
||||
blocked: boolean;
|
||||
/** Stored artwork (§9.6) — a row renders without a TMDB call. */
|
||||
poster_path: string | null;
|
||||
/** TMDB's rating, out of 10; `null` when TMDB has no votes for it. */
|
||||
vote_average: number | null;
|
||||
/** The §5.7 rule relaxed to allow an import, when a file carries one. */
|
||||
waiver?: unknown;
|
||||
}
|
||||
@@ -28,6 +32,8 @@ export interface LibrarySeriesHit {
|
||||
original_language: string | null;
|
||||
root_id: number;
|
||||
blocked: boolean;
|
||||
poster_path: string | null;
|
||||
vote_average: number | null;
|
||||
}
|
||||
|
||||
export interface LibraryEpisodeHit {
|
||||
@@ -39,6 +45,9 @@ export interface LibraryEpisodeHit {
|
||||
tag: string;
|
||||
/** The episode title — what the search matched on. */
|
||||
title: string;
|
||||
/** The series' poster — an episode has no artwork of its own worth showing at row size. */
|
||||
poster_path: string | null;
|
||||
vote_average: number | null;
|
||||
}
|
||||
|
||||
export type LibraryResult = LibraryMovie | LibrarySeriesHit | LibraryEpisodeHit;
|
||||
@@ -51,6 +60,10 @@ export interface TmdbMovie {
|
||||
original_language: string;
|
||||
year: number | null;
|
||||
overview: string | null;
|
||||
poster_path: string | null;
|
||||
/** TMDB's rating, out of 10; `null` when TMDB has no votes for it. */
|
||||
vote_average: number | null;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
export interface TmdbSeries {
|
||||
@@ -60,6 +73,9 @@ export interface TmdbSeries {
|
||||
original_language: string;
|
||||
year: number | null;
|
||||
overview: string | null;
|
||||
poster_path: string | null;
|
||||
vote_average: number | null;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
export type TmdbResult = TmdbMovie | TmdbSeries;
|
||||
@@ -142,6 +158,8 @@ export interface ApiSeries {
|
||||
overrides: Record<string, unknown>;
|
||||
upstream_ended: boolean;
|
||||
blocked: boolean;
|
||||
poster_path: string | null;
|
||||
vote_average: number | null;
|
||||
status: string;
|
||||
wanted_episodes: number;
|
||||
available_episodes: number;
|
||||
|
||||
Reference in New Issue
Block a user