From c2b0e610ec9212e63dfcca9af6c2d6c271e2d1cd Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:53:39 +0300 Subject: [PATCH] Plan Discover per selected catalog --- src/ffi.rs | 1 - src/headless_engine/discover.rs | 2 + src/platform_plan.rs | 100 +++++++++++++---------------- src/search_plan.rs | 74 +++++++++------------ tests/wire/core_invoke_methods.txt | 1 - 5 files changed, 76 insertions(+), 102 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 9054055..1e267c1 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -390,7 +390,6 @@ fn route_search_plan(method: &str, args_json: &str) -> Outcome { field_str(&args, "selectedType")?, )) } - "discoverSortPlan" => opt_json(search_plan::discover_sort_plan_json(args_json)), "librarySortPlan" => opt_json(search_plan::library_sort_plan_json(args_json)), "detailSeriesLookupId" => Ok(Value::String(search_plan::detail_series_lookup_id( &arg_str(args_json, "id")?, diff --git a/src/headless_engine/discover.rs b/src/headless_engine/discover.rs index 19c0c92..0f6e9a8 100644 --- a/src/headless_engine/discover.rs +++ b/src/headless_engine/discover.rs @@ -84,6 +84,8 @@ pub(super) fn dispatch_catalog_filters( let generation = engine.bump_generation(GenerationKey::Discover); let profile_value = profile.unwrap_or_else(|| engine.state.profile.active.clone()); let profile_id = active_profile_id(&engine.state, &profile_value); + engine.state.discover.content_type = content_type.clone(); + engine.state.discover.catalogs = serde_json::json!([]); vec![engine.effect( EffectKind::ReadDiscoverCatalogFilters, generation, diff --git a/src/platform_plan.rs b/src/platform_plan.rs index 501c79d..6c07ade 100644 --- a/src/platform_plan.rs +++ b/src/platform_plan.rs @@ -1,7 +1,8 @@ use crate::addon_protocol::{ - build_resource_url, catalog_supports_extra as manifest_catalog_supports_extra, supports_resource, + build_resource_url, catalog_supports_extra as manifest_catalog_supports_extra, + supports_resource, }; -use crate::content_identity::parse_extra_args_json; +use crate::content_identity::{parse_extra_args_json, stable_feed_part}; use crate::repository_flow::addon_streams_with_provider_json; use crate::stream_policy::stream_playback_info_json; use serde::Deserialize; @@ -23,6 +24,8 @@ struct ResourceFetchPlanRequest { #[serde(default)] catalog_id: Option, #[serde(default)] + catalog_key: Option, + #[serde(default)] id: Option, #[serde(default)] request_ids: Vec, @@ -150,25 +153,44 @@ pub(crate) fn resource_fetch_plan_json(request_json: &str) -> Option { } } "discover" => { - let genre = request.genre.as_deref(); - for catalog in discover_catalog_options( - &request.addons, - request.content_type.as_deref().unwrap_or(""), - ) { - let extra = genre - .filter(|_| catalog.supports_genre) - .map(|value| json!({"genre": value}).to_string()); - requests.push(json!({ - "url": build_resource_url( - &catalog.transport_url, - "catalog", - &catalog.content_type, - &catalog.id, - extra.as_deref() - ), - "kind": "discover", - "catalogKey": catalog.key - })); + let catalog_key = request.catalog_key.as_deref()?; + for addon in &request.addons { + let Some(transport_url) = addon_transport_url(addon) else { + continue; + }; + for catalog in addon_catalogs(addon) { + let Some(content_type) = catalog.get("type").and_then(Value::as_str) else { + continue; + }; + let Some(id) = catalog.get("id").and_then(Value::as_str) else { + continue; + }; + let key = format!( + "discover:{}:{}:{}", + stable_feed_part(transport_url), + stable_feed_part(content_type), + stable_feed_part(id), + ); + if key != catalog_key { + continue; + } + let extra = request + .extra + .iter() + .filter(|(name, _)| catalog_supports_extra(&catalog, name)) + .map(|(name, value)| (name.clone(), value.clone())) + .collect::>(); + let extra = (!extra.is_empty()).then(|| Value::Object(extra).to_string()); + requests.push(json!({ + "url": build_resource_url(transport_url, "catalog", content_type, id, extra.as_deref()), + "kind": "discover", + "catalogKey": key + })); + break; + } + if !requests.is_empty() { + break; + } } } "metaDetail" => { @@ -545,42 +567,6 @@ fn search_category_name(addon: &Value, catalog: &Value, content_type: &str) -> S format!("{addon_name} - {catalog_name}") } -struct DiscoverCatalog { - key: String, - transport_url: String, - content_type: String, - id: String, - supports_genre: bool, -} - -fn discover_catalog_options(addons: &[Value], selected_type: &str) -> Vec { - let mut options = Vec::new(); - for addon in addons { - let Some(transport_url) = addon_transport_url(addon) else { - continue; - }; - for catalog in addon_catalogs(addon) { - let Some(content_type) = catalog.get("type").and_then(Value::as_str) else { - continue; - }; - let Some(id) = catalog.get("id").and_then(Value::as_str) else { - continue; - }; - if !selected_type.is_empty() && content_type != selected_type { - continue; - } - options.push(DiscoverCatalog { - key: format!("{}:{}", transport_url, id), - transport_url: transport_url.to_string(), - content_type: content_type.to_string(), - id: id.to_string(), - supports_genre: catalog_supports_extra(&catalog, "genre"), - }); - } - } - options -} - fn playback_title(meta: Option<&Value>, episode: Option<&Value>, stream: &Value) -> Value { let content_title = meta .and_then(|value| value.get("name")) diff --git a/src/search_plan.rs b/src/search_plan.rs index 0af68db..280a2fe 100644 --- a/src/search_plan.rs +++ b/src/search_plan.rs @@ -131,47 +131,36 @@ fn discover_catalog_label(raw_name: Option<&str>, id: &str) -> String { } } -fn catalog_extra_options(catalog: &Value, extra_name: &str) -> Vec { +fn catalog_extras(catalog: &Value) -> Vec { catalog .get("extra") .and_then(Value::as_array) .into_iter() .flatten() - .filter(|extra| { - extra - .get("name") - .and_then(Value::as_str) - .is_some_and(|name| name.eq_ignore_ascii_case(extra_name)) - }) - .flat_map(|extra| { - extra + .filter_map(|extra| { + let name = extra.get("name").and_then(Value::as_str)?.trim(); + if name.is_empty() { + return None; + } + let options = extra .get("options") .and_then(Value::as_array) - .cloned() - .unwrap_or_default() + .into_iter() + .flatten() + .filter_map(|option| option.as_str().map(str::trim).map(str::to_string)) + .filter(|option| !option.is_empty()) + .collect::>(); + (!options.is_empty()).then(|| { + json!({ + "name": name, + "options": options, + "isRequired": catalog_requires_extra(catalog, name) + }) + }) }) - .filter_map(|value| value.as_str().map(str::trim).map(str::to_string)) - .filter(|value| !value.is_empty()) .collect() } -fn catalog_genres(catalog: &Value) -> Vec { - let mut genres = catalog - .get("genres") - .and_then(Value::as_array) - .into_iter() - .flatten() - .filter_map(|value| value.as_str().map(str::trim).map(str::to_string)) - .filter(|value| !value.is_empty()) - .collect::>(); - for option in catalog_extra_options(catalog, "genre") { - if !genres.contains(&option) { - genres.push(option); - } - } - genres -} - fn manifest_supports_catalog(manifest: &Value) -> bool { serde_json::to_string(manifest) .ok() @@ -276,11 +265,6 @@ pub(crate) fn discover_catalog_options_json( continue; } let transport_url = addon_transport_url(&addon); - let source_key = manifest - .get("id") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .unwrap_or(transport_url); for catalog in manifest .get("catalogs") .and_then(Value::as_array) @@ -304,24 +288,28 @@ pub(crate) fn discover_catalog_options_json( else { continue; }; - if catalog_has_required_extra_except(catalog, &["genre"]) { + let extras = catalog_extras(catalog); + let supported_extra_names = extras + .iter() + .filter_map(|extra| extra.get("name").and_then(Value::as_str)) + .collect::>(); + if catalog_has_required_extra_except(catalog, &supported_extra_names) { continue; } - let label = discover_catalog_label(catalog.get("name").and_then(Value::as_str), id); - let genres = catalog_genres(catalog); + let catalog_label = + discover_catalog_label(catalog.get("name").and_then(Value::as_str), id); options.push(json!({ "key": format!( "discover:{}:{}:{}", - content_identity::stable_feed_part(source_key), + content_identity::stable_feed_part(transport_url), content_identity::stable_feed_part(type_value), - content_identity::stable_feed_part(&label) + content_identity::stable_feed_part(id) ), - "label": label, + "label": format!("{}: {}", addon_manifest_name(&addon), catalog_label), "transportUrl": transport_url, "type": type_value, "id": id, - "genres": genres, - "requiresGenre": catalog_requires_extra(catalog, "genre") + "extras": extras })); } } diff --git a/tests/wire/core_invoke_methods.txt b/tests/wire/core_invoke_methods.txt index b3e27a4..6e58c5c 100644 --- a/tests/wire/core_invoke_methods.txt +++ b/tests/wire/core_invoke_methods.txt @@ -26,7 +26,6 @@ detailSeasonLoadPlan detailSeriesLookupId detectAnimePlayback discoverCatalogOptions -discoverSortPlan effectiveMetadataFeedSelection engine.completeEffect engine.create