feat(meta,app,android,ios,cli): film backdrops and logos from TMDB by id

TVDB has no films, so ani.zip has no fanart or logo for one, and most of
the titles in a real library with nothing wide to draw were films. ani.zip
does name the TMDB entry of every AniList match. With a TMDB key stored
and AniList chosen, an entry left without a backdrop or a logo is asked
for /{movie,tv}/{id}/images: one request, by id, so no title name goes to
TMDB. A bare backdrop and a PNG logo in the viewer's language are
preferred, and a failure there only costs the art.

The TMDB key field is now offered with either provider, labelled optional
with AniList. TMDB as the provider fetches its logo the same way, and
backdrops at w1280 rather than w780. A forced run re-enriches hand-picked
titles too, so saving a key and matching everything again reaches them.
This commit is contained in:
nvrl committed 2026-10-29 16:50:01 +01:00
1 parent 70952fa7ca
commit a7b87cdc79
15 files changed
+413 -89

No files matched your search

+8 -7
View File
@@ -460,7 +460,7 @@ impl Enrichment {
/// Resolve a service for `config`, if one can be had.
fn build(config: MetadataConfig) -> Self {
let service = config.enabled.then(|| {
let key = pstr_meta::settings::api_key(config.provider);
let key = pstr_meta::settings::api_key(ProviderId::Tmdb);
MetadataService::new(&config, key)
.inspect_err(|error| tracing::debug!("metadata provider unavailable: {error}"))
.ok()
@@ -508,12 +508,13 @@ impl Engine {
Appearance::default()
});
// A provider with an API key needs it out of the OS credential store,
// and that can be a D-Bus round trip to a keyring that has to unlock
// first — not something to hold the first frame for. Built on the
// blocking pool instead; until it lands, everything that needs the
// provider waits for it rather than concluding there is none.
let deferred = config.enabled && config.provider.needs_api_key();
// The TMDB key, which every provider looks for, comes out of the OS
// credential store, and that can be a D-Bus round trip to a keyring
// that has to unlock first — not something to hold the first frame
// for. Built on the blocking pool instead; until it lands, everything
// that needs the provider waits for it rather than concluding there is
// none.
let deferred = config.enabled;
let enrichment = if deferred {
Enrichment {
config: config.clone(),
+25 -18
View File
@@ -901,28 +901,35 @@ fn metadata_form(
}
}
// TMDB's is the only key there is: the provider's with TMDB chosen,
// and with AniList, the art of films TVDB has none for.
ui.add_space(theme::space::L);
if settings.provider.needs_api_key() {
ui.add_space(theme::space::L);
ui.label(ui::muted(format!("{} API key", settings.provider.label())));
ui.horizontal(|ui| {
ui.add(
egui::TextEdit::singleline(api_key)
.password(true)
.hint_text("v3 API key")
.desired_width(300.0),
);
if ui.button("Save key").clicked() {
actions.push(Action::SetApiKey {
provider: settings.provider,
key: std::mem::take(api_key),
});
}
});
ui.label(ui::muted("TMDB API key"));
} else {
ui.label(ui::muted(
"Stored in your system keyring, never in a config file. Leave the box \
empty and save to forget it.",
"TMDB API key, optional. With one, films get TMDB's backdrops and logos, \
asked for by the film's TMDB id alone.",
));
}
ui.horizontal(|ui| {
ui.add(
egui::TextEdit::singleline(api_key)
.password(true)
.hint_text("v3 API key")
.desired_width(300.0),
);
if ui.button("Save key").clicked() {
actions.push(Action::SetApiKey {
provider: ProviderId::Tmdb,
key: std::mem::take(api_key),
});
}
});
ui.label(ui::muted(
"Stored in your system keyring, never in a config file. Leave the box \
empty and save to forget it.",
));
ui.add_space(theme::space::L);
ui.horizontal(|ui| {