fanarttv

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 13 Imported by: 0

README

module-fanart-tv

Mosaic's fanart.tv artwork module. It supplies posters, backdrops, clearlogos, clearart, banners, disc art and per-season art for titles another source has already identified.

MIT-licensed, its own Go module, importing only the published sdk and contracts surfaces.

What it is for

Artwork used to arrive as a by-product of asking a question about titles: whichever module described the content supplied whatever images it happened to carry. Cinemeta has a poster, a background and sometimes a logo. TMDB has more and used to discard most of it. Neither has clearart or banners at all — a gap ADR 0034 recorded as waiting on exactly this kind of source.

This module closes it, and does one thing beyond filling gaps: it returns every image fanart.tv has, as a candidate set (ADR 0074), so the Platform can choose well now and a user can choose differently later.

What it is not

It is not a metadata source and cannot become one. fanart.tv has no titles, no overviews, no years, no cast, no search and no catalogs. There is no query that turns "Blade Runner" into a result — you must already know which film you mean.

So it declares no RoleMetadata, and that restraint is enforced rather than intended: boundary_test.go fails if the manifest ever grows the role. ADR 0035 makes a registered metadata role part of the composition-root check that a deployment can identify content, and a module that cannot name a film satisfying that check would produce a Mosaic that boots and finds nothing — a failure no build and no test would report.

How it is reached

Every other source module is invoked because a search or catalog result named it in a ContentRef. This one is never named, because it produces no results.

Instead the Platform runs an artwork enrichment pass after materialising a work (ADR 0075), handing every registered artwork provider the work's shared external identities — the same mechanism ADR 0073 built for stream providers. Import exists only because v1.Capability requires it, and always refuses.

Candidates from several providers union rather than compete, so installing this alongside TMDB gives you both sets with provenance on each.

Two mappings that carry the quality

Most of what makes artwork look better than the metadata source's own is two translations that are easy to get silently wrong:

  • lang: "00" means textless, not a language. It becomes an empty Language, which is what lets the Platform prefer a backdrop with no burned-in title to sit under a hero's clearlogo. Carried through as a language code, every textless image would look foreign and the preference would never fire. This is the single most visible improvement the module makes.
  • An HD variant outranks its standard-definition twin. Likes accumulate per image, so an older SD logo frequently has more of them than the HD one that replaced it. Ranking on likes alone would systematically pick the lower resolution. It is the one ordering fanart.tv's own data cannot express.

Credentials

fanart.tv requires a project key. Mosaic's release build links one in with -ldflags -X, so a deployment has working artwork before anyone configures anything, and a user can override it in Settings › fanart.tv. An optional personal key grants early access to artwork uploaded but not yet promoted.

The bundled key is not a secret once the binary ships and nothing here pretends otherwise — a linked string is recoverable with strings. It is read-only, reaches only a public artwork index, and is revocable centrally. What it costs is a shared rate limit, which is why a user can replace it. resolveKeys is the only function that reads it; it is never written into the settings document, never rendered, and never logged.

Honest limits

  • A series needs a TVDB id. fanart.tv keys television by TVDB id and nothing else. module-tmdb binds one; module-cinemeta binds only imdb, so a series imported through Cinemeta gets no artwork from here. Not fixable in this module, and recorded in ADR 0075 rather than papered over.
  • Episode stills are not fetched. A metadata provider returns every episode's still in one call; asking here would be a request per episode for data the Platform already has. Series and season art only.
  • The artwork type table should be verified against fanart.tv's current documentation. The response-key → slot mapping is deliberately one table of data so a renamed or added type is a one-line correction. An unrecognised key is ignored rather than guessed at, so unverified coverage under-delivers rather than mis-delivers — but it is under-delivery until checked.
  • The settings screen carries the key in its action payloads. ADR 0021's configureModule replaces the whole settings document with no merge, so every control must echo the credential back. This is the same finding module-tmdb recorded, reached independently — two modules hitting it is what makes it an SDK item rather than a quirk.

Building and testing

Everything runs in the container:

docker compose -f docker-compose.test.yml run --rm test

The tests are hermetic — a fake fanart.tv over httptest, reached by rewriting the request host through the injected http.Client — so they need no network and no API key. There is no fanart.tv key CI could hold that is not somebody's, and the API base URL is a constant on purpose: a settable field so tests could point elsewhere would put a seam in the production type that only tests use.

This module requires SDK v0.21.0, which adds RoleArtwork, ArtworkProvider and the candidate types. Until that tag is pushed it builds only inside a Go workspace over the sibling sdk and sdui checkouts, and go.sum cannot be completed.

Documentation

Overview

Package fanarttv is Mosaic's fanart.tv artwork module.

It fills one content role — v1.RoleArtwork (ADR 0075) — plus its own settings screen, and what it does *not* do is the shape of it.

It illustrates content; it does not identify it

fanart.tv has no titles, no overviews, no years, no cast, no search and no catalogs. There is no query that turns "Blade Runner" into a result: you must already know which film you mean, and hand it an identifier. So this module can answer exactly one question — *what does this look like* — and none of the others.

That is why it declares no v1.RoleMetadata, and the temptation to is the specific mistake ADR 0075 exists to prevent. ADR 0035 makes a registered metadata role part of the composition-root check that a deployment can identify content; a module that cannot name a film satisfying that check would produce a Mosaic that boots and finds nothing. `boundary_test.go` asserts the manifest never grows the role.

It is reached by enrichment, never by a ref

Every other source module is invoked because a search or catalog result named it in a v1.ContentRef. This one is never named, because it produces no results. The Platform reaches it through the artwork enrichment pass it runs after materialising a work (ADR 0075), addressing it by the work's *shared* external identity — the same mechanism ADR 0073 built for stream providers.

Capability.Import therefore exists only because v1.Capability requires it, and always refuses.

What it returns is a candidate set, not an answer

The module ranks nothing and chooses nothing. It returns every image fanart.tv has as [v1.ArtworkCandidate]s carrying language and the source's own like count, and the Platform's selection rule (ADR 0074) resolves which one fills a slot — because that choice is ultimately a user's, and a module cannot hold it.

Two mappings in here are load-bearing and easy to get silently wrong:

  • **`lang: "00"` means textless, not a language.** It maps to an empty Language, which is what lets the Platform prefer a backdrop with no burned-in title to sit under a hero's clearlogo. Carried through as a language, every textless image would look foreign and the preference would never fire.
  • **An HD variant outranks its standard-definition twin.** Likes accumulate per image, so an older SD logo frequently has more of them than the HD one that replaced it. This is the one ranking fanart.tv's own data cannot express.

Honest limits

  • **A series needs a TVDB id.** fanart.tv keys television by TVDB id and nothing else. `module-tmdb` binds one; `module-cinemeta` binds only `imdb`, so a series imported through Cinemeta gets no artwork from here. Recorded in ADR 0075 rather than papered over — it is not fixable in this module.
  • **Episode stills are not fetched.** A metadata provider already returns every episode's still in one call; asking here would be a request per episode for data the Platform has in bulk. Series and season art only.
  • **The artwork type table is a guess that must be verified.** The mapping from fanart.tv's response keys to slots is data in one table precisely so that a type which has been renamed or added is a one-line correction. Check it against fanart.tv's current documentation before trusting the coverage.

Index

Constants

View Source
const (
	// CapabilityID is the id the Platform registers this module under and the id
	// stamped onto every candidate it supplies, so a set assembled from several
	// providers stays attributable (ADR 0074).
	//
	// Unlike every module before it, this id never appears in a ContentRef: this
	// module sources nothing, so nothing ever routes an import back to it.
	CapabilityID = "fanart-tv"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Capability

type Capability struct {
	// contains filtered or unexported fields
}

Capability is the fanart.tv artwork module.

It fills exactly one content role — RoleArtwork — plus its own settings screen, and the roles it does *not* fill are the whole shape of it. fanart.tv has no titles, no overviews, no years, no cast, no search and no catalogs; there is no query that turns "Blade Runner" into a result here, because you must already know which film you mean. It cannot describe content and must never claim it can (ADR 0075).

func New

func New(httpClient *http.Client) *Capability

New builds the capability over an HTTP client (nil for a default). The Platform passes its own, which carries the netguard dial guard and the outbound telemetry seam (ADR 0055).

The client is built per invocation rather than here, because the credential comes from the settings document the Platform hands in on each call and a user may change it between two of them.

func (*Capability) Artwork

Artwork resolves artwork candidates for content this module did not source (RoleArtwork, ADR 0075).

It returns an empty response and no error when it cannot address the content — a film with neither a TMDB nor an IMDb id, a series with no TVDB id, or a title fanart.tv simply has nothing for. All three are ordinary and none is a failure: erroring would make an artwork lookup able to disrupt an import, and the art it might have added is by definition an improvement on top of art the node already has.

func (*Capability) Import

Import is required by v1.Capability and is unreachable for this module.

Import materialises the item named by a ContentRef, and a ref names the capability that produced it. This module fills no read role, so it produces no refs, so nothing can name it — the method exists because the interface requires it, not because there is a path to it.

It returns an error rather than an empty success. An empty success would say "I materialised nothing, and that is fine", which would let a bug that routed an import here pass silently; an error says what is actually true.

**This is a finding, not a workaround.** v1.Capability bundles identity with the one write verb, which fits a module that sources content and does not fit one that enriches it. An enrichment-only module has to stub the verb. Worth taking to the SDK if a second such module appears.

func (*Capability) Manifest

func (c *Capability) Manifest() v1.Manifest

Manifest is the module's self-declaration, including the provider roles it fills (ADR 0027).

**The roles it does not declare matter more than the ones it does.** It does not declare RoleMetadata, and the temptation to is the specific mistake ADR 0075 exists to prevent: ADR 0035 makes a registered RoleMetadata part of the composition-root check that Mosaic can identify content, and a module that cannot name a film has no business satisfying it. Declaring the role to reach ContentMetadata's image fields would produce a deployment that boots and cannot identify anything — a failure no test would catch.

func (*Capability) SettingsUI

SettingsUI renders the module's own settings screen as SDUI (RoleSettingsUI, ADR 0038): set or replace the project key, add a personal key for early access, and read the attribution fanart.tv's terms expect.

Every mutating control is an Invoke of the Platform's configureModule command carrying the complete new settings document, so the Platform stays the one that persists them and the module never holds state between invocations. The screen is returned as serialised UINode JSON, which is what keeps the SDK SDUI-agnostic.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a fanart.tv web service client.

func NewClient

func NewClient(httpClient *http.Client, apiKey, clientKey string) *Client

NewClient builds a client over an HTTP client (nil for a default). The Platform passes its own, which carries the netguard dial guard and the outbound telemetry seam (ADR 0055).

func (*Client) Artwork

func (c *Client) Artwork(ctx context.Context, mediaType v1.MediaType, identities []v1.ExternalIdentity, season int) ([]v1.ArtworkCandidate, error)

Artwork fetches the images fanart.tv has for one title, scoped to the series itself (season 0) or to one of its seasons.

The response is decoded into a map keyed by artwork type rather than a struct, because the type vocabulary *is* the schema: fanart.tv adds types over time, and a struct would silently drop the ones this build predates. The identity fields (`name`, `tmdb_id`, …) decode as something that is not an array and are skipped by the decoder.

type Settings

type Settings struct {
	// APIKey is the user's own fanart.tv project key.
	//
	// **It only ever holds the user's key.** It is never populated from
	// defaultAPIKey as a convenience: configureModule replaces the whole
	// settings document, so the bundled key reaching this field would write a
	// shared build-time credential into a user's stored settings the next time
	// they touched any control.
	APIKey string `json:"apiKey,omitempty"`
	// ClientKey is a user's personal fanart.tv key, which grants early access to
	// artwork that has been uploaded but not yet promoted. It is optional and
	// independent of APIKey — a user may supply one, both or neither.
	ClientKey string `json:"clientKey,omitempty"`
}

Settings is this module's user-managed configuration (ADR 0021).

The Platform stores it uninterpreted and hands it back on every invocation; what the fields mean is this module's business.

Directories

Path Synopsis
cmd
module-fanart-tv command
Command module-fanart-tv runs this module as its own process, for a Platform that hosts it out of process (ADR 0064, ADR 0077).
Command module-fanart-tv runs this module as its own process, for a Platform that hosts it out of process (ADR 0064, ADR 0077).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL