Project Structure
Project Structure
The plugin architecture uses a single entry point (plugin.js) that dynamically loads all other feature components.
The ordered module list lives in js/component-scripts.json (paths relative to js/; entries starting with // are ordering notes). Modules execute in manifest order, and that ordering is load-bearing: a module placed before one whose exports it reads at load time will bind undefined. Add new modules to the manifest after their producers.
In production the server concatenates the manifest into one script, GET /JellyfinEnhanced/bundle.js (Services/ClientScriptBundle.cs: built once per process, cache-keyed and immutable like every other script, with a sections source map at bundle.js.map so DevTools and stack traces still show the original file names and lines). The bundle wraps each file in a function and publishes them as window.__JE_BUNDLE_MODULES; once the configuration is applied, plugin.js inserts the script and runs the functions in manifest order in short slices scheduled in the browser's idle time (requestIdleCallback, capped), so evaluating 150+ modules never delays jellyfin-web's own first view. A module that throws at its top level is reported and skipped, not re-run; if the request fails or the bundle does not parse, every file is injected individually with script.async = false (parallel download, manifest-order execution), exactly as before the bundle existed. Dev mode (DevMode config) always uses the individual files.
plugin.js also fetches everything it needs before the component stage in one request, GET /JellyfinEnhanced/bootstrap (version, public config, admin-only private config, the Custom Tabs / Plugin Pages presence flags, the five per-user documents and the manifest), falling back to the individual endpoints if that fails.
Three client scripts are not in the manifest and are loaded by their own dedicated loaders: others/splashscreen.js and extras/login-image.js (both injected early, before the component stage, so they can affect the login screen) and enhanced/translations.js (loaded at the start of initialize(), in parallel with the bootstrap request, ahead of the component stage).
The client is delivered by Services/ScriptInjectionStartupFilter.cs, which injects plugin.js into the web client; all js/** files are embedded resources (JellyfinEnhanced.csproj) served by GetScript in Controllers/JellyfinEnhancedController.cs.
File Structure
All client-side scripts live in Jellyfin.Plugin.JellyfinEnhanced/js/, grouped by feature. Server-side directories are summarised below. The Spoiler Guard services, event handlers, models and identity helpers are listed in full because they implement the server half of Spoiler Guard, whose client companion lives in js/enhanced/spoilerguard/; the remaining services are elided. Note the client never references these classes by name — it interacts with them through endpoints, cookies and image responses.
Jellyfin.Plugin.JellyfinEnhanced/
├── JellyfinEnhanced.cs # Plugin entry point, GetViews()
├── PluginServiceRegistrator.cs # DI registration
├── Configuration/ # PluginConfiguration.cs, UserConfiguration*.cs,
│ # configPage.html + configPage.css — the admin
│ # settings page
├── Controllers/ # JellyfinEnhancedController.cs — every
│ # /JellyfinEnhanced/* endpoint the client calls
├── PluginPages/ # HTML wrappers for the sidebar/plugin pages
├── Assets/
│ └── PosterTags/ # Embedded fonts, icon paths, flags, age-rating
│ # colours + licences for Native Poster Tags
│ # (generated by scripts/build_poster_assets.py)
├── Helpers/ Extensions/ ScheduledTasks/
│ └── Helpers/Jellyseerr/ # SeerrHttpHelper, ParentalRatingDecision,
│ # ParentalTagDecision, SeerrCertificationExtractor,
│ # SeerrTagSignatureExtractor
├── EventHandlers/
│ ├── ContinueWatchingPlaybackEvents.cs
│ ├── MaintenancePlaybackReminder.cs
│ ├── SpoilerAutoEnableEvents.cs
│ └── UserTopologyEvents.cs
├── Model/
│ ├── TagCacheEntry.cs
│ ├── Arr/ # ArrInstance.cs, ArrItem.cs, ArrType.cs
│ └── Jellyseerr/ # JellyseerrPermission.cs, JellyseerrUser.cs
├── Services/
│ ├── … # root-level services (Radarr, Sonarr,
│ │ # TagCache*, CdnAsset, WatchlistMonitor,
│ │ # SeerrParentalFilter, MaintenanceMode*,
│ │ # TmdbResponseCache, TmdbCompanyTvDiscover,
│ │ # ItemStatsService, ScriptInjectionStartupFilter,
│ │ # ClientScriptBundle, …)
│ ├── Identity/
│ │ └── RequestIdentityService.cs
│ ├── PosterTags/ # Native Poster Tags (experimental)
│ │ ├── PosterTagModel.cs # semantic layout contract (groups, corners, tags)
│ │ ├── PosterTagSettings.cs # effective per-user settings + digest
│ │ ├── PosterTagResolver.cs # tag-cache entry → layout (JS parity)
│ │ ├── Resolution/ # quality, language-flag, genre-icon, rating and
│ │ │ └── … # age-rating rules ported from js/tags/
│ │ ├── Rendering/ # PosterTagRenderer.cs (SkiaSharp drawing),
│ │ │ └── … # PosterTagAssets.cs (embedded asset loading)
│ │ ├── NativeClientPolicy.cs # which clients get drawn-in tags
│ │ ├── PosterTagVariantToken.cs # signed per-user image tag suffix
│ │ ├── PosterTagSettingsProvider.cs
│ │ ├── PosterTagDataProvider.cs
│ │ ├── PosterTagImageFilter.cs # composites tags into Primary image responses
│ │ └── CompositeImageCache.cs # memory + disk cache of drawn posters
│ └── SpoilerGuard/
│ ├── ImageBlurService.cs
│ ├── SpoilerBlurImageFilter.cs
│ ├── SpoilerIdentityService.cs
│ ├── SpoilerIdentityTagFilter.cs
│ ├── SpoilerFieldStripFilter.cs
│ ├── SpoilerTagDataStripper.cs
│ ├── SpoilerAutoEnableFilter.cs
│ ├── SpoilerAutoEnableArmer.cs
│ ├── SpoilerLibraryAddAutoEnabler.cs
│ ├── SpoilerExistingTitlesApplier.cs
│ ├── SpoilerSeerrPendingPromoter.cs
│ └── SpoilerUserResolver.cs
└── js/
├── plugin.js
├── component-scripts.json # ordered module manifest (bundle + loader order)
├── fonts/ # Material Symbols Rounded/Outlined: *-subset.woff2 (~5 KB each,
│ # generated by scripts/material-symbols/subset.py) under the
│ # private 'JE Material Symbols …' families JE's rules use (on the
│ # private .je-msym-* classes), plus the full fonts under the
│ # public names for themes/custom CSS
├── locales/ # 26 translation files (en.json, de.json, …)
├── core/
│ ├── api-client.js
│ ├── dom-observer.js
│ ├── lifecycle.js
│ ├── media-language.js
│ ├── navigation.js
│ ├── session.js
│ ├── tag-renderer-base.js
│ └── ui-kit.js
├── enhanced/
│ ├── config.js
│ ├── events.js
│ ├── features-random-button.js
│ ├── helpers.js
│ ├── icons.js
│ ├── native-tabs.js
│ ├── themer.js
│ ├── translations.js
│ ├── ui-styles.js
│ ├── bookmarks/
│ │ ├── bookmarks.js
│ │ ├── bookmarks-library-init.js
│ │ ├── bookmarks-library-items.js
│ │ ├── bookmarks-library-modals.js
│ │ ├── bookmarks-library-page.js
│ │ ├── bookmarks-library-render.js
│ │ ├── bookmarks-library-replacements.js
│ │ └── bookmarks-library-styles.js
│ ├── hiddencontent/ # 16 modules: data, filter, save, panel, dialogs,
│ │ └── … # buttons, styles, init, custom-tab + the page
│ │ # (nav, state, render, cards, admin, init, styles)
│ ├── homeremoval/
│ │ ├── features-remove-home.js
│ │ └── features-remove-multiselect.js
│ ├── itemdetails/
│ │ ├── features-details-media-info.js
│ │ ├── features-details-page.js
│ │ └── features-release-dates.js
│ ├── player/
│ │ ├── auto-skip.js
│ │ ├── osd-rating.js
│ │ ├── pausescreen.js
│ │ ├── playback-rating-badge.js
│ │ ├── playback.js
│ │ └── subtitles.js
│ ├── settingspanel/
│ │ ├── ui-entry-points.js
│ │ ├── ui-panel.js
│ │ ├── ui-panel-hidden-content.js
│ │ ├── ui-panel-language.js
│ │ ├── ui-panel-settings.js
│ │ ├── ui-panel-shortcut-editor.js
│ │ ├── ui-panel-template.js
│ │ └── ui-release-notes.js
│ └── spoilerguard/ # 12 modules: state, ids, identity, snooze,
│ └── … # styles, dialog, detail-button, seerr-toggle,
│ # settings-tab, image-refresh, watched-refresh, index
├── jellyseerr/
│ ├── api.js
│ ├── hss-discovery-handler.js
│ ├── issue-reporter.js
│ ├── item-details.js
│ ├── jellyseerr.js
│ ├── modal.js
│ ├── request-manager.js
│ ├── seamless-scroll.js
│ ├── seerr-detail-link.js
│ ├── seerr-status.js
│ ├── discovery/
│ │ ├── discovery-base.js
│ │ ├── discovery-filter-utils.js
│ │ ├── collection-discovery.js
│ │ ├── genre-discovery.js
│ │ ├── network-discovery.js
│ │ ├── person-discovery.js
│ │ └── tag-discovery.js
│ ├── moreinfo/ # 8 modules: styles, data, seasons, badges,
│ │ └── … # render, actions, actions-tv, init
│ ├── recommendations/ # 8 modules: styles, catalog, data, render,
│ │ └── … # page, category, init, custom-tab
│ └── ui/ # 10 modules: icons, styles, popover, badges,
│ └── … # cards, buttons, quota, results,
│ # request-modals, season-modal
├── arr/
│ ├── arr-links.js
│ ├── arr-tag-links.js
│ ├── calendar/ # 7 modules: styles, data, render-events,
│ │ └── … # render-views, actions, init, custom-tab
│ └── requests/ # 8 modules: styles, data, render-helpers,
│ └── … # render-cards, render, actions, init, custom-tab
├── tags/
│ ├── genretags.js
│ ├── languagetags.js
│ ├── peopletags.js
│ ├── qualitytags.js
│ ├── ratingtags.js
│ ├── ageratingtags.js
│ ├── userreviewtags.js
│ ├── tag-cache-store.js
│ └── tag-pipeline.js
├── elsewhere/
│ ├── elsewhere.js
│ └── reviews.js
├── extras/
│ ├── active-streams.js
│ ├── colored-activity-icons.js
│ ├── colored-ratings.js
│ ├── login-image.js
│ ├── plugin-icons.js
│ └── theme-selector.js
└── others/
├── letterboxd-links.js
└── splashscreen.js
Outside the plugin project, scripts/ holds maintenance tooling: generate_config_flag_groups.py (regenerates docs/advanced/config-flag-groups.json, run by the docs CI), validate-translations.js (locale file checks) and build_poster_assets.py (downloads the pinned upstream fonts, icons and flags, verifies their checksums and writes Assets/PosterTags/; --check verifies the committed assets are up to date).
How modules are organised
Each module is an IIFE. Features larger than a single file get their own directory.
The page-style directories that were split most recently (arr/calendar/, arr/requests/, and largely jellyseerr/moreinfo/ and jellyseerr/recommendations/) use a concern-based suffix pattern:
| Suffix | Responsibility |
|---|---|
-styles |
CSS injection only |
-data |
State object and data access (fetching, caching) |
-render |
HTML/DOM construction (may split further, e.g. -render-cards) |
-actions |
User interactions — filters, pagination, buttons |
-init |
Bootstrap, navigation wiring and the public surface |
-custom-tab |
Mounts the feature inside a Custom Tabs / native tab panel |
Other directories use different concern names and prefixes — spoilerguard/ uses bare names (state.js, snooze.js, styles.js), settingspanel/ and jellyseerr/ui/ use a ui- prefix, jellyseerr/discovery/ puts the concern first (discovery-base.js) or last (genre-discovery.js), and itemdetails//homeremoval/ keep the features- prefix from the file they were split out of. Follow the local convention of the directory you are touching rather than applying the table above universally.
Modules in the same directory share state through a namespace object. In most directories every module that touches shared state runs the same idempotent guard, so whichever of them loads first seeds the shape (-custom-tab and standalone -styles modules consume the public surface instead and never reference the namespace):
// run by every module that shares the directory's state
const P = (JE.internals.requestsPage = JE.internals.requestsPage || { /* state */ });
jellyseerr/ui/ and jellyseerr/moreinfo/ instead nominate a single owner — the first module that uses the namespace seeds the literal (ui-icons.js and more-info-modal-data.js respectively) and the rest read it directly (const internal = JE.internals.jellyseerrUi;), so a duplicated default can never silently diverge.
Directory names avoid hyphens (settingspanel, not settings-panel). Embedded-resource names are derived from the file path, and a hyphen in a directory segment is rewritten to an underscore while file-name hyphens are preserved — a hyphenated directory therefore makes its modules unreachable at runtime.
Component Breakdown
plugin.js: The main entry point. It loads the plugin configuration, user settings and translations (one bootstrap request plus the translations module), then loads thecomponent-scripts.jsonmodules in dependency order — the server-side bundle in production, individual files in dev mode or as a fallback (the three dedicated-loader scripts noted above are injected separately).
/core/: Shared primitives used across every feature. Introduced to remove logic that was previously duplicated per module. *api-client.js: The single HTTP layer —JE.core.api.{fetch,jf,plugin}with auth headers, retry/backoff, response caching, request deduplication, concurrency limiting andAbortControllersupport. *dom-observer.js: MultiplexedMutationObservermanagement — one shared body observer with named subscribers, plus dedicated observers andwaitForElement. *lifecycle.js: Per-feature registration of observers, timers and listeners so they can be torn down together on navigation. *media-language.js: Shared audio-language → flag resolution (JE.core.mediaLanguage.resolveFlag) used by the Language Tags overlay and the details-page audio-language row. Region-aware: an explicit region subtag (pt-BR,es-419,zh-Hant) resolves to that region's flag; bare base languages keep their default flag. *navigation.js: One deduped SPA navigation dispatcher (onNavigate,onViewPage), replacing the ad-hochashchange/viewshowlisteners that previously double-fired on hash navigation and missedpushStatenavigation. *session.js: Identity-epoch tracker for SPA user switches. Logging out and back in as a different user never reloads the page, so this module detects the transition (anApiClient.setAuthenticationInfohook plus navigation/storage fallbacks), runs every registered per-feature reset handler (JE.session.onUserChange), and emitsje:user-changed;plugin.jsthen re-fetches the incoming user's data and emitsje:user-data-loaded. Async loaders captureJE.session.getEpoch()and drop stale results after a switch. *tag-renderer-base.js: The shared poster-tag engine — overlay creation, positioning, tagged-card deduplication, caching and reinitialisation. The five poster-overlay renderers (genre, language, quality, rating, age rating) supply a spec;peopletags.jsanduserreviewtags.jsdo not use it. *ui-kit.js:escapeHtml,toast, deduped CSS injection, and scroll-friendly tap detection (addTouchTapListener).
/enhanced/: Core "Jellyfin Enhanced" functionality. *config.js: Manages all settings, both from the plugin backend and the user's local storage. *events.js: Listens for user input, browser events and DOM changes to trigger the appropriate functions from other components. *features-random-button.js: The random item button. *helpers.js: Utility functions shared across the enhanced components. *icons.js: Icon selection and rendering (emoji, Lucide or Material UI). *native-tabs.js: Shared registry for JE-created home tabs, used when a feature is shown as a native tab rather than via the Custom Tabs plugin. Reconciles against the live page on every pass: keeps each tab at the position its index names (Jellyfin resolves tabs by position), places ours after every index another plugin occupies (relabelling them if a claim appears later), and handles the stablejeTab=<id>deep links. *themer.js: Theme detection and Enhanced Panel styling. *translations.js: Loads and caches translations. Loaded by its own loader at the start ofinitialize(), ahead of the component stage, soJE.tis resolved before any component runs. *ui-styles.js: Global stylesheet for the injected UI, including the settings panel's responsive rules. */bookmarks/:bookmarks.jshandles playback bookmarks (creation viaB, timeline markers, navigation); thebookmarks-library-*modules provide the management interface — listing, orphan cleanup, duplicate detection and time-offset adjustment. */hiddencontent/: Per-user hidden content — the data/filter/save layer, the settings panel section, and the standalone management page. */homeremoval/: "Remove from Continue Watching / Next Up", including multi-select. */itemdetails/: Detail-page enhancements — media info, file size, audio languages and release dates. */player/: Everything that touches the video player —playback.js(speed, seeking, track cycling),auto-skip.js(media-segment driven intro/outro skipping),subtitles.js(styling and presets),pausescreen.js(custom pause overlay),osd-rating.js(TMDB/Rotten Tomatoes in the OSD),playback-rating-badge.js(age rating and genres card when playback starts). */settingspanel/: The user settings panel — entry points, the HTML template, the section navigation shell, and per-section wiring (settings, language, hidden content, shortcut editor, release notes). */spoilerguard/: Client-side companion for Spoiler Guard — the per-show/movie/collection toggle, in-memory opt-in and override state, the settings pane, and the soft image refresh after toggles and watched-state changes. The actual blur/strip happens server-side.index.jspublishes the publicJE.spoilerBlursurface once every implementation module has loaded.
/jellyseerr/: Seerr integration. *api.js: Communication with the Seerr proxy endpoints on the Jellyfin server. *hss-discovery-handler.js: Intercepts clicks on Home Screen Sections discover cards and opens the Seerr More Info modal instead of navigating to the external Seerr site. *issue-reporter.js: Report problems with media items directly from Jellyfin. *item-details.js: Similar and Recommended rows on item detail pages, plus the "Request More" button for series with unrequested seasons. *jellyseerr.js: The Seerr search-results integration — intercepts Jellyfin's search page, renders Seerr results and handles their pagination/infinite scroll. Gated onJellyseerrShowSearchResults; the other Seerr components initialise independently of it. *modal.js: Advanced request modals. *request-manager.js: Thin alias ontoJE.core.api.manager, kept as a stable public surface. *seamless-scroll.js: The buffer-fill infinite-scroll engine shared by the search row, the discovery modules and the Recommendations category page: keeps several screens of cards rendered ahead, passes each consumer a hint (deficit, engagement, empty-page budget) so it can fetch several pages in parallel and prefetch the next, pauses on a "Keep looking" button after 40 consecutive empty pages, and retries failures with backoff. Also owns the deduplicator helper. *seerr-detail-link.js/seerr-status.js: Detail-page link into Seerr, and the shared media/display status constants. */discovery/:discovery-base.jsowns the whole discovery lifecycle — three pagination strategies, abort handling, config gating, card rendering, filtering and cleanup. Each ofgenre,network,person,tagandcollectionsupplies a small spec describing how to resolve its feeds.discovery-filter-utils.jsprovides shared TV/Movies/All filtering and card creation. */moreinfo/: The Seerr More Info modal — cast, crew, extended metadata, seasons and request actions. */recommendations/: The Recommendations page — Trending/Popular/Upcoming rows plus Studios and Networks tiles, with a "View All" category page. Available as a sidebar page or a tab. */ui/: All visual elements of the integration — result cards, request buttons, status badges, quota display, the download-progress popover and the season/request modals.
/arr/: Sonarr and Radarr integration. *arr-links.js: Links to Sonarr, Radarr and Bazarr on item detail pages, for administrators only. *arr-tag-links.js: Synced arr tags as clickable links on item detail pages, with show/hide filtering. */calendar/: The calendar page — upcoming items from Radarr and Sonarr, available via the sidebar or a tab. */requests/: The requests page — requests, download queue, issues and import history from the arrs and Seerr, available via the sidebar or a tab.
/tags/: Poster tag renderers. Five of them (genre, language, quality, rating, age rating) are built oncore/tag-renderer-base.js;peopletags.jsanduserreviewtags.jsrender independently. *genretags.js: Genre information as tags on posters. *languagetags.js: Audio language as flag icons on posters. *peopletags.js: Age and birthplace for cast members, with country flags and deceased indicators. *qualitytags.js: Quality information (4K, HDR, Atmos) as tags on posters. *ratingtags.js: TMDB and Rotten Tomatoes ratings as badges on posters. *ageratingtags.js: The parental / age rating (PG-13, TV-MA, FSK 12, ...) as a colour-coded badge on posters, reusing the Colored Ratings colour table. *userreviewtags.js: The average user-review rating across all users, composed into the ratings overlay rather than rendered as its own poster tag. Resolves from the averages that ride on the server tag cache; asks/reviews/ratingsin batches only without it. *tag-cache-store.js: The browser's copy of the server tag cache in IndexedDB, one per server and user, so a page load renders tags from it and only fetches what changed. Falls back to memory-only when IndexedDB is unavailable. *tag-pipeline.js: Shared server-backed tag cache feeding the renderers in bulk: restores the stored copy (or downloads the cache), keeps it current with?since=deltas, and scans cards in idle slices.
/elsewhere/: Discovering media on other streaming services, and reviews. *elsewhere.js: Powers the "Jellyfin Elsewhere" feature. *reviews.js: TMDB reviews and the plugin's own per-user reviews on item detail pages (the reviewsuserreviewtags.jsaverages).
/extras/: Optional scripts that extend functionality. *active-streams.js: Shows currently active streams. *colored-activity-icons.js: Material Design activity icons with custom colours. *colored-ratings.js: Colour-coded rating backgrounds on item detail pages. *login-image.js: User profile images instead of text on the manual login page. *plugin-icons.js: Custom plugin icons on the dashboard, plus custom config-page links. *theme-selector.js: Jellyfish colour palette selection, with an optional daily random theme.
/others/: Miscellaneous utility scripts. *letterboxd-links.js: Letterboxd external links on movie and person (actor) detail pages. *splashscreen.js: The custom splash screen shown while the application loads.
/Services/SpoilerGuard/: Server-side C# services that implement Spoiler Guard. *ImageBlurService.cs: SkiaSharp Gaussian blur, stock-card rendering, and the pre-encoded fail-closed fallback JPEG, with result caching. *SpoilerBlurImageFilter.cs: Intercepts image responses and replaces the bytes for unwatched items — safe parent art, blur, or the fail-closed dark card, depending on mode and availability. *SpoilerIdentityService.cs: Mints and resolves stable per-user image identity markers. *SpoilerIdentityTagFilter.cs: Stamps item DTO image tags with per-user identity markers so native image requests can be resolved without relying on IP address. *SpoilerFieldStripFilter.cs: Strips or rewrites metadata (titles, synopses, ratings, chapter names, cast, tags, taglines, air dates) in API responses for unwatched items, honoring per-user overrides. *SpoilerTagDataStripper.cs: Removes spoiler-sensitive poster-tag data for guarded items; shared by the tag-cache endpoints and Native Poster Tags so both hide the same things. *SpoilerAutoEnableFilter.cs: The shared auto-enable scope gate (content type + library allow-list) used by the first-play, Seerr-request and library-add modes. *SpoilerAutoEnableArmer.cs: The shared bulk-arming path (target users incl. disabled accounts, per-user library access, one lockedspoilerblur.jsonwrite per user) used by the library-add batcher and the apply-to-existing-titles run. *SpoilerLibraryAddAutoEnabler.cs: Implements "Auto-enable when new titles are added to the library" — batches a scan'sItemAddedburst and arms every new Series/Movie for every user with access to its library, one file write per user. *SpoilerExistingTitlesApplier.cs: Applies the saved auto-enable scope to titles already in the library on demand (dry-run preview for the config page, skips titles a user already has or has fully watched). Run byScheduledTasks/SpoilerApplyExistingTitlesTask.cs("Spoiler Guard: apply to existing titles", no default triggers). *SpoilerSeerrPendingPromoter.cs: Promotes pending pre-acquisition entries (registered from the Seerr More Info modal or auto-enable on request) into real per-item protection when the content lands in the library. *SpoilerUserResolver.cs: Loads per-user Spoiler Guard state for the requesting user identified byRequestIdentityService.
/Services/ClientScriptBundle.cs: Builds and caches the component bundle (/JellyfinEnhanced/bundle.js) and its index source map from the embeddedjs/component-scripts.jsonmanifest; also the source of the ordered list returned by/bootstrap.
/Services/PosterTags/: Native Poster Tags (experimental) — draws each user's poster tags into Primary images for native apps that don't run the web overlays. Off unlessNativePosterTagsEnabledis set. *PosterTagModel.cs: The semantic layout passed from resolution to rendering — tag groups, their corners and stack order, and the tags themselves. Visual details belong to the renderer. *PosterTagSettings.cs/PosterTagSettingsProvider.cs: A user's effective tag settings, merged from theirsettings.jsonand the admin defaults exactly asenhanced/config.jsdoes, with a digest of every rendering-relevant value; the provider caches them per user and invalidates on save and config changes. *PosterTagResolver.cs+/Resolution/: Turns aTagCacheEntryinto a layout, porting the web renderers' rules (quality detection and category order, genre icons, language flags, rating and user-review gating, age-rating colours). */Rendering/:PosterTagRenderer.csdraws a layout onto the poster with SkiaSharp (HarfBuzz shaping, the web's sizes and colours);PosterTagAssets.csloads the embeddedAssets/PosterTags/resources once. *NativeClientPolicy.cs: Skips clients that run the web overlays themselves (built-in list plusNativePosterTagsWebClientNames). *PosterTagVariantToken.cs: Mints and verifies the signed-jetsuffix stamped onto image tags (bySpoilerGuard/SpoilerIdentityTagFilter.cs), so a poster URL changes whenever its drawn content would. *PosterTagDataProvider.cs: Fetches the item's tag data (server tag cache or on-demand), checks the user can see the item, and applies Spoiler Guard stripping viaSpoilerGuard/SpoilerTagDataStripper.cs. *PosterTagImageFilter.cs: The image action filter that composites the tags; registered beforeSpoilerBlurImageFilter, so it runs after the blur. Any failure serves the original image. *CompositeImageCache.cs: Byte-bounded memory LRU plus a disk cache under Jellyfin's cache path, with in-flight coalescing and bounded render concurrency.
/Services/Identity/: Shared request identity helpers. *RequestIdentityService.cs: Resolves the current request identity using authenticated claims first, then image identity markers, single-user installs, cookies, and shared-IP session candidates.
/EventHandlers/(Spoiler Guard): *SpoilerAutoEnableEvents.cs: Implements "Auto-enable on first play of a new show" — adds a series to the user's Spoiler Guard list on a fresh S1E1 play. *UserTopologyEvents.cs: Invalidates single-user and marker lookup caches when users are created or deleted.
- Maintenance Mode (server side):
*
/Services/MaintenanceModeService.cs: Owns the maintenance state file (who was disabled, end time, source), applies/restores user policies, resolves the{countdown}/{ends_at}tokens and sends the session popups. */Services/MaintenanceScheduleService.cs: 30-second hosted timer that opens/closes the daily scheduled window and expires timed windows. */EventHandlers/MaintenancePlaybackReminder.cs: Re-sends the notification to affected users on playback start while maintenance is active (throttled per session).
/Model/(Spoiler Guard): *TagCacheEntry.cs: Pre-computed per-item tag data served to clients in bulk; carries the parent-series ID so the Spoiler Guard filter can strip cache entries for unwatched episodes without per-request library lookups.
- Audio Language Tags:
*
/Helpers/AudioLanguageTagHelper.cs: Maps stream language codes to tag names (base and regional) and computes the tag changes for an item. */ScheduledTasks/AudioLanguageTagsSyncTask.cs: "Sync Audio Language Tags to Jellyfin" (no default triggers). Writes the tags for every movie and series. */Services/TagCacheService.cs: Applies the same tags to a movie or series when its cache entry is rebuilt after a library event.