diff --git a/.sqlx/query-6a25900e3815470be997f96bfee10f2079242e8d755da6387dbb8a0f40fe6ea1.json b/.sqlx/query-16778fe075b61babcb7f5436113e91bf5f86e9a968d7b61f12daa022cc27f4d1.json similarity index 77% rename from .sqlx/query-6a25900e3815470be997f96bfee10f2079242e8d755da6387dbb8a0f40fe6ea1.json rename to .sqlx/query-16778fe075b61babcb7f5436113e91bf5f86e9a968d7b61f12daa022cc27f4d1.json index 60f5209..07f99ac 100644 --- a/.sqlx/query-6a25900e3815470be997f96bfee10f2079242e8d755da6387dbb8a0f40fe6ea1.json +++ b/.sqlx/query-16778fe075b61babcb7f5436113e91bf5f86e9a968d7b61f12daa022cc27f4d1.json @@ -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" } diff --git a/.sqlx/query-ccf61e6cd96c0d048e054fdfa06af5a2551a87ccb172c02795e75129e35bfc63.json b/.sqlx/query-268dc95276f57bf3c457c98d172ef5e6f9acf430ae419ca68dc3800b0390b5b7.json similarity index 68% rename from .sqlx/query-ccf61e6cd96c0d048e054fdfa06af5a2551a87ccb172c02795e75129e35bfc63.json rename to .sqlx/query-268dc95276f57bf3c457c98d172ef5e6f9acf430ae419ca68dc3800b0390b5b7.json index 2f7d6fe..a16ec64 100644 --- a/.sqlx/query-ccf61e6cd96c0d048e054fdfa06af5a2551a87ccb172c02795e75129e35bfc63.json +++ b/.sqlx/query-268dc95276f57bf3c457c98d172ef5e6f9acf430ae419ca68dc3800b0390b5b7.json @@ -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" } diff --git a/.sqlx/query-7b6b0d6bbfcd752bd3a9d97f8d64529bd7aab96ad817fdb9ac9829a525a9f1d7.json b/.sqlx/query-53a2b5bcc6a753b8ef0a5f4110a69cc8ece28966cfa003f4bfe1190c55831a23.json similarity index 77% rename from .sqlx/query-7b6b0d6bbfcd752bd3a9d97f8d64529bd7aab96ad817fdb9ac9829a525a9f1d7.json rename to .sqlx/query-53a2b5bcc6a753b8ef0a5f4110a69cc8ece28966cfa003f4bfe1190c55831a23.json index c74bb9f..45d5138 100644 --- a/.sqlx/query-7b6b0d6bbfcd752bd3a9d97f8d64529bd7aab96ad817fdb9ac9829a525a9f1d7.json +++ b/.sqlx/query-53a2b5bcc6a753b8ef0a5f4110a69cc8ece28966cfa003f4bfe1190c55831a23.json @@ -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" } diff --git a/.sqlx/query-5fc9aa28b8329895c03c7be43024069d8a065ddd37ea77bb3058aa8ec1cab52e.json b/.sqlx/query-6394f95e9e437ed3c46bb325c2d946ce75f94e00bf0d3aa6ac54811080725cc2.json similarity index 74% rename from .sqlx/query-5fc9aa28b8329895c03c7be43024069d8a065ddd37ea77bb3058aa8ec1cab52e.json rename to .sqlx/query-6394f95e9e437ed3c46bb325c2d946ce75f94e00bf0d3aa6ac54811080725cc2.json index 239289d..dafa631 100644 --- a/.sqlx/query-5fc9aa28b8329895c03c7be43024069d8a065ddd37ea77bb3058aa8ec1cab52e.json +++ b/.sqlx/query-6394f95e9e437ed3c46bb325c2d946ce75f94e00bf0d3aa6ac54811080725cc2.json @@ -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" } diff --git a/.sqlx/query-9fa86070879694b17a8b42e0717f70bd071b8169e6469f0d568754fc6b26c863.json b/.sqlx/query-6f41ffaa0679bbbe51ea5d54fbdbf757e0f85d1d86ee6322253f223fae49265f.json similarity index 68% rename from .sqlx/query-9fa86070879694b17a8b42e0717f70bd071b8169e6469f0d568754fc6b26c863.json rename to .sqlx/query-6f41ffaa0679bbbe51ea5d54fbdbf757e0f85d1d86ee6322253f223fae49265f.json index 646a6b8..183928e 100644 --- a/.sqlx/query-9fa86070879694b17a8b42e0717f70bd071b8169e6469f0d568754fc6b26c863.json +++ b/.sqlx/query-6f41ffaa0679bbbe51ea5d54fbdbf757e0f85d1d86ee6322253f223fae49265f.json @@ -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" } diff --git a/.sqlx/query-b4074ba8e74383de4a351f1fceae938fa952b522db9798039339b01e284636c2.json b/.sqlx/query-833f1514dad9e77075cd8c1b38d483de6a9ba5be46a8e7d89a05fe33f3f20044.json similarity index 58% rename from .sqlx/query-b4074ba8e74383de4a351f1fceae938fa952b522db9798039339b01e284636c2.json rename to .sqlx/query-833f1514dad9e77075cd8c1b38d483de6a9ba5be46a8e7d89a05fe33f3f20044.json index 322712b..fbf66fb 100644 --- a/.sqlx/query-b4074ba8e74383de4a351f1fceae938fa952b522db9798039339b01e284636c2.json +++ b/.sqlx/query-833f1514dad9e77075cd8c1b38d483de6a9ba5be46a8e7d89a05fe33f3f20044.json @@ -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" } diff --git a/.sqlx/query-af1b98d2aa84eac69323823e232b61897d4f6bef978e3bf46ba0d999f90c4acd.json b/.sqlx/query-8c61a4e4f6b3b8cc02cd4b6289f6889889ab06d9a77f26c4d745659c8e1f4a07.json similarity index 77% rename from .sqlx/query-af1b98d2aa84eac69323823e232b61897d4f6bef978e3bf46ba0d999f90c4acd.json rename to .sqlx/query-8c61a4e4f6b3b8cc02cd4b6289f6889889ab06d9a77f26c4d745659c8e1f4a07.json index 3d8bd76..7ae4af3 100644 --- a/.sqlx/query-af1b98d2aa84eac69323823e232b61897d4f6bef978e3bf46ba0d999f90c4acd.json +++ b/.sqlx/query-8c61a4e4f6b3b8cc02cd4b6289f6889889ab06d9a77f26c4d745659c8e1f4a07.json @@ -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" } diff --git a/.sqlx/query-61e9081eaedc7a69ada44b057c59553511e64634b75386d523f82c2d03d78f1a.json b/.sqlx/query-9e5df0da99c02d3bd2f9235bb53f1caac0b1b104ed78f34799f494d85d1eccc2.json similarity index 73% rename from .sqlx/query-61e9081eaedc7a69ada44b057c59553511e64634b75386d523f82c2d03d78f1a.json rename to .sqlx/query-9e5df0da99c02d3bd2f9235bb53f1caac0b1b104ed78f34799f494d85d1eccc2.json index b8949df..3157baf 100644 --- a/.sqlx/query-61e9081eaedc7a69ada44b057c59553511e64634b75386d523f82c2d03d78f1a.json +++ b/.sqlx/query-9e5df0da99c02d3bd2f9235bb53f1caac0b1b104ed78f34799f494d85d1eccc2.json @@ -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" } diff --git a/.sqlx/query-45d5b3f5e133289014b2f6e10fc14070b4b4ec2f18d97f7546f3b866004c404b.json b/.sqlx/query-9f0164d699ffd66f31e7916827c81e01d283f35bb91ec2f34f63240792c01152.json similarity index 77% rename from .sqlx/query-45d5b3f5e133289014b2f6e10fc14070b4b4ec2f18d97f7546f3b866004c404b.json rename to .sqlx/query-9f0164d699ffd66f31e7916827c81e01d283f35bb91ec2f34f63240792c01152.json index 0ff3d5c..3822d6e 100644 --- a/.sqlx/query-45d5b3f5e133289014b2f6e10fc14070b4b4ec2f18d97f7546f3b866004c404b.json +++ b/.sqlx/query-9f0164d699ffd66f31e7916827c81e01d283f35bb91ec2f34f63240792c01152.json @@ -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" } diff --git a/.sqlx/query-71534a7c100984bfe082a1348474263fa67c980e532fc037cc77b82c1109ecaa.json b/.sqlx/query-ab536da7864e712754f1742e1f1f67d1d5cef884dafcaae1ae9ce498fc852e32.json similarity index 82% rename from .sqlx/query-71534a7c100984bfe082a1348474263fa67c980e532fc037cc77b82c1109ecaa.json rename to .sqlx/query-ab536da7864e712754f1742e1f1f67d1d5cef884dafcaae1ae9ce498fc852e32.json index db5a0a9..3c135a7 100644 --- a/.sqlx/query-71534a7c100984bfe082a1348474263fa67c980e532fc037cc77b82c1109ecaa.json +++ b/.sqlx/query-ab536da7864e712754f1742e1f1f67d1d5cef884dafcaae1ae9ce498fc852e32.json @@ -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" } diff --git a/.sqlx/query-0e1cffb9b344a41af0cc64bbe79b2bf303aae47593eaf8df5908a1df1480e4f6.json b/.sqlx/query-ad1a3b886179c638051ed9bf34ddbc153097fd7626f68b316d810ccc8305222d.json similarity index 73% rename from .sqlx/query-0e1cffb9b344a41af0cc64bbe79b2bf303aae47593eaf8df5908a1df1480e4f6.json rename to .sqlx/query-ad1a3b886179c638051ed9bf34ddbc153097fd7626f68b316d810ccc8305222d.json index 987db2a..c7b663a 100644 --- a/.sqlx/query-0e1cffb9b344a41af0cc64bbe79b2bf303aae47593eaf8df5908a1df1480e4f6.json +++ b/.sqlx/query-ad1a3b886179c638051ed9bf34ddbc153097fd7626f68b316d810ccc8305222d.json @@ -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" } diff --git a/.sqlx/query-7c76ab54e1c32a1e98cfc04ffb709382d09fc1eea9f1ddf3b613446a4c1bdb31.json b/.sqlx/query-cf040af066e35e9bcb969c84ac180784d5bc9a2bf0630cd6fef2bb8f4cb467b5.json similarity index 81% rename from .sqlx/query-7c76ab54e1c32a1e98cfc04ffb709382d09fc1eea9f1ddf3b613446a4c1bdb31.json rename to .sqlx/query-cf040af066e35e9bcb969c84ac180784d5bc9a2bf0630cd6fef2bb8f4cb467b5.json index 313730c..9906276 100644 --- a/.sqlx/query-7c76ab54e1c32a1e98cfc04ffb709382d09fc1eea9f1ddf3b613446a4c1bdb31.json +++ b/.sqlx/query-cf040af066e35e9bcb969c84ac180784d5bc9a2bf0630cd6fef2bb8f4cb467b5.json @@ -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" } diff --git a/.sqlx/query-c4bf148c4a953467fb16d7fc726e06d96575ba9ff2ef75bc75c32062430a50d7.json b/.sqlx/query-da5d4fba049300621dc1c3d44ce008835310ae18c9904cea0080267186d64a34.json similarity index 74% rename from .sqlx/query-c4bf148c4a953467fb16d7fc726e06d96575ba9ff2ef75bc75c32062430a50d7.json rename to .sqlx/query-da5d4fba049300621dc1c3d44ce008835310ae18c9904cea0080267186d64a34.json index efaeb6d..d7d861c 100644 --- a/.sqlx/query-c4bf148c4a953467fb16d7fc726e06d96575ba9ff2ef75bc75c32062430a50d7.json +++ b/.sqlx/query-da5d4fba049300621dc1c3d44ce008835310ae18c9904cea0080267186d64a34.json @@ -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" } diff --git a/.sqlx/query-af0b8b088dbcbe51203888836bb844d004d18baa022c6d77ae5e1e551f8cb096.json b/.sqlx/query-f2ae7a2dec9da2a2bfebb2a4dbd8e4e1db0fc5f15cee6a9c78af077ee1277153.json similarity index 68% rename from .sqlx/query-af0b8b088dbcbe51203888836bb844d004d18baa022c6d77ae5e1e551f8cb096.json rename to .sqlx/query-f2ae7a2dec9da2a2bfebb2a4dbd8e4e1db0fc5f15cee6a9c78af077ee1277153.json index e55afa7..b59d108 100644 --- a/.sqlx/query-af0b8b088dbcbe51203888836bb844d004d18baa022c6d77ae5e1e551f8cb096.json +++ b/.sqlx/query-f2ae7a2dec9da2a2bfebb2a4dbd8e4e1db0fc5f15cee6a9c78af077ee1277153.json @@ -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" } diff --git a/.sqlx/query-44886371d0d5966ce83ca4b10f6e4d424b5eb3598a8c9940e59ab23b4fae95f5.json b/.sqlx/query-f986006f35b44d5e0ba9ad7ff9aa6aa695ee3b54e15b7259df2cddaba4ce2644.json similarity index 74% rename from .sqlx/query-44886371d0d5966ce83ca4b10f6e4d424b5eb3598a8c9940e59ab23b4fae95f5.json rename to .sqlx/query-f986006f35b44d5e0ba9ad7ff9aa6aa695ee3b54e15b7259df2cddaba4ce2644.json index c8388f6..66d9680 100644 --- a/.sqlx/query-44886371d0d5966ce83ca4b10f6e4d424b5eb3598a8c9940e59ab23b4fae95f5.json +++ b/.sqlx/query-f986006f35b44d5e0ba9ad7ff9aa6aa695ee3b54e15b7259df2cddaba4ce2644.json @@ -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" } diff --git a/crates/arr-api/src/movies.rs b/crates/arr-api/src/movies.rs index 487fc95..b315b78 100644 --- a/crates/arr-api/src/movies.rs +++ b/crates/arr-api/src/movies.rs @@ -25,6 +25,11 @@ pub struct Movie { pub blocked: bool, pub search_attempts: i64, pub last_searched_at: Option, + /// 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, + /// TMDB's rating, out of 10; `null` when TMDB has no votes for it. + pub vote_average: Option, /// 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, @@ -244,7 +249,7 @@ pub(crate) fn pool(state: &AppState) -> Result<&sqlx::SqlitePool, ApiError> { } async fn load_movie(state: &AppState, id: i64) -> Result { - 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, ) -> Result>, 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) -> Result, 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?; diff --git a/crates/arr-api/src/search.rs b/crates/arr-api/src/search.rs index 0d82ed8..02418a0 100644 --- a/crates/arr-api/src/search.rs +++ b/crates/arr-api/src/search.rs @@ -59,6 +59,10 @@ pub struct LibrarySeries { pub original_language: Option, pub root_id: i64, pub blocked: bool, + /// Stored artwork (§9.6), so a row renders without a TMDB call. + pub poster_path: Option, + /// TMDB's rating, out of 10; `null` when TMDB has no votes for it. + pub vote_average: Option, } #[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, + /// The series' rating, out of 10; `null` when TMDB has no votes for it. + pub vote_average: Option, } #[derive(Debug, Clone, Copy, Serialize, ToSchema)] @@ -91,6 +100,10 @@ pub struct TmdbMovie { pub year: Option, pub overview: Option, pub poster_path: Option, + /// TMDB's rating, out of 10; `null` when TMDB has no votes for it. + pub vote_average: Option, + /// 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, pub overview: Option, pub poster_path: Option, + /// TMDB's rating, out of 10; `null` when TMDB has no votes for it. + pub vote_average: Option, + /// 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::() .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::() .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 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 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 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 diff --git a/crates/arr-api/src/series.rs b/crates/arr-api/src/series.rs index d15b617..fa480c0 100644 --- a/crates/arr-api/src/series.rs +++ b/crates/arr-api/src/series.rs @@ -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, + /// TMDB's rating, out of 10; `null` when TMDB has no votes for it. + pub vote_average: Option, /// `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, + /// TMDB's rating, out of 10; `null` when TMDB has no votes for it. + vote_average: Option, } 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 Result { - 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, ) -> Result>, 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? }; diff --git a/crates/arr-meta/src/model.rs b/crates/arr-meta/src/model.rs index dfe64b7..05d8125 100644 --- a/crates/arr-meta/src/model.rs +++ b/crates/arr-meta/src/model.rs @@ -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, /// Path fragment, not a URL. TMDB's image base is a separate concern. pub poster_path: Option, + /// TMDB's rating, out of 10, when TMDB has votes for it; zero normalised + /// away like [`Movie::vote_average`]. + pub vote_average: Option, + /// 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, + /// 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, pub overview: Option, pub poster_path: Option, + /// Zero normalised away, like [`Movie::vote_average`]. + pub vote_average: Option, + /// 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, + /// How many votes the rating rests on. + pub vote_count: u32, pub seasons: Vec, } @@ -310,6 +325,10 @@ pub(crate) struct RawSeriesSearchResult { overview: Option, #[serde(default)] poster_path: Option, + #[serde(default)] + vote_average: f64, + #[serde(default)] + vote_count: u32, } impl From for SeriesSearchResult { @@ -321,6 +340,8 @@ impl From 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, #[serde(default)] external_ids: Option, @@ -382,6 +405,7 @@ impl From 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 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, pub series: Vec, @@ -465,6 +489,10 @@ pub(crate) struct RawSearchResult { overview: Option, #[serde(default)] poster_path: Option, + #[serde(default)] + vote_average: f64, + #[serde(default)] + vote_count: u32, } impl From for MovieSearchResult { @@ -477,6 +505,8 @@ impl From 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, } @@ -572,6 +604,7 @@ impl From 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, } } } diff --git a/web/src/library.ts b/web/src/library.ts index e032dd8..ad6531f 100644 --- a/web/src/library.ts +++ b/web/src/library.ts @@ -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; diff --git a/web/src/search.ts b/web/src/search.ts index 5bba581..78e17e2 100644 --- a/web/src/search.ts +++ b/web/src/search.ts @@ -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; upstream_ended: boolean; blocked: boolean; + poster_path: string | null; + vote_average: number | null; status: string; wanted_episodes: number; available_episodes: number;