Plan Discover per selected catalog

This commit is contained in:
KhooLy 2026-07-10 13:53:39 +03:00
parent 83df69a7dd
commit c2b0e610ec
5 changed files with 76 additions and 102 deletions

View file

@ -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")?,

View file

@ -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,

View file

@ -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<String>,
#[serde(default)]
catalog_key: Option<String>,
#[serde(default)]
id: Option<String>,
#[serde(default)]
request_ids: Vec<String>,
@ -150,25 +153,44 @@ pub(crate) fn resource_fetch_plan_json(request_json: &str) -> Option<String> {
}
}
"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::<Map<_, _>>();
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<DiscoverCatalog> {
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"))

View file

@ -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<String> {
fn catalog_extras(catalog: &Value) -> Vec<Value> {
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::<Vec<_>>();
(!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<String> {
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::<Vec<_>>();
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::<Vec<_>>();
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
}));
}
}

View file

@ -26,7 +26,6 @@ detailSeasonLoadPlan
detailSeriesLookupId
detectAnimePlayback
discoverCatalogOptions
discoverSortPlan
effectiveMetadataFeedSelection
engine.completeEffect
engine.create