JPKribs.Jellyfin.Base
2026.10.6
dotnet add package JPKribs.Jellyfin.Base --version 2026.10.6
NuGet\Install-Package JPKribs.Jellyfin.Base -Version 2026.10.6
<PackageReference Include="JPKribs.Jellyfin.Base" Version="2026.10.6" />
<PackageVersion Include="JPKribs.Jellyfin.Base" Version="2026.10.6" />
<PackageReference Include="JPKribs.Jellyfin.Base" />
paket add JPKribs.Jellyfin.Base --version 2026.10.6
#r "nuget: JPKribs.Jellyfin.Base, 2026.10.6"
#:package JPKribs.Jellyfin.Base@2026.10.6
#addin nuget:?package=JPKribs.Jellyfin.Base&version=2026.10.6
#tool nuget:?package=JPKribs.Jellyfin.Base&version=2026.10.6
Shared configuration UI assets and C# helpers used in my Jellyfin plugins, delivered as a NuGet package. The source and assets are compiled into the consuming plugin at build time, so each plugin stays a single DLL with nothing extra to ship.
Components
CSS and JS are authored as per component sources under src/ and bundled, minified, into jpkribs_shared.css and jpkribs_shared.js by scripts/bundle.sh. The consuming plugin embeds them under its own namespace.
Styles and client helpers
setTabsandinitCollapsibles: tab bar wiring over the native Jellyfin pages and collapsible section toggling.createConfigPage(view, options): wires the config-page lifecycle every plugin repeats — builds the shared bag, sets the dashboard tabs, runsbind()once, runsload()on everyviewshow, and runs registered cleanups (pollers, table observers) plusonHide()onviewhide. Import it and call it from the page module's default export; it returns a page handle withshared,addCleanup(fn), andcreatePoller(fn, ms)(whose teardown is auto-registered).createShared(view, pluginId, apiPrefix): a per view helper bag withescapeHtml,setStatus,getConfig/saveConfig/apiRequest,saveConfigWith,formatSize/formatDuration/formatDate,badge,statusBadge,emptySection,renderCards,copyToClipboard,createPoller,createChangeTracker,createSearchableComboBox,createDebouncedSearch,createUserSelector/createUserMultiSelector, andpollTaskProgress.setStatuscolors via classes (jpk-status-ok/jpk-status-bad) rather than inline styles and takes an options object (e.g.{ timeout: 0 }to persist).saveConfigWith(mutator, statusId, messages)collapses the get-config → mutate → save → report-status chain, re-reading the freshest config before saving.createPaginatedTable(view, shared, options): the searchable, filterable, infinite scrolling table that reads thePagedResultshape. Exposesreload()(clears selection),refresh()(keeps it), anddestroy()(disconnects the scroll observer and pending search timer — call onviewhide).openDialog/confirmDialog/promptDialog: modal behavior over the shippedjpk-dialog/jpk-modal-*markup — backdrop-click and Escape dismissal, focus trapping, and footer buttons.confirmDialog(message, opts)resolves to a boolean;promptDialog(fields, opts)resolves to a{ name: value }map or null.createChipSelect(options): a typeahead multi-select — type to search, click a result to add it as a removable chip — backed by a static option list or an asyncsearchFn, with arrow-key navigation.single: truekeeps at most one selection (a searchable single-picker);setOptions(list)swaps the static list after construction (for choices that load async);getItems()returns the{ value, label }pairs so a caller keying an opaque id (e.g. a person id) can persist and later restore the friendly label without re-fetching, andonChange(values, items)hands back both. Built on thejpk-tagsandjpk-combostyles. Returns{ element, getValue, getItems, setValue, setOptions, clear, refresh, destroy }.createCheckboxList(options): a searchable list of checkbox rows for picking many arbitrary items (the generic sibling ofcreateUserMultiSelector), with optional thumbnails, sublabels, note pills, a select-all row, and disabled rows. Fed by a static array or an asyncfetchItems. Returns{ element, ready, getValue, setValue, setDisabled, refresh, destroy }.createSortableCardList(listEl, options): records as cards in priority order, each with a numbered head, a status dot, badges, up and down arrows, drag to reorder, and a body the open card shows. Hand it aneditorelement and it moves into whichever card is open, parking in a holder while the list re-renders so fields and bindings survive. The caller renders a card's text throughrender(item, index)and saves order inonMove. Returns{ render, setItems, getSelected, setSelected, destroy }. Built on thejpk-card-listandjpk-card-item-*styles.createChoiceGroup(groupEl, options): radio cards, onejpk-choicelabel per option with a name and a sentence under it, for a mode or strategy setting that deserves a line of explanation. Keeps theselectedclass in step with the checked input and reports changes. Returns{ get, set, destroy }.jpk-step-titleandjpk-step-number: a round number badge inside a collapsible title, for a form split into ordered steps.BADGE_STATES: the status namesbadges.csstints plus the raw color modifiers, so a plugin's status-code-to-badge mapping has one source of truth.createUserSelector(options)andcreateUserMultiSelector(options)(also on thecreateShared(...)bag): a single-user dropdown and a checkbox list of users, both fed byApiClient.getUsers(). AnadminFilterof'all'/'exclude'/'only'drops or keeps administrators (e.g.'exclude'for an approved non-admin list). Each returns{ element, ready, getValue, setValue, refresh, destroy }.jpk-fieldandjpk-field-row: a compact label and control field plus a row that places several side by side, for dense multi field layouts.- Inline-edit primitives (
jpk-bulk-edit-bar/jpk-bulk-edit-title,jpk-edit-row,jpk-edit-line/jpk-edit-line-spacer,jpk-edit-secondary): a bulk-edit toolbar above a table plus table rows that expand into stacked lines of fields, for tag/metadata editors. jpk-empty-section(andcreateShared(...).emptySection(text)): a bordered, centered fallback box for an empty region.- Status callouts:
jpk-info(blue),jpk-success(green),jpk-warn(amber),jpk-error(red), andjpk-neutral(gray) are soft inline notices that share one shape, plusjpk-error-messagefor a mono block of raw failure text. - Form spacing and field system: one
--jpk-field-gaptoken normalizes every config field row over the emby defaults, and text inputs, text areas, and selects share one dark surface, border, and chevron so every field matches. Collapsible sections stay flat and share the form left edge.
C# helpers
PluginBase<TPlugin, TConfiguration>: singleton accessor, lock guarded config read and mutate, andGetSharedPages(prefix)to register the shared assets under a per plugin name.PluginScheduledTask: scheduled task base with the configurable task defaults and anEveryIntervaltrigger helper.PagedResult<T>,PagedQuery, andToPagedResult: the page and count contract the shared paginated table reads.PagedResultalso exposesPage,PageSize,TotalPages, andHasMore(computed from the skip/take window) for page-based callers.JpkHttpandHttpResult: outbound HTTP over Jellyfin's default client that returns a status and body and never throws on network failure.TemplateLoader: loads and fills{{KEY}}placeholders in HTML templates embedded fromtemplates/.StatusPage: renders the themed, self contained status card (logo, heading, message, optional spinner and button) from thestatustemplate. The template also exposes a raw{{CONTENT}}slot, so a plugin can render its own markup, a form or a multi state auth shell, inside the same card.FaviconResolver: resolves and caches the web client's favicon from disk so a plugin serving standalone pages can reuse the server's real icon. The status card links a relativefavicon.ico, so a consumer that wants the tab icon exposes a siblingfavicon.icoendpoint backed by this resolver.SecretProtector: encrypts plugin credentials at rest with ASP.NET Core Data Protection. Construct one per plugin with a stable, uniquepurpose(e.g. the plugin namespace) so keys are isolated.Protect/Unprotecttag values with anenc:v1:prefix and read pre-migration plaintext back unchanged.ResolveIncoming(incoming, stored)handles the config-page round-trip: a page shows a placeholder and posts backSecretProtector.KeptSentinel(mirrored bySECRET_KEPTon the JS side) when the admin leaves a secret field untouched, so the real secret never travels to the browser — kept means the stored value is preserved, any other value is re-encrypted, and empty clears it. TheIDataProtectionProvideris optional. When the host supplies none it degrades to a logged warning (plaintext) rather than failing to load. Defense in depth: the key lives in the Jellyfin data directory, so it guards leaked or synced config files and backups, not a fully compromised host.CircuitBreaker: trips open after a configurable number of consecutive failures for a named service and stays open for a cooldown, so a plugin can stop hammering an unreachable endpoint.RecordSuccess/RecordFailuredrive it.AllowOperation(out reason)gates the next call.BackoffPolicy: a stateless companion toCircuitBreakerfor per-entity backoff persisted on the entity itself. The caller passes in the currentBackoffState(a consecutive-failure count and an optional pause deadline) and stores the one returned byRecordFailure/RecordSuccess, so a paused record survives restarts. The pause escalates exponentially from a base delay, capped at a maximum, and only engages past a failure threshold.ConcurrentTaskRunner.RunAsync: runs a worker over a collection with a bounded degree of parallelism and reports 0-100 progress as items finish, wrapping the SemaphoreSlim-plus-Interlocked pattern a scheduled task otherwise reimplements. Pairs withPluginScheduledTask.JsonFileStore<T>: a thread-safe JSON file store for the runtime state a plugin keeps beside its configuration (usage counters, lockouts, cursors).Loadtolerates a missing or corrupt file by returning a fresh value;Saveis atomic (temp file then swap);Update(mutate)does an atomic read-modify-write.ActivityLogger: writes a plugin's events to Jellyfin's activity log where administrators already look.Logis fire and forget,LogAsynccompletes once the entry is stored, and neither throws. Names and overviews are cut to the 512 characters the activity log keeps (Trimdoes the same for a caller). Namespace each entrytypeunder the plugin.DatabaseSchema: versions a SQLite database a plugin owns.Open(connection, currentVersion, minReaderVersion, create, migrate)creates an empty database, migrates an older one, and stamps the version inPRAGMA user_versionwith the oldest version that can still read the file in aSchemaInfotable. A newer database this build can read is used untouched (UsedNewer). One it cannot read (TooNew) or a failed migration (MigrationFailed) writes nothing, and the caller closes the connection, callsSetAside(path, reason, keep, logger)to move the file and its journal aside as a timestamped backup, and opens a fresh one. Raise the minimum reader only when a migration changes or removes something older builds rely on. Written againstDbConnection, so plugins without SQLite take on no dependency.StringUtilities.EscapeJsString: escapes a string for safe embedding inside a JavaScript string literal, neutralising quotes, line breaks, and the</script>/ line-separator sequences that break out of an inline script block.RetryPolicy.ExecuteWithRetryAsync: retries an operation with exponential backoff and jitter, but only for transient faults (timeouts, socket errors, 5xx/429, generic IO). Permanent errors throw immediately.FileNameSanitizer: turns an arbitrary string into a safe cross-platform file name (strips invalid/control chars, collapses runs, handles reserved names and length), with aSanitizeTempFileNamehelper for cache files.HashUtilities: 32-char lowercase SHA-256 fingerprints of a string or stream (content identity, not a security primitive).FormatUtilities:FormatBytes(human-readable sizes) andTruncateForLogfor tidy log lines.StreamUtilities.CopyWithSpeedLimitAsync: copies a stream with an optional bytes-per-second cap (download throttling).StringNormalizationUtility.NormalizeStringArray: trims, whitespace-filters, and case-insensitively sorts a string list into a canonical form (or null when empty).
Usage
Add the package:
<PackageReference Include="JPKribs.Jellyfin.Base" Version="2026.10.6" />
Extend the base and yield the shared pages:
public class Plugin : PluginBase<Plugin, PluginConfiguration>
{
public Plugin(IApplicationPaths paths, IXmlSerializer xml) : base(paths, xml) { }
public override Guid Id => Guid.Parse("...");
public override string Name => "My Plugin";
public override IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo { Name = "myplugin", EmbeddedResourcePath = "..." };
foreach (var page in GetSharedPages("myplugin"))
{
yield return page;
}
}
}
Reference the assets from a config page:
<link rel="stylesheet" href="configurationpage?name=myplugin_jpkribs_shared.css">
import { createShared, setTabs, initCollapsibles } from '/web/configurationpage?name=myplugin_jpkribs_shared.js';
For a multi tab plugin, register one native page per tab and call setTabs on viewshow:
var TABS = [
{ href: 'configurationpage?name=myplugin_overview', name: 'Overview' },
{ href: 'configurationpage?name=myplugin_settings', name: 'Settings' }
];
setTabs('myplugin', 0, TABS);
Releasing
Versions are dates. Start the Release workflow from the Actions tab and leave the version blank: it tests, bundles, packs with today's date in America/Denver as YYYY.M.D, pushes to nuget.org, tags vYYYY.M.D, and creates the GitHub release with generated notes, all in one run. A second release on the same day becomes YYYY.M.D.1. The version is never written into the repository, and a version that already exists on nuget.org or as a tag is refused rather than skipped. A version that should not have shipped is hidden with the Unlist Package Version workflow. scripts/pack.sh makes a local package dated the same way for testing a consumer before release.
Learn more about Target Frameworks and .NET Standard.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2026.10.6 | 42 | 10/4/2026 |
| 2026.10.5 | 102 | 10/4/2026 |
| 2026.10.4 | 43 | 10/4/2026 |
| 2026.10.3 | 42 | 10/4/2026 |
| 2026.7.32 | 45 | 10/3/2026 |
| 2026.7.31 | 645 | 7/31/2026 |
| 2026.7.12 | 565 | 7/11/2026 |
| 2026.7.11 | 185 | 7/11/2026 |
| 2026.6.23 | 698 | 6/8/2026 |
| 2026.6.22 | 254 | 6/8/2026 |
| 2026.6.21 | 240 | 6/8/2026 |
| 2026.6.20 | 243 | 6/8/2026 |
| 2026.6.19 | 265 | 6/7/2026 |
| 2026.6.18 | 240 | 6/7/2026 |
| 2026.6.17 | 239 | 6/7/2026 |
| 2026.6.16 | 246 | 6/7/2026 |
| 2026.6.15 | 251 | 6/6/2026 |
| 2026.6.14 | 245 | 6/6/2026 |
| 2026.6.13 | 247 | 6/6/2026 |
| 2026.6.12 | 244 | 6/6/2026 |