Mediatypes en metadata
Elk mediatype volgt hetzelfde patroon: de scanner registreert entities op basis van het pad,
*_FOUND-events triggeren verrijking vanuit een externe provider, afbeeldingen lopen via
IMAGE_FOUND. Metadata-providers staan in worker/.../events/; hun HTTP-clients zijn
gecentraliseerd in worker/.../http/MetadataRestClients, en elke externe base-URL is een
property met de echte service als default — de e2e van de chart serveert ze allemaal vanuit één
WireMock-pod en faalt op dead-lettered events, dus een hardcoded URL breekt de CI.
Talen
app.ister.languages / ISTER_LANGUAGES (ISO-639-1-tags, default en,nl, ontsloten als
LanguageProperties) is dé app-brede talenlijst. De movie/show/episode-handlers halen TMDB-details
één keer per geconfigureerde tag op, wat per taal per item één MetadataEntity-rij oplevert; de
taal wordt opgeslagen als ISO-639-3, ook al gebruikt de fetch ISO-639-1. De eerste tag is de
primaire/fallback-taal. Dezelfde lijst stuurt het zoekschema aan (hoofdstuk 6). Een
taal toevoegen vereist een re-scan plus een reindex.
Films, shows, afleveringen (TMDB)
De MOVIE_FOUND- / SHOW_FOUND- / EPISODE_FOUND-handlers halen TMDB-details per taal op, slaan
MetadataEntity-rijen op en downloaden posters/achtergronden (verstuurd als IMAGE_FOUND op de
cache-directory).
Uit de details-response wordt meer gehaald dan titel/overview. Per taal krijgt de metadata-rij
ook de gelokaliseerde genre-lijst (kommagescheiden namen — dit activeert het GENRE-filter en de
genre_<tag>-zoekvelden voor video) en de tagline. Taalonafhankelijke feiten zijn kolommen op
de entiteit zelf, één keer gezet bij de eerste geslaagde taal: tmdbId, imdbId,
voteAverage/voteCount (null zolang er nul stemmen zijn), runtime (films/afleveringen, in
minuten), status, homepage, originCountry, studios, collectie-id/-naam (films, uit
belongs_to_collection) en networks (shows). Na de talenlus doet TmdbExtrasService per item nog
een paar extra endpoint-aanroepen: release dates/content ratings (keuring voor
app.ister.worker.tmdb.certification-country, standaard US, met terugval naar US en daarna
willekeurig), videos (één YouTube-trailerkey, met voorkeur voor officiële trailers), keywords en
TV-external-ids (imdb). Elke extras-aanroep is afzonderlijk fout-tolerant: een fout wordt gelogd en
laat het veld null in plaats van het event te dead-letteren (de chart-e2e stubt TMDB met WireMock).
Credits komen in dezelfde pass mee: movie credits, show aggregate credits en episode credits (cast
- guest stars) worden
PersonEntity- +CreditEntity-rijen, direct in de database geschreven. EenCreditEntitykoppelt een persoon aan precies één van movie/show/episode. EenPersonEntitywordt gedeeld tussen acteurs en muziekartiesten; TMDB-castleden worden gededupliceerd tegen bestaande personen op exacte naam + geboortejaar. Het GraphQL-typeCreditontsluit de terugverwijzingen (movie/show/episode, batch-resolved inCreditController), zodat een filmografie opvraagbaar is viapersonById { credits { movie/show/episode } }.
Gerelateerde series
Show.related(limit) (ShowController, ShowRepository.findRelatedShowIds) maakt van diezelfde
verrijking een "meer zoals dit"-lijst, zonder ook maar één extra provider-aanroep: het scoort de
andere shows uit dezelfde bibliotheek op gedeelde TMDB-keywords (3,0 per stuk, afgetopt op vijf
zodat één lange keywordlijst niet alles overheerst), gedeelde genres (1,5 per stuk) en gedeelde
castleden (1,0 per stuk, afgetopt op drie). Een overlappend netwerk, land van herkomst of een
releasejaar binnen vijf jaar levert elk een halve punt op, maar rangschikt alleen shows die al
inhoudelijk overlappen — een gedeeld uitzendjaar is op zichzelf geen relatie. Shows die niets delen
vallen af, dus een show zonder TMDB-verrijking geeft een lege lijst in plaats van willekeurige buren,
en de lijst wordt beter met elke refreshMetadata.
Keywords, networks en origin countries zijn de met komma-spatie samengevoegde strings op
show_entity; genres staan per taal op de metadata-rijen, dus de query koppelt de metadata-rijen van
beide shows op taal (de genrenamen zijn immers vertaald) en neemt de best passende taal. De
kandidaten zitten per definitie in de bibliotheek waartoe de aanroeper via showById al toegang
kreeg, dus het veld heeft geen eigen toegangscontrole nodig. Het is bewust géén @BatchMapping:
alleen de detailpagina van een show vraagt het op, voor één show tegelijk.
Muziek (MusicBrainz)
Artiest-directories worden PersonEntity-rijen (PERSON_FOUND), albums AlbumEntity
(ALBUM_FOUND), tracks lopen via AUDIO_FILE_FOUND (ffprobe + ID3-tags + embedded cover).
Album-identiteit komt uit het pad, nooit uit tags. De artiest van een track komt juist wél uit
de artist-tag (met de pad-artiest als fallback): op verzamelalbums bestaat de uitvoerende per
track alleen daar, terwijl het album de pad-afgeleide albumartiest houdt. De worker-HandleAlbumFound bevraagt
MusicBrainz en downloadt de release-group-cover; de disk-kant
(HandlePersonFound/HandleAlbumFound) zoekt naar artist.nfo/album.nfo. Artiesten krijgen een
birthYear (MusicBrainz life-span, of de mapnaam) — precies zodat de TMDB-acteur-dedup hierboven ze
kan matchen.
Elke track draagt daarnaast credits (TrackCreditEntity): de primaire artiest — dezelfde persoon
als TrackEntity.personEntity — plus eventuele featured gasten, één rij per persoon. Die koppeltabel
maakt de verzamelalbum- en gastoptredens van een artiest bereikbaar vanaf haar pagina
(hoofdstuk 7); het album houdt één albumartiest.
Biografieën en portretten (Wikipedia/Wikidata)
WikipediaService (worker) verrijkt personen met meertalige biografieën en portretten: Wikidata
resolveert de entiteit en zijn afbeelding/sitelinks, het Wikipedia-summary-endpoint (een
URL-template-property) levert de extracts per taal. Dezelfde service voedt de beschrijvingen van
stripseries.
Boeken (LibraryType.BOOK)
De directorygrammatica is auteur-eerst: Author/Book.epub en Author/Book/NNN_Chapter.mp3. Alle
formaten van één (genormaliseerde) boeknaam convergeren op één BookEntity (auteur =
PersonEntity); formaten zijn bijlagen — epubs koppelen via MediaFileEntity.bookEntity,
audiobook-mp3's via ChapterEntity (gestreamd over hetzelfde audio-only HLS-pad als tracks).
HandleAudioFileFound brancht op library-type en maakt chapters in plaats van tracks.
- Media overlays (EPUB 3-voorleesaudio) worden gemarkeerd op
MediaFileEntity.mediaOverlays, uitsluitend gedetecteerd uit de inhoud van de epub (SMIL-entries in het OPF-manifest, geparst doordisk/.../epub/EpubParser) — nooit uit de bestandsnaam. BOOK_FOUNDtriggert Open Library-verrijking (beschrijving, en een cover alleen als er nog geen is); Wikidata voegt reekslidmaatschap toe (reeksnaam + positie); NFO-data wordt gededupliceerd tegen provider-data, zodat re-scans de beschrijvingen niet verdubbelen.- Reeksen (
BookSeriesService, core) komen uit drie bronnen met vaste voorrang: epub-reeksmetadata (calibre / EPUB 3 belongs-to-collection) is autoritatief en herschrijft de koppeling bij elke scan; een pad-prefix-heuristiek vult reeksloze boeken wanneer ≥2 boeken van de auteur de prefix vóór een-/:-scheider delen; en Wikidata-reeksontdekking (WikidataBookSeriesService.discoverSeries, gedraaid vanuitBOOK_FOUND) koppelt een reeksloos boek aan een van de bestaande reeksen van de auteur via zijn P179-statement (part of series) — ze maakt nooit een reeks aan, en vereist een P50-labelmatch (auteur) zodat de gelijknamige film of game nooit kan koppelen. De positie/oorspronkelijk-jaar-verrijking voor boeken die al een reeks hebben (findBookInSeries) past dezelfde P50-check toe: het reekslabel alleen kan de film niet afwijzen, want een filmreeks kan exact hetzelfde label dragen als de boekenreeks ("Harry Potter"). Labelmatching accepteert naast de geconfigureerde talen ook Wikidata'smul-label (standaard voor alle talen): bots consolideren labels die in alle talen gelijk zijn daarin en verwijderen de per-taal-labels. Ontdekking dekt wat de andere twee niet zien: titels zonder scheider ("Harry Potter en de steen der wijzen") en audiobook-only boeken zonder epub-metadata. Wanneer epub-metadata een reeks aanmaakt, wordtBOOK_FOUNDeenmalig opnieuw afgevuurd voor de reeksloze boeken van de auteur, zodat ontdekking binnen één scan convergeert ongeacht de scanvolgorde. De analyze-backfill dispatchtBOOK_FOUNDbovendien opnieuw voor elk reeksloos boek waarvan de auteur een reeks heeft — een herscan slaat al bekende bestanden over, dus zonder dit zou een boek waarvan de ontdekking te vroeg draaide (of destijds tegen Wikidata faalde) voor altijd buiten zijn reeks blijven. - Epubs worden door de client lazy gelezen via
GET /epub/{mediaFileId}/resource/{entry}(hoofdstuk 7); de leespositie is eenWatchStatusEntitymetreadingLocation(epubcfi) +readingProgress.
Strips (LibraryType.COMIC, migratie V23)
Strips zijn serie-eerst, het omgekeerde van de boekengrammatica: {root}/{Series Name (start year)}/Volume 27.cbz (ook Vol 3 - Subtitle.pdf, Issue 8.epub, en getolereerde wilde patronen;
cover.jpg in de serie-directory is serie-artwork). Het (YYYY)-suffix is het startjaar van de
serie, geen auteursjaar — strips hebben geen auteur in het pad; het parsen zit in ComicPathObject
ComicFileNameParser, en alles dat dieper genest is dan de serie-directory wordt genegeerd.
ComicScanner maakt uit het pad de SeriesEntity en het volume (een BookEntity zonder auteur)
aan en hangt het bestand eraan als MediaFileEntity; alle formaten van één volume (zelfde basename)
convergeren op één volume-rij. Inhoud lezen gebeurt asynchroon: HandleComicFileFound (disk) leest
cbz via CbzParser en pdf via PDFBox (PdfParser) — de paginatelling gaat op de
MediaFileEntity, embedded ComicInfo.xml (cbz) wordt volume-metadata en kan de uit de
bestandsnaam afgeleide reekspositie en titel verfijnen, en de cover (eerste cbz-pagina, of
gerenderde pdf-pagina 1) wordt naar de cache geëxtraheerd. Epub-volumes hergebruiken de volledige
EPUB_FILE_FOUND-pijplijn. COMIC_SERIES_FOUND (worker, HandleComicSeriesFound) voegt per taal
seriebeschrijvingen en een thumbnail toe vanuit Wikipedia/Wikidata — lokale artwork wint altijd en
wordt nooit overschreven. Pagina's worden aan de reader geserveerd door ComicResourceController
(/comic/{mediaFileId}/manifest, /page/{index}, /file).
Podcasts (LibraryType.PODCAST)
Het eerste feed-gebaseerde librarytype: er is geen library-directory. subscribePodcast(feedUrl)
of de uurlijkse PodcastRefreshScheduler (met lastRefreshedAt-guard, zodat meerdere nodes niet
dubbel sweepen) stuurt PODCAST_REFRESH_REQUESTED (globale queue). De RssFeedParser van de worker
haalt de feed op met een conditional GET (ETag/Last-Modified), capt op 500 items, synct
kanaal-metadata + cover en maakt PodcastEpisodeEntity-rijen, gededupliceerd op guid.
De nieuwste N afleveringen (app.ister.worker.podcast.auto-download-count, default 3) krijgen
PODCAST_EPISODE_DOWNLOAD_REQUESTED op de cache-directory-queue van de node die de refresh
deed; de disk-handler downloadt de enclosure (volgt redirects) naar {cache}/podcasts/ en stuurt
AUDIO_FILE_FOUND, waarna afspelen identiek is aan tracks. Oudere afleveringen downloaden
on-demand via de downloadPodcastEpisode-mutation. Retentie: de dagelijkse cache-cleanup verwijdert
downloads ouder dan podcast-retention-days (default 30), behalve als iemand middenin de aflevering
zit — de afleverings-rij blijft bestaan en kan opnieuw downloaden. Zoeken in de podcastdirectory
loopt via de gratis iTunes Search API (ItunesSearchService, api-module).
NFO-bestanden
HandleNfoFileFound (disk) parst XML-NFO-bestanden naar metadata: titel, beschrijving en
releasedatum voor film/tv, biografie voor artiesten, review voor albums, boek-metadata voor boeken.
PERSON_FOUND/ALBUM_FOUND aan de disk-kant zoeken proactief naar artist.nfo/album.nfo naast
de media. NFO- en provider-metadata worden gededupliceerd, zodat de een nooit rijkere data van de
ander overschrijft.