use std::collections::BTreeMap;
use serde::Serialize;
use nako_addon_protocol::AddonSecretReferenceFieldDeclaration;
use crate::config::{AvFieldPolicyPreset, ProviderConfig, ProviderId};
use crate::engine::{
ProviderExternalIdCapability, ProviderFieldPolicy, ProviderFieldQualityDescriptor,
QueryExternalIdAlias,
};
use crate::providers::{
render_drift::ProviderRenderDriftCaseDescriptor, rendered_page::RenderedPageSupportConfig,
};
use crate::{Config, providers::MetadataProvider};
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ProviderDescriptor {
pub id: &'static str,
pub enabled: bool,
pub available: bool,
pub capabilities: Vec<&'static str>,
pub status: ProviderStatus,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderStatus {
Ready,
Disabled,
Unavailable,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct ProviderDiagnostics {
pub supported: Vec<ProviderDescriptor>,
pub enabled: Vec<&'static str>,
pub disabled: Vec<&'static str>,
pub unavailable: Vec<&'static str>,
pub network_policy: BTreeMap<&'static str, bool>,
}
pub struct ProviderRegistry {
config: Config,
catalog: Vec<ProviderCatalogEntry>,
}
pub struct ProviderAssembly {
pub providers: Vec<Box<dyn MetadataProvider>>,
pub diagnostics: ProviderDiagnostics,
}
#[derive(Clone, Copy)]
pub(crate) struct ProviderRenderedPageSupport {
config: for<'a> fn(&'a ProviderConfig) -> Option<&'a RenderedPageSupportConfig>,
}
impl ProviderRenderedPageSupport {
#[must_use]
pub(crate) const fn new(
config: for<'a> fn(&'a ProviderConfig) -> Option<&'a RenderedPageSupportConfig>,
) -> Self {
Self { config }
}
#[must_use]
fn proxy_policy_configured(self, provider_config: &ProviderConfig) -> bool {
(self.config)(provider_config)
.is_some_and(RenderedPageSupportConfig::proxy_policy_configured)
}
#[must_use]
fn session_key_configured(self, provider_config: &ProviderConfig) -> bool {
(self.config)(provider_config)
.is_some_and(RenderedPageSupportConfig::session_key_configured)
}
}
const TITLE_FIELDS: &[&str] = &["title", "original_title", "sort_title"];
const OUTLINE_FIELDS: &[&str] = &["overview", "outline", "tagline"];
const TAG_FIELDS: &[&str] = &["genres", "tags", "tag"];
const RELEASE_FIELDS: &[&str] = &["release_date", "release"];
const RUNTIME_FIELDS: &[&str] = &["runtime_minutes", "runtime"];
const DIRECTOR_FIELDS: &[&str] = &["directors", "director"];
const SERIES_FIELDS: &[&str] = &["series"];
const STUDIO_FIELDS: &[&str] = &["studio"];
const PUBLISHER_FIELDS: &[&str] = &["publisher", "maker", "label"];
const ACTOR_FIELDS: &[&str] = &["actors", "actor", "all_actors"];
const WANTED_FIELDS: &[&str] = &["wanted_count", "wanted"];
const SCORE_FIELDS: &[&str] = &["community_score_milli", "score"];
const SCORE_VOTE_FIELDS: &[&str] = &["community_vote_count"];
const THUMB_FIELDS: &[&str] = &["thumb_url", "thumb"];
const POSTER_FIELDS: &[&str] = &["poster", "backdrop", "artwork"];
const EXTRAFANART_FIELDS: &[&str] = &["extrafanart_urls", "extrafanart"];
const TRAILER_FIELDS: &[&str] = &["trailer_url", "trailer"];
#[derive(Clone, Copy)]
pub(crate) struct ProviderDefaultFieldPreference {
order: u16,
fields: &'static [&'static str],
}
impl ProviderDefaultFieldPreference {
#[must_use]
const fn new(order: u16, fields: &'static [&'static str]) -> Self {
Self { order, fields }
}
#[must_use]
pub(crate) const fn title(order: u16) -> Self {
Self::new(order, TITLE_FIELDS)
}
#[must_use]
pub(crate) const fn outline(order: u16) -> Self {
Self::new(order, OUTLINE_FIELDS)
}
#[must_use]
pub(crate) const fn tags(order: u16) -> Self {
Self::new(order, TAG_FIELDS)
}
#[must_use]
pub(crate) const fn release(order: u16) -> Self {
Self::new(order, RELEASE_FIELDS)
}
#[must_use]
pub(crate) const fn runtime(order: u16) -> Self {
Self::new(order, RUNTIME_FIELDS)
}
#[must_use]
pub(crate) const fn directors(order: u16) -> Self {
Self::new(order, DIRECTOR_FIELDS)
}
#[must_use]
pub(crate) const fn series(order: u16) -> Self {
Self::new(order, SERIES_FIELDS)
}
#[must_use]
pub(crate) const fn studio(order: u16) -> Self {
Self::new(order, STUDIO_FIELDS)
}
#[must_use]
pub(crate) const fn publisher(order: u16) -> Self {
Self::new(order, PUBLISHER_FIELDS)
}
#[must_use]
pub(crate) const fn actors(order: u16) -> Self {
Self::new(order, ACTOR_FIELDS)
}
#[must_use]
pub(crate) const fn wanted(order: u16) -> Self {
Self::new(order, WANTED_FIELDS)
}
#[must_use]
pub(crate) const fn score(order: u16) -> Self {
Self::new(order, SCORE_FIELDS)
}
#[must_use]
pub(crate) const fn score_votes(order: u16) -> Self {
Self::new(order, SCORE_VOTE_FIELDS)
}
#[must_use]
pub(crate) const fn thumb(order: u16) -> Self {
Self::new(order, THUMB_FIELDS)
}
#[must_use]
pub(crate) const fn poster(order: u16) -> Self {
Self::new(order, POSTER_FIELDS)
}
#[must_use]
pub(crate) const fn extrafanart(order: u16) -> Self {
Self::new(order, EXTRAFANART_FIELDS)
}
#[must_use]
pub(crate) const fn trailer(order: u16) -> Self {
Self::new(order, TRAILER_FIELDS)
}
#[must_use]
fn order(self) -> u16 {
self.order
}
#[must_use]
fn fields(self) -> &'static [&'static str] {
self.fields
}
}
impl ProviderRegistry {
#[must_use]
pub fn from_config(config: Config) -> Self {
Self::with_catalog(config, super::provider_catalog())
}
fn with_catalog(config: Config, catalog: Vec<ProviderCatalogEntry>) -> Self {
Self { config, catalog }
}
#[must_use]
pub fn catalog() -> Vec<ProviderCatalogEntry> {
super::provider_catalog()
}
#[must_use]
pub fn provider_schema_properties(
config: &Config,
) -> serde_json::Map<String, serde_json::Value> {
Self::catalog()
.into_iter()
.map(|entry| {
(
entry.id.as_str().to_owned(),
serde_json::json!({
"type": "boolean",
"default": config.provider_enabled(entry.id)
}),
)
})
.collect()
}
#[must_use]
pub fn secret_reference_fields(config: &Config) -> Vec<AddonSecretReferenceFieldDeclaration> {
Self::catalog()
.into_iter()
.filter(|entry| config.provider_enabled(entry.id))
.filter_map(|entry| entry.secret_reference)
.collect()
}
#[must_use]
pub fn rendered_page_proxy_policy_configured(config: &Config) -> bool {
Self::catalog().into_iter().any(|entry| {
let Some(support) = entry.rendered_page_support else {
return false;
};
config
.provider_config(entry.id)
.is_some_and(|provider_config| support.proxy_policy_configured(provider_config))
})
}
#[must_use]
pub fn rendered_page_session_key_configured(config: &Config) -> bool {
Self::catalog().into_iter().any(|entry| {
let Some(support) = entry.rendered_page_support else {
return false;
};
config
.provider_config(entry.id)
.is_some_and(|provider_config| support.session_key_configured(provider_config))
})
}
#[must_use]
pub fn external_id_aliases(&self) -> Vec<QueryExternalIdAlias> {
self.catalog
.iter()
.flat_map(|entry| {
entry
.external_id_capabilities
.iter()
.flat_map(|capability| {
capability.top_level_fields.iter().map(|top_level_field| {
QueryExternalIdAlias::new(
*top_level_field,
capability.provider,
capability.reject_non_positive_numeric,
)
})
})
})
.collect()
}
#[must_use]
pub fn external_id_capabilities(&self) -> Vec<ProviderExternalIdCapability> {
self.catalog
.iter()
.flat_map(|entry| entry.external_id_capabilities.iter().copied())
.collect()
}
#[must_use]
pub fn provider_field_policy(preset: AvFieldPolicyPreset) -> ProviderFieldPolicy {
match preset {
AvFieldPolicyPreset::Default => Self::default_av_provider_field_policy(),
AvFieldPolicyPreset::QualityScores => Self::quality_score_provider_field_policy(),
AvFieldPolicyPreset::None => ProviderFieldPolicy::default(),
}
}
#[must_use]
pub fn quality_score_provider_field_policy() -> ProviderFieldPolicy {
ProviderFieldPolicy::from_provider_field_quality_descriptors(
Self::catalog()
.into_iter()
.map(|entry| (entry.id.as_str(), entry.field_quality)),
)
}
#[must_use]
fn default_av_provider_field_policy() -> ProviderFieldPolicy {
let mut provider_preferences =
BTreeMap::<&'static str, Vec<(u16, usize, &'static str)>>::new();
for (catalog_index, entry) in Self::catalog().into_iter().enumerate() {
for preference in entry.default_field_preferences {
for field in preference.fields() {
let providers = provider_preferences.entry(*field).or_default();
if !providers
.iter()
.any(|(_, _, provider)| provider.eq_ignore_ascii_case(entry.id.as_str()))
{
providers.push((preference.order(), catalog_index, entry.id.as_str()));
}
}
}
}
let preferences = provider_preferences
.into_iter()
.map(|(field, mut providers)| {
providers
.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
(
field,
providers
.into_iter()
.map(|(_, _, provider)| provider)
.collect::<Vec<_>>(),
)
})
.collect::<Vec<_>>();
ProviderFieldPolicy::from_field_provider_preferences(preferences)
}
#[must_use]
pub fn providers(&self) -> Vec<Box<dyn MetadataProvider>> {
self.assemble().providers
}
#[must_use]
pub fn diagnostics(&self) -> ProviderDiagnostics {
self.assemble().diagnostics
}
#[must_use]
pub fn assemble(&self) -> ProviderAssembly {
let mut providers = Vec::new();
let mut supported = Vec::new();
let mut network_policy = BTreeMap::new();
for entry in &self.catalog {
if let Some(key) = entry.network_policy_key {
let configured = self
.config
.provider_config(entry.id)
.is_some_and(|provider| (entry.proxy_configured)(provider));
network_policy.insert(key, configured);
}
let enabled = self.config.provider_enabled(entry.id);
let status = if enabled {
match (entry.build)(&self.config) {
ProviderBuildStatus::Ready(provider) => {
providers.push(provider);
ProviderStatus::Ready
}
ProviderBuildStatus::Unavailable => ProviderStatus::Unavailable,
}
} else {
ProviderStatus::Disabled
};
supported.push(ProviderDescriptor {
id: entry.id.as_str(),
enabled,
available: status == ProviderStatus::Ready,
capabilities: entry.capabilities.to_vec(),
status,
});
}
let enabled = supported
.iter()
.filter(|provider| provider.enabled && provider.available)
.map(|provider| provider.id)
.collect();
let disabled = supported
.iter()
.filter(|provider| provider.status == ProviderStatus::Disabled)
.map(|provider| provider.id)
.collect();
let unavailable = supported
.iter()
.filter(|provider| provider.status == ProviderStatus::Unavailable)
.map(|provider| provider.id)
.collect();
let diagnostics = ProviderDiagnostics {
supported,
enabled,
disabled,
unavailable,
network_policy,
};
ProviderAssembly {
providers,
diagnostics,
}
}
}
#[derive(Clone)]
pub struct ProviderCatalogEntry {
pub(crate) id: ProviderId,
pub(crate) default_enabled: bool,
pub(crate) enabled_env_var: &'static str,
pub(crate) capabilities: &'static [&'static str],
pub(crate) field_quality: ProviderFieldQualityDescriptor,
pub(crate) default_field_preferences: &'static [ProviderDefaultFieldPreference],
pub(crate) secret_reference: Option<AddonSecretReferenceFieldDeclaration>,
pub(crate) external_id_capabilities: &'static [ProviderExternalIdCapability],
pub(crate) load_config: for<'a> fn(ProviderConfigInput<'a>) -> ProviderConfig,
pub(crate) proxy_configured: fn(&ProviderConfig) -> bool,
pub(crate) network_policy_key: Option<&'static str>,
pub(crate) rendered_page_support: Option<ProviderRenderedPageSupport>,
pub(crate) render_drift_case: Option<ProviderRenderDriftCaseDescriptor>,
pub(crate) build: fn(&Config) -> ProviderBuildStatus,
}
pub enum ProviderBuildStatus {
Ready(Box<dyn MetadataProvider>),
Unavailable,
}
pub struct ProviderConfigInput<'a> {
pub(crate) enabled: bool,
pub(crate) lookup: &'a mut dyn FnMut(&str) -> Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{
AvFieldPolicyPreset, BangumiProviderConfig, BrowserWorkerProviderConfig, ProviderConfig,
ThePornDbProviderConfig, TmdbProviderConfig,
};
use crate::engine::ExternalIdValueKind;
#[test]
fn registry_builds_enabled_available_providers() {
let registry = ProviderRegistry::from_config(Config::default());
let providers = registry.providers();
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].id(), ProviderId::Fixture);
}
#[test]
fn registry_builds_quality_score_av_field_policy_from_provider_quality_descriptors() {
let policy = ProviderRegistry::provider_field_policy(AvFieldPolicyPreset::QualityScores);
assert_eq!(
policy.providers_for("title"),
&[
"theporndb".to_owned(),
"prestige".to_owned(),
"caribbean".to_owned(),
"1pondo".to_owned(),
"10musume".to_owned(),
"dmm".to_owned(),
"xcity".to_owned(),
"mgstage".to_owned(),
"javdb".to_owned(),
"fc2".to_owned(),
"jav321".to_owned(),
"fc2ppvdb".to_owned(),
"airav".to_owned(),
"javbus".to_owned(),
"avsox".to_owned(),
"javlibrary".to_owned(),
]
);
assert_eq!(
policy.providers_for("actors"),
&[
"theporndb".to_owned(),
"javlibrary".to_owned(),
"javdb".to_owned(),
"jav321".to_owned(),
"dmm".to_owned(),
"fc2ppvdb".to_owned(),
"mgstage".to_owned(),
"prestige".to_owned(),
"airav".to_owned(),
"caribbean".to_owned(),
"1pondo".to_owned(),
"10musume".to_owned(),
"javbus".to_owned(),
"fc2".to_owned(),
"avsox".to_owned(),
"xcity".to_owned(),
]
);
assert_eq!(
policy.providers_for("score"),
policy.providers_for("actors")
);
assert_eq!(
policy.providers_for("community_score_milli"),
policy.providers_for("actors")
);
assert_eq!(
policy.providers_for("trailer_url"),
&[
"theporndb".to_owned(),
"prestige".to_owned(),
"caribbean".to_owned(),
"1pondo".to_owned(),
"10musume".to_owned(),
"mgstage".to_owned(),
"dmm".to_owned(),
"javdb".to_owned(),
"fc2ppvdb".to_owned(),
"fc2".to_owned(),
"jav321".to_owned(),
"xcity".to_owned(),
"javbus".to_owned(),
"airav".to_owned(),
]
);
}
#[test]
fn registry_builds_default_av_field_policy_from_supported_provider_preferences() {
let policy = ProviderRegistry::provider_field_policy(AvFieldPolicyPreset::Default);
assert_eq!(
policy.providers_for("title"),
&[
"theporndb".to_owned(),
"mgstage".to_owned(),
"dmm".to_owned(),
"javbus".to_owned(),
"jav321".to_owned(),
"javlibrary".to_owned(),
]
);
assert_eq!(
policy.providers_for("overview"),
&[
"theporndb".to_owned(),
"dmm".to_owned(),
"jav321".to_owned()
]
);
assert_eq!(
policy.providers_for("outline"),
&[
"theporndb".to_owned(),
"dmm".to_owned(),
"jav321".to_owned()
]
);
assert_eq!(
policy.providers_for("actors"),
&[
"theporndb".to_owned(),
"javbus".to_owned(),
"javlibrary".to_owned(),
"javdb".to_owned(),
]
);
assert_eq!(
policy.providers_for("actor"),
policy.providers_for("actors")
);
assert_eq!(
policy.providers_for("thumb_url"),
&["theporndb".to_owned(), "javbus".to_owned()]
);
assert_eq!(
policy.providers_for("poster"),
&["theporndb".to_owned(), "javbus".to_owned()]
);
assert_eq!(
policy.providers_for("extrafanart_urls"),
&["javbus".to_owned()]
);
assert_eq!(policy.providers_for("release_date"), &["javbus".to_owned()]);
assert_eq!(
policy.providers_for("runtime_minutes"),
&["javbus".to_owned()]
);
assert_eq!(policy.providers_for("tags"), &["javbus".to_owned()]);
assert_eq!(policy.providers_for("directors"), &["javbus".to_owned()]);
assert_eq!(policy.providers_for("series"), &["javbus".to_owned()]);
assert_eq!(policy.providers_for("studio"), &["javbus".to_owned()]);
assert_eq!(policy.providers_for("publisher"), &["javbus".to_owned()]);
assert_eq!(
policy.providers_for("trailer_url"),
&["mgstage".to_owned(), "dmm".to_owned()]
);
assert_eq!(
policy.providers_for("wanted_count"),
&["javlibrary".to_owned(), "javdb".to_owned()]
);
assert_eq!(
policy.providers_for("score"),
&[
"jav321".to_owned(),
"javlibrary".to_owned(),
"javdb".to_owned(),
]
);
assert_eq!(
policy.providers_for("community_score_milli"),
&[
"jav321".to_owned(),
"javlibrary".to_owned(),
"javdb".to_owned(),
]
);
assert_eq!(
policy.providers_for("community_vote_count"),
&["javlibrary".to_owned(), "javdb".to_owned()]
);
}
#[test]
fn registry_does_not_build_disabled_providers() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![ProviderConfig::disabled(ProviderId::Fixture)],
..Config::default()
});
assert!(registry.providers().is_empty());
}
#[test]
fn registry_reports_redaction_safe_provider_diagnostics() {
let registry = ProviderRegistry::from_config(Config::default());
let diagnostics = registry.diagnostics();
assert_eq!(diagnostics.enabled, vec!["fixture"]);
assert_eq!(
diagnostics.disabled,
vec![
"tmdb",
"bangumi",
"browser_worker",
"douban",
"javdb",
"dmm",
"xcity",
"fc2",
"fc2ppvdb",
"caribbean",
"1pondo",
"10musume",
"jav321",
"javbus",
"javlibrary",
"airav",
"avsox",
"mgstage",
"prestige",
"theporndb",
"anilist"
]
);
assert!(diagnostics.unavailable.is_empty());
assert_eq!(
diagnostics.supported[0],
ProviderDescriptor {
id: "fixture",
enabled: true,
available: true,
capabilities: vec!["metadata_suggestion"],
status: ProviderStatus::Ready,
}
);
assert_eq!(
diagnostics.supported[1],
ProviderDescriptor {
id: "tmdb",
enabled: false,
available: false,
capabilities: vec!["metadata_suggestion", "movie_search", "tv_search"],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[2],
ProviderDescriptor {
id: "bangumi",
enabled: false,
available: false,
capabilities: vec!["metadata_suggestion", "subject_search", "anime_search"],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[3],
ProviderDescriptor {
id: "browser_worker",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"rendered_page_extraction",
"rendered_page_recipe",
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[4],
ProviderDescriptor {
id: "douban",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"movie_search",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[5],
ProviderDescriptor {
id: "javdb",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"javdb_movie_search",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[6],
ProviderDescriptor {
id: "dmm",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"dmm_direct_lookup",
"dmm_movie_search",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[7],
ProviderDescriptor {
id: "xcity",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"xcity_movie_search",
"xcity_direct_url",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[8],
ProviderDescriptor {
id: "fc2",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"fc2_direct_lookup",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[9],
ProviderDescriptor {
id: "fc2ppvdb",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"fc2ppvdb_direct_lookup",
"fc2_long_tail",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[10],
ProviderDescriptor {
id: "caribbean",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"caribbean_direct_lookup",
"official_uncensored",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[11],
ProviderDescriptor {
id: "1pondo",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"1pondo_direct_lookup",
"official_uncensored",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[12],
ProviderDescriptor {
id: "10musume",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"10musume_direct_lookup",
"official_uncensored",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[13],
ProviderDescriptor {
id: "jav321",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"jav321_direct_lookup",
"jav321_post_form_search",
"raw_html_parse"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[14],
ProviderDescriptor {
id: "javbus",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"javbus_direct_lookup",
"javbus_movie_search",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[15],
ProviderDescriptor {
id: "javlibrary",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"javlibrary_direct_lookup",
"javlibrary_movie_search",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[16],
ProviderDescriptor {
id: "airav",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"airav_movie_search",
"airav_direct_url",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[17],
ProviderDescriptor {
id: "avsox",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"avsox_movie_search",
"avsox_direct_url",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[18],
ProviderDescriptor {
id: "mgstage",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_direct_lookup",
"mgstage_direct_lookup",
"mgstage_amateur_route",
"browser_worker_rendered_html"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[19],
ProviderDescriptor {
id: "prestige",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"prestige_direct_lookup",
"prestige_movie_search",
"prestige_official_api"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[20],
ProviderDescriptor {
id: "theporndb",
enabled: false,
available: false,
capabilities: vec![
"metadata_suggestion",
"av_number_search",
"theporndb_scene_search",
"theporndb_direct_lookup",
"theporndb_scene_hash_lookup",
"theporndb_official_api"
],
status: ProviderStatus::Disabled,
}
);
assert_eq!(
diagnostics.supported[21],
ProviderDescriptor {
id: "anilist",
enabled: false,
available: false,
capabilities: vec!["metadata_suggestion", "anime_search", "graphql_api"],
status: ProviderStatus::Disabled,
}
);
}
#[test]
fn registry_exposes_provider_external_id_capabilities() {
let registry = ProviderRegistry::from_config(Config::default());
let capabilities = registry.external_id_capabilities();
assert!(capabilities.iter().any(|capability| {
capability.provider == "tmdb"
&& capability.value_kind == ExternalIdValueKind::Numeric
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"tmdb_id")
&& capability.reject_non_positive_numeric
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "imdb"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"imdb_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "browser_worker"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"browser_worker_url")
&& !capability.reject_non_positive_numeric
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "browser_worker_recipe"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability
.top_level_fields
.contains(&"browser_worker_recipe_url")
&& !capability.reject_non_positive_numeric
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "anilist"
&& capability.value_kind == ExternalIdValueKind::Numeric
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"anilist_id")
&& capability.reject_non_positive_numeric
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "mal"
&& capability.value_kind == ExternalIdValueKind::Numeric
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"mal_id")
&& capability.reject_non_positive_numeric
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "douban"
&& capability.value_kind == ExternalIdValueKind::Numeric
&& !capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.is_empty()
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "javdb"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"javdb_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "dmm"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"dmm_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "dmm_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"dmm_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "xcity_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"xcity_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "av_number"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"av_number")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "fc2"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"fc2_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "fc2ppvdb"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"fc2ppvdb_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "fc2ppvdb_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"fc2ppvdb_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "caribbean"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"caribbean_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "caribbean_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"caribbean_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "1pondo"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"1pondo_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "1pondo_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"1pondo_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "10musume"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"10musume_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "10musume_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"10musume_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "jav321"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"jav321_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "jav321_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"jav321_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "javbus"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"javbus_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "javbus_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"javbus_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "javlibrary"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"javlibrary_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "javlibrary_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"javlibrary_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "airav_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"airav_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "avsox_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"avsox_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "mgstage"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"mgstage_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "mgstage_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"mgstage_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "prestige"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"prestige_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "prestige_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"prestige_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "theporndb"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"theporndb_id")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "theporndb_url"
&& capability.value_kind == ExternalIdValueKind::Url
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"theporndb_url")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "file_oshash"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"file_oshash")
}));
assert!(capabilities.iter().any(|capability| {
capability.provider == "file_phash"
&& capability.value_kind == ExternalIdValueKind::Opaque
&& capability.accepts_direct_lookup
&& capability.emits
&& capability.top_level_fields.contains(&"file_phash")
}));
}
#[test]
fn registry_derives_legacy_external_id_aliases_from_capabilities() {
let registry = ProviderRegistry::from_config(Config::default());
let aliases = registry.external_id_aliases();
assert!(aliases.contains(&QueryExternalIdAlias::new("tmdb_id", "tmdb", true)));
assert!(aliases.contains(&QueryExternalIdAlias::new("imdb_id", "imdb", true)));
assert!(aliases.contains(&QueryExternalIdAlias::new("bangumi_id", "bangumi", true)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"browser_worker_url",
"browser_worker",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"browser_worker_recipe_url",
"browser_worker_recipe",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("anilist_id", "anilist", true)));
assert!(aliases.contains(&QueryExternalIdAlias::new("mal_id", "mal", true)));
assert!(!aliases.iter().any(|alias| alias.provider == "douban"));
assert!(aliases.contains(&QueryExternalIdAlias::new("av_number", "av_number", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("javdb_id", "javdb", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("dmm_id", "dmm", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("dmm_url", "dmm_url", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("xcity_id", "xcity", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("xcity_url", "xcity_url", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("fc2_id", "fc2", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("fc2ppvdb_id", "fc2ppvdb", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"fc2ppvdb_url",
"fc2ppvdb_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"caribbean_id",
"caribbean",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"caribbean_url",
"caribbean_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("1pondo_id", "1pondo", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"1pondo_url",
"1pondo_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("10musume_id", "10musume", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"10musume_url",
"10musume_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("jav321_id", "jav321", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"jav321_url",
"jav321_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("javbus_id", "javbus", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"javbus_url",
"javbus_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"javlibrary_id",
"javlibrary",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"javlibrary_url",
"javlibrary_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("airav_id", "airav", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("airav_url", "airav_url", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("avsox_id", "avsox", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("avsox_url", "avsox_url", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new("mgstage_id", "mgstage", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"mgstage_url",
"mgstage_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new("prestige_id", "prestige", false)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"prestige_url",
"prestige_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"theporndb_id",
"theporndb",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"theporndb_url",
"theporndb_url",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"file_oshash",
"file_oshash",
false
)));
assert!(aliases.contains(&QueryExternalIdAlias::new(
"file_phash",
"file_phash",
false
)));
}
#[test]
fn registry_reports_disabled_provider_diagnostics() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![ProviderConfig::disabled(ProviderId::Fixture)],
..Config::default()
});
let diagnostics = registry.diagnostics();
assert!(diagnostics.enabled.is_empty());
assert_eq!(
diagnostics.disabled,
vec![
"fixture",
"tmdb",
"bangumi",
"browser_worker",
"douban",
"javdb",
"dmm",
"xcity",
"fc2",
"fc2ppvdb",
"caribbean",
"1pondo",
"10musume",
"jav321",
"javbus",
"javlibrary",
"airav",
"avsox",
"mgstage",
"prestige",
"theporndb",
"anilist"
]
);
assert!(diagnostics.unavailable.is_empty());
assert_eq!(diagnostics.supported[0].status, ProviderStatus::Disabled);
}
#[test]
fn registry_reports_unavailable_provider_diagnostics_without_building_it() {
fn unavailable_provider(_config: &Config) -> ProviderBuildStatus {
ProviderBuildStatus::Unavailable
}
let mut entry = crate::providers::fixture::catalog_entry();
entry.build = unavailable_provider;
let registry = ProviderRegistry::with_catalog(Config::default(), vec![entry]);
let diagnostics = registry.diagnostics();
assert!(registry.providers().is_empty());
assert!(diagnostics.enabled.is_empty());
assert!(diagnostics.disabled.is_empty());
assert_eq!(diagnostics.unavailable, vec!["fixture"]);
assert_eq!(diagnostics.supported[0].status, ProviderStatus::Unavailable);
}
#[test]
fn registry_reports_enabled_tmdb_without_token_as_unavailable() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![
ProviderConfig::disabled(ProviderId::Fixture),
ProviderConfig::tmdb(true, TmdbProviderConfig::from_env_lookup(|_| None)),
],
..Config::default()
});
let diagnostics = registry.diagnostics();
assert!(registry.providers().is_empty());
assert!(diagnostics.enabled.is_empty());
assert_eq!(
diagnostics.disabled,
vec![
"fixture",
"bangumi",
"browser_worker",
"douban",
"javdb",
"dmm",
"xcity",
"fc2",
"fc2ppvdb",
"caribbean",
"1pondo",
"10musume",
"jav321",
"javbus",
"javlibrary",
"airav",
"avsox",
"mgstage",
"prestige",
"theporndb",
"anilist"
]
);
assert_eq!(diagnostics.unavailable, vec!["tmdb"]);
assert_eq!(diagnostics.supported[1].status, ProviderStatus::Unavailable);
}
#[test]
fn registry_reports_enabled_theporndb_without_token_as_unavailable() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![
ProviderConfig::disabled(ProviderId::Fixture),
ProviderConfig::theporndb(true, ThePornDbProviderConfig::from_env_lookup(|_| None)),
],
..Config::default()
});
let diagnostics = registry.diagnostics();
assert!(registry.providers().is_empty());
assert!(diagnostics.enabled.is_empty());
assert_eq!(diagnostics.unavailable, vec!["theporndb"]);
assert_eq!(
diagnostics
.supported
.iter()
.find(|descriptor| descriptor.id == "theporndb")
.unwrap()
.status,
ProviderStatus::Unavailable
);
}
#[test]
fn registry_builds_enabled_tmdb_when_token_is_configured() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![
ProviderConfig::disabled(ProviderId::Fixture),
ProviderConfig::tmdb(
true,
TmdbProviderConfig {
read_access_token: Some("tmdb-token".to_owned()),
api_base_url: "https://tmdb.example/3".to_owned(),
language: "en-US".to_owned(),
include_adult: false,
proxy_url: None,
},
),
],
..Config::default()
});
let providers = registry.providers();
let diagnostics = registry.diagnostics();
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].id(), ProviderId::Tmdb);
assert_eq!(diagnostics.enabled, vec!["tmdb"]);
assert!(diagnostics.unavailable.is_empty());
}
#[test]
fn registry_builds_enabled_bangumi_without_token() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![
ProviderConfig::disabled(ProviderId::Fixture),
ProviderConfig::disabled(ProviderId::Tmdb),
ProviderConfig::bangumi(true, BangumiProviderConfig::from_env_lookup(|_| None)),
],
..Config::default()
});
let providers = registry.providers();
let diagnostics = registry.diagnostics();
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].id(), ProviderId::Bangumi);
assert_eq!(diagnostics.enabled, vec!["bangumi"]);
assert!(diagnostics.unavailable.is_empty());
}
#[test]
fn registry_builds_enabled_browser_worker_with_default_base_url() {
let registry = ProviderRegistry::from_config(Config {
providers: vec![
ProviderConfig::disabled(ProviderId::Fixture),
ProviderConfig::disabled(ProviderId::Tmdb),
ProviderConfig::disabled(ProviderId::Bangumi),
ProviderConfig::browser_worker(
true,
BrowserWorkerProviderConfig::from_env_lookup(|_| None),
),
],
..Config::default()
});
let providers = registry.providers();
let diagnostics = registry.diagnostics();
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].id(), ProviderId::BrowserWorker);
assert_eq!(diagnostics.enabled, vec!["browser_worker"]);
assert!(diagnostics.unavailable.is_empty());
}
}