Placard 1.0.0 — bake library names onto backdrops

Jellyfin 10.11 plugin: a scheduled task that labels each library card with the
library name over a representative backdrop (Noto Sans, centered, soft shadow),
matching Jellyfin's default cover style. Supports source rules (top-rated /
random / newest) and per-library pinned sources. Sets images via the internal
provider API so the home-screen view cache updates without a server restart.
This commit is contained in:
flan
2026-07-19 19:35:06 +00:00
commit 0c59673e91
14 changed files with 630 additions and 0 deletions
@@ -0,0 +1,35 @@
using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.Placard.Configuration;
/// <summary>Rule for picking which child item's backdrop represents a library.</summary>
public enum SourceRule
{
TopRated = 0,
Random = 1,
Newest = 2
}
public class PluginConfiguration : BasePluginConfiguration
{
/// <summary>Master on/off.</summary>
public bool Enabled { get; set; } = true;
/// <summary>How to choose the source backdrop for each library.</summary>
public SourceRule Source { get; set; } = SourceRule.TopRated;
/// <summary>Darkening overlay opacity, 0-255 (default ~41%).</summary>
public int ScrimOpacity { get; set; } = 105;
/// <summary>Label height as a percent of image height.</summary>
public int FontHeightPercent { get; set; } = 20;
/// <summary>Also label Live TV (SMPTE color bars) and Playlists.</summary>
public bool IncludeSpecialViews { get; set; } = true;
/// <summary>
/// Per-library pinned source titles, one per line as "Library Name=Item Title".
/// A pinned library uses that item's backdrop instead of the <see cref="Source"/> rule.
/// </summary>
public string PinnedSources { get; set; } = string.Empty;
}
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Placard</title>
</head>
<body>
<div id="PlacardConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<form id="PlacardConfigForm">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">Placard</h2>
</div>
<p class="fieldDescription">
Bakes each library's name onto a representative backdrop, matching Jellyfin's default card style.
</p>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="Enabled" />
<span>Enabled</span>
</label>
</div>
<div class="selectContainer">
<label class="selectLabel" for="Source">Backdrop source</label>
<select is="emby-select" id="Source" name="Source">
<option value="0">Highest rated title</option>
<option value="1">Random title</option>
<option value="2">Newest title</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ScrimOpacity">Darkening (0-255)</label>
<input is="emby-input" type="number" id="ScrimOpacity" min="0" max="255" />
<div class="fieldDescription">How much to darken the backdrop so the label reads. ~105 matches the default.</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="FontHeightPercent">Label size (% of height)</label>
<input is="emby-input" type="number" id="FontHeightPercent" min="8" max="40" />
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="IncludeSpecialViews" />
<span>Include Live TV (color bars) and Playlists</span>
</label>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="PinnedSources">Pinned sources</label>
<textarea is="emby-textarea" id="PinnedSources" rows="6"
placeholder="Movies=The Dark Knight&#10;Anime=Death Note"></textarea>
<div class="fieldDescription">
One per line as <code>Library Name=Item Title</code>. Pinned libraries use that title's
backdrop instead of the rule above. Leave blank to use the rule everywhere.
</div>
</div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>Save</span>
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
var PlacardPluginId = "b6f8e2a4-1c3d-4e5f-9a7b-2d4c6e8f0a1b";
document.querySelector('#PlacardConfigPage').addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(PlacardPluginId).then(function (config) {
document.querySelector('#Enabled').checked = config.Enabled;
document.querySelector('#Source').value = config.Source;
document.querySelector('#ScrimOpacity').value = config.ScrimOpacity;
document.querySelector('#FontHeightPercent').value = config.FontHeightPercent;
document.querySelector('#IncludeSpecialViews').checked = config.IncludeSpecialViews;
document.querySelector('#PinnedSources').value = config.PinnedSources || '';
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#PlacardConfigForm').addEventListener('submit', function (e) {
e.preventDefault();
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(PlacardPluginId).then(function (config) {
config.Enabled = document.querySelector('#Enabled').checked;
config.Source = parseInt(document.querySelector('#Source').value, 10);
config.ScrimOpacity = parseInt(document.querySelector('#ScrimOpacity').value, 10);
config.FontHeightPercent = parseInt(document.querySelector('#FontHeightPercent').value, 10);
config.IncludeSpecialViews = document.querySelector('#IncludeSpecialViews').checked;
config.PinnedSources = document.querySelector('#PinnedSources').value;
ApiClient.updatePluginConfiguration(PlacardPluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});
return false;
});
</script>
</div>
</body>
</html>