feat(tv): search indexers by series tvdb id
This commit is contained in:
@@ -331,16 +331,17 @@ async fn movie_releases(
|
||||
|
||||
/// Classified releases for one episode (`DESIGN.md` §6.1, §9.3).
|
||||
///
|
||||
/// The series carries no TVDB ID yet, so `tv_request` falls back to a text
|
||||
/// search built from the title and the `SxxEyy` tag. Both widen the result
|
||||
/// set rather than narrowing it, which the buckets already handle.
|
||||
/// A series with a stored TVDB id is searched by it; `tv_request` falls back
|
||||
/// to a text search built from the title and the `SxxEyy` tag when the id is
|
||||
/// missing or the indexer does not take `tvdbid`. Both widen the result set
|
||||
/// rather than narrowing it, which the buckets already handle.
|
||||
async fn episode_releases(
|
||||
state: &AppState,
|
||||
episode_id: i64,
|
||||
) -> Result<Vec<ClassifiedRelease>, ApiError> {
|
||||
let database = state.database().ok_or(ApiError::Unavailable)?;
|
||||
let episode = sqlx::query!(
|
||||
r#"SELECT s.title AS "series_title!: String", s.original_language,
|
||||
r#"SELECT s.title AS "series_title!: String", s.tvdb_id AS series_tvdb_id, s.original_language,
|
||||
se.number AS "season_number!: i64", e.number AS "episode_number!: i64"
|
||||
FROM episodes e
|
||||
JOIN seasons se ON se.id = e.season_id
|
||||
@@ -365,7 +366,7 @@ async fn episode_releases(
|
||||
let original_language = title_language(original_language, &[]);
|
||||
|
||||
let target = TvTarget {
|
||||
tvdb_id: None,
|
||||
tvdb_id: episode.series_tvdb_id.and_then(|id| u64::try_from(id).ok()),
|
||||
title: episode.series_title,
|
||||
selector: TvSelector::Episode {
|
||||
season: u32::try_from(episode.season_number).unwrap_or_default(),
|
||||
@@ -456,7 +457,7 @@ fn prowlarr_client(state: &AppState) -> Result<ProwlarrClient, ApiError> {
|
||||
ProwlarrClient::new(upstreams.prowlarr_url.clone(), api_key).map_err(|_| ApiError::Unavailable)
|
||||
}
|
||||
|
||||
fn tmdb_client(state: &AppState) -> Result<arr_meta::TmdbClient, ApiError> {
|
||||
pub(crate) fn tmdb_client(state: &AppState) -> Result<arr_meta::TmdbClient, ApiError> {
|
||||
let upstreams = state.upstreams();
|
||||
let key = upstreams
|
||||
.tmdb_api_key
|
||||
@@ -820,6 +821,113 @@ mod tests {
|
||||
assert_eq!(releases[0]["parsed"]["episode"]["episodes"][0], 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn episode_releases_search_by_tvdb_id_when_the_indexer_takes_one() {
|
||||
let tmdb = MockServer::start().await;
|
||||
let prowlarr = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/indexer"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_json(serde_json::json!([{"id":9,"name":"tracker","enable":true}])),
|
||||
)
|
||||
.mount(&prowlarr)
|
||||
.await;
|
||||
Mock::given(method("GET")).and(path("/9/api")).and(query_param("t", "caps"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("<caps><searching><search available=\"yes\" supportedParams=\"q\"/><tv-search available=\"yes\" supportedParams=\"q,tvdbid,season,ep\"/></searching></caps>"))
|
||||
.mount(&prowlarr).await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/9/api"))
|
||||
.and(query_param("t", "tvsearch"))
|
||||
.and(query_param("tvdbid", "361391"))
|
||||
.and(query_param("season", "1"))
|
||||
.and(query_param("ep", "2"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r"<rss><channel><item><title>Bluey.S01E02.1080p.WEB-DL</title><guid>ep</guid><link>https://tracker/ep</link><size>1500000000</size></item></channel></rss>"))
|
||||
.mount(&prowlarr).await;
|
||||
|
||||
let (_dir, state, base) = application(&tmdb, &prowlarr).await;
|
||||
let episode_id = episode_with_tvdb_id(&state, Some(361_391)).await;
|
||||
|
||||
let response = reqwest::get(format!("{base}/api/releases?episode_id={episode_id}"))
|
||||
.await
|
||||
.expect("releases");
|
||||
assert_eq!(response.status(), 200);
|
||||
let releases: Vec<serde_json::Value> = response.json().await.expect("json");
|
||||
assert_eq!(releases.len(), 1);
|
||||
assert_eq!(releases[0]["guid"], "ep");
|
||||
}
|
||||
|
||||
/// §6.1: an indexer whose caps do not advertise `tvdbid` still gets
|
||||
/// searched, by text — a missing or unsupported id widens the query,
|
||||
/// it never skips the indexer.
|
||||
#[tokio::test]
|
||||
async fn an_indexer_without_id_search_still_gets_a_text_query() {
|
||||
let tmdb = MockServer::start().await;
|
||||
let prowlarr = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/v1/indexer"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_json(serde_json::json!([{"id":9,"name":"tracker","enable":true}])),
|
||||
)
|
||||
.mount(&prowlarr)
|
||||
.await;
|
||||
Mock::given(method("GET")).and(path("/9/api")).and(query_param("t", "caps"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("<caps><searching><search available=\"yes\" supportedParams=\"q\"/><tv-search available=\"yes\" supportedParams=\"q,season,ep\"/></searching></caps>"))
|
||||
.mount(&prowlarr).await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/9/api"))
|
||||
.and(query_param("t", "search"))
|
||||
.and(query_param("q", "Bluey S01E02"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r"<rss><channel><item><title>Bluey.S01E02.1080p.WEB-DL</title><guid>ep</guid><link>https://tracker/ep</link><size>1500000000</size></item></channel></rss>"))
|
||||
.mount(&prowlarr).await;
|
||||
|
||||
let (_dir, state, base) = application(&tmdb, &prowlarr).await;
|
||||
let episode_id = episode_with_tvdb_id(&state, Some(361_391)).await;
|
||||
|
||||
let response = reqwest::get(format!("{base}/api/releases?episode_id={episode_id}"))
|
||||
.await
|
||||
.expect("releases");
|
||||
assert_eq!(response.status(), 200);
|
||||
let releases: Vec<serde_json::Value> = response.json().await.expect("json");
|
||||
assert_eq!(releases.len(), 1);
|
||||
assert_eq!(releases[0]["parsed"]["episode"]["episodes"][0], 2);
|
||||
}
|
||||
|
||||
async fn episode_with_tvdb_id(state: &crate::state::AppState, tvdb_id: Option<i64>) -> i64 {
|
||||
use sqlx::Row as _;
|
||||
let pool = state.database().expect("database").pool();
|
||||
let root_id: i64 =
|
||||
sqlx::query_scalar("SELECT id FROM roots WHERE kind = 'tv' AND audience = 'main'")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("TV root");
|
||||
let series_id: i64 = sqlx::query(
|
||||
"INSERT INTO series (tmdb_id, tvdb_id, title, year, original_language, root_id) VALUES (82728, ?, 'Bluey', 2018, 'en', ?)",
|
||||
)
|
||||
.bind(tvdb_id)
|
||||
.bind(root_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("series")
|
||||
.last_insert_rowid();
|
||||
let season_id: i64 = sqlx::query(
|
||||
"INSERT INTO seasons (series_id, number) VALUES (?, 1) RETURNING id",
|
||||
)
|
||||
.bind(series_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("season")
|
||||
.get(0);
|
||||
sqlx::query_scalar(
|
||||
"INSERT INTO episodes (season_id, number, title, wanted) VALUES (?, 2, 'Hospital', 1) RETURNING id",
|
||||
)
|
||||
.bind(season_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("episode")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_manual_search_names_exactly_one_title() {
|
||||
let tmdb = MockServer::start().await;
|
||||
|
||||
Reference in New Issue
Block a user