feat(core,meta,app,android,ios,cli): TVmaze for the shows AniList does not have

With AniList chosen, a title AniList has no match for is now searched on
TVmaze, keyless, before it is given up on. One provider for the whole library
left live-action shows in a share of anime unmatched, and switching to TMDB
would have dropped every AniList match, chain and shelf.

The service holds a chain of providers. A match is stored under the provider
that gave it, and enrichment and refreshes go to that provider; a miss is
stored under the last provider asked, and is_fresh only trusts a miss from the
end of the chain, so AniList misses stored before this are asked again at once.
A provider failure still ends the lookup instead of falling through.

The hand matcher searches every provider in the chain and labels each result
with its source. TVmaze is never offered in the settings and is not asked
about titles known to be films.
This commit is contained in:
nvrl committed 2026-10-29 16:33:16 +01:00
1 parent bb5a6638c4
commit 3ca431feca
18 files changed
+892 -195

No files matched your search

+4 -13
View File
@@ -2605,8 +2605,6 @@ impl Engine {
}
return self.emit(Event::Error(self.no_service_reason()));
};
let provider = service.provider();
let (stored, enriched) = {
let catalog = self.catalog.lock();
match catalog
@@ -2618,14 +2616,7 @@ impl Engine {
}
};
let pending = pstr_meta::service::plan(
titles,
&stored,
&enriched,
provider,
service.has_details(),
force,
);
let pending = pstr_meta::service::plan(titles, &stored, &enriched, &service.chain(), force);
if pending.is_empty() {
if automatic {
return;
@@ -2636,7 +2627,7 @@ impl Engine {
self.emit(Event::Status(format!(
"matching {} titles against {}…",
pending.len(),
provider.label()
service.label()
)));
let (mut matched, mut unmatched, mut failed) = (0usize, 0usize, 0usize);
@@ -2673,7 +2664,7 @@ impl Engine {
episodes_found += enrichment.episodes.len();
let stored = self.catalog.lock().set_enrichment(
&outcome.title_key,
provider,
outcome.provider,
now(),
enrichment,
);
@@ -2755,7 +2746,7 @@ impl Engine {
if let Some(enrichment) = &outcome.enrichment {
let stored = engine.catalog.lock().set_enrichment(
&title.key,
service.provider(),
outcome.provider,
now(),
enrichment,
);
+6 -4
View File
@@ -55,8 +55,8 @@ pub fn show(
.color(theme::text()),
);
ui.label(ui::muted(format!(
"Everything {} answers with, in its own order — nothing is scored here.",
provider.label()
"Everything {} answer with, in their own order — nothing is scored here.",
pstr_meta::service::chain_label(provider)
)));
ui.add_space(theme::space::L);
@@ -279,8 +279,9 @@ fn poster(art: &mut Art<'_>, found: &TitleMetadata) -> Option<egui::TextureHandl
.texture(&key, || engine.request_poster(key.clone(), url))
}
/// `Film · 2017 · 26 episodes · ★ 8.2`, with whatever the provider left out
/// simply absent.
/// `Film · 2017 · 26 episodes · ★ 8.2 · AniList`, with whatever the provider
/// left out simply absent. The provider last, because one search asks more
/// than one, and the same show can come back from each.
fn facts(found: &TitleMetadata) -> String {
let mut parts: Vec<String> = vec![
match found.kind {
@@ -298,5 +299,6 @@ fn facts(found: &TitleMetadata) -> String {
if let Some(rating) = found.rating {
parts.push(format!("{} {rating:.1}", egui_phosphor::regular::STAR));
}
parts.push(found.provider.label().to_string());
parts.join(" · ")
}
+3 -3
View File
@@ -874,8 +874,8 @@ fn metadata_form(
without this every tile is a pair of initials. Turning it on sends the titles \
in your library — not your files, and nothing about what you have watched — to \
the provider you choose, over HTTPS, each time a new one appears. With AniList, \
the ids it matched — not the titles — then go to ani.zip for episode names and \
wide artwork.",
a title it has nothing for is then sent to TVmaze, and the ids AniList matched — \
not the titles — go to ani.zip for episode names and wide artwork.",
));
ui.add_space(theme::space::L);
@@ -889,7 +889,7 @@ fn metadata_form(
if settings.enabled {
ui.add_space(theme::space::L);
ui.label(ui::muted("Provider"));
for provider in ProviderId::ALL {
for provider in ProviderId::CHOOSABLE {
if ui
.radio(settings.provider == provider, provider.label())
.on_hover_text(provider.description())