genre management
Publish image / publish (push) Failing after 14s

This commit is contained in:
Worph
2026-08-01 20:22:06 +02:00
parent c7299d549c
commit 631beb1141
8 changed files with 1125 additions and 16 deletions
+49 -4
View File
@@ -22,7 +22,7 @@ use meta_feeder_sdk::types::{DiscoveryRecord, GatewayError, Hash, PluginHealth};
use tracing::warn;
use crate::card::{split_record_id, Card, CardSource};
use crate::consts::DEFAULT_CARD_TOP_N;
use crate::consts::{DEFAULT_CARD_DISCOVERY_N, DEFAULT_CARD_TOP_N};
use crate::resolve::Resolver;
use crate::tmdb_budget::{TmdbBudget, DEFAULT_TMDB_BURST, DEFAULT_TMDB_REFILL_PER_SEC};
use crate::tmdb_client::TmdbClient;
@@ -39,6 +39,8 @@ pub struct TmdbConfig {
pub tmdb_burst: Option<f64>,
#[serde(default)]
pub card_top_n: Option<usize>,
#[serde(default)]
pub card_discovery_n: Option<usize>,
}
impl TmdbConfig {
@@ -51,6 +53,9 @@ impl TmdbConfig {
.and_then(|v| v.parse().ok()),
tmdb_burst: std::env::var("TMDB_BURST").ok().and_then(|v| v.parse().ok()),
card_top_n: std::env::var("CARD_TOP_N").ok().and_then(|v| v.parse().ok()),
card_discovery_n: std::env::var("CARD_DISCOVERY_N")
.ok()
.and_then(|v| v.parse().ok()),
}
}
}
@@ -62,6 +67,8 @@ pub struct TmdbCardPlugin {
/// `depends_on` is satisfied; queries just return nothing.
resolver: Option<Resolver>,
top_n: usize,
/// Cards per keyword-less catalog row (see [`DEFAULT_CARD_DISCOVERY_N`]).
discovery_n: usize,
/// Test hook: `(api_base, image_base)` overriding TMDB's real endpoints, so
/// the contract test can point the client at a wiremock server.
api_bases: Option<(String, String)>,
@@ -79,6 +86,7 @@ impl TmdbCardPlugin {
config: TmdbConfig::from_env(),
resolver: None,
top_n: DEFAULT_CARD_TOP_N,
discovery_n: DEFAULT_CARD_DISCOVERY_N,
api_bases: None,
}
}
@@ -111,6 +119,9 @@ impl TmdbCardPlugin {
if file_cfg.card_top_n.is_some() {
self.config.card_top_n = file_cfg.card_top_n;
}
if file_cfg.card_discovery_n.is_some() {
self.config.card_discovery_n = file_cfg.card_discovery_n;
}
}
}
}
@@ -142,6 +153,11 @@ impl FeederPlugin for TmdbCardPlugin {
self.load_config(cache_dir);
let cache: MidhashCache = meta_feeder_sdk::common::open_midhash_cache(cache_dir, "tmdb")?;
self.top_n = self.config.card_top_n.filter(|n| *n > 0).unwrap_or(DEFAULT_CARD_TOP_N);
self.discovery_n = self
.config
.card_discovery_n
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_CARD_DISCOVERY_N);
let token = self.config.tmdb_token.trim().to_string();
if token.is_empty() {
@@ -189,15 +205,33 @@ impl FeederPlugin for TmdbCardPlugin {
self.served_file_types(),
self.served_content_kinds(),
) {
// One misconfiguration lands here often enough to deserve a name.
// A browse row written against the *indexer* feeder's dialect says
// `contentKind:episode`, which is not in `served_content_kinds` — so
// the row is rejected here, before TMDB is touched, and the operator
// sees an empty wall with no error anywhere. A card describes a WORK,
// and a work is a `series`, never an `episode`.
if crate::discovery::is_discovery_query(query) {
warn!(
target: "meta-share::gateway",
upstream = "tmdb",
content_kind = ?query.filters.get("contentKind"),
"discovery row rejected by the served-kinds gate: a card is a \
WORK — use contentKind:series for TV rows (not episode/tv/tvshow)"
);
}
return Ok(Vec::new());
}
let Some(resolver) = self.resolver() else {
return Ok(Vec::new()); // soft-skipped (no token)
};
// An explicit `tmdbid:` filter resolves exactly one card — the direct
// "give me this work" lookup a deep link uses. Otherwise the free text
// goes through the principal search.
// Three shapes, in precedence order:
// 1. an explicit `tmdbid:` filter — the direct "give me this work"
// lookup a deep link (or the detail page) uses;
// 2. a keyword-less catalog query (`popular:true contentKind:series`)
// — a client's home row, answered from TMDB's catalog endpoints;
// 3. free text — the principal search.
let cards: Vec<Card> = if let Some(id) = query
.filters
.get("tmdbid")
@@ -206,6 +240,9 @@ impl FeederPlugin for TmdbCardPlugin {
{
let kind = kind_hint(query, resolver, id).await;
resolver.card_by_id(id, kind).await.into_iter().collect()
} else if crate::discovery::is_discovery_query(query) {
let n = self.discovery_n.min(max_results.max(1));
crate::discovery::cards_for_discovery(resolver, query, n).await
} else {
let free_text = query.free_text.trim();
if free_text.is_empty() {
@@ -284,6 +321,13 @@ impl FeederPlugin for TmdbCardPlugin {
Blank keeps the built-in default (10). Unlike the old anchor \
top-N this costs no indexer requests — only TMDB.",
),
F::number("card_discovery_n", "Cards per discovery row").with_help(
"How many cards a keyword-less browse row (popular / trending / \
top-rated) returns. Blank keeps the built-in default (20 — \
exactly one TMDB catalog page, so a row costs ONE request). \
Separate from 'Cards per search': a row is a browse surface, \
not an answer to a question.",
),
],
}
}
@@ -294,6 +338,7 @@ impl FeederPlugin for TmdbCardPlugin {
"tmdb_rate_per_sec": self.config.tmdb_rate_per_sec,
"tmdb_burst": self.config.tmdb_burst,
"card_top_n": self.config.card_top_n,
"card_discovery_n": self.config.card_discovery_n,
})
}
}