Ga naar hoofdinhoud

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. Een CreditEntity koppelt een persoon aan precies één van movie/show/episode. Een PersonEntity wordt gedeeld tussen acteurs en muziekartiesten; TMDB-castleden worden gededupliceerd tegen bestaande personen op exacte naam + geboortejaar. Het GraphQL-type Credit ontsluit de terugverwijzingen (movie/show/episode, batch-resolved in CreditController), zodat een filmografie opvraagbaar is via personById { 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 door disk/.../epub/EpubParser) — nooit uit de bestandsnaam.
  • BOOK_FOUND triggert 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 vanuit BOOK_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's mul-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, wordt BOOK_FOUND eenmalig opnieuw afgevuurd voor de reeksloze boeken van de auteur, zodat ontdekking binnen één scan convergeert ongeacht de scanvolgorde. De analyze-backfill dispatcht BOOK_FOUND bovendien 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 een WatchStatusEntity met readingLocation (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.