ZL154/AchievementBadges_for_Jellyfin

A Jellyfin plugin that adds achievement-style badges to user profiles based on viewing activity. Users unlock badges for milestones such as first watch, binge sessions and late-night viewing. Designed to gamify the Jellyfin experience and encourage engagement across libraries.

HTML

70

386 commits

updated Oct 1, 2026

See the code

README

achievement-banner

 █████╗  ██████╗██╗  ██╗██╗███████╗██╗   ██╗███████╗███╗   ███╗███████╗███╗   ██╗████████╗
██╔══██╗██╔════╝██║  ██║██║██╔════╝██║   ██║██╔════╝████╗ ████║██╔════╝████╗  ██║╚══██╔══╝
███████║██║     ███████║██║█████╗  ██║   ██║█████╗  ██╔████╔██║█████╗  ██╔██╗ ██║   ██║
██╔══██║██║     ██╔══██║██║██╔══╝  ╚██╗ ██╔╝██╔══╝  ██║╚██╔╝██║██╔══╝  ██║╚██╗██║   ██║
██║  ██║╚██████╗██║  ██║██║███████╗ ╚████╔╝ ███████╗██║ ╚═╝ ██║███████╗██║ ╚████║   ██║
╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝╚═╝╚══════╝  ╚═══╝  ╚══════╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═══╝   ╚═╝

OpenSSF Scorecard OpenSSF Best Practices

🏆 Achievement Badges for Jellyfin

A full progression, gamification and achievement system for Jellyfin that rewards users based on real viewing activity. Think Xbox Gamerscore meets Letterboxd meets Steam profile customization, built natively into your media server.

Status: Active development — v2.4.1 is live: everything reported against 2.4.0, including two faults that made a correct install look broken on Jellyfin 12 (an uninstall that failed outright, and a page that only worked in a private window), plus a targeted badge cap you can raise to 1000, a top-center toast placement with a server default, an option to keep login-hidden accounts out of other users' views, and a fix for a collision that had been breaking your server's whole API document since 2.1.0. See What's new in v2.4.1. v2.4.0 brought Jellyfin 12 support (a dedicated 12.0 build in every release, and the web side reworked for the new layout and authorization), targeted badges that point at one series, season, collection, playlist, album or item, JellyEmu game achievements, and your shop bling on the shareable card. Before that, v2.3.1 fixed the music feature and cleaned up the leaderboards, and 2.3.0 brought friend profile cards, three shareable card skins (Console / Metro / Aurora), Tracearr history crediting, and the profile data-loss + gzip injection fixes every published build needs. Built on v2.2 "Your Screen, Your Rules", the v2.1 "Open Library" expansion, and v2.0 "Choose Your Loadout".


📑 Table of contents


✨ Overview

Over 200 built-in achievements across 35+ categories, a 10-tier rank ladder from Rookie to Immortal, a full score economy with combos, prestige, daily/weekly quests, a Score Shop with 70+ cosmetics, power-up consumables, a Friends drawer with messaging, plus admin power features like custom badges, seasonal challenges, webhook notifications, and a full audit log.

Designed to integrate cleanly with modern Jellyfin setups and themes like NetFin, ElegantFin, or StarTrack.


🚀 What's new in v2.4.1 — what 2.4.0 got wrong, and a cap you can raise

Everything reported against 2.4.0, including two faults that made a correct install look broken on Jellyfin 12, plus a cap the people who author targeted badges by the hundred can now raise. Drop-in upgrade from v2.4.0 — no schema change, no migration. Full notes in docs/release-notes/v2.4.1.md.

🟣 The two Jellyfin 12 faults (#140, #141)

  • Uninstall failed outright, and the plugin's image 404'd. Each release ships two packages from one source tree, and both assemblies were stamped x.y.z.0. Jellyfin then read two different numbers for one install — the dashboard shows the assembly's version, uninstall and the image endpoint resolve through the manifest's — so on Jellyfin 12 the plugin installed as 2.4.0.1, displayed as 2.4.0.0, and could not be removed. Each package now stamps the version it ships as. Stuck on 2.4.0.1? Updating through the catalogue is enough; the restart that loads the new build deletes the old folder.
  • The page worked in a private window and nowhere else. A browser that cached Jellyfin's page before the plugin was installed revalidates it, Jellyfin answers 304 Not Modified from the file on disk, and there is no page body for the plugin to inject into — so the browser keeps using a copy with no plugin in it, forever. Worst on servers with a read-only web directory, where the on-disk patch can never take either. The middleware now forces a full page while the on-disk patch has not taken. Nobody needs to clear their browsing data — a normal reload after updating is enough.

🎯 A targeted badge cap you can raise (#129)

  • The cap is now a setting, under Custom badges → Targeted badge cap (1 to 1000, default unchanged at 50). The page shows how many distinct targets your enabled badges reference against the cap, and names the ones past it so you can see what is not being computed.
  • The per-play check no longer grows with the number of targets. Series, seasons and folders are tested against the played item's ancestors in one lookup; collections and playlists use a member list cached per target and refreshed when the container changes. Hundreds of targets are fine on any server.

🎨 Fixes reported against 2.4.0

  • The Revamp style reached the rest of Jellyfin (#133). With UI: Revamp selected, the plugin was restyling Jellyfin's own controls and every other plugin's settings page — checkboxes went invisible, text inputs lost their border and height, text selection changed colour everywhere. Twelve rules named generic elements while keying only on an attribute that lives on <body>; they are now anchored to the plugin's own two surfaces, and a test refuses any new rule of that shape.
  • A Custom Tab that rendered nothing (#131). With themes that ship their own copy of Jellyfin's home page (Abyss's Spotlight), Custom Tabs' server-side panel injection silently fails to match, leaving the tab button with no panel behind it — a blank Achievements page. The plugin now builds the missing panel itself.
  • Toasts gain a top-center placement, and a server default (#136). Top-center joins the five existing placements, and an admin can set the default for everyone who has not picked one; a user's own choice still wins. Two bugs turned up underneath it: the default UI style from #43 was never actually stored, and every main config save silently switched the page integrations back off.
  • Login-hidden accounts can be kept out of other users' views (#138). Off by default. When on, an account with Jellyfin's Hide this user from login screens is visible only to itself, administrators, other hidden accounts, and accounts it is already mutual friends with. Note that Jellyfin ticks that box for accounts it creates, so on a server that keeps the default every account is hidden and the option changes nothing.

📄 The plugin was breaking your server's API document

Found while testing this release, not reported by anyone, and present since 2.1.0 on both Jellyfin 10.11 and 12: GET /api-docs/openapi.json answered 500 for the whole server.

Jellyfin builds one OpenAPI document from itself and every installed plugin, and each schema in it is keyed by the bare type name. This plugin's media-type enum was called MediaType, which is also the name of one of Jellyfin's own enums — and a duplicate name is not resolved, it throws, taking the entire document down. Swagger UI, the dashboard's API browser and every generator that reads the spec were all broken, by a plugin that started cleanly and logged nothing about it.

The enum is now BadgeMediaType. Its numbers and the property carrying it are unchanged, so there is nothing to migrate. With the collision gone the document builds, and this plugin's 126 routes appear in it for the first time. A test now walks the plugin's public types against Jellyfin's and fails on any shared name, because nothing else could catch this: the break was in the host's document, not in this plugin's.

If your API docs page is still broken after updating, another plugin is colliding the same way — the server log names both types.

Big thanks to @camarigor for the cap and the per-play rework (#130), the Custom Tabs repair (#132), the Revamp scoping (#134), the toast work (#137), the hidden-accounts option (#139), and for catching what the first pass at #140/#141 got wrong (#144); to @Roboatlas21 for diagnosing both Jellyfin 12 faults from his logs (#140, #141); to @Tschiyo for the cap request (#129); to @Borededdy for the blank Custom Tab (#131); to @clarjon1 for the Revamp leak (#133); to @Verdancy-Rin for the top-center placement (#136); and to @Digital-Yeti for the hidden-accounts request (#138).


🧩 Core features

🏅 Badge system

  • 200+ built-in achievements across categories: Films, Series, Binge, Night Watching, Morning, Afternoon (12-17h), Prime Time (19-22h), Weekend, Exploration, Streaks, Episode/Film Marathons, Eras, World, Languages, Genres, Runtime, Total Time, Holidays (Christmas, New Year, Halloween, Eid, Valentine's, Easter, Lunar New Year, Diwali, US Thanksgiving, Independence Day, Bonfire Night, Boxing Day, Mother's Day, Father's Day), Library Completion, Loyalty, People, Rewatch, Anime, Studio Specialist, Pilot vs Completer, and Hidden categories
  • 8 v2.0 easter-egg badges — First Anniversary, Late-Night Sage, Quadruple Feature, Dawn Chorus, Universal Watcher, Archivist Supreme, Deep Cut, Saga Marathon (don't appear until unlocked)
  • 18 music badges (v2.1.0) — plays, listening hours, and unique albums / artists / genres / decades. Audio plays pass the same 80% real-listen gate as video.
  • 8 book badges (v2.1.0) — books completed, audiobook listening hours, and series completed. Audiobook plays route per the audiobook-counting policy (Books-only default / Music-only / Both).
  • Improved anime detection (v2.1.0, #25) — matches Genres and Tags on the item and its parent Series, with admin-configurable libraries / genres / tags.
  • Admin-authored custom badges (v2.1.0) — compound AND/OR criteria across any metric; see Custom badges.
  • Targeted badges (v2.4.0, #107): point a badge at one specific series, season, collection, playlist, album or item; see Targeted badges.
  • Shop cosmetics on the shareable card (v2.4.0, #42): the equipped custom title and badge frame show on the shareable profile card and on the friends-drawer profile card, under the same privacy toggles as the equipped badges.
  • Game achievements (v2.4.0, #115): sessions, hours, distinct games and platforms for games played through JellyEmu, plus custom badges per platform, per developer and per game; see Game achievements.
  • 6 rarity tiers — Common, Uncommon, Rare, Epic, Legendary, Mythic
  • Hidden/secret badges displayed as ??? until unlocked
  • Library completion milestones that auto-scale to any library structure
  • Per-person tracking — Director and Actor affinity badges
  • Per-genre tracking — unique genre counters with dedicated badges
  • Era / country / language breakdowns via item metadata
  • Watch streaks — current and best streak badges
  • Daily login streak — loyalty rewards for consistent visits

Standout sub-collections:

  • 🎌 Anime tier (5 badges) — Genre-tag detection (any genre containing "anime", case-insensitive). Anime Curious / Anime Fan / Otaku / Anime Veteran / All-Otaku at 5 / 15 / 50 / 200 / 500 anime items.
  • 🎬 Studio specialists (6 badges) — Letterboxd-style "I watch a lot of A24" badges, parameterised by BaseItem.Studios:
BadgeStudioThreshold
Spirited AwayStudio Ghibli5
A24 AcolyteA2415
It's Not TVHBO25
Netflix and WatchNetflix50
Auntie BeebBBC25
House of MouseDisney50
  • 🎟️ Pilot vs Completer (6 badges) — every series whose S1E1 you watch is added to a per-user "pilots watched" set. If any other episode of that series follows, the series graduates into "continued past pilot". Pilot Tester / Window Shopper / Commitment Issues for sampling 5 / 20 / 50 series and bailing after the pilot; Hooked / Sticks the Landing / Always In for continuing past the pilot on 5 / 20 / 50 series.

Backfill credits these too — the watch-history backfill task passes Studios, SeriesId, SeasonNumber, EpisodeNumber through to the achievement service, so historical episodes credit anime / studio / pilot badges. Time-of-day buckets only count NEW sessions.

🎖️ Rank system

  • 10 tiers from Rookie → Novice → Viewer → Regular → Enthusiast → Binger → Connoisseur → Maestro → Legend → Immortal
  • Rank computed from your achievement score with progress bar to next tier
  • Theme unlocks — the achievements page changes gradient/border color as you climb
  • Custom rank titles (new in v2.0) — equip a cosmetic title (Cinephile / Curator / Tastemaker etc.) to replace the auto-generated tier name
  • Sidebar badge showcase + header dots display your current equipped badges at a glance

💰 Score economy

  • Every playback accrues 5 base points into a score bank
  • Combo multiplier — consecutive watches within 15 minutes stack up to +100% bonus
  • Spend bank to buy locked badges directly, or in the new Shop on power-ups + cosmetics (v2.0)
  • Gift score to other users on your server
  • Daily login bonus (v2.0) — +10 score on first real watch of each UTC day
  • Rarity-based badge scoring (10-150 pts), scaled by prestige level

⭐ Prestige

  • Reach Legend rank (12,000 score) to unlock prestige
  • Resets badges + counters but keeps your lifetime score and awards a prestige star
  • Each prestige level adds a +50% score multiplier to future badge unlocks
  • Visible on profile and leaderboard

🎯 Daily & weekly quests

  • 3 concurrent daily quests rotating from 12 built-in templates
  • 3 concurrent weekly quests rotating from 8 built-in templates
  • Deterministic rotation — everyone on the server gets the same quests per day/week
  • Completing quests pays into the score bank
  • Reroll (new in v2.0) — 1 daily reroll per UTC day, 1 weekly reroll per ISO week. Disabled pill shows you've used it, comes back at the next reset.
  • Admin quest customization — add / edit / remove daily + weekly quests from the admin page, replace built-in quests by Id, or disable built-ins your server can't satisfy

⚡ Power-ups & Score Shop

  • 3 consumable power-ups — XP Boost, Double Credit, Streak Freeze
  • 70+ cosmetic items across 6 kinds — themes, frames, titles, avatars, backgrounds, borders
  • Steam-style storefront with featured carousel, live previews, milestone progress bars, "NEED N MORE SCORE" affordability indicators
  • Daily login bonus drops a random power-up on first ≥80% real watch of each UTC day
  • All purchases audit-logged; admin testing tools to grant items without watch grinding

🎨 Profile customization

  • 14 Profile Themes — repaint the whole achievements page in any of 14 styles
  • 7 Badge Frames — animated edges on your equipped badges
  • 10 Custom Rank Titles — 3 auto-unlock + 7 shop-only
  • 14 Avatars — emoji swap for the rank-medal icon
  • 8 Animated Backgrounds — 4 CSS + 4 HD MP4 video loops, viewport-fixed with dimming overlay
  • 6 Profile Borders — animated effects on the hero card
  • Seen by everyone (v2.4.0) — every equipped cosmetic shows on your shareable profile card and on the card friends open from the drawer, under the same privacy toggles as your equipped badges. See What's new in v2.4.0.

👥 Friends drawer

  • Bi-directional friendship with a proper request / accept flow — nobody follows you silently
  • Global floating button anchored bottom-left on every Jellyfin page — not just the achievements tab. Auto-hides on /dashboard + /plugins pages and during media playback; reappears as soon as you leave either state
  • Xbox-guide-style side drawer with four sub-tabs: Friends / Requests / Find / Messages
  • Profile cards — hover a name for a summary, click to pin a larger card: rank, completion, score, streak, equipped badges, and since v2.4.0 the friend's custom title, badge frame, theme accent, profile border and avatar
  • Hover/click profile cards (v2.3.0, #76) — hovering a friend's name or avatar opens a summary card (rank tier, completion, score, best streak, equipped showcase); clicking pins a larger card that closes on Escape or an outside click and is keyboard-reachable. Served by a public-summary endpoint gated by the same leaderboard opt-out, so it exposes nothing extra
  • Jellyfin profile-image avatars per friend row (initials fall back when no image is set)
  • Online / offline status pulled live from Jellyfin's ISessionManager, with a 15-minute grace window so casual browsing still counts as online (not just active playback)
  • Now playing display — see the series + episode title each online friend is watching
  • Offline — last watched X — offline friends now show what they watched most recently (mirrors the online "Watching X" treatment). Backed by IUserDataManager with reflection-based LastPlayedDate lookup
  • Each friend's equipped badges shown next to their row (respects their privacy prefs)
  • Type-to-search user picker in the Find tab (not a giant dropdown of every server user)
  • Red unread badge on the floating button + Requests tab when someone has sent you a friend request
  • Mutual indicator on friend rows; Auto-accept kicks in if the target has already sent a request to you
  • Privacy toggles in user settings: Appear offline to friends (always shows you as offline), Hide what I'm watching (still online, but the series/episode is hidden), and Hide my last watched when offline. Enforced server-side in FriendsService.BuildFriendRow — can't be bypassed by client tampering
  • Compact request rows — Accept / Decline / Cancel buttons are icon-only with tooltips, so the Requests tab doesn't visually bloat
  • Drawer follows the Classic/Revamp toggle — sets body[data-ab-style="revamp"] so the same tokens apply globally

💬 Messaging

Xbox-Guide-style chat built into the Friends drawer. No external service, no WebSockets, everything stored on your own server.

  • Messages tab next to Friends / Requests / Find, with threads newest-first, avatars, last-message preview + timestamp, and a per-conversation unread count
  • 1:1 DMs and group chats (2–20 participants, named, rename + leave + members list)
  • Image attachments — PNG / JPEG / GIF / WebP up to 8 MB, server-side magic-byte verification so nobody can smuggle a .exe renamed to .png. Click-to-zoom lightbox
  • Read receipts — single green ✓ for delivered, double green ✓✓ for read. Per-participant readBy map so group receipts work too
  • Edit and delete your own messages via WhatsApp-style dropdown — click anywhere on your own bubble to open the menu, 24-hour edit window
  • Clear conversation button in the chat gear menu (wipes both sides, confirm dialog)
  • Block user — bi-directional; existing messages kept, new ones rejected. Stored in UserAchievementProfile.Preferences.BlockedUsers
  • Per-peer mute (localStorage-only, no server round-trip) + per-user prefs in the standalone settings panel:
    • Master message-notifications toggle
    • Message sound (subtle Web Audio chime, no audio file)
    • Mute during playback — suppresses notifications while Jellyfin's video player is active
  • Notifications:
    • In-app toast (top-center, 5s auto-dismiss, click to open the thread)
    • Browser Notification API when the tab isn't visible and permission is granted
    • Sound chime on inbound message
  • Unread indicators — pulsing red dot on the chat button in the friends list + count pill on the Messages tab + combined badge on the floating friends button
  • Anti-flicker rendering — content-hashed; the chat pane only repaints when something actually changed, preserving scroll position
  • Auto-polling — 6 s inside an open chat, 8 s on the Messages tab, 20 s drawer-wide when closed; all small, all [Authorize]-gated
  • Rate limits — 20 messages per minute per sender, max 1000 chars per message, 2000-message FIFO cap per conversation
  • Friendship gate: only mutual friends can DM; group creators can only add their own friends. Admin's FriendsSimpleMode treats the whole server as one friend list for messaging too (with the #138 option on, only between users who can see each other)
  • Storage — single messages.json + attachments.json + attachments/<id>.<ext> on disk under plugins/configurations/achievementbadges/. Atomic writes via temp file + File.Move so a crash mid-send can't corrupt the store. Messages survive server restarts

🏅 Rarity percentage chip

  • Every badge card on the achievements page shows a coloured chip with the % of users on your server who have unlocked that badge
  • Green ≥ 50% (common on this server), amber 10–50% (uncommon), red < 10% (rare / flex-worthy)
  • Fed by a /badges/rarity-stats endpoint with a 5-minute server-side cache so it doesn't re-scan every profile on every page load
  • Scarcity signal lets users see "nobody else on this server has this badge" at a glance

💾 Data-loss recovery

  • Load() walks primary badges.json → .bak → .recovery before giving up. A flaky primary file with a clean backup recovers silently
  • .bak is rotated on every successful save
  • .recovery captures in-session state when the primary is quarantined, so restart never feels like a fresh reset
  • LastLoadSummary exposed via the /test endpoint so admins can see recovery activity
  • Unparseable primary files are quarantined to badges.json.corrupt-<timestamp> (not deleted) for manual recovery

📊 Stats & visualization

  • Recap tab — weekly / monthly / yearly breakdowns with top genres, directors, actors
  • Year Wrapped — Spotify-style end-of-year recap with hero, your numbers, your highlights, your favorites
  • Watch heatmap — GitHub-style calendar (30/90/180/365 day range) colored by intensity
  • Genre radar chart — SVG spider chart of your genre distribution
  • Watch clock — 24-hour polar chart of when you actually watch
  • Stats snapshot — histogram of unlocked / score / best streak
  • Category leaderboards — Score, Movies, Episodes, Hours, Best Streak, Series

🏠 UI integration

  • Sidebar entry auto-injected into the Jellyfin nav menu (works on web, iOS, and Android after restart)
  • Jellyfin 12: the modern layout has no navigation drawer, so the entry sits in the avatar menu, right below Profile, and the equipped strip in the toolbar; the legacy layouts (desktop-legacy, mobile-legacy, TV) keep the drawer entry.
  • Equipped badge showcase in header + profile (configurable slot count, 1-10), ringed with your equipped badge frame
  • Xbox-style unlock toasts with per-rarity colors (6 tiers), Xbox logo → trophy swap, shimmer sweep, and confetti on rare+ unlocks
  • Achievement sound — Xbox 360 chime for common/uncommon, rare Xbox One chime for rare/epic/legendary/mythic
  • Diamond spritesheet for legendary/mythic unlocks (147-frame rotating crystal animation)
  • Configurable unlock bursts — group simultaneous unlocks into one score/count summary (default), or play each achievement animation separately
  • Per-device toast delivery — choose all signed-in devices (default) or only the Jellyfin device that earned the achievement
  • Toasts during playback — unlocks fire within ~1s of earning via playback event hooks + DOM fallback
  • Admin toast preview — test buttons for each rarity tier
  • Standalone achievements page at #/achievements with the new Loadout tab (v2.0) for managing power-ups, shop, and cosmetics
  • Optional page hosts — reuse the same page inside Custom Tabs or register it with Plugin Pages; both are admin opt-in and the stock page always remains available
  • Modular per-user navigation — independently show or hide the Custom Tabs entry, Plugin Pages entry, and header trophy without affecting achievement tracking or each other
  • Shareable profile card (three skins, v2.3.0) — server-rendered HTML at /Plugins/AchievementBadges/users/{id}/profile-card, in Console (default), Metro, or Aurora Spine, each drawn with the owner's rank colour as the accent. Users pick their skin in preferences; ?style= overrides per link, and a request with no style falls back to the owner's choice
  • Friend popover style (v2.3.0) — the drawer profile card comes in a detailed or a compact layout, chosen per user
  • Classic / Revamp toggle — every screen the plugin renders has a Classic/Revamp toggle (ab-style-pref localStorage):
    • Revamp — magazine-spread hero with massive Geist 700 rank name, 220px conic completion donut, asymmetric stats grid, chapter-numbered tabs (01 / MY BADGES, 02 / QUESTS…), control-panel filter strip, ambient drift orb, film grain overlay, day-streak pulse, rank-name shimmer, full page entrance cascade
    • Classic — the v1.8.10 look you know
    • Friends drawer follows the toggle too. Admin page has HUD corner brackets, conic rim sheen on Mythic + Legendary cards, animated KPI count-up, tier-staggered reveal.
  • Revamp admin operator strip — Use my account / Refresh / Simulate playback buttons + live status banner sit at the top of the personal section in Revamp mode. Long-running ops like Scan all users visibly pulse with status text.

⚙️ User preferences

A gear icon on the achievements page opens a full settings panel with auto-save:

  • Toast controls — enable/disable toasts, sound, confetti, milestone toasts
  • Unlock toast style — grouped summary or individual animations
  • Toast device scope — every signed-in client or only the device that triggered the unlock
  • Minimum toast rarity — filter out common spam (All / Rare+ / Epic+ / Legendary+)
  • Privacy — hide from leaderboard, compare profiles, activity feed, prestige board
  • Navigation integrations — independently show/hide the Custom Tabs entry, Plugin Pages entry, and header/profile trophy shortcut
  • Achievement page theme — Default, Dark, or Light (legacy site-wide theme, separate from v2.0's profile-theme cosmetics)
  • Spoiler mode — hides locked badge descriptions with "???" so you discover them naturally
  • Equipped badge slots — choose how many badges show in your showcase (1-10)
  • Auto-equip new unlocks — newly earned badges automatically fill empty slots
  • Per-user language picker — overrides the admin's server-wide default
  • Hide my last watched when offline — friends won't see what you watched most recently when you're offline
  • Mute toasts during playback + Mute toast sound during playback — keep the unlock animation/sound out of the way while actively watching
  • Message notifications + sound + mute-during-playback — full control over the Friends drawer's chat notifications
  • Shareable card skin (v2.3.0) — Console / Metro / Aurora Spine for your own profile card
  • Friend popover style (v2.3.0) — detailed or compact layout for the hover/click card
  • Toast position (v2.3.0, #136): place unlock toasts in any of the four corners, top-center or bottom-center, or leave it on the server default your admin picks

🌍 Languages

  • 8 languages — English, Français, Español, Deutsch, Italiano, Português, 中文 (简体), 日本語
  • ~2,200 UI keys translated across every tab (achievements profile, stats, heatmap, streak calendar, prestige leaderboard, notification prefs, server stats, compare, leaderboards, recap, quests, admin page, and every v2.0 Loadout surface)
  • All 200+ built-in badges translated — titles + descriptions per language, with hand-tailored cultural references (the Spirited Away badge is Le Voyage de Chihiro / El Viaje de Chihiro / Chihiros Reise / La Città Incantata / A Viagem de Chihiro / 千与千寻 / 千と千尋)
  • Badge categories + rarities also localised ("Binge" → "Marathon", "Legendary" → "Légendaire", etc.)
  • All v2.0 cosmetic names + descriptions translated — Cinephile / 影迷 / シネフィル / Cinéphile, every theme, every avatar, every background
  • Per-user language picker in preferences; admin can set a server-wide default
  • Globe language picker (v2.1.0) — a globe dropdown in the admin-page header switches the UI language live and persists across reloads
  • Fully localized admin page (v2.1.0) — the Integrity & anti-abuse and v2.0 testing-tools sections (previously English-only) are now translated across all 8 locales, as are the new music + book badges
  • Translations loaded client-side + server-side (BadgeLocalizer) so both UI chrome and badge titles on the leaderboard / showcase / admin grid / equipped showcase localise together

🛠️ Admin features

  • Feature Controls — kill switches for leaderboard, compare, activity feed, prestige, quests
  • Force Privacy Mode — override all users to hidden from all social features
  • Keep accounts hidden from the login screen out of other users' views (#138): an account with Jellyfin's Hide this user from login screens stays out of the leaderboards, the activity feed, friend search, compare and profile cards for everyone else. Hidden accounts, administrators and the account's existing friends still see it. Off by default, and with every account hidden from the login screen it changes nothing even when on, since hidden accounts see each other
  • Max Equipped Badges — server-wide cap (1-10)
  • Restrict Badge Visibility — users can only see their own badges
  • Disable Badge Categories — hide entire categories (e.g. "Late Night" for family servers)
  • Custom Welcome Message — text shown on the achievements page
  • Reset User Progress — wipe a specific user's badges via admin endpoint (v2.4.0: refuses a malformed or unknown user id instead of answering 200)
  • Diagnosable errors (v2.4.0) — every admin-page failure names the request that failed, and a watch-history scan that finds nothing tells you to check the account's library access
  • Enable/disable individual badges — useful if your server can't satisfy some criteria
  • Visual badge editor — form-based creator for custom badges
  • Custom badge builder (v2.1.0) — simple badges via an admin form, or compound AND/OR criteria + import/export via the /Plugins/AchievementBadges/custom-badges API. See Custom badges
  • Daily-badge audit & cleanup (v2.1.0, #27) — scans every profile for time-windowed badges wrongly awarded by pre-v2.1.0 backfills and clears them (re-earnable organically); audit-logged
  • JSON editor alternative for power users
  • Seasonal challenges — time-limited goals with start/end dates
  • Challenge templates — one-click add for Monthly Marathon, October Horror, New Year, Summer Blockbuster
  • Webhook notifications — Discord/Slack-compatible POST on every unlock
    • HMAC-SHA256 signing — when admin sets WebhookSigningSecret, every outbound POST carries X-AchievementBadges-Signature: sha256=<hex> + X-AchievementBadges-Timestamp: <unix>. Receivers verify with HMAC(secret, timestamp + "." + raw_body) and reject stale timestamps to prevent replay. Same envelope as Stripe and GitHub. Empty secret = legacy unsigned behaviour (backward compatible).
  • Audit log — last 5,000 events with timestamps. AdminAuditLogFilter writes an entry on every RequiresElevation action — answer "who unlocked X for whom last Tuesday" without grepping runtime logs.
  • Progress injection — set arbitrary counter values for testing / gifting
  • v2.0 testing tools — grant score / power-up / cosmetic to a user, backfill milestones across all profiles
  • Server-user picker on the admin/config pages for easier multi-user testing
  • Manual badge revoke (v1.9.8) — un-award a specific badge from a specific user when needed
  • Integrity test injection (v1.9.8) — verify daily-cap and suspicious-rate audit flag end-to-end on a throwaway user
  • Admin auth lockdown — all admin endpoints require elevated permissions
  • Default UI style + lock (#43) — pick whether users start on Classic or Revamp, and optionally make it the only choice so the achievements page matches your Jellyfin theme. A user's own pick is remembered rather than erased, so it returns if you lift the lock
  • Default toast position (#136): where unlock toasts appear for everyone who has not picked a placement; a user's own choice always wins
  • Tracearr watch history (#45) — set a Tracearr URL and API token to credit plays the library scan cannot see. See Tracearr integration

🔒 Tracking

  • Watch history backfill — scans existing Jellyfin play history to retroactively award badges on install
  • Auto-evaluation on startup — new badges from plugin updates auto-unlock if your existing counters already satisfy them, no manual scan needed
  • Live playback tracker — unlocks fire during viewing, past the 80% completion threshold
  • Real-watch credit gate (v1.9.8) — playback events require ≥80% accumulated play ticks with seeks excluded; mark-as-played + seek-to-end + spam-click no longer credit badges
  • 60-second minimum runtime filter so Projectionist prerolls and bumpers can't credit
  • Daily credit cap (default 200/user/day, configurable) + SuspiciousRatePerHour audit flag (default 30/h, configurable) for soft anti-abuse
  • Rewatch detection — dedupes within 6 hours, counts rewatches beyond that
  • Watch-time carry (v2.3.0) — partial viewings are banked and survive a session break, a restart, and a media-file replacement or quality upgrade (keyed by media identity, #89), with an admin retention window (#87); a scan then clears the carry of exactly what it credits (#91/#92) so already-counted minutes can't finish a later rewatch
  • Library + artist completion wiring (v2.3.0) — the library-completion badges are recomputed during the scan (#80) and per-artist discography completion is tracked (#81), so both unlock through normal use
  • People metadata extraction — uses ILibraryManager.GetPeople() for directors/actors

🎨 Custom badges

Added in v2.1.0 "Open Library". Admin-only. All endpoints require admin elevation.

Define your own badges with compound AND/OR criteria the built-in catalog can't express. The plugin config page (Dashboard → Plugins → Achievement Badges → Custom Badges) has a form for simple badges (single metric + threshold). For compound criteria, icon control, and import/export, use the API below.

Criteria model

A badge's criteria is a tree. A node is either:

  • a leaf: { "metric": "<Metric>", "threshold": <int>, "metricParameter": "<optional>" } — true when the metric value ≥ threshold.
  • a compound: { "operator": "And" | "Or", "children": [ <node>, ... ] } — true when And(all)/Or(any) of children are true.

Limits: max depth 5, max 64 nodes per badge, max 500 badges per instance.

Targeted badges

Added in v2.4.0, from #107.

Two metrics point at one specific thing in your library rather than at an aggregate:

  • ContainerCompletionPercent — played items over total items of one series, season, collection, playlist or album. Threshold 100 means "you finished it".
  • ItemPlayCount — how many times one specific item has been played. Threshold 1 is "you watched this movie"; larger thresholds are for the track you keep going back to.

Both take their target in metricParameter, formatted "{guid}|{name}". The config page has a library picker that writes it for you. Both halves are stored on purpose: the GUID survives a rename, and the name survives an item being deleted and re-added, so the badge re-resolves itself and rewrites the GUID.

For an arbitrary group of episodes, such as one story arc of a long-running show, make a Jellyfin collection and point a ContainerCompletionPercent badge at it. That keeps the grouping in a native Jellyfin feature instead of a badge-editor episode list.

Targeted badges are evaluated against your existing history, so a badge you author today unlocks immediately for anyone who already finished the target, with no scan needed. How many distinct targets all badges may reference is set on the config page, under Custom badges, as Targeted badge cap (1 to 1000, default 50). The page shows how many targets the enabled badges reference against the cap and names the ones past it, which are not computed. Every play checks each target with a set lookup: series, seasons and folders against the played item's ancestors, collections and playlists against a cached member list that is refreshed when the container changes, so hundreds of targets of any kind are fine on any server.

Template — finish one series

// POST /Plugins/AchievementBadges/custom-badges
{
  "name": "Straw Hat",
  "description": "Finish One Piece: Alabasta.",
  "rarity": "Epic",
  "media": "TV",
  "criteria": {
    "metric": "ContainerCompletionPercent",
    "metricParameter": "8f4e2a1b9c3d4e5f6a7b8c9d0e1f2a3b|One Piece: Alabasta",
    "threshold": 100
  }
}

Game achievements (JellyEmu)

Added in v2.4.0, from #115.

Games played through JellyEmu earn achievements with no setup on the JellyEmu side. JellyEmu reports every game session to Jellyfin as a playback session and tags each game with JellyEmu, Game and its platform; this plugin reads those and measures each session as the smaller of what the emulator reported and what the plugin saw on its own clock, with a floor (MinGameSessionSeconds, default 60) so a misclick is not a play and a cap (MaxGameSessionMinutes, default 360) so a tab left open is not a night of play.

Built in, under the Games category: session ladders (1, 25, 100), distinct games (10, 50), hours (10, 100) and platforms (3, 8). For custom badges, the builder offers:

  • GamePlatformGames with the platform tag as parameter (SNES, SegaGenesis, PlayStation; the spelling is JellyEmu's, matched without regard to case): "play 10 Sega Genesis games".
  • GameStudioGames with the developer or publisher as parameter: "play 5 games by Capcom".
  • GameHours with a specific game as target, chosen in the picker like any other targeted badge: "30 hours in this game".

Two limits worth knowing: play time counts from the moment this version is installed, because games carry no played flag to rebuild history from; and nothing here reads RetroAchievements, whose unlocks cannot happen in JellyEmu's browser emulator (see the discussion on #115).

Template — simple badge

// POST /Plugins/AchievementBadges/custom-badges
{
  "name": "Cinephile",
  "description": "Watch 100 films.",
  "rarity": "Rare",            // Common | Uncommon | Rare | Epic | Legendary | Mythic
  "media": "Film",             // Film | TV | Music | Book | Anime | Multi
  "iconUrl": "",               // optional external https URL; blank = default trophy
  "enabled": true,
  "criteria": { "metric": "MoviesWatched", "threshold": 100 }
}

Template — compound (AND / OR)

{
  "name": "Renaissance Viewer",
  "description": "Watch 100 films AND listen to 50 music tracks.",
  "rarity": "Epic",
  "media": "Multi",
  "enabled": true,
  "criteria": {
    "operator": "And",
    "children": [
      { "metric": "MoviesWatched",   "threshold": 100 },
      { "metric": "MusicPlaysTotal", "threshold": 50 }
    ]
  }
}

Nested example — "(finish 5 horror series OR 10h of audiobooks) AND watch 25 anime items":

{
  "name": "Eclectic",
  "description": "Genre-spanning dedication.",
  "rarity": "Legendary",
  "media": "Multi",
  "enabled": true,
  "criteria": {
    "operator": "And",
    "children": [
      {
        "operator": "Or",
        "children": [
          { "metric": "SeriesCompleted",         "threshold": 5,  "metricParameter": "Horror" },
          { "metric": "AudiobookListeningHours",  "threshold": 10 }
        ]
      },
      { "metric": "AnimeItemsWatched", "threshold": 25 }
    ]
  }
}

Endpoints

MethodRoutePurpose
GET/Plugins/AchievementBadges/custom-badgesList all
GET/Plugins/AchievementBadges/custom-badges/{id}Fetch one
POST/Plugins/AchievementBadges/custom-badgesCreate (fresh id assigned)
PUT/Plugins/AchievementBadges/custom-badges/{id}Update
DELETE/Plugins/AchievementBadges/custom-badges/{id}Delete
GET/Plugins/AchievementBadges/custom-badges/exportExport all as JSON
POST/Plugins/AchievementBadges/custom-badges/importBulk import (fresh ids)
GET/Plugins/AchievementBadges/custom-badges/targetsTargeted badge cap: configured value, applied cap, bounds, targets observed, and the names past it (#129)
POST/Plugins/AchievementBadges/custom-badges/targetsSet the cap (1-1000, clamped); answers with the fresh summary (#129)

All metrics

Every value AchievementMetric exposes, 71 of them. Entries marked * accept a metricParameter.

  • Watching — TotalItemsWatched, MoviesWatched, SeriesCompleted, LongSeriesCompleted, VeryLongSeriesCompleted, RewatchCount, ShortItemsWatched, LongestItemMinutes, TotalMinutesWatched, SeriesSampledOnly, SeriesBingedAfterPilot

  • Streaks and days — DaysWatched, CurrentWatchStreak, BestWatchStreak, DaysLoggedIn, CurrentLoginStreak, BestLoginStreak, MaxEpisodesInSingleDay, MaxMoviesInSingleDay, MaxMinutesInSingleDay

  • Time of day — LateNightSessions, EarlyMorningSessions, AfternoonSessions, PrimeTimeSessions, WeekendSessions

  • Variety — UniqueGenresWatched, UniqueCountriesWatched, UniqueLanguagesWatched, UniqueDecadesWatched, UniqueLibrariesVisited, TopDirectorCount, TopActorCount, MaxLibraryItemCount

  • Completion — LibraryCompletionPercent *, LibrariesAt100Percent, BadgesUnlockedPercent, ArtistCompletionPercent *

  • Music — MusicPlaysTotal, MusicListeningHours, UniqueMusicAlbums, UniqueMusicArtists, UniqueMusicGenres, UniqueMusicDecades, MusicGenrePlays *, MusicGenreListeningHours *

  • Books — BooksCompleted, AudiobookListeningHours, UniqueBookSeriesCompleted

  • Score and prestige — PrestigeLevel, LifetimeScore, BestComboCount

  • Parameterized by name — GenreItemsWatched *, PersonItemsWatched *, StudioItemsWatched *, DecadeItemsWatched *, DayOfWeekItemsWatched *

  • Holidays and dates — WatchedOnChristmas, WatchedOnNewYear, WatchedOnHalloween, WatchedOnEid, WatchedOnValentines, WatchedOnEaster, WatchedOnLunarNewYear, WatchedOnDiwali, WatchedOnThanksgiving, WatchedOnIndependenceDayUS, WatchedOnBonfireNight, WatchedOnBoxingDay, WatchedOnMothersDay, WatchedOnFathersDay

  • Anime — AnimeItemsWatched

Parameterized metrics match their parameter case-insensitively. GenreItemsWatched with metricParameter of "horror" counts only horror items; LibraryCompletionPercent with "Movies" reads that one library, and without a parameter it reads whichever library the user is furthest through. The same applies to ArtistCompletionPercent, which without a parameter reads the user's best artist.


📡 Tracearr integration

Optional. Set Tracearr URL and Tracearr API token in the plugin settings, generate the token in Tracearr under Settings, and a watch history scan will also read your Tracearr history. Leave either field empty and nothing changes.

It exists because a library scan has two blind spots it cannot fix on its own:

  • Media you deleted. The scan reads IsPlayed on items that still exist, so a film you watched and later removed is invisible to it. Tracearr recorded the play when it happened and still has it.
  • How many times you watched something. IsPlayed is a boolean, so no number of viewings can produce a rewatch count. That leaves the Rewatch badges unreachable for anyone who installed the plugin after they had already been watching.

Only plays Tracearr considers finished are credited, each on the date it happened rather than today, so an old viewing cannot manufacture a streak. A play the library already accounted for is not counted again: the first viewing of a known item is skipped, and only repeats become rewatches.

Nothing is required on the Tracearr side. It uses the existing public v2 API.


🛡️ Security & operations

  • Default class-level rate limit (user-60-per-min) on every controller route, with stricter overrides preserved on cooldown routes. Static-asset routes (CSS / JS / translations / video bgs) opt out via [DisableRateLimiting] so multi-user households behind a shared NAT don't collectively exhaust the limit.
  • CSP + X-Content-Type-Options + X-Frame-Options + Referrer-Policy + Permissions-Policy on the anonymous profile-card endpoint
  • Per-user chat attachment quota (200 files / 200 MB total) closes a disk-exhaustion vector
  • SVG sanitizer blocks <animate> / <set> SMIL elements that could mutate attributes mid-render
  • Webhook URL DNS resolution bounded at 3s (was OS default 5-30s)
  • Audit-log endpoint limit clamped to [1, 1000] at controller
  • MessagingService + AchievementBadgeService are IDisposable so the debounced-save Timer is released on plugin reload
  • Unit security regression tests — SSRF, IPv6 SSRF, scheme rejection, malformed URL rejection, dangerous SVG element rejection, on-event-handler rejection, external DTD rejection, oversized payload rejection, external <use href> rejection
  • GitHub Actions CI runs the tests + dotnet list package --vulnerable + gitleaks on every push, every PR, and weekly cron
  • CodeQL + OpenSSF Scorecard + OpenSSF Best Practices Level 1 (project 12937)
  • SECURITY.md with full threat model, trust boundaries, defences-in-place inventory, continuous verification matrix, disclosure SLA, and safe-harbour for researchers
  • Sigstore-signed + SLSA build-provenance attested release artifacts — verify with cosign verify-blob or gh attestation verify
  • Reproducible NuGet restore — every transitive package locked by hash via RestorePackagesWithLockFile, CI runs in locked mode so drift fails fast

⚡ Plugin-wide efficiency

  • Debounced Save() in AchievementBadgeService and MessagingService — coalesces back-to-back disk writes from playback and messaging hot paths into one flush per 1.5s
  • FriendsService.LastWatched cache — 90s TTL, invalidated on play, eliminates per-friend 50-item DB query on every friends-list call
  • WriteIndented = false on production stores (~50% smaller badges.json)
  • Embedded resource cache in client-script + asset routes — one read per process
  • Cache-Control: public, max-age=86400, immutable on assets with version-only cache busting; video backgrounds use HTTP Range so the browser only streams the bytes it needs to start playing
  • Cache-Control: no-cache, must-revalidate on translations (v2.0) so users always get fresh strings after a plugin update without hard-refreshing
  • Middleware marker fast-path (last-4KB scan instead of full body)

⚙️ Installation

  1. Go to Dashboard → Plugins → Repositories
  2. Add:
https://raw.githubusercontent.com/ZL154/AchievementBadges_for_Jellyfin/main/manifest.json
  1. Save and refresh plugins
  2. Install Achievement Badges
  3. Restart Jellyfin
  4. Go to Dashboard → Plugins → Achievement Badges → Settings
  5. Click Scan watch history (or Scan all users) to backfill from your existing play data
  6. Open Achievements (avatar menu on Jellyfin 12, sidebar on 10.11) or go to #/achievements — and try the Loadout tab

🔧 Requirements

  • Jellyfin 10.11.x (.NET 9) or Jellyfin 12.0.x (.NET 10). Every release ships one package per line and the catalog picks the right one for your server: version x.y.z.0 is the 10.11 build, x.y.z.1 is the 12 build. Installing by hand? Take the zip whose 4th version segment matches your server. Upgrading the server from 10.11 to 12 will offer the .1 build as a plugin update afterwards.
  • File Transformation plugin (strongly recommended) — ensures sidebar, dashboard UI, profile showcase and achievements page inject reliably across Jellyfin Web updates. Without it most UI injection still works via the plugin's own middleware, but File Transformation gives the most robust integration.

Optional but helpful

  • Proper metadata provider (TMDb, OMDb) — required for Director/Actor badges to populate. Badges based on item.People will stay empty if your library doesn't have people scraped
  • Home Screen Sections plugin — lets the achievement home widget inject more reliably
  • Custom Tabs or Plugin Pages — optional alternative hosts for the Achievements page; enable the matching integration under Page integrations in plugin settings. Saving installs/repairs the owned Custom Tabs entry without changing existing tabs; restart Jellyfin afterward.
    • Custom Tabs injects the tab's content panel by patching Jellyfin's home page on the server, so a theme that ships its own copy of that page (Abyss's Spotlight, for one) can leave the tab button with no panel behind it, which used to show as a blank Achievements page (#131). The plugin now builds the missing panel itself, so the tab keeps working; a panel it had to build carries data-ab-repaired-panel="true". Other Custom Tabs tabs are still empty on such a server, since only this one is ours to repair.

What each feature needs

FeatureDepends on
Sidebar + header injectionNothing (works standalone)
Custom Tabs page hostCustom Tabs plugin + enable the integration, save, and restart Jellyfin
Plugin Pages page hostPlugin Pages plugin + Jellyfin restart after enabling integration
Watch history backfillPlayed flag on items (Jellyfin default)
Genre badgesItems with Genres metadata
Director/Actor badgesItems with People metadata (TMDb/OMDb scrape)
Era / decade badgesItems with ProductionYear metadata
Country badgesItems with ProductionLocations metadata
Language badgesItems with OriginalLanguage metadata
Runtime badgesItems with RunTimeTicks populated
Library completionAt least one library folder with items
Webhook notificationsA webhook URL (Discord, Slack, or generic)
Animated video backgroundsModern browser with H.264 + <video> autoplay-muted support (all evergreen browsers)

🔍 Troubleshooting

The plugin injects its scripts into Jellyfin's index.html at startup. If the web directory isn't writable, the injection fails silently and no UI loads (no sidebar entry, no toasts, no achievements page).

Diagnose: visit https://your-server/Plugins/AchievementBadges/test — the JSON response shows:

  • DiagIndexFound — whether index.html was located
  • DiagIndexPatched — whether the script tags were successfully written
  • DiagLastError — the exact error if patching failed (usually Unauthorized: Access denied)

Common cause: on Docker or Linux installs, Jellyfin doesn't have write access to /usr/share/jellyfin/web/. Fix by granting write permission:

# Docker: run inside the container
chmod -R a+w /usr/share/jellyfin/web/

# Systemd: fix ownership
sudo chown -R jellyfin:jellyfin /usr/share/jellyfin/web/

Then restart Jellyfin. The plugin will patch index.html on the next startup.

Can't (or won't) make the web dir writable? Use the JavaScript Injector plugin (v2.1.0, #26). On bare-metal Linux where /usr/share/jellyfin/web is owned by root, install the JavaScript Injector plugin. On the next startup, when the on-disk patch fails, Achievement Badges detects that plugin and writes the exact script URLs you need into the /Plugins/AchievementBadges/test diagnostics (DiagJsInjectorGuidance) and the server log. Paste these three into JS Injector's settings — one per entry:

/Plugins/AchievementBadges/client-script/sidebar
/Plugins/AchievementBadges/client-script/standalone
/Plugins/AchievementBadges/client-script/enhance

Restart Jellyfin and the UI loads through JS Injector — no writable web directory required. (The plugin doesn't call JS Injector's API directly, so this keeps working across JS Injector versions; the on-disk patch remains the default when the dir is writable.)

Still broken? The plugin has a middleware fallback that rewrites index.html at runtime (no disk write needed). If that's also failing, check whether a reverse proxy (nginx/Caddy) is caching a stale index.html from before the plugin was installed. Clear the proxy cache or restart it.

NixOS (read-only /nix/store)

NixOS serves Jellyfin's web files from the immutable Nix store, so neither the disk patcher nor the middleware can modify index.html. Use a NixOS overlay to inject the script tags at build time:

nixpkgs.overlays = [
  (final: prev: {
    jellyfin-web = prev.jellyfin-web.overrideAttrs (finalAttrs: previousAttrs: {
      installPhase = ''
        runHook preInstall
        sed -i 's#</body>#<!-- achievementbadges-bootstrap --><script src="/Plugins/AchievementBadges/client-script/sidebar"></script><script src="/Plugins/AchievementBadges/client-script/standalone" defer></script><script src="/Plugins/AchievementBadges/client-script/enhance" defer></script></body>#' dist/index.html
        mkdir -p $out/share
        cp -a dist $out/share/jellyfin-web
        runHook postInstall
      '';
    });
  })
];

The plugin DLL serves the JS files from embedded resources — the three <script> tags just tell the browser to load them. Rebuild your NixOS config after adding the overlay and restart Jellyfin.

The dashboard's API docs page is blank (/api-docs/openapi.json returns 500)

Jellyfin builds one OpenAPI document from the server and every installed plugin, and each type in it is keyed by its bare class name. If two loaded types want the same name the document does not pick one — it fails, and the whole document 500s. Badges keep working, nothing is logged by the plugin, and the only visible symptom is the API docs page.

This plugin caused it from 2.1.0 to 2.4.0 (MediaType, colliding with Jellyfin's own) and no longer does. If the page is still broken on v2.4.1+, another plugin is colliding — the server log names both types:

Can't use schemaId "$PluginConfiguration" for type "$Jellyfin.Plugin.A.PluginConfiguration".
The same schemaId is already used for type "$Jellyfin.Plugin.B.Configuration.PluginConfiguration"

PluginConfiguration is the usual culprit, since most plugins name their config class that. Report it to whichever plugin appears there; there is nothing to change on your server.

Video backgrounds don't play

The 4 HD video cosmetics need browser MP4/H.264 + autoplay-muted support. Every evergreen browser handles this fine, but if you see a static page where there should be motion:

  • Check the browser's autoplay policy (Chrome: chrome://flags/#autoplay-policy)
  • The video element is set to muted + playsinline so it should autoplay everywhere; if your browser still blocks it, click anywhere on the page once
  • Low-end devices (old TVs, basic streamers) may stutter — switch to a CSS-only bg or "None" via Loadout → Cosmetics

📡 API endpoints

User-facing (require auth)

GET    /Plugins/AchievementBadges/users/{userId}                      — full badge list
GET    /Plugins/AchievementBadges/users/{userId}/summary              — unlocked/total/score
GET    /Plugins/AchievementBadges/users/{userId}/rank                 — rank tier + next tier
GET    /Plugins/AchievementBadges/users/{userId}/equipped             — equipped badges
POST   /Plugins/AchievementBadges/users/{userId}/equipped/{badgeId}
DELETE /Plugins/AchievementBadges/users/{userId}/equipped/{badgeId}
GET    /Plugins/AchievementBadges/users/{userId}/recap?period=week|month|year
GET    /Plugins/AchievementBadges/users/{userId}/watch-calendar?days=90
GET    /Plugins/AchievementBadges/users/{userId}/quests               — daily + weekly + reroll state
GET    /Plugins/AchievementBadges/users/{userId}/bank                 — score bank + prestige
POST   /Plugins/AchievementBadges/users/{userId}/prestige
POST   /Plugins/AchievementBadges/users/{userId}/buy-badge/{badgeId}
POST   /Plugins/AchievementBadges/users/{userId}/gift/{toUserId}?amount=N
GET    /Plugins/AchievementBadges/users/{userId}/chase/{badgeId}      — items to watch to finish a badge
GET    /Plugins/AchievementBadges/users/{userId}/recommendations      — top 3 closest-to-unlock
GET    /Plugins/AchievementBadges/users/{userId}/profile-card         — HTML profile card
GET    /Plugins/AchievementBadges/users/{userId}/unlocks-since?since=ISO&deviceId=ID
GET    /Plugins/AchievementBadges/users/{userId}/library-completion
POST   /Plugins/AchievementBadges/users/{userId}/login-ping
GET    /Plugins/AchievementBadges/users/{userId}/directory            (#138) users this user may see, for friend search and compare
GET    /Plugins/AchievementBadges/leaderboard?limit=10
GET    /Plugins/AchievementBadges/leaderboard/{category}?limit=10     — score|movies|episodes|hours|streak|series
GET    /Plugins/AchievementBadges/embedded-page                       — Plugin Pages host fragment
GET    /Plugins/AchievementBadges/server/stats

v2.0 — Power-ups, Shop, Cosmetics, Quest reroll

GET    /Plugins/AchievementBadges/users/{userId}/powerups             — inventory + active state + ScoreBank
POST   /Plugins/AchievementBadges/users/{userId}/powerups/use/{type}  — XpBoost | DoubleCredit (StreakFreeze auto-only)
GET    /Plugins/AchievementBadges/shop/catalog                        — full shop catalog
POST   /Plugins/AchievementBadges/users/{userId}/shop/purchase        — body: {"ItemId":"..."}
GET    /Plugins/AchievementBadges/users/{userId}/cosmetics            — owned + equipped state + LifetimeScore
POST   /Plugins/AchievementBadges/users/{userId}/cosmetics/equip      — body: {"CosmeticId":"..."}
POST   /Plugins/AchievementBadges/users/{userId}/cosmetics/unequip?kind=ProfileTheme|BadgeFrame|RankTitle|Avatar|Background|ProfileBorder
POST   /Plugins/AchievementBadges/users/{userId}/quests/daily/reroll
POST   /Plugins/AchievementBadges/users/{userId}/quests/weekly/reroll
GET    /Plugins/AchievementBadges/asset/{name}                        — animated background mp4s (range-enabled)

Admin-only (require RequiresElevation)

POST   /Plugins/AchievementBadges/users/{userId}/backfill
POST   /Plugins/AchievementBadges/backfill-all
POST   /Plugins/AchievementBadges/users/{userId}/reset
POST   /Plugins/AchievementBadges/users/{userId}/reset-badge/{badgeId}
POST   /Plugins/AchievementBadges/users/{userId}/library-completion/recompute
POST   /Plugins/AchievementBadges/users/{userId}/import
GET    /Plugins/AchievementBadges/users/{userId}/export
GET/POST  /Plugins/AchievementBadges/admin/badge-catalog              — enable/disable badges
GET/POST  /Plugins/AchievementBadges/admin/custom-badges              — custom badge definitions
GET/POST  /Plugins/AchievementBadges/admin/challenges                 — seasonal challenges
GET       /Plugins/AchievementBadges/admin/challenge-templates        — one-click templates
GET/POST  /Plugins/AchievementBadges/admin/webhook                    — webhook config (incl. HMAC secret)
GET/POST  /Plugins/AchievementBadges/admin/ui-features                — UI feature toggles
GET       /Plugins/AchievementBadges/admin/audit-log?limit=200
POST      /Plugins/AchievementBadges/admin/users/{userId}/inject-counters
GET/POST  /Plugins/AchievementBadges/admin/feature-config             — feature kill switches + admin controls
DELETE    /Plugins/AchievementBadges/admin/users/{userId}/reset       — wipe user's achievement progress

POST   /Plugins/AchievementBadges/admin/users/{userId}/test/inject-playbacks    — verify integrity caps end-to-end (v1.9.8)
DELETE /Plugins/AchievementBadges/admin/users/{userId}/badges/{badgeId}          — manual badge revoke (v1.9.8)

POST   /Plugins/AchievementBadges/admin/users/{userId}/grant-score              — v2.0 testing
POST   /Plugins/AchievementBadges/admin/users/{userId}/grant-powerup/{type}     — v2.0 testing
POST   /Plugins/AchievementBadges/admin/users/{userId}/grant-cosmetic           — v2.0 testing
POST   /Plugins/AchievementBadges/admin/backfill-milestones                     — v2.0: retroactively unlock title milestones across all profiles

📸 Screenshots

Xbox-style unlock toast

Pops up during playback when a badge unlocks. Xbox circle pops in with pulse rings, expands into a banner, trophy rotates (or diamond spritesheet for rare unlocks), text slides up, shimmer sweeps across, then everything collapses. Per-rarity color, glow, and sound.

Xbox-style unlock toast

Live demo: download achievement-combined.html (regular) or achievement-combined-rare.html (rare with diamond) and open in a browser. Click anywhere to start the sound. Loops every 10.5s.

The standalone Achievements page

The full profile view, shown in the Jellyfin sidebar. Rank progress bar, day streak, score, completion percentage, and the tab bar (My Badges, Quests, Recap, Leaderboard, Compare, Activity, Wrapped, Stats, and the new Loadout tab from v2.0).

Achievements page

Badge grid

200+ badges across 35+ categories, each with live progress bars and an Equip button. Unlocked badges show in color with a green status tag; locked badges dim. Rarity-colored borders let you scan the grid visually.

Badge grid overview

Rarity tiers in action

Genre specialist badges and streak extremes across all six rarity colors — Common, Uncommon, Rare, Epic, Legendary, Mythic.

Genre + rarity badges

Daily and weekly quests

Rotating quests from a template pool. Everyone on the server gets the same daily + weekly challenges so people can race each other. Completing them pays into the score bank. In v2.0, each section header has a reroll button.

Daily and weekly quests

Recap

Weekly, monthly and yearly breakdowns of what you've actually watched — total items, active days, top genres, top directors, and top actors.

Recap view

Year Wrapped

Spotify-style end-of-year recap with a big gradient hero, "your numbers" (movies, episodes, active days, best streak, total hours), "your highlights" (biggest day, biggest month, most-watched weekday) and "your favorites" (top genres/directors/actors).

Year Wrapped view

Leaderboard

Podium view for the top 3, ranked list below. Switch categories with the tab row: Score, Movies, Episodes, Hours, Best Streak, Series. (Usernames blurred as User 1–10.)

Leaderboard view

Compare profiles

Head-to-head profile comparison between any two users on your server. Gradient bars show the relative values on 12 core metrics, and the bottom pills break down how many badges each user has that the other doesn't. (Usernames blurred as User 1 / User 2.)

Compare profiles view

Streak calendar

GitHub-style year calendar of your watch activity. Current streak, best ever, and total active days at a glance.

Streak calendar

Watch heatmap

90-day heatmap grid, colored by daily watch volume. Click the range button to switch between 30/90/180/365 days.

Watch heatmap

Genre radar + watch clock

SVG spider chart showing your top-5 genre distribution, and a 24-hour polar chart of when you actually watch.

Genre radar + watch clock

Admin panel

Every admin section is collapsible so the page stays clean: webhook notifications, toast preview, UI feature toggles, visual badge editor, challenge templates, audit log, progress injection, custom badges, seasonal challenges, per-badge enable/disable, and the v2.0 testing tools (grant score / power-up / cosmetic, backfill milestones).

Admin panel

Advanced options

Scan watch history, reset badges, scan all users, or load a specific user ID — all from one row under the Advanced options toggle.

Advanced options

Sidebar entry

Auto-injected into the Jellyfin nav menu — no theme changes required.

Sidebar entry


📜 Release history

Full per-version notes and signed binaries live on the GitHub Releases page:

➡ github.com/ZL154/AchievementBadges_for_Jellyfin/releases

Highlights:

  • v2.4.1 — what 2.4.0 got wrong, and a cap you can raise: the Jellyfin 12 package now reports the version it shipped as, so uninstall and the plugin image work (#140); a browser that cached Jellyfin's page before the install gets the plugin instead of a stale copy (#141); a targeted badge cap settable to 1000 with the overflow named (#129); the Revamp style no longer restyles Jellyfin's own controls (#133); a Custom Tab under a page-replacing theme renders again (#131); top-center toasts plus a server default, and the default UI style actually persists (#136/#43); login-hidden accounts can be kept out of other users' views (#138); and a type-name collision that had been 500ing the server's whole OpenAPI document since 2.1.0 is gone
  • v2.4.0 — Jellyfin 12, targeted badges, games: two packages per release (x.y.z.0 for 10.11, x.y.z.1 for 12) with the entry in 12's avatar menu and the Authorization header 12 requires (#109/#117/#122); ContainerCompletionPercent + ItemPlayCount with a library picker (#107/#108); JellyEmu game achievements (#115/#120); every shop cosmetic on the shareable card and the drawer card (#42/#119); toast position under Revamp (#116/#118); Pastel page leak, invisible borders/frames, stale stylesheet token, #97 diagnostics
  • v2.3.1 — music fixes + leaderboards: tracks stop inheriting album/artist genres so custom music badges count correctly (#94), the scan replays played music so discography badges rebuild (#96), both leaderboards show your own rank, deleted accounts are excluded + prunable, and the Blades skin becomes Aurora
  • v2.3.0 — Friends & Foundations: hover/click friend profile cards (#76) behind a privacy-gated summary endpoint; three shareable card skins (Console / Metro / Aurora Spine) chosen per user; library completion now computes during the scan (#80) and new artist discography completion (#81, #24); Tracearr history crediting (#77/#84/#85); watch-time carry across restarts and file replacements (#87/#89/#91/#92); admin-set default UI style + lock (#43); plus the profile data-loss (#59/#60) and gzip injection (#46) fixes for published builds
  • v2.2.0 — Your Screen, Your Rules: optional Custom Tabs + Plugin Pages hosts and independent per-user navigation controls (#37); grouped/individual and all-device/origin-device unlock notification modes (#38); clickable, keyboard-accessible real unlock toasts; 10-toast admin grouping preview; build-specific client cache keys; full 8-language coverage
  • v2.1.3 — Open Library patch: per-genre music badges so genre filters actually filter (#24), custom badges fully delete + purge earned copies + ID-preserving migration (#24), header UI survives another header-injecting plugin (#36), badge-category label fix, and full localization of the badge/quest builders across all 8 languages
  • v2.1.0 — Open Library: music + book achievements, custom badge builder (compound AND/OR), JS Injector fallback (#26), anime detection via Genres+Tags+Series (#25), daily-badge backfill fix + audit/cleanup tool (#27), globe language picker + full admin-page localization
  • v2.0.0 — Choose Your Loadout: power-ups, score shop, 70+ cosmetics, 8 video backgrounds, full i18n
  • v1.9.8 — Integrity release: closes playback-credit exploits (mark-as-played, seek-to-end, spam-click no longer credit), real-watch credit gate (≥80% accumulated play ticks with seeks excluded), 60s minimum runtime filter, daily credit cap, suspicious-rate audit flag, manual badge revoke
  • v1.9.7 — Security-only: per-user chat attachment quota, profile-card CSP tightening, SVG sanitizer adds SMIL blocking, webhook DNS timeout
  • v1.9.6 — Toast description fix, playback popups default-on, server-user picker on admin/config pages
  • v1.9.5 — Chat attachment fix, security hardening
  • v1.9.4 — 32 new badges (afternoon, prime time, holidays expansion, anime tier, studio specialists, pilot vs completer) + 8-language hand-tailored translations
  • v1.9.0 — Revamp UI (Classic/Revamp toggle), Friends drawer Revamp, "Offline — last watched", HMAC webhook signing, security upgrade (rate limit, CSP, audit log filter, GitHub Actions CI)
  • v1.8 — Full messaging suite (1:1 + groups, attachments, read receipts, edit/delete, block, notifications)
  • v1.7 — Friends drawer foundation, hand-translated French by @frenchyx24

🗂️ Previous release notes

Full notes for earlier versions, newest first.

🚀 What's new in v2.4.0 — Jellyfin 12, targeted badges, games

Achievement Badges runs on Jellyfin 12, gains the first badges that can point at one specific thing in your library, learns to count games, and finally shows the bling you bought in the shop to everyone else. Drop-in upgrade from v2.3.x — no schema breakage or manual migration. Full notes in docs/release-notes/v2.4.0.md.

🟣 Jellyfin 12 (#109, #117, #122)

  • Two packages per release. x.y.z.0 is the Jellyfin 10.11 build (.NET 9) and x.y.z.1 the Jellyfin 12 build (.NET 10). The plugin catalog picks the right one for your server; upgrading the server from 10.11 to 12 offers the .1 build as a plugin update afterwards.
  • The Achievements entry moved to the avatar menu. Jellyfin 12's modern layout hides the old drawer and header, so the entry now sits right below Profile in the avatar menu and the equipped-badge strip sits in the toolbar beside the avatar. The legacy layouts (desktop-legacy, mobile-legacy, TV) keep the drawer entry.
  • Every call sends the Authorization header 12 requires. Jellyfin 12 switches legacy authorization off, so the old X-Emby-Token alone answers 401 — which made every earlier version's page dead on a 12 server. Both headers are sent now; 10.11 accepts either.
  • The Revamp admin page fits inside the MUI dashboard, links use #/ routes, and the tab is named Achievements while the page is open.

🎯 Targeted badges (#107)

  • Two new metrics point a badge at one thing: ContainerCompletionPercent (played items over total in one series, season, collection, playlist or album — 100 means you finished it) and ItemPlayCount (Jellyfin's own play count for a single movie, episode or track — 1 is "watched it", 3005 is the Childish Gambino badge).
  • A library picker in the admin page finds the target by name; the stored reference keeps the id and the name together, so the badge survives both a rename and a delete-and-re-add.
  • Retroactive on creation — anyone who already finished the target unlocks it immediately, no scan — and it moves on Jellyfin's played flag, so marking a season watched by hand counts.
  • Arbitrary groupings ("One Piece: Alabasta") go through a Jellyfin collection. See Targeted badges.

🎮 Game achievements (#115)

  • Games played through JellyEmu now earn achievements. JellyEmu reports each session to Jellyfin as a playback session; the plugin now measures those on their own terms (session length, floored and capped, both configurable) instead of dropping them as runtime-less books.
  • Nine built-in badges under a new Games category (sessions, distinct games, hours, platforms) and three custom-badge metrics (distinct games on a platform, distinct games by a developer, hours in one specific game via the picker). See Game achievements.

🪪 Everything you equip shows on your cards (#42)

  • Your custom title, badge frame, profile theme, profile border, animated background and avatar now all show on the shareable profile card (all three skins) and on the card that opens when someone clicks your name in the friends drawer — under the same privacy toggles as the equipped badges. Animated backgrounds play the actual video loop behind the card.
  • Two things that never worked on the achievements page itself are fixed on the way: profile borders never showed under the Revamp style, and badge frames applied only to one row of one tab — they now ring the showcase pills, the header strip and the drawer showcase.
  • "Pastel has un-hidden elements" (TsunamicFlame's report in #42) was real: every gradient theme let Jellyfin's own page show through the top of the achievements page. Fixed for all 14 themes.

🩺 Diagnostics for #97

  • Admin-page errors now name the request that failed (Request failed: 400 (GET users/…/summary)), and a failed reload is no longer reported as a failed reset or scan.
  • Reset answers 400 for a malformed id and 404 for an account Jellyfin doesn't know, instead of 200 for anything.
  • A watch-history scan that finds nothing says so and points at the account's library access, in all 8 languages.

🧱 Fixes

  • Toast position never applied under Revamp — all five positions rendered top-right (#116, @camarigor).
  • The Revamp stylesheet was cached under a stale token in three places, so stylesheet changes could reach browsers a day late.
  • ~25 Users/Me requests fired on the login page; the admin hero announced "v1.9.2 / ABI 10.11.0.0" forever; the shop's "Auto-unlock at N score" pill was clipped.

Big thanks to @camarigor for the Jellyfin 12 build and web-client work (#117, #122), targeted badges (#108), game achievements (#120), the shop cosmetics on the card (#119), the toast fix (#118) and the dependency round (#128); to @Lyxon1337 for the Jellyfin 12 report; to @unknownTGG for the targeted-badges request; and to @TsunamicFlame for #42, #115, #116 and the field testing behind them.



🚀 What's new in v2.3.1

A fast follow to 2.3.0, fixing the music feature it shipped and cleaning up the leaderboards. Drop-in upgrade from v2.3.0 / v2.2.x — no schema breakage.

  • Music actually counts (#93/#94, #95/#96). Tracks no longer inherit genres and tags from their album or artist, so a "50 disco tracks" badge stops counting dance and pop plays. And the watch-history scan now replays played music, so the artist discography badges from 2.3.0 can be rebuilt instead of only ever building from live playback. Thanks to @Daemon-Network for finding both within a day of release.
  • See your own rank. The admin and user-facing leaderboards now always show your position — highlighted if you land in the top ten, appended with your true rank if you rank lower (across all six category boards on the standalone page).
  • Deleted accounts cleaned up. They no longer appear on leaderboards as raw GUIDs or inflate the user count, the admin Stats and Leaderboard tabs refresh on open, and a new Prune deleted accounts admin button reclaims their storage.
  • The Blades shareable card skin is now Aurora, matching its redesign, localized across all eight languages.

🚀 What's new in v2.3.0 — Friends & Foundations

Achievement Badges gains a social layer — see how your friends are doing, and share your own card — on top of a run of reliability fixes, several of which matter to anyone on the published v2.2.0 build. Drop-in upgrade from v2.2.x / v2.1.x / v2.0.x — no schema breakage or manual migration.

👋 Friend profile cards (#76)

Hover a friend's name or avatar in the drawer for a summary card — rank tier, completion, score, best streak and equipped showcase. Click to pin a larger card that survives the pointer leaving, closes on Escape or an outside click, and is reachable by keyboard. It's backed by a public-summary endpoint that's privacy-gated to expose nothing the leaderboard doesn't: anyone opted out of being listed is equally invisible here.

🪪 Shareable card skins

Three server-rendered profile-card skins — Console (default), Metro, and Aurora Spine — each drawn with the card owner's own rank colour as the single accent. Every user picks their own skin in preferences; a card opened without an explicit style falls back to the owner's choice.

🎯 Completion tracking that finally runs

  • Library completion (#80, #79). The five library-completion badges are now computed during the watch-history scan, so they can unlock through normal use instead of sitting at zero on every install.
  • Artist discography completion (#81, #24). Played tracks over total tracks per artist — a badge for completing any artist, plus a parameterised metric for a specific one.

📡 Tracearr history crediting (#77 / #84 / #85)

Credit genuine first watches the library scan can't prove on its own — media you deleted, and true rewatch counts — from Tracearr's public v2 API. A standalone sync button is made idempotent by a ledger, so pressing it twice credits nothing the second time. Configured admin-side with a URL and token; nothing is required on the Tracearr side. See Tracearr integration.

🔔 Notifications and layout

  • Per-user toast position: any of the four corners, top-center or bottom-center, applied live, with a server default the admin sets for everyone who has not chosen (#136).
  • Unlock toasts moved off the subtitle line, and the floating friends button hides while the video player is on screen.
  • Admin-set default UI style, optionally locked (#43) — start users on Classic or Revamp, and optionally make it the only choice so the page matches your Jellyfin theme; a user's own pick is remembered and returns if the lock is lifted.

🧱 Reliability

  • Watch-time carry survives a session break, a restart, and a media-file replacement or quality upgrade (keyed by media identity, #89), with a configurable retention window (#87). The scan clears the carry of exactly what it credits (#91/#92), so a later partial rewatch can't reach the completion gate on minutes already counted.
  • Counter resilience — rebuilt counters are floored and dated snapshots kept so a scan never loses progress; the score bank is floored on rebuild so deleted media can't zero a balance; backfill is serialised per user; and failed loads surface instead of being drawn as empty or loading.

🩹 Fixes for anyone on v2.2.0

  • Empty twin profiles could swallow unlocked badges (#59/#60). GetOrCreateProfile now normalises the user id first, so the friends and messaging paths can't create an empty profile beside the live one that later folds over it. Opening the friends panel was enough to trigger it.
  • The injected UI never mounted under gzip (#46). The client bootstrap now injects into compressed responses instead of skipping them — every browser asks for gzip, so on v2.2.0 the sidebar, widget, item ribbon and toasts silently failed to load. Also behind the #37 and #36 reports.

🌍 Localization

Every new interface string is translated across all 8 UI languages, and the admin user-picker strings were completed in the seven non-English locales for full key parity.

Big thanks to @camarigor for the friend profiles, Tracearr integration, the library and artist completion wiring, the watch-carry work, and the profile data-loss + gzip injection fixes.


🚀 What's new in v2.2.0 — Your Screen, Your Rules

This release makes Achievement Badges fit each user's Jellyfin layout and notification preferences while keeping the stock Achievements page available at all times. Drop-in upgrade from v2.1.x / v2.0.x — no schema breakage or manual migration.

  • Optional Custom Tabs and Plugin Pages hosts (#37). Admins can opt into either integration independently. Both reuse the same Achievements surface instead of maintaining duplicate pages, and compatibility failures stay isolated so they cannot prevent Jellyfin from starting.
  • Per-user navigation controls (#37). Each user can independently show or hide the Custom Tabs entry, Plugin Pages entry, and header trophy from Achievement settings or Jellyfin's native user settings. Hiding navigation never disables tracking, notifications, or the stock page.
  • Grouped or individual unlock bursts (#38). Simultaneous unlocks can collapse into one summary with a combined score, or play one at a time using the existing queue.
  • Device-scoped unlock notifications (#38). Users can keep the default delivery to every signed-in client or restrict a toast to the Jellyfin device that earned it. The server validates the originating device instead of trusting the browser alone.
  • Unlock toasts now lead somewhere useful. Click a real single unlock (or focus it and press Enter/Space) to open My Badges, clear filters, scroll to the exact badge, and briefly highlight it. Grouped summaries open Recently unlocked. Synthetic admin previews remain deliberately non-navigating and never write achievement data.
  • Better notification testing. A new Test 10 unlocks admin control exercises the saved grouping preference without creating unlocks or modifying a profile.
  • Fresh client assets on every build. Script cache keys now include the compiled module ID, preventing browsers from retaining an older same-version UI during testing or deployment.
  • Full localization. Every new option, help message, navigation label, and toast action is translated across all 8 UI languages (English, French, Spanish, German, Italian, Portuguese, Chinese, Japanese).

🚀 What's new in v2.1.3

A bug-fix patch resolving the outstanding reports from #24 and #36, plus a full builder-localization pass. Drop-in upgrade from v2.1.x / v2.0.x — no schema breakage; a one-time, ID-preserving custom-badge migration runs automatically.

  • Genre-filtered music badges now actually filter by genre (#24). Music plays and listening time are tracked per genre (case-insensitive), so a "play 50 disco tracks" badge no longer ticks up when you listen to metal. Three new builder metrics — Music plays of a genre, Music hours of a genre, Items watched of a genre — reveal a genre field when picked.
  • Custom badges fully delete (#24). The visual builder, the badge list, and delete now share one store (they were previously split, so builder-made badges couldn't be removed), and deleting a badge purges its earned + equipped copies from every user profile. Pre-existing badges are migrated on first startup with their IDs preserved.
  • Header UI survives another header-injecting plugin (#36). When a second plugin (e.g. Ratings) stripped the badge scripts from index.html, the plugin used to think it was still patched and never re-injected. The guard now keys on the actual script tag, so a stripped page is always repaired.
  • Badge category labels fixed (#24). Categories like Studio Specialist and Books no longer show as raw category.* keys, and every built-in category is now translated.
  • Full builder localization. The badge-builder metric list, quest builder, power-up + cosmetic pickers, and status messages are now translated across all 8 languages (en/fr/es/de/it/pt/zh/ja).

🚀 What's new in v2.1.0 — Open Library

Achievements expand beyond film & TV, and admins get real badge-authoring power. Drop-in upgrade from v2.0.x — no schema breakage, no data migration.

🎵 Music achievements

18 built-in music badges driven by real listening — total plays, listening hours, and unique albums / artists / genres / decades. Audio items now flow through the same playback-credit pipeline (80% real-listen gate) as films and episodes.

📚 Book achievements

8 book badges — books completed, audiobook listening hours, and book series completed. An audiobook-counting policy lets admins choose whether audiobook plays count toward Books only (default), Music only, or Both.

How ebook completion is detected (important): Jellyfin has no "finished reading an ebook" signal — unlike audiobooks and music, which stream through a session and track listening time automatically, an ebook is text with no runtime. So an ebook counts once it's marked Played (the ✓ / "Mark as watched" toggle on the item). Simply reaching the last page in Jellyfin's reader does not auto-mark it, so book badges won't move until the book is marked played. This is a Jellyfin limitation, not something the plugin can detect on its own.

🛠️ Custom badge builder

Define your own badges with compound AND/OR criteria across any metric — film, TV, music, books and more. Simple badges via the admin form; compound criteria, icons, and import/export via the API. Full guide + copy-paste templates: Custom badges.

🌐 JS Injector fallback (issue #26)

On bare-metal Linux where /usr/share/jellyfin/web isn't writable, the on-disk UI injection used to fail silently. The plugin now detects the JavaScript Injector plugin and surfaces the exact script URLs to paste in. Details: Troubleshooting.

🇯🇵 Anime detection fix (issue #25)

Anime is now detected from Genres and Tags, read from both the item and its parent Series, with admin-configurable libraries / genres / tags — fixing anime badges that never fired when the classification lived on the Series or in Tags.

🗓️ Daily-badge backfill fix + audit tool (issue #27)

Time-windowed badges (daily / weekly / monthly) are now skipped during the initial history scan, so they no longer false-unlock from lifetime totals. A new admin audit + cleanup tool finds and clears badges wrongly awarded by pre-v2.1.0 backfills (re-earnable organically afterward).

🌍 Globe language picker + full admin localization

A globe dropdown on the admin page switches the UI language live and persists across reloads, and the entire admin page — including the Integrity and testing-tools sections — is now localized across all 8 languages.


❤ Support the project

Achievement Badges is built and maintained in my spare time. If it's useful to you and you'd like to support ongoing development, any of these means a lot:

Not expected, just appreciated. Contributions — issues, PRs, translation fixes — are equally valuable.


🙏 Credits & thanks

  • @frenchyx24 — full French translation of all 171 built-in badges (hand-translated titles + descriptions, merged in v1.7.2 from issue #5). Also filed the original multi-language feature request and the deactivate-equipped-badges / quest-customization / Xbox-logo bug reports that shaped v1.6.1 → v1.7.x. Merci beaucoup !
  • @camarigor — the v2.4.0 Jellyfin 12 build and web-client rework (#117/#122), targeted badges (#108), JellyEmu game achievements (#120), the shop cosmetics on the shareable card (#119) and the toast-position fix (#118); before that the v2.3.0 friend profile cards and public-summary endpoint (#76), Tracearr history integration (#77/#84/#85), library + artist completion wiring (#80/#81), the watch-time carry reliability work (#87/#89/#91/#92), the music fixes in v2.3.1 (#94/#96), and the profile data-loss (#59/#60) and gzip-injection (#46) fixes.
  • @Daemon-Network — the original Music & Books request (#24), and thorough real-music-library testing of 2.3.0 that surfaced the three music bugs fixed in v2.3.1.
  • @TsunamicFlame — the Custom Tabs / Plugin Pages request (#37), the friend profile cards and shop-cosmetics-on-the-card request (#42), the JellyEmu game achievements request (#115), the toast-position report (#116), and the field testing behind all of them.
  • @unknownTGG — the targeted-badges request (#107) that became ContainerCompletionPercent and ItemPlayCount.
  • @Lyxon1337 — the Jellyfin 12 report (#109).
  • xdnewlun1 (Techno Cricket, CCDC) — responsible disclosure of 12 security findings in v1.6.0 including the critical IDOR that led to the UserOwnershipFilter.
  • Uenify — the Xbox-style toast animation (circle grow, banner sweep, shimmer, text slide) is a port of his CodePen to vanilla JS + per-rarity colour palettes.
  • Translations for es / de / it / pt / zh-CN / ja started from an automated pass — native-speaker polish welcomed via PR.

📜 License

This project is released under the MIT License — one of the most permissive open-source licenses in common use.

Summary:

You canYou mustYou cannot
Use it on any Jellyfin server, personal or commercialKeep the copyright + license notice in any redistributionHold the authors liable if something breaks
Fork and modify however you wantClaim the authors endorse your fork
Redistribute modified or unmodified copies
Bundle it with proprietary software
Include it in a paid product

If you just want to run the plugin, none of this affects you — install it and enjoy.

Contributions

Pull requests are welcome. By submitting a contribution you agree that your changes will be licensed under the same MIT terms. Keep contributions focused (one feature or fix per PR) and include a short description of what changed and why in the PR body.

Third-party attributions

  • Jellyfin (GPL-2.0) — this plugin is a third-party extension for Jellyfin and is not affiliated with or endorsed by the Jellyfin project. At build time it references Jellyfin.Controller and Jellyfin.Model NuGet packages, which remain under their own GPL-2.0 license.
  • Xbox-style unlock toast — the animation style is inspired by Adam Cosman's Xbox One Achievement codepen and was reimplemented from scratch. No original assets from that codepen ship with this plugin.
  • Material Icons (Apache 2.0) — icon glyphs referenced in the UI are provided by Jellyfin's own web client and are licensed by Google.
  • Background video assets (v2.0) — sourced from public-domain / royalty-free stock and transcoded to H.264 1080p loops for embedding. No external CDN; all assets ship inside the plugin DLL.

See LICENSE for the full license text and third-party notices.


⭐ If you use this plugin, consider starring the repository.

achievements
csharp
dotnet
gamification
jellyfin
jellyfin-plugin
jellyfin-web
media-server
self-hosted

ZL154/AchievementBadges_for_Jellyfin

A Jellyfin plugin that adds achievement-style badges to user profiles based on viewing activity. Users unlock badges for milestones such as first watch, binge sessions and late-night viewing. Designed to gamify the Jellyfin experience and encourage engagement across libraries.

HTML

70

386 commits

updated Oct 1, 2026

See the code

README

achievement-banner

 █████╗  ██████╗██╗  ██╗██╗███████╗██╗   ██╗███████╗███╗   ███╗███████╗███╗   ██╗████████╗
██╔══██╗██╔════╝██║  ██║██║██╔════╝██║   ██║██╔════╝████╗ ████║██╔════╝████╗  ██║╚══██╔══╝
███████║██║     ███████║██║█████╗  ██║   ██║█████╗  ██╔████╔██║█████╗  ██╔██╗ ██║   ██║
██╔══██║██║     ██╔══██║██║██╔══╝  ╚██╗ ██╔╝██╔══╝  ██║╚██╔╝██║██╔══╝  ██║╚██╗██║   ██║
██║  ██║╚██████╗██║  ██║██║███████╗ ╚████╔╝ ███████╗██║ ╚═╝ ██║███████╗██║ ╚████║   ██║
╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝╚═╝╚══════╝  ╚═══╝  ╚══════╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═══╝   ╚═╝

OpenSSF Scorecard OpenSSF Best Practices

🏆 Achievement Badges for Jellyfin

A full progression, gamification and achievement system for Jellyfin that rewards users based on real viewing activity. Think Xbox Gamerscore meets Letterboxd meets Steam profile customization, built natively into your media server.

Status: Active development — v2.4.1 is live: everything reported against 2.4.0, including two faults that made a correct install look broken on Jellyfin 12 (an uninstall that failed outright, and a page that only worked in a private window), plus a targeted badge cap you can raise to 1000, a top-center toast placement with a server default, an option to keep login-hidden accounts out of other users' views, and a fix for a collision that had been breaking your server's whole API document since 2.1.0. See What's new in v2.4.1. v2.4.0 brought Jellyfin 12 support (a dedicated 12.0 build in every release, and the web side reworked for the new layout and authorization), targeted badges that point at one series, season, collection, playlist, album or item, JellyEmu game achievements, and your shop bling on the shareable card. Before that, v2.3.1 fixed the music feature and cleaned up the leaderboards, and 2.3.0 brought friend profile cards, three shareable card skins (Console / Metro / Aurora), Tracearr history crediting, and the profile data-loss + gzip injection fixes every published build needs. Built on v2.2 "Your Screen, Your Rules", the v2.1 "Open Library" expansion, and v2.0 "Choose Your Loadout".


📑 Table of contents


✨ Overview

Over 200 built-in achievements across 35+ categories, a 10-tier rank ladder from Rookie to Immortal, a full score economy with combos, prestige, daily/weekly quests, a Score Shop with 70+ cosmetics, power-up consumables, a Friends drawer with messaging, plus admin power features like custom badges, seasonal challenges, webhook notifications, and a full audit log.

Designed to integrate cleanly with modern Jellyfin setups and themes like NetFin, ElegantFin, or StarTrack.


🚀 What's new in v2.4.1 — what 2.4.0 got wrong, and a cap you can raise

Everything reported against 2.4.0, including two faults that made a correct install look broken on Jellyfin 12, plus a cap the people who author targeted badges by the hundred can now raise. Drop-in upgrade from v2.4.0 — no schema change, no migration. Full notes in docs/release-notes/v2.4.1.md.

🟣 The two Jellyfin 12 faults (#140, #141)

  • Uninstall failed outright, and the plugin's image 404'd. Each release ships two packages from one source tree, and both assemblies were stamped x.y.z.0. Jellyfin then read two different numbers for one install — the dashboard shows the assembly's version, uninstall and the image endpoint resolve through the manifest's — so on Jellyfin 12 the plugin installed as 2.4.0.1, displayed as 2.4.0.0, and could not be removed. Each package now stamps the version it ships as. Stuck on 2.4.0.1? Updating through the catalogue is enough; the restart that loads the new build deletes the old folder.
  • The page worked in a private window and nowhere else. A browser that cached Jellyfin's page before the plugin was installed revalidates it, Jellyfin answers 304 Not Modified from the file on disk, and there is no page body for the plugin to inject into — so the browser keeps using a copy with no plugin in it, forever. Worst on servers with a read-only web directory, where the on-disk patch can never take either. The middleware now forces a full page while the on-disk patch has not taken. Nobody needs to clear their browsing data — a normal reload after updating is enough.

🎯 A targeted badge cap you can raise (#129)

  • The cap is now a setting, under Custom badges → Targeted badge cap (1 to 1000, default unchanged at 50). The page shows how many distinct targets your enabled badges reference against the cap, and names the ones past it so you can see what is not being computed.
  • The per-play check no longer grows with the number of targets. Series, seasons and folders are tested against the played item's ancestors in one lookup; collections and playlists use a member list cached per target and refreshed when the container changes. Hundreds of targets are fine on any server.

🎨 Fixes reported against 2.4.0

  • The Revamp style reached the rest of Jellyfin (#133). With UI: Revamp selected, the plugin was restyling Jellyfin's own controls and every other plugin's settings page — checkboxes went invisible, text inputs lost their border and height, text selection changed colour everywhere. Twelve rules named generic elements while keying only on an attribute that lives on <body>; they are now anchored to the plugin's own two surfaces, and a test refuses any new rule of that shape.
  • A Custom Tab that rendered nothing (#131). With themes that ship their own copy of Jellyfin's home page (Abyss's Spotlight), Custom Tabs' server-side panel injection silently fails to match, leaving the tab button with no panel behind it — a blank Achievements page. The plugin now builds the missing panel itself.
  • Toasts gain a top-center placement, and a server default (#136). Top-center joins the five existing placements, and an admin can set the default for everyone who has not picked one; a user's own choice still wins. Two bugs turned up underneath it: the default UI style from #43 was never actually stored, and every main config save silently switched the page integrations back off.
  • Login-hidden accounts can be kept out of other users' views (#138). Off by default. When on, an account with Jellyfin's Hide this user from login screens is visible only to itself, administrators, other hidden accounts, and accounts it is already mutual friends with. Note that Jellyfin ticks that box for accounts it creates, so on a server that keeps the default every account is hidden and the option changes nothing.

📄 The plugin was breaking your server's API document

Found while testing this release, not reported by anyone, and present since 2.1.0 on both Jellyfin 10.11 and 12: GET /api-docs/openapi.json answered 500 for the whole server.

Jellyfin builds one OpenAPI document from itself and every installed plugin, and each schema in it is keyed by the bare type name. This plugin's media-type enum was called MediaType, which is also the name of one of Jellyfin's own enums — and a duplicate name is not resolved, it throws, taking the entire document down. Swagger UI, the dashboard's API browser and every generator that reads the spec were all broken, by a plugin that started cleanly and logged nothing about it.

The enum is now BadgeMediaType. Its numbers and the property carrying it are unchanged, so there is nothing to migrate. With the collision gone the document builds, and this plugin's 126 routes appear in it for the first time. A test now walks the plugin's public types against Jellyfin's and fails on any shared name, because nothing else could catch this: the break was in the host's document, not in this plugin's.

If your API docs page is still broken after updating, another plugin is colliding the same way — the server log names both types.

Big thanks to @camarigor for the cap and the per-play rework (#130), the Custom Tabs repair (#132), the Revamp scoping (#134), the toast work (#137), the hidden-accounts option (#139), and for catching what the first pass at #140/#141 got wrong (#144); to @Roboatlas21 for diagnosing both Jellyfin 12 faults from his logs (#140, #141); to @Tschiyo for the cap request (#129); to @Borededdy for the blank Custom Tab (#131); to @clarjon1 for the Revamp leak (#133); to @Verdancy-Rin for the top-center placement (#136); and to @Digital-Yeti for the hidden-accounts request (#138).


🧩 Core features

🏅 Badge system

  • 200+ built-in achievements across categories: Films, Series, Binge, Night Watching, Morning, Afternoon (12-17h), Prime Time (19-22h), Weekend, Exploration, Streaks, Episode/Film Marathons, Eras, World, Languages, Genres, Runtime, Total Time, Holidays (Christmas, New Year, Halloween, Eid, Valentine's, Easter, Lunar New Year, Diwali, US Thanksgiving, Independence Day, Bonfire Night, Boxing Day, Mother's Day, Father's Day), Library Completion, Loyalty, People, Rewatch, Anime, Studio Specialist, Pilot vs Completer, and Hidden categories
  • 8 v2.0 easter-egg badges — First Anniversary, Late-Night Sage, Quadruple Feature, Dawn Chorus, Universal Watcher, Archivist Supreme, Deep Cut, Saga Marathon (don't appear until unlocked)
  • 18 music badges (v2.1.0) — plays, listening hours, and unique albums / artists / genres / decades. Audio plays pass the same 80% real-listen gate as video.
  • 8 book badges (v2.1.0) — books completed, audiobook listening hours, and series completed. Audiobook plays route per the audiobook-counting policy (Books-only default / Music-only / Both).
  • Improved anime detection (v2.1.0, #25) — matches Genres and Tags on the item and its parent Series, with admin-configurable libraries / genres / tags.
  • Admin-authored custom badges (v2.1.0) — compound AND/OR criteria across any metric; see Custom badges.
  • Targeted badges (v2.4.0, #107): point a badge at one specific series, season, collection, playlist, album or item; see Targeted badges.
  • Shop cosmetics on the shareable card (v2.4.0, #42): the equipped custom title and badge frame show on the shareable profile card and on the friends-drawer profile card, under the same privacy toggles as the equipped badges.
  • Game achievements (v2.4.0, #115): sessions, hours, distinct games and platforms for games played through JellyEmu, plus custom badges per platform, per developer and per game; see Game achievements.
  • 6 rarity tiers — Common, Uncommon, Rare, Epic, Legendary, Mythic
  • Hidden/secret badges displayed as ??? until unlocked
  • Library completion milestones that auto-scale to any library structure
  • Per-person tracking — Director and Actor affinity badges
  • Per-genre tracking — unique genre counters with dedicated badges
  • Era / country / language breakdowns via item metadata
  • Watch streaks — current and best streak badges
  • Daily login streak — loyalty rewards for consistent visits

Standout sub-collections:

  • 🎌 Anime tier (5 badges) — Genre-tag detection (any genre containing "anime", case-insensitive). Anime Curious / Anime Fan / Otaku / Anime Veteran / All-Otaku at 5 / 15 / 50 / 200 / 500 anime items.
  • 🎬 Studio specialists (6 badges) — Letterboxd-style "I watch a lot of A24" badges, parameterised by BaseItem.Studios:
BadgeStudioThreshold
Spirited AwayStudio Ghibli5
A24 AcolyteA2415
It's Not TVHBO25
Netflix and WatchNetflix50
Auntie BeebBBC25
House of MouseDisney50
  • 🎟️ Pilot vs Completer (6 badges) — every series whose S1E1 you watch is added to a per-user "pilots watched" set. If any other episode of that series follows, the series graduates into "continued past pilot". Pilot Tester / Window Shopper / Commitment Issues for sampling 5 / 20 / 50 series and bailing after the pilot; Hooked / Sticks the Landing / Always In for continuing past the pilot on 5 / 20 / 50 series.

Backfill credits these too — the watch-history backfill task passes Studios, SeriesId, SeasonNumber, EpisodeNumber through to the achievement service, so historical episodes credit anime / studio / pilot badges. Time-of-day buckets only count NEW sessions.

🎖️ Rank system

  • 10 tiers from Rookie → Novice → Viewer → Regular → Enthusiast → Binger → Connoisseur → Maestro → Legend → Immortal
  • Rank computed from your achievement score with progress bar to next tier
  • Theme unlocks — the achievements page changes gradient/border color as you climb
  • Custom rank titles (new in v2.0) — equip a cosmetic title (Cinephile / Curator / Tastemaker etc.) to replace the auto-generated tier name
  • Sidebar badge showcase + header dots display your current equipped badges at a glance

💰 Score economy

  • Every playback accrues 5 base points into a score bank
  • Combo multiplier — consecutive watches within 15 minutes stack up to +100% bonus
  • Spend bank to buy locked badges directly, or in the new Shop on power-ups + cosmetics (v2.0)
  • Gift score to other users on your server
  • Daily login bonus (v2.0) — +10 score on first real watch of each UTC day
  • Rarity-based badge scoring (10-150 pts), scaled by prestige level

⭐ Prestige

  • Reach Legend rank (12,000 score) to unlock prestige
  • Resets badges + counters but keeps your lifetime score and awards a prestige star
  • Each prestige level adds a +50% score multiplier to future badge unlocks
  • Visible on profile and leaderboard

🎯 Daily & weekly quests

  • 3 concurrent daily quests rotating from 12 built-in templates
  • 3 concurrent weekly quests rotating from 8 built-in templates
  • Deterministic rotation — everyone on the server gets the same quests per day/week
  • Completing quests pays into the score bank
  • Reroll (new in v2.0) — 1 daily reroll per UTC day, 1 weekly reroll per ISO week. Disabled pill shows you've used it, comes back at the next reset.
  • Admin quest customization — add / edit / remove daily + weekly quests from the admin page, replace built-in quests by Id, or disable built-ins your server can't satisfy

⚡ Power-ups & Score Shop

  • 3 consumable power-ups — XP Boost, Double Credit, Streak Freeze
  • 70+ cosmetic items across 6 kinds — themes, frames, titles, avatars, backgrounds, borders
  • Steam-style storefront with featured carousel, live previews, milestone progress bars, "NEED N MORE SCORE" affordability indicators
  • Daily login bonus drops a random power-up on first ≥80% real watch of each UTC day
  • All purchases audit-logged; admin testing tools to grant items without watch grinding

🎨 Profile customization

  • 14 Profile Themes — repaint the whole achievements page in any of 14 styles
  • 7 Badge Frames — animated edges on your equipped badges
  • 10 Custom Rank Titles — 3 auto-unlock + 7 shop-only
  • 14 Avatars — emoji swap for the rank-medal icon
  • 8 Animated Backgrounds — 4 CSS + 4 HD MP4 video loops, viewport-fixed with dimming overlay
  • 6 Profile Borders — animated effects on the hero card
  • Seen by everyone (v2.4.0) — every equipped cosmetic shows on your shareable profile card and on the card friends open from the drawer, under the same privacy toggles as your equipped badges. See What's new in v2.4.0.

👥 Friends drawer

  • Bi-directional friendship with a proper request / accept flow — nobody follows you silently
  • Global floating button anchored bottom-left on every Jellyfin page — not just the achievements tab. Auto-hides on /dashboard + /plugins pages and during media playback; reappears as soon as you leave either state
  • Xbox-guide-style side drawer with four sub-tabs: Friends / Requests / Find / Messages
  • Profile cards — hover a name for a summary, click to pin a larger card: rank, completion, score, streak, equipped badges, and since v2.4.0 the friend's custom title, badge frame, theme accent, profile border and avatar
  • Hover/click profile cards (v2.3.0, #76) — hovering a friend's name or avatar opens a summary card (rank tier, completion, score, best streak, equipped showcase); clicking pins a larger card that closes on Escape or an outside click and is keyboard-reachable. Served by a public-summary endpoint gated by the same leaderboard opt-out, so it exposes nothing extra
  • Jellyfin profile-image avatars per friend row (initials fall back when no image is set)
  • Online / offline status pulled live from Jellyfin's ISessionManager, with a 15-minute grace window so casual browsing still counts as online (not just active playback)
  • Now playing display — see the series + episode title each online friend is watching
  • Offline — last watched X — offline friends now show what they watched most recently (mirrors the online "Watching X" treatment). Backed by IUserDataManager with reflection-based LastPlayedDate lookup
  • Each friend's equipped badges shown next to their row (respects their privacy prefs)
  • Type-to-search user picker in the Find tab (not a giant dropdown of every server user)
  • Red unread badge on the floating button + Requests tab when someone has sent you a friend request
  • Mutual indicator on friend rows; Auto-accept kicks in if the target has already sent a request to you
  • Privacy toggles in user settings: Appear offline to friends (always shows you as offline), Hide what I'm watching (still online, but the series/episode is hidden), and Hide my last watched when offline. Enforced server-side in FriendsService.BuildFriendRow — can't be bypassed by client tampering
  • Compact request rows — Accept / Decline / Cancel buttons are icon-only with tooltips, so the Requests tab doesn't visually bloat
  • Drawer follows the Classic/Revamp toggle — sets body[data-ab-style="revamp"] so the same tokens apply globally

💬 Messaging

Xbox-Guide-style chat built into the Friends drawer. No external service, no WebSockets, everything stored on your own server.

  • Messages tab next to Friends / Requests / Find, with threads newest-first, avatars, last-message preview + timestamp, and a per-conversation unread count
  • 1:1 DMs and group chats (2–20 participants, named, rename + leave + members list)
  • Image attachments — PNG / JPEG / GIF / WebP up to 8 MB, server-side magic-byte verification so nobody can smuggle a .exe renamed to .png. Click-to-zoom lightbox
  • Read receipts — single green ✓ for delivered, double green ✓✓ for read. Per-participant readBy map so group receipts work too
  • Edit and delete your own messages via WhatsApp-style dropdown — click anywhere on your own bubble to open the menu, 24-hour edit window
  • Clear conversation button in the chat gear menu (wipes both sides, confirm dialog)
  • Block user — bi-directional; existing messages kept, new ones rejected. Stored in UserAchievementProfile.Preferences.BlockedUsers
  • Per-peer mute (localStorage-only, no server round-trip) + per-user prefs in the standalone settings panel:
    • Master message-notifications toggle
    • Message sound (subtle Web Audio chime, no audio file)
    • Mute during playback — suppresses notifications while Jellyfin's video player is active
  • Notifications:
    • In-app toast (top-center, 5s auto-dismiss, click to open the thread)
    • Browser Notification API when the tab isn't visible and permission is granted
    • Sound chime on inbound message
  • Unread indicators — pulsing red dot on the chat button in the friends list + count pill on the Messages tab + combined badge on the floating friends button
  • Anti-flicker rendering — content-hashed; the chat pane only repaints when something actually changed, preserving scroll position
  • Auto-polling — 6 s inside an open chat, 8 s on the Messages tab, 20 s drawer-wide when closed; all small, all [Authorize]-gated
  • Rate limits — 20 messages per minute per sender, max 1000 chars per message, 2000-message FIFO cap per conversation
  • Friendship gate: only mutual friends can DM; group creators can only add their own friends. Admin's FriendsSimpleMode treats the whole server as one friend list for messaging too (with the #138 option on, only between users who can see each other)
  • Storage — single messages.json + attachments.json + attachments/<id>.<ext> on disk under plugins/configurations/achievementbadges/. Atomic writes via temp file + File.Move so a crash mid-send can't corrupt the store. Messages survive server restarts

🏅 Rarity percentage chip

  • Every badge card on the achievements page shows a coloured chip with the % of users on your server who have unlocked that badge
  • Green ≥ 50% (common on this server), amber 10–50% (uncommon), red < 10% (rare / flex-worthy)
  • Fed by a /badges/rarity-stats endpoint with a 5-minute server-side cache so it doesn't re-scan every profile on every page load
  • Scarcity signal lets users see "nobody else on this server has this badge" at a glance

💾 Data-loss recovery

  • Load() walks primary badges.json → .bak → .recovery before giving up. A flaky primary file with a clean backup recovers silently
  • .bak is rotated on every successful save
  • .recovery captures in-session state when the primary is quarantined, so restart never feels like a fresh reset
  • LastLoadSummary exposed via the /test endpoint so admins can see recovery activity
  • Unparseable primary files are quarantined to badges.json.corrupt-<timestamp> (not deleted) for manual recovery

📊 Stats & visualization

  • Recap tab — weekly / monthly / yearly breakdowns with top genres, directors, actors
  • Year Wrapped — Spotify-style end-of-year recap with hero, your numbers, your highlights, your favorites
  • Watch heatmap — GitHub-style calendar (30/90/180/365 day range) colored by intensity
  • Genre radar chart — SVG spider chart of your genre distribution
  • Watch clock — 24-hour polar chart of when you actually watch
  • Stats snapshot — histogram of unlocked / score / best streak
  • Category leaderboards — Score, Movies, Episodes, Hours, Best Streak, Series

🏠 UI integration

  • Sidebar entry auto-injected into the Jellyfin nav menu (works on web, iOS, and Android after restart)
  • Jellyfin 12: the modern layout has no navigation drawer, so the entry sits in the avatar menu, right below Profile, and the equipped strip in the toolbar; the legacy layouts (desktop-legacy, mobile-legacy, TV) keep the drawer entry.
  • Equipped badge showcase in header + profile (configurable slot count, 1-10), ringed with your equipped badge frame
  • Xbox-style unlock toasts with per-rarity colors (6 tiers), Xbox logo → trophy swap, shimmer sweep, and confetti on rare+ unlocks
  • Achievement sound — Xbox 360 chime for common/uncommon, rare Xbox One chime for rare/epic/legendary/mythic
  • Diamond spritesheet for legendary/mythic unlocks (147-frame rotating crystal animation)
  • Configurable unlock bursts — group simultaneous unlocks into one score/count summary (default), or play each achievement animation separately
  • Per-device toast delivery — choose all signed-in devices (default) or only the Jellyfin device that earned the achievement
  • Toasts during playback — unlocks fire within ~1s of earning via playback event hooks + DOM fallback
  • Admin toast preview — test buttons for each rarity tier
  • Standalone achievements page at #/achievements with the new Loadout tab (v2.0) for managing power-ups, shop, and cosmetics
  • Optional page hosts — reuse the same page inside Custom Tabs or register it with Plugin Pages; both are admin opt-in and the stock page always remains available
  • Modular per-user navigation — independently show or hide the Custom Tabs entry, Plugin Pages entry, and header trophy without affecting achievement tracking or each other
  • Shareable profile card (three skins, v2.3.0) — server-rendered HTML at /Plugins/AchievementBadges/users/{id}/profile-card, in Console (default), Metro, or Aurora Spine, each drawn with the owner's rank colour as the accent. Users pick their skin in preferences; ?style= overrides per link, and a request with no style falls back to the owner's choice
  • Friend popover style (v2.3.0) — the drawer profile card comes in a detailed or a compact layout, chosen per user
  • Classic / Revamp toggle — every screen the plugin renders has a Classic/Revamp toggle (ab-style-pref localStorage):
    • Revamp — magazine-spread hero with massive Geist 700 rank name, 220px conic completion donut, asymmetric stats grid, chapter-numbered tabs (01 / MY BADGES, 02 / QUESTS…), control-panel filter strip, ambient drift orb, film grain overlay, day-streak pulse, rank-name shimmer, full page entrance cascade
    • Classic — the v1.8.10 look you know
    • Friends drawer follows the toggle too. Admin page has HUD corner brackets, conic rim sheen on Mythic + Legendary cards, animated KPI count-up, tier-staggered reveal.
  • Revamp admin operator strip — Use my account / Refresh / Simulate playback buttons + live status banner sit at the top of the personal section in Revamp mode. Long-running ops like Scan all users visibly pulse with status text.

⚙️ User preferences

A gear icon on the achievements page opens a full settings panel with auto-save:

  • Toast controls — enable/disable toasts, sound, confetti, milestone toasts
  • Unlock toast style — grouped summary or individual animations
  • Toast device scope — every signed-in client or only the device that triggered the unlock
  • Minimum toast rarity — filter out common spam (All / Rare+ / Epic+ / Legendary+)
  • Privacy — hide from leaderboard, compare profiles, activity feed, prestige board
  • Navigation integrations — independently show/hide the Custom Tabs entry, Plugin Pages entry, and header/profile trophy shortcut
  • Achievement page theme — Default, Dark, or Light (legacy site-wide theme, separate from v2.0's profile-theme cosmetics)
  • Spoiler mode — hides locked badge descriptions with "???" so you discover them naturally
  • Equipped badge slots — choose how many badges show in your showcase (1-10)
  • Auto-equip new unlocks — newly earned badges automatically fill empty slots
  • Per-user language picker — overrides the admin's server-wide default
  • Hide my last watched when offline — friends won't see what you watched most recently when you're offline
  • Mute toasts during playback + Mute toast sound during playback — keep the unlock animation/sound out of the way while actively watching
  • Message notifications + sound + mute-during-playback — full control over the Friends drawer's chat notifications
  • Shareable card skin (v2.3.0) — Console / Metro / Aurora Spine for your own profile card
  • Friend popover style (v2.3.0) — detailed or compact layout for the hover/click card
  • Toast position (v2.3.0, #136): place unlock toasts in any of the four corners, top-center or bottom-center, or leave it on the server default your admin picks

🌍 Languages

  • 8 languages — English, Français, Español, Deutsch, Italiano, Português, 中文 (简体), 日本語
  • ~2,200 UI keys translated across every tab (achievements profile, stats, heatmap, streak calendar, prestige leaderboard, notification prefs, server stats, compare, leaderboards, recap, quests, admin page, and every v2.0 Loadout surface)
  • All 200+ built-in badges translated — titles + descriptions per language, with hand-tailored cultural references (the Spirited Away badge is Le Voyage de Chihiro / El Viaje de Chihiro / Chihiros Reise / La Città Incantata / A Viagem de Chihiro / 千与千寻 / 千と千尋)
  • Badge categories + rarities also localised ("Binge" → "Marathon", "Legendary" → "Légendaire", etc.)
  • All v2.0 cosmetic names + descriptions translated — Cinephile / 影迷 / シネフィル / Cinéphile, every theme, every avatar, every background
  • Per-user language picker in preferences; admin can set a server-wide default
  • Globe language picker (v2.1.0) — a globe dropdown in the admin-page header switches the UI language live and persists across reloads
  • Fully localized admin page (v2.1.0) — the Integrity & anti-abuse and v2.0 testing-tools sections (previously English-only) are now translated across all 8 locales, as are the new music + book badges
  • Translations loaded client-side + server-side (BadgeLocalizer) so both UI chrome and badge titles on the leaderboard / showcase / admin grid / equipped showcase localise together

🛠️ Admin features

  • Feature Controls — kill switches for leaderboard, compare, activity feed, prestige, quests
  • Force Privacy Mode — override all users to hidden from all social features
  • Keep accounts hidden from the login screen out of other users' views (#138): an account with Jellyfin's Hide this user from login screens stays out of the leaderboards, the activity feed, friend search, compare and profile cards for everyone else. Hidden accounts, administrators and the account's existing friends still see it. Off by default, and with every account hidden from the login screen it changes nothing even when on, since hidden accounts see each other
  • Max Equipped Badges — server-wide cap (1-10)
  • Restrict Badge Visibility — users can only see their own badges
  • Disable Badge Categories — hide entire categories (e.g. "Late Night" for family servers)
  • Custom Welcome Message — text shown on the achievements page
  • Reset User Progress — wipe a specific user's badges via admin endpoint (v2.4.0: refuses a malformed or unknown user id instead of answering 200)
  • Diagnosable errors (v2.4.0) — every admin-page failure names the request that failed, and a watch-history scan that finds nothing tells you to check the account's library access
  • Enable/disable individual badges — useful if your server can't satisfy some criteria
  • Visual badge editor — form-based creator for custom badges
  • Custom badge builder (v2.1.0) — simple badges via an admin form, or compound AND/OR criteria + import/export via the /Plugins/AchievementBadges/custom-badges API. See Custom badges
  • Daily-badge audit & cleanup (v2.1.0, #27) — scans every profile for time-windowed badges wrongly awarded by pre-v2.1.0 backfills and clears them (re-earnable organically); audit-logged
  • JSON editor alternative for power users
  • Seasonal challenges — time-limited goals with start/end dates
  • Challenge templates — one-click add for Monthly Marathon, October Horror, New Year, Summer Blockbuster
  • Webhook notifications — Discord/Slack-compatible POST on every unlock
    • HMAC-SHA256 signing — when admin sets WebhookSigningSecret, every outbound POST carries X-AchievementBadges-Signature: sha256=<hex> + X-AchievementBadges-Timestamp: <unix>. Receivers verify with HMAC(secret, timestamp + "." + raw_body) and reject stale timestamps to prevent replay. Same envelope as Stripe and GitHub. Empty secret = legacy unsigned behaviour (backward compatible).
  • Audit log — last 5,000 events with timestamps. AdminAuditLogFilter writes an entry on every RequiresElevation action — answer "who unlocked X for whom last Tuesday" without grepping runtime logs.
  • Progress injection — set arbitrary counter values for testing / gifting
  • v2.0 testing tools — grant score / power-up / cosmetic to a user, backfill milestones across all profiles
  • Server-user picker on the admin/config pages for easier multi-user testing
  • Manual badge revoke (v1.9.8) — un-award a specific badge from a specific user when needed
  • Integrity test injection (v1.9.8) — verify daily-cap and suspicious-rate audit flag end-to-end on a throwaway user
  • Admin auth lockdown — all admin endpoints require elevated permissions
  • Default UI style + lock (#43) — pick whether users start on Classic or Revamp, and optionally make it the only choice so the achievements page matches your Jellyfin theme. A user's own pick is remembered rather than erased, so it returns if you lift the lock
  • Default toast position (#136): where unlock toasts appear for everyone who has not picked a placement; a user's own choice always wins
  • Tracearr watch history (#45) — set a Tracearr URL and API token to credit plays the library scan cannot see. See Tracearr integration

🔒 Tracking

  • Watch history backfill — scans existing Jellyfin play history to retroactively award badges on install
  • Auto-evaluation on startup — new badges from plugin updates auto-unlock if your existing counters already satisfy them, no manual scan needed
  • Live playback tracker — unlocks fire during viewing, past the 80% completion threshold
  • Real-watch credit gate (v1.9.8) — playback events require ≥80% accumulated play ticks with seeks excluded; mark-as-played + seek-to-end + spam-click no longer credit badges
  • 60-second minimum runtime filter so Projectionist prerolls and bumpers can't credit
  • Daily credit cap (default 200/user/day, configurable) + SuspiciousRatePerHour audit flag (default 30/h, configurable) for soft anti-abuse
  • Rewatch detection — dedupes within 6 hours, counts rewatches beyond that
  • Watch-time carry (v2.3.0) — partial viewings are banked and survive a session break, a restart, and a media-file replacement or quality upgrade (keyed by media identity, #89), with an admin retention window (#87); a scan then clears the carry of exactly what it credits (#91/#92) so already-counted minutes can't finish a later rewatch
  • Library + artist completion wiring (v2.3.0) — the library-completion badges are recomputed during the scan (#80) and per-artist discography completion is tracked (#81), so both unlock through normal use
  • People metadata extraction — uses ILibraryManager.GetPeople() for directors/actors

🎨 Custom badges

Added in v2.1.0 "Open Library". Admin-only. All endpoints require admin elevation.

Define your own badges with compound AND/OR criteria the built-in catalog can't express. The plugin config page (Dashboard → Plugins → Achievement Badges → Custom Badges) has a form for simple badges (single metric + threshold). For compound criteria, icon control, and import/export, use the API below.

Criteria model

A badge's criteria is a tree. A node is either:

  • a leaf: { "metric": "<Metric>", "threshold": <int>, "metricParameter": "<optional>" } — true when the metric value ≥ threshold.
  • a compound: { "operator": "And" | "Or", "children": [ <node>, ... ] } — true when And(all)/Or(any) of children are true.

Limits: max depth 5, max 64 nodes per badge, max 500 badges per instance.

Targeted badges

Added in v2.4.0, from #107.

Two metrics point at one specific thing in your library rather than at an aggregate:

  • ContainerCompletionPercent — played items over total items of one series, season, collection, playlist or album. Threshold 100 means "you finished it".
  • ItemPlayCount — how many times one specific item has been played. Threshold 1 is "you watched this movie"; larger thresholds are for the track you keep going back to.

Both take their target in metricParameter, formatted "{guid}|{name}". The config page has a library picker that writes it for you. Both halves are stored on purpose: the GUID survives a rename, and the name survives an item being deleted and re-added, so the badge re-resolves itself and rewrites the GUID.

For an arbitrary group of episodes, such as one story arc of a long-running show, make a Jellyfin collection and point a ContainerCompletionPercent badge at it. That keeps the grouping in a native Jellyfin feature instead of a badge-editor episode list.

Targeted badges are evaluated against your existing history, so a badge you author today unlocks immediately for anyone who already finished the target, with no scan needed. How many distinct targets all badges may reference is set on the config page, under Custom badges, as Targeted badge cap (1 to 1000, default 50). The page shows how many targets the enabled badges reference against the cap and names the ones past it, which are not computed. Every play checks each target with a set lookup: series, seasons and folders against the played item's ancestors, collections and playlists against a cached member list that is refreshed when the container changes, so hundreds of targets of any kind are fine on any server.

Template — finish one series

// POST /Plugins/AchievementBadges/custom-badges
{
  "name": "Straw Hat",
  "description": "Finish One Piece: Alabasta.",
  "rarity": "Epic",
  "media": "TV",
  "criteria": {
    "metric": "ContainerCompletionPercent",
    "metricParameter": "8f4e2a1b9c3d4e5f6a7b8c9d0e1f2a3b|One Piece: Alabasta",
    "threshold": 100
  }
}

Game achievements (JellyEmu)

Added in v2.4.0, from #115.

Games played through JellyEmu earn achievements with no setup on the JellyEmu side. JellyEmu reports every game session to Jellyfin as a playback session and tags each game with JellyEmu, Game and its platform; this plugin reads those and measures each session as the smaller of what the emulator reported and what the plugin saw on its own clock, with a floor (MinGameSessionSeconds, default 60) so a misclick is not a play and a cap (MaxGameSessionMinutes, default 360) so a tab left open is not a night of play.

Built in, under the Games category: session ladders (1, 25, 100), distinct games (10, 50), hours (10, 100) and platforms (3, 8). For custom badges, the builder offers:

  • GamePlatformGames with the platform tag as parameter (SNES, SegaGenesis, PlayStation; the spelling is JellyEmu's, matched without regard to case): "play 10 Sega Genesis games".
  • GameStudioGames with the developer or publisher as parameter: "play 5 games by Capcom".
  • GameHours with a specific game as target, chosen in the picker like any other targeted badge: "30 hours in this game".

Two limits worth knowing: play time counts from the moment this version is installed, because games carry no played flag to rebuild history from; and nothing here reads RetroAchievements, whose unlocks cannot happen in JellyEmu's browser emulator (see the discussion on #115).

Template — simple badge

// POST /Plugins/AchievementBadges/custom-badges
{
  "name": "Cinephile",
  "description": "Watch 100 films.",
  "rarity": "Rare",            // Common | Uncommon | Rare | Epic | Legendary | Mythic
  "media": "Film",             // Film | TV | Music | Book | Anime | Multi
  "iconUrl": "",               // optional external https URL; blank = default trophy
  "enabled": true,
  "criteria": { "metric": "MoviesWatched", "threshold": 100 }
}

Template — compound (AND / OR)

{
  "name": "Renaissance Viewer",
  "description": "Watch 100 films AND listen to 50 music tracks.",
  "rarity": "Epic",
  "media": "Multi",
  "enabled": true,
  "criteria": {
    "operator": "And",
    "children": [
      { "metric": "MoviesWatched",   "threshold": 100 },
      { "metric": "MusicPlaysTotal", "threshold": 50 }
    ]
  }
}

Nested example — "(finish 5 horror series OR 10h of audiobooks) AND watch 25 anime items":

{
  "name": "Eclectic",
  "description": "Genre-spanning dedication.",
  "rarity": "Legendary",
  "media": "Multi",
  "enabled": true,
  "criteria": {
    "operator": "And",
    "children": [
      {
        "operator": "Or",
        "children": [
          { "metric": "SeriesCompleted",         "threshold": 5,  "metricParameter": "Horror" },
          { "metric": "AudiobookListeningHours",  "threshold": 10 }
        ]
      },
      { "metric": "AnimeItemsWatched", "threshold": 25 }
    ]
  }
}

Endpoints

MethodRoutePurpose
GET/Plugins/AchievementBadges/custom-badgesList all
GET/Plugins/AchievementBadges/custom-badges/{id}Fetch one
POST/Plugins/AchievementBadges/custom-badgesCreate (fresh id assigned)
PUT/Plugins/AchievementBadges/custom-badges/{id}Update
DELETE/Plugins/AchievementBadges/custom-badges/{id}Delete
GET/Plugins/AchievementBadges/custom-badges/exportExport all as JSON
POST/Plugins/AchievementBadges/custom-badges/importBulk import (fresh ids)
GET/Plugins/AchievementBadges/custom-badges/targetsTargeted badge cap: configured value, applied cap, bounds, targets observed, and the names past it (#129)
POST/Plugins/AchievementBadges/custom-badges/targetsSet the cap (1-1000, clamped); answers with the fresh summary (#129)

All metrics

Every value AchievementMetric exposes, 71 of them. Entries marked * accept a metricParameter.

  • Watching — TotalItemsWatched, MoviesWatched, SeriesCompleted, LongSeriesCompleted, VeryLongSeriesCompleted, RewatchCount, ShortItemsWatched, LongestItemMinutes, TotalMinutesWatched, SeriesSampledOnly, SeriesBingedAfterPilot

  • Streaks and days — DaysWatched, CurrentWatchStreak, BestWatchStreak, DaysLoggedIn, CurrentLoginStreak, BestLoginStreak, MaxEpisodesInSingleDay, MaxMoviesInSingleDay, MaxMinutesInSingleDay

  • Time of day — LateNightSessions, EarlyMorningSessions, AfternoonSessions, PrimeTimeSessions, WeekendSessions

  • Variety — UniqueGenresWatched, UniqueCountriesWatched, UniqueLanguagesWatched, UniqueDecadesWatched, UniqueLibrariesVisited, TopDirectorCount, TopActorCount, MaxLibraryItemCount

  • Completion — LibraryCompletionPercent *, LibrariesAt100Percent, BadgesUnlockedPercent, ArtistCompletionPercent *

  • Music — MusicPlaysTotal, MusicListeningHours, UniqueMusicAlbums, UniqueMusicArtists, UniqueMusicGenres, UniqueMusicDecades, MusicGenrePlays *, MusicGenreListeningHours *

  • Books — BooksCompleted, AudiobookListeningHours, UniqueBookSeriesCompleted

  • Score and prestige — PrestigeLevel, LifetimeScore, BestComboCount

  • Parameterized by name — GenreItemsWatched *, PersonItemsWatched *, StudioItemsWatched *, DecadeItemsWatched *, DayOfWeekItemsWatched *

  • Holidays and dates — WatchedOnChristmas, WatchedOnNewYear, WatchedOnHalloween, WatchedOnEid, WatchedOnValentines, WatchedOnEaster, WatchedOnLunarNewYear, WatchedOnDiwali, WatchedOnThanksgiving, WatchedOnIndependenceDayUS, WatchedOnBonfireNight, WatchedOnBoxingDay, WatchedOnMothersDay, WatchedOnFathersDay

  • Anime — AnimeItemsWatched

Parameterized metrics match their parameter case-insensitively. GenreItemsWatched with metricParameter of "horror" counts only horror items; LibraryCompletionPercent with "Movies" reads that one library, and without a parameter it reads whichever library the user is furthest through. The same applies to ArtistCompletionPercent, which without a parameter reads the user's best artist.


📡 Tracearr integration

Optional. Set Tracearr URL and Tracearr API token in the plugin settings, generate the token in Tracearr under Settings, and a watch history scan will also read your Tracearr history. Leave either field empty and nothing changes.

It exists because a library scan has two blind spots it cannot fix on its own:

  • Media you deleted. The scan reads IsPlayed on items that still exist, so a film you watched and later removed is invisible to it. Tracearr recorded the play when it happened and still has it.
  • How many times you watched something. IsPlayed is a boolean, so no number of viewings can produce a rewatch count. That leaves the Rewatch badges unreachable for anyone who installed the plugin after they had already been watching.

Only plays Tracearr considers finished are credited, each on the date it happened rather than today, so an old viewing cannot manufacture a streak. A play the library already accounted for is not counted again: the first viewing of a known item is skipped, and only repeats become rewatches.

Nothing is required on the Tracearr side. It uses the existing public v2 API.


🛡️ Security & operations

  • Default class-level rate limit (user-60-per-min) on every controller route, with stricter overrides preserved on cooldown routes. Static-asset routes (CSS / JS / translations / video bgs) opt out via [DisableRateLimiting] so multi-user households behind a shared NAT don't collectively exhaust the limit.
  • CSP + X-Content-Type-Options + X-Frame-Options + Referrer-Policy + Permissions-Policy on the anonymous profile-card endpoint
  • Per-user chat attachment quota (200 files / 200 MB total) closes a disk-exhaustion vector
  • SVG sanitizer blocks <animate> / <set> SMIL elements that could mutate attributes mid-render
  • Webhook URL DNS resolution bounded at 3s (was OS default 5-30s)
  • Audit-log endpoint limit clamped to [1, 1000] at controller
  • MessagingService + AchievementBadgeService are IDisposable so the debounced-save Timer is released on plugin reload
  • Unit security regression tests — SSRF, IPv6 SSRF, scheme rejection, malformed URL rejection, dangerous SVG element rejection, on-event-handler rejection, external DTD rejection, oversized payload rejection, external <use href> rejection
  • GitHub Actions CI runs the tests + dotnet list package --vulnerable + gitleaks on every push, every PR, and weekly cron
  • CodeQL + OpenSSF Scorecard + OpenSSF Best Practices Level 1 (project 12937)
  • SECURITY.md with full threat model, trust boundaries, defences-in-place inventory, continuous verification matrix, disclosure SLA, and safe-harbour for researchers
  • Sigstore-signed + SLSA build-provenance attested release artifacts — verify with cosign verify-blob or gh attestation verify
  • Reproducible NuGet restore — every transitive package locked by hash via RestorePackagesWithLockFile, CI runs in locked mode so drift fails fast

⚡ Plugin-wide efficiency

  • Debounced Save() in AchievementBadgeService and MessagingService — coalesces back-to-back disk writes from playback and messaging hot paths into one flush per 1.5s
  • FriendsService.LastWatched cache — 90s TTL, invalidated on play, eliminates per-friend 50-item DB query on every friends-list call
  • WriteIndented = false on production stores (~50% smaller badges.json)
  • Embedded resource cache in client-script + asset routes — one read per process
  • Cache-Control: public, max-age=86400, immutable on assets with version-only cache busting; video backgrounds use HTTP Range so the browser only streams the bytes it needs to start playing
  • Cache-Control: no-cache, must-revalidate on translations (v2.0) so users always get fresh strings after a plugin update without hard-refreshing
  • Middleware marker fast-path (last-4KB scan instead of full body)

⚙️ Installation

  1. Go to Dashboard → Plugins → Repositories
  2. Add:
https://raw.githubusercontent.com/ZL154/AchievementBadges_for_Jellyfin/main/manifest.json
  1. Save and refresh plugins
  2. Install Achievement Badges
  3. Restart Jellyfin
  4. Go to Dashboard → Plugins → Achievement Badges → Settings
  5. Click Scan watch history (or Scan all users) to backfill from your existing play data
  6. Open Achievements (avatar menu on Jellyfin 12, sidebar on 10.11) or go to #/achievements — and try the Loadout tab

🔧 Requirements

  • Jellyfin 10.11.x (.NET 9) or Jellyfin 12.0.x (.NET 10). Every release ships one package per line and the catalog picks the right one for your server: version x.y.z.0 is the 10.11 build, x.y.z.1 is the 12 build. Installing by hand? Take the zip whose 4th version segment matches your server. Upgrading the server from 10.11 to 12 will offer the .1 build as a plugin update afterwards.
  • File Transformation plugin (strongly recommended) — ensures sidebar, dashboard UI, profile showcase and achievements page inject reliably across Jellyfin Web updates. Without it most UI injection still works via the plugin's own middleware, but File Transformation gives the most robust integration.

Optional but helpful

  • Proper metadata provider (TMDb, OMDb) — required for Director/Actor badges to populate. Badges based on item.People will stay empty if your library doesn't have people scraped
  • Home Screen Sections plugin — lets the achievement home widget inject more reliably
  • Custom Tabs or Plugin Pages — optional alternative hosts for the Achievements page; enable the matching integration under Page integrations in plugin settings. Saving installs/repairs the owned Custom Tabs entry without changing existing tabs; restart Jellyfin afterward.
    • Custom Tabs injects the tab's content panel by patching Jellyfin's home page on the server, so a theme that ships its own copy of that page (Abyss's Spotlight, for one) can leave the tab button with no panel behind it, which used to show as a blank Achievements page (#131). The plugin now builds the missing panel itself, so the tab keeps working; a panel it had to build carries data-ab-repaired-panel="true". Other Custom Tabs tabs are still empty on such a server, since only this one is ours to repair.

What each feature needs

FeatureDepends on
Sidebar + header injectionNothing (works standalone)
Custom Tabs page hostCustom Tabs plugin + enable the integration, save, and restart Jellyfin
Plugin Pages page hostPlugin Pages plugin + Jellyfin restart after enabling integration
Watch history backfillPlayed flag on items (Jellyfin default)
Genre badgesItems with Genres metadata
Director/Actor badgesItems with People metadata (TMDb/OMDb scrape)
Era / decade badgesItems with ProductionYear metadata
Country badgesItems with ProductionLocations metadata
Language badgesItems with OriginalLanguage metadata
Runtime badgesItems with RunTimeTicks populated
Library completionAt least one library folder with items
Webhook notificationsA webhook URL (Discord, Slack, or generic)
Animated video backgroundsModern browser with H.264 + <video> autoplay-muted support (all evergreen browsers)

🔍 Troubleshooting

The plugin injects its scripts into Jellyfin's index.html at startup. If the web directory isn't writable, the injection fails silently and no UI loads (no sidebar entry, no toasts, no achievements page).

Diagnose: visit https://your-server/Plugins/AchievementBadges/test — the JSON response shows:

  • DiagIndexFound — whether index.html was located
  • DiagIndexPatched — whether the script tags were successfully written
  • DiagLastError — the exact error if patching failed (usually Unauthorized: Access denied)

Common cause: on Docker or Linux installs, Jellyfin doesn't have write access to /usr/share/jellyfin/web/. Fix by granting write permission:

# Docker: run inside the container
chmod -R a+w /usr/share/jellyfin/web/

# Systemd: fix ownership
sudo chown -R jellyfin:jellyfin /usr/share/jellyfin/web/

Then restart Jellyfin. The plugin will patch index.html on the next startup.

Can't (or won't) make the web dir writable? Use the JavaScript Injector plugin (v2.1.0, #26). On bare-metal Linux where /usr/share/jellyfin/web is owned by root, install the JavaScript Injector plugin. On the next startup, when the on-disk patch fails, Achievement Badges detects that plugin and writes the exact script URLs you need into the /Plugins/AchievementBadges/test diagnostics (DiagJsInjectorGuidance) and the server log. Paste these three into JS Injector's settings — one per entry:

/Plugins/AchievementBadges/client-script/sidebar
/Plugins/AchievementBadges/client-script/standalone
/Plugins/AchievementBadges/client-script/enhance

Restart Jellyfin and the UI loads through JS Injector — no writable web directory required. (The plugin doesn't call JS Injector's API directly, so this keeps working across JS Injector versions; the on-disk patch remains the default when the dir is writable.)

Still broken? The plugin has a middleware fallback that rewrites index.html at runtime (no disk write needed). If that's also failing, check whether a reverse proxy (nginx/Caddy) is caching a stale index.html from before the plugin was installed. Clear the proxy cache or restart it.

NixOS (read-only /nix/store)

NixOS serves Jellyfin's web files from the immutable Nix store, so neither the disk patcher nor the middleware can modify index.html. Use a NixOS overlay to inject the script tags at build time:

nixpkgs.overlays = [
  (final: prev: {
    jellyfin-web = prev.jellyfin-web.overrideAttrs (finalAttrs: previousAttrs: {
      installPhase = ''
        runHook preInstall
        sed -i 's#</body>#<!-- achievementbadges-bootstrap --><script src="/Plugins/AchievementBadges/client-script/sidebar"></script><script src="/Plugins/AchievementBadges/client-script/standalone" defer></script><script src="/Plugins/AchievementBadges/client-script/enhance" defer></script></body>#' dist/index.html
        mkdir -p $out/share
        cp -a dist $out/share/jellyfin-web
        runHook postInstall
      '';
    });
  })
];

The plugin DLL serves the JS files from embedded resources — the three <script> tags just tell the browser to load them. Rebuild your NixOS config after adding the overlay and restart Jellyfin.

The dashboard's API docs page is blank (/api-docs/openapi.json returns 500)

Jellyfin builds one OpenAPI document from the server and every installed plugin, and each type in it is keyed by its bare class name. If two loaded types want the same name the document does not pick one — it fails, and the whole document 500s. Badges keep working, nothing is logged by the plugin, and the only visible symptom is the API docs page.

This plugin caused it from 2.1.0 to 2.4.0 (MediaType, colliding with Jellyfin's own) and no longer does. If the page is still broken on v2.4.1+, another plugin is colliding — the server log names both types:

Can't use schemaId "$PluginConfiguration" for type "$Jellyfin.Plugin.A.PluginConfiguration".
The same schemaId is already used for type "$Jellyfin.Plugin.B.Configuration.PluginConfiguration"

PluginConfiguration is the usual culprit, since most plugins name their config class that. Report it to whichever plugin appears there; there is nothing to change on your server.

Video backgrounds don't play

The 4 HD video cosmetics need browser MP4/H.264 + autoplay-muted support. Every evergreen browser handles this fine, but if you see a static page where there should be motion:

  • Check the browser's autoplay policy (Chrome: chrome://flags/#autoplay-policy)
  • The video element is set to muted + playsinline so it should autoplay everywhere; if your browser still blocks it, click anywhere on the page once
  • Low-end devices (old TVs, basic streamers) may stutter — switch to a CSS-only bg or "None" via Loadout → Cosmetics

📡 API endpoints

User-facing (require auth)

GET    /Plugins/AchievementBadges/users/{userId}                      — full badge list
GET    /Plugins/AchievementBadges/users/{userId}/summary              — unlocked/total/score
GET    /Plugins/AchievementBadges/users/{userId}/rank                 — rank tier + next tier
GET    /Plugins/AchievementBadges/users/{userId}/equipped             — equipped badges
POST   /Plugins/AchievementBadges/users/{userId}/equipped/{badgeId}
DELETE /Plugins/AchievementBadges/users/{userId}/equipped/{badgeId}
GET    /Plugins/AchievementBadges/users/{userId}/recap?period=week|month|year
GET    /Plugins/AchievementBadges/users/{userId}/watch-calendar?days=90
GET    /Plugins/AchievementBadges/users/{userId}/quests               — daily + weekly + reroll state
GET    /Plugins/AchievementBadges/users/{userId}/bank                 — score bank + prestige
POST   /Plugins/AchievementBadges/users/{userId}/prestige
POST   /Plugins/AchievementBadges/users/{userId}/buy-badge/{badgeId}
POST   /Plugins/AchievementBadges/users/{userId}/gift/{toUserId}?amount=N
GET    /Plugins/AchievementBadges/users/{userId}/chase/{badgeId}      — items to watch to finish a badge
GET    /Plugins/AchievementBadges/users/{userId}/recommendations      — top 3 closest-to-unlock
GET    /Plugins/AchievementBadges/users/{userId}/profile-card         — HTML profile card
GET    /Plugins/AchievementBadges/users/{userId}/unlocks-since?since=ISO&deviceId=ID
GET    /Plugins/AchievementBadges/users/{userId}/library-completion
POST   /Plugins/AchievementBadges/users/{userId}/login-ping
GET    /Plugins/AchievementBadges/users/{userId}/directory            (#138) users this user may see, for friend search and compare
GET    /Plugins/AchievementBadges/leaderboard?limit=10
GET    /Plugins/AchievementBadges/leaderboard/{category}?limit=10     — score|movies|episodes|hours|streak|series
GET    /Plugins/AchievementBadges/embedded-page                       — Plugin Pages host fragment
GET    /Plugins/AchievementBadges/server/stats

v2.0 — Power-ups, Shop, Cosmetics, Quest reroll

GET    /Plugins/AchievementBadges/users/{userId}/powerups             — inventory + active state + ScoreBank
POST   /Plugins/AchievementBadges/users/{userId}/powerups/use/{type}  — XpBoost | DoubleCredit (StreakFreeze auto-only)
GET    /Plugins/AchievementBadges/shop/catalog                        — full shop catalog
POST   /Plugins/AchievementBadges/users/{userId}/shop/purchase        — body: {"ItemId":"..."}
GET    /Plugins/AchievementBadges/users/{userId}/cosmetics            — owned + equipped state + LifetimeScore
POST   /Plugins/AchievementBadges/users/{userId}/cosmetics/equip      — body: {"CosmeticId":"..."}
POST   /Plugins/AchievementBadges/users/{userId}/cosmetics/unequip?kind=ProfileTheme|BadgeFrame|RankTitle|Avatar|Background|ProfileBorder
POST   /Plugins/AchievementBadges/users/{userId}/quests/daily/reroll
POST   /Plugins/AchievementBadges/users/{userId}/quests/weekly/reroll
GET    /Plugins/AchievementBadges/asset/{name}                        — animated background mp4s (range-enabled)

Admin-only (require RequiresElevation)

POST   /Plugins/AchievementBadges/users/{userId}/backfill
POST   /Plugins/AchievementBadges/backfill-all
POST   /Plugins/AchievementBadges/users/{userId}/reset
POST   /Plugins/AchievementBadges/users/{userId}/reset-badge/{badgeId}
POST   /Plugins/AchievementBadges/users/{userId}/library-completion/recompute
POST   /Plugins/AchievementBadges/users/{userId}/import
GET    /Plugins/AchievementBadges/users/{userId}/export
GET/POST  /Plugins/AchievementBadges/admin/badge-catalog              — enable/disable badges
GET/POST  /Plugins/AchievementBadges/admin/custom-badges              — custom badge definitions
GET/POST  /Plugins/AchievementBadges/admin/challenges                 — seasonal challenges
GET       /Plugins/AchievementBadges/admin/challenge-templates        — one-click templates
GET/POST  /Plugins/AchievementBadges/admin/webhook                    — webhook config (incl. HMAC secret)
GET/POST  /Plugins/AchievementBadges/admin/ui-features                — UI feature toggles
GET       /Plugins/AchievementBadges/admin/audit-log?limit=200
POST      /Plugins/AchievementBadges/admin/users/{userId}/inject-counters
GET/POST  /Plugins/AchievementBadges/admin/feature-config             — feature kill switches + admin controls
DELETE    /Plugins/AchievementBadges/admin/users/{userId}/reset       — wipe user's achievement progress

POST   /Plugins/AchievementBadges/admin/users/{userId}/test/inject-playbacks    — verify integrity caps end-to-end (v1.9.8)
DELETE /Plugins/AchievementBadges/admin/users/{userId}/badges/{badgeId}          — manual badge revoke (v1.9.8)

POST   /Plugins/AchievementBadges/admin/users/{userId}/grant-score              — v2.0 testing
POST   /Plugins/AchievementBadges/admin/users/{userId}/grant-powerup/{type}     — v2.0 testing
POST   /Plugins/AchievementBadges/admin/users/{userId}/grant-cosmetic           — v2.0 testing
POST   /Plugins/AchievementBadges/admin/backfill-milestones                     — v2.0: retroactively unlock title milestones across all profiles

📸 Screenshots

Xbox-style unlock toast

Pops up during playback when a badge unlocks. Xbox circle pops in with pulse rings, expands into a banner, trophy rotates (or diamond spritesheet for rare unlocks), text slides up, shimmer sweeps across, then everything collapses. Per-rarity color, glow, and sound.

Xbox-style unlock toast

Live demo: download achievement-combined.html (regular) or achievement-combined-rare.html (rare with diamond) and open in a browser. Click anywhere to start the sound. Loops every 10.5s.

The standalone Achievements page

The full profile view, shown in the Jellyfin sidebar. Rank progress bar, day streak, score, completion percentage, and the tab bar (My Badges, Quests, Recap, Leaderboard, Compare, Activity, Wrapped, Stats, and the new Loadout tab from v2.0).

Achievements page

Badge grid

200+ badges across 35+ categories, each with live progress bars and an Equip button. Unlocked badges show in color with a green status tag; locked badges dim. Rarity-colored borders let you scan the grid visually.

Badge grid overview

Rarity tiers in action

Genre specialist badges and streak extremes across all six rarity colors — Common, Uncommon, Rare, Epic, Legendary, Mythic.

Genre + rarity badges

Daily and weekly quests

Rotating quests from a template pool. Everyone on the server gets the same daily + weekly challenges so people can race each other. Completing them pays into the score bank. In v2.0, each section header has a reroll button.

Daily and weekly quests

Recap

Weekly, monthly and yearly breakdowns of what you've actually watched — total items, active days, top genres, top directors, and top actors.

Recap view

Year Wrapped

Spotify-style end-of-year recap with a big gradient hero, "your numbers" (movies, episodes, active days, best streak, total hours), "your highlights" (biggest day, biggest month, most-watched weekday) and "your favorites" (top genres/directors/actors).

Year Wrapped view

Leaderboard

Podium view for the top 3, ranked list below. Switch categories with the tab row: Score, Movies, Episodes, Hours, Best Streak, Series. (Usernames blurred as User 1–10.)

Leaderboard view

Compare profiles

Head-to-head profile comparison between any two users on your server. Gradient bars show the relative values on 12 core metrics, and the bottom pills break down how many badges each user has that the other doesn't. (Usernames blurred as User 1 / User 2.)

Compare profiles view

Streak calendar

GitHub-style year calendar of your watch activity. Current streak, best ever, and total active days at a glance.

Streak calendar

Watch heatmap

90-day heatmap grid, colored by daily watch volume. Click the range button to switch between 30/90/180/365 days.

Watch heatmap

Genre radar + watch clock

SVG spider chart showing your top-5 genre distribution, and a 24-hour polar chart of when you actually watch.

Genre radar + watch clock

Admin panel

Every admin section is collapsible so the page stays clean: webhook notifications, toast preview, UI feature toggles, visual badge editor, challenge templates, audit log, progress injection, custom badges, seasonal challenges, per-badge enable/disable, and the v2.0 testing tools (grant score / power-up / cosmetic, backfill milestones).

Admin panel

Advanced options

Scan watch history, reset badges, scan all users, or load a specific user ID — all from one row under the Advanced options toggle.

Advanced options

Sidebar entry

Auto-injected into the Jellyfin nav menu — no theme changes required.

Sidebar entry


📜 Release history

Full per-version notes and signed binaries live on the GitHub Releases page:

➡ github.com/ZL154/AchievementBadges_for_Jellyfin/releases

Highlights:

  • v2.4.1 — what 2.4.0 got wrong, and a cap you can raise: the Jellyfin 12 package now reports the version it shipped as, so uninstall and the plugin image work (#140); a browser that cached Jellyfin's page before the install gets the plugin instead of a stale copy (#141); a targeted badge cap settable to 1000 with the overflow named (#129); the Revamp style no longer restyles Jellyfin's own controls (#133); a Custom Tab under a page-replacing theme renders again (#131); top-center toasts plus a server default, and the default UI style actually persists (#136/#43); login-hidden accounts can be kept out of other users' views (#138); and a type-name collision that had been 500ing the server's whole OpenAPI document since 2.1.0 is gone
  • v2.4.0 — Jellyfin 12, targeted badges, games: two packages per release (x.y.z.0 for 10.11, x.y.z.1 for 12) with the entry in 12's avatar menu and the Authorization header 12 requires (#109/#117/#122); ContainerCompletionPercent + ItemPlayCount with a library picker (#107/#108); JellyEmu game achievements (#115/#120); every shop cosmetic on the shareable card and the drawer card (#42/#119); toast position under Revamp (#116/#118); Pastel page leak, invisible borders/frames, stale stylesheet token, #97 diagnostics
  • v2.3.1 — music fixes + leaderboards: tracks stop inheriting album/artist genres so custom music badges count correctly (#94), the scan replays played music so discography badges rebuild (#96), both leaderboards show your own rank, deleted accounts are excluded + prunable, and the Blades skin becomes Aurora
  • v2.3.0 — Friends & Foundations: hover/click friend profile cards (#76) behind a privacy-gated summary endpoint; three shareable card skins (Console / Metro / Aurora Spine) chosen per user; library completion now computes during the scan (#80) and new artist discography completion (#81, #24); Tracearr history crediting (#77/#84/#85); watch-time carry across restarts and file replacements (#87/#89/#91/#92); admin-set default UI style + lock (#43); plus the profile data-loss (#59/#60) and gzip injection (#46) fixes for published builds
  • v2.2.0 — Your Screen, Your Rules: optional Custom Tabs + Plugin Pages hosts and independent per-user navigation controls (#37); grouped/individual and all-device/origin-device unlock notification modes (#38); clickable, keyboard-accessible real unlock toasts; 10-toast admin grouping preview; build-specific client cache keys; full 8-language coverage
  • v2.1.3 — Open Library patch: per-genre music badges so genre filters actually filter (#24), custom badges fully delete + purge earned copies + ID-preserving migration (#24), header UI survives another header-injecting plugin (#36), badge-category label fix, and full localization of the badge/quest builders across all 8 languages
  • v2.1.0 — Open Library: music + book achievements, custom badge builder (compound AND/OR), JS Injector fallback (#26), anime detection via Genres+Tags+Series (#25), daily-badge backfill fix + audit/cleanup tool (#27), globe language picker + full admin-page localization
  • v2.0.0 — Choose Your Loadout: power-ups, score shop, 70+ cosmetics, 8 video backgrounds, full i18n
  • v1.9.8 — Integrity release: closes playback-credit exploits (mark-as-played, seek-to-end, spam-click no longer credit), real-watch credit gate (≥80% accumulated play ticks with seeks excluded), 60s minimum runtime filter, daily credit cap, suspicious-rate audit flag, manual badge revoke
  • v1.9.7 — Security-only: per-user chat attachment quota, profile-card CSP tightening, SVG sanitizer adds SMIL blocking, webhook DNS timeout
  • v1.9.6 — Toast description fix, playback popups default-on, server-user picker on admin/config pages
  • v1.9.5 — Chat attachment fix, security hardening
  • v1.9.4 — 32 new badges (afternoon, prime time, holidays expansion, anime tier, studio specialists, pilot vs completer) + 8-language hand-tailored translations
  • v1.9.0 — Revamp UI (Classic/Revamp toggle), Friends drawer Revamp, "Offline — last watched", HMAC webhook signing, security upgrade (rate limit, CSP, audit log filter, GitHub Actions CI)
  • v1.8 — Full messaging suite (1:1 + groups, attachments, read receipts, edit/delete, block, notifications)
  • v1.7 — Friends drawer foundation, hand-translated French by @frenchyx24

🗂️ Previous release notes

Full notes for earlier versions, newest first.

🚀 What's new in v2.4.0 — Jellyfin 12, targeted badges, games

Achievement Badges runs on Jellyfin 12, gains the first badges that can point at one specific thing in your library, learns to count games, and finally shows the bling you bought in the shop to everyone else. Drop-in upgrade from v2.3.x — no schema breakage or manual migration. Full notes in docs/release-notes/v2.4.0.md.

🟣 Jellyfin 12 (#109, #117, #122)

  • Two packages per release. x.y.z.0 is the Jellyfin 10.11 build (.NET 9) and x.y.z.1 the Jellyfin 12 build (.NET 10). The plugin catalog picks the right one for your server; upgrading the server from 10.11 to 12 offers the .1 build as a plugin update afterwards.
  • The Achievements entry moved to the avatar menu. Jellyfin 12's modern layout hides the old drawer and header, so the entry now sits right below Profile in the avatar menu and the equipped-badge strip sits in the toolbar beside the avatar. The legacy layouts (desktop-legacy, mobile-legacy, TV) keep the drawer entry.
  • Every call sends the Authorization header 12 requires. Jellyfin 12 switches legacy authorization off, so the old X-Emby-Token alone answers 401 — which made every earlier version's page dead on a 12 server. Both headers are sent now; 10.11 accepts either.
  • The Revamp admin page fits inside the MUI dashboard, links use #/ routes, and the tab is named Achievements while the page is open.

🎯 Targeted badges (#107)

  • Two new metrics point a badge at one thing: ContainerCompletionPercent (played items over total in one series, season, collection, playlist or album — 100 means you finished it) and ItemPlayCount (Jellyfin's own play count for a single movie, episode or track — 1 is "watched it", 3005 is the Childish Gambino badge).
  • A library picker in the admin page finds the target by name; the stored reference keeps the id and the name together, so the badge survives both a rename and a delete-and-re-add.
  • Retroactive on creation — anyone who already finished the target unlocks it immediately, no scan — and it moves on Jellyfin's played flag, so marking a season watched by hand counts.
  • Arbitrary groupings ("One Piece: Alabasta") go through a Jellyfin collection. See Targeted badges.

🎮 Game achievements (#115)

  • Games played through JellyEmu now earn achievements. JellyEmu reports each session to Jellyfin as a playback session; the plugin now measures those on their own terms (session length, floored and capped, both configurable) instead of dropping them as runtime-less books.
  • Nine built-in badges under a new Games category (sessions, distinct games, hours, platforms) and three custom-badge metrics (distinct games on a platform, distinct games by a developer, hours in one specific game via the picker). See Game achievements.

🪪 Everything you equip shows on your cards (#42)

  • Your custom title, badge frame, profile theme, profile border, animated background and avatar now all show on the shareable profile card (all three skins) and on the card that opens when someone clicks your name in the friends drawer — under the same privacy toggles as the equipped badges. Animated backgrounds play the actual video loop behind the card.
  • Two things that never worked on the achievements page itself are fixed on the way: profile borders never showed under the Revamp style, and badge frames applied only to one row of one tab — they now ring the showcase pills, the header strip and the drawer showcase.
  • "Pastel has un-hidden elements" (TsunamicFlame's report in #42) was real: every gradient theme let Jellyfin's own page show through the top of the achievements page. Fixed for all 14 themes.

🩺 Diagnostics for #97

  • Admin-page errors now name the request that failed (Request failed: 400 (GET users/…/summary)), and a failed reload is no longer reported as a failed reset or scan.
  • Reset answers 400 for a malformed id and 404 for an account Jellyfin doesn't know, instead of 200 for anything.
  • A watch-history scan that finds nothing says so and points at the account's library access, in all 8 languages.

🧱 Fixes

  • Toast position never applied under Revamp — all five positions rendered top-right (#116, @camarigor).
  • The Revamp stylesheet was cached under a stale token in three places, so stylesheet changes could reach browsers a day late.
  • ~25 Users/Me requests fired on the login page; the admin hero announced "v1.9.2 / ABI 10.11.0.0" forever; the shop's "Auto-unlock at N score" pill was clipped.

Big thanks to @camarigor for the Jellyfin 12 build and web-client work (#117, #122), targeted badges (#108), game achievements (#120), the shop cosmetics on the card (#119), the toast fix (#118) and the dependency round (#128); to @Lyxon1337 for the Jellyfin 12 report; to @unknownTGG for the targeted-badges request; and to @TsunamicFlame for #42, #115, #116 and the field testing behind them.



🚀 What's new in v2.3.1

A fast follow to 2.3.0, fixing the music feature it shipped and cleaning up the leaderboards. Drop-in upgrade from v2.3.0 / v2.2.x — no schema breakage.

  • Music actually counts (#93/#94, #95/#96). Tracks no longer inherit genres and tags from their album or artist, so a "50 disco tracks" badge stops counting dance and pop plays. And the watch-history scan now replays played music, so the artist discography badges from 2.3.0 can be rebuilt instead of only ever building from live playback. Thanks to @Daemon-Network for finding both within a day of release.
  • See your own rank. The admin and user-facing leaderboards now always show your position — highlighted if you land in the top ten, appended with your true rank if you rank lower (across all six category boards on the standalone page).
  • Deleted accounts cleaned up. They no longer appear on leaderboards as raw GUIDs or inflate the user count, the admin Stats and Leaderboard tabs refresh on open, and a new Prune deleted accounts admin button reclaims their storage.
  • The Blades shareable card skin is now Aurora, matching its redesign, localized across all eight languages.

🚀 What's new in v2.3.0 — Friends & Foundations

Achievement Badges gains a social layer — see how your friends are doing, and share your own card — on top of a run of reliability fixes, several of which matter to anyone on the published v2.2.0 build. Drop-in upgrade from v2.2.x / v2.1.x / v2.0.x — no schema breakage or manual migration.

👋 Friend profile cards (#76)

Hover a friend's name or avatar in the drawer for a summary card — rank tier, completion, score, best streak and equipped showcase. Click to pin a larger card that survives the pointer leaving, closes on Escape or an outside click, and is reachable by keyboard. It's backed by a public-summary endpoint that's privacy-gated to expose nothing the leaderboard doesn't: anyone opted out of being listed is equally invisible here.

🪪 Shareable card skins

Three server-rendered profile-card skins — Console (default), Metro, and Aurora Spine — each drawn with the card owner's own rank colour as the single accent. Every user picks their own skin in preferences; a card opened without an explicit style falls back to the owner's choice.

🎯 Completion tracking that finally runs

  • Library completion (#80, #79). The five library-completion badges are now computed during the watch-history scan, so they can unlock through normal use instead of sitting at zero on every install.
  • Artist discography completion (#81, #24). Played tracks over total tracks per artist — a badge for completing any artist, plus a parameterised metric for a specific one.

📡 Tracearr history crediting (#77 / #84 / #85)

Credit genuine first watches the library scan can't prove on its own — media you deleted, and true rewatch counts — from Tracearr's public v2 API. A standalone sync button is made idempotent by a ledger, so pressing it twice credits nothing the second time. Configured admin-side with a URL and token; nothing is required on the Tracearr side. See Tracearr integration.

🔔 Notifications and layout

  • Per-user toast position: any of the four corners, top-center or bottom-center, applied live, with a server default the admin sets for everyone who has not chosen (#136).
  • Unlock toasts moved off the subtitle line, and the floating friends button hides while the video player is on screen.
  • Admin-set default UI style, optionally locked (#43) — start users on Classic or Revamp, and optionally make it the only choice so the page matches your Jellyfin theme; a user's own pick is remembered and returns if the lock is lifted.

🧱 Reliability

  • Watch-time carry survives a session break, a restart, and a media-file replacement or quality upgrade (keyed by media identity, #89), with a configurable retention window (#87). The scan clears the carry of exactly what it credits (#91/#92), so a later partial rewatch can't reach the completion gate on minutes already counted.
  • Counter resilience — rebuilt counters are floored and dated snapshots kept so a scan never loses progress; the score bank is floored on rebuild so deleted media can't zero a balance; backfill is serialised per user; and failed loads surface instead of being drawn as empty or loading.

🩹 Fixes for anyone on v2.2.0

  • Empty twin profiles could swallow unlocked badges (#59/#60). GetOrCreateProfile now normalises the user id first, so the friends and messaging paths can't create an empty profile beside the live one that later folds over it. Opening the friends panel was enough to trigger it.
  • The injected UI never mounted under gzip (#46). The client bootstrap now injects into compressed responses instead of skipping them — every browser asks for gzip, so on v2.2.0 the sidebar, widget, item ribbon and toasts silently failed to load. Also behind the #37 and #36 reports.

🌍 Localization

Every new interface string is translated across all 8 UI languages, and the admin user-picker strings were completed in the seven non-English locales for full key parity.

Big thanks to @camarigor for the friend profiles, Tracearr integration, the library and artist completion wiring, the watch-carry work, and the profile data-loss + gzip injection fixes.


🚀 What's new in v2.2.0 — Your Screen, Your Rules

This release makes Achievement Badges fit each user's Jellyfin layout and notification preferences while keeping the stock Achievements page available at all times. Drop-in upgrade from v2.1.x / v2.0.x — no schema breakage or manual migration.

  • Optional Custom Tabs and Plugin Pages hosts (#37). Admins can opt into either integration independently. Both reuse the same Achievements surface instead of maintaining duplicate pages, and compatibility failures stay isolated so they cannot prevent Jellyfin from starting.
  • Per-user navigation controls (#37). Each user can independently show or hide the Custom Tabs entry, Plugin Pages entry, and header trophy from Achievement settings or Jellyfin's native user settings. Hiding navigation never disables tracking, notifications, or the stock page.
  • Grouped or individual unlock bursts (#38). Simultaneous unlocks can collapse into one summary with a combined score, or play one at a time using the existing queue.
  • Device-scoped unlock notifications (#38). Users can keep the default delivery to every signed-in client or restrict a toast to the Jellyfin device that earned it. The server validates the originating device instead of trusting the browser alone.
  • Unlock toasts now lead somewhere useful. Click a real single unlock (or focus it and press Enter/Space) to open My Badges, clear filters, scroll to the exact badge, and briefly highlight it. Grouped summaries open Recently unlocked. Synthetic admin previews remain deliberately non-navigating and never write achievement data.
  • Better notification testing. A new Test 10 unlocks admin control exercises the saved grouping preference without creating unlocks or modifying a profile.
  • Fresh client assets on every build. Script cache keys now include the compiled module ID, preventing browsers from retaining an older same-version UI during testing or deployment.
  • Full localization. Every new option, help message, navigation label, and toast action is translated across all 8 UI languages (English, French, Spanish, German, Italian, Portuguese, Chinese, Japanese).

🚀 What's new in v2.1.3

A bug-fix patch resolving the outstanding reports from #24 and #36, plus a full builder-localization pass. Drop-in upgrade from v2.1.x / v2.0.x — no schema breakage; a one-time, ID-preserving custom-badge migration runs automatically.

  • Genre-filtered music badges now actually filter by genre (#24). Music plays and listening time are tracked per genre (case-insensitive), so a "play 50 disco tracks" badge no longer ticks up when you listen to metal. Three new builder metrics — Music plays of a genre, Music hours of a genre, Items watched of a genre — reveal a genre field when picked.
  • Custom badges fully delete (#24). The visual builder, the badge list, and delete now share one store (they were previously split, so builder-made badges couldn't be removed), and deleting a badge purges its earned + equipped copies from every user profile. Pre-existing badges are migrated on first startup with their IDs preserved.
  • Header UI survives another header-injecting plugin (#36). When a second plugin (e.g. Ratings) stripped the badge scripts from index.html, the plugin used to think it was still patched and never re-injected. The guard now keys on the actual script tag, so a stripped page is always repaired.
  • Badge category labels fixed (#24). Categories like Studio Specialist and Books no longer show as raw category.* keys, and every built-in category is now translated.
  • Full builder localization. The badge-builder metric list, quest builder, power-up + cosmetic pickers, and status messages are now translated across all 8 languages (en/fr/es/de/it/pt/zh/ja).

🚀 What's new in v2.1.0 — Open Library

Achievements expand beyond film & TV, and admins get real badge-authoring power. Drop-in upgrade from v2.0.x — no schema breakage, no data migration.

🎵 Music achievements

18 built-in music badges driven by real listening — total plays, listening hours, and unique albums / artists / genres / decades. Audio items now flow through the same playback-credit pipeline (80% real-listen gate) as films and episodes.

📚 Book achievements

8 book badges — books completed, audiobook listening hours, and book series completed. An audiobook-counting policy lets admins choose whether audiobook plays count toward Books only (default), Music only, or Both.

How ebook completion is detected (important): Jellyfin has no "finished reading an ebook" signal — unlike audiobooks and music, which stream through a session and track listening time automatically, an ebook is text with no runtime. So an ebook counts once it's marked Played (the ✓ / "Mark as watched" toggle on the item). Simply reaching the last page in Jellyfin's reader does not auto-mark it, so book badges won't move until the book is marked played. This is a Jellyfin limitation, not something the plugin can detect on its own.

🛠️ Custom badge builder

Define your own badges with compound AND/OR criteria across any metric — film, TV, music, books and more. Simple badges via the admin form; compound criteria, icons, and import/export via the API. Full guide + copy-paste templates: Custom badges.

🌐 JS Injector fallback (issue #26)

On bare-metal Linux where /usr/share/jellyfin/web isn't writable, the on-disk UI injection used to fail silently. The plugin now detects the JavaScript Injector plugin and surfaces the exact script URLs to paste in. Details: Troubleshooting.

🇯🇵 Anime detection fix (issue #25)

Anime is now detected from Genres and Tags, read from both the item and its parent Series, with admin-configurable libraries / genres / tags — fixing anime badges that never fired when the classification lived on the Series or in Tags.

🗓️ Daily-badge backfill fix + audit tool (issue #27)

Time-windowed badges (daily / weekly / monthly) are now skipped during the initial history scan, so they no longer false-unlock from lifetime totals. A new admin audit + cleanup tool finds and clears badges wrongly awarded by pre-v2.1.0 backfills (re-earnable organically afterward).

🌍 Globe language picker + full admin localization

A globe dropdown on the admin page switches the UI language live and persists across reloads, and the entire admin page — including the Integrity and testing-tools sections — is now localized across all 8 languages.


❤ Support the project

Achievement Badges is built and maintained in my spare time. If it's useful to you and you'd like to support ongoing development, any of these means a lot:

Not expected, just appreciated. Contributions — issues, PRs, translation fixes — are equally valuable.


🙏 Credits & thanks

  • @frenchyx24 — full French translation of all 171 built-in badges (hand-translated titles + descriptions, merged in v1.7.2 from issue #5). Also filed the original multi-language feature request and the deactivate-equipped-badges / quest-customization / Xbox-logo bug reports that shaped v1.6.1 → v1.7.x. Merci beaucoup !
  • @camarigor — the v2.4.0 Jellyfin 12 build and web-client rework (#117/#122), targeted badges (#108), JellyEmu game achievements (#120), the shop cosmetics on the shareable card (#119) and the toast-position fix (#118); before that the v2.3.0 friend profile cards and public-summary endpoint (#76), Tracearr history integration (#77/#84/#85), library + artist completion wiring (#80/#81), the watch-time carry reliability work (#87/#89/#91/#92), the music fixes in v2.3.1 (#94/#96), and the profile data-loss (#59/#60) and gzip-injection (#46) fixes.
  • @Daemon-Network — the original Music & Books request (#24), and thorough real-music-library testing of 2.3.0 that surfaced the three music bugs fixed in v2.3.1.
  • @TsunamicFlame — the Custom Tabs / Plugin Pages request (#37), the friend profile cards and shop-cosmetics-on-the-card request (#42), the JellyEmu game achievements request (#115), the toast-position report (#116), and the field testing behind all of them.
  • @unknownTGG — the targeted-badges request (#107) that became ContainerCompletionPercent and ItemPlayCount.
  • @Lyxon1337 — the Jellyfin 12 report (#109).
  • xdnewlun1 (Techno Cricket, CCDC) — responsible disclosure of 12 security findings in v1.6.0 including the critical IDOR that led to the UserOwnershipFilter.
  • Uenify — the Xbox-style toast animation (circle grow, banner sweep, shimmer, text slide) is a port of his CodePen to vanilla JS + per-rarity colour palettes.
  • Translations for es / de / it / pt / zh-CN / ja started from an automated pass — native-speaker polish welcomed via PR.

📜 License

This project is released under the MIT License — one of the most permissive open-source licenses in common use.

Summary:

You canYou mustYou cannot
Use it on any Jellyfin server, personal or commercialKeep the copyright + license notice in any redistributionHold the authors liable if something breaks
Fork and modify however you wantClaim the authors endorse your fork
Redistribute modified or unmodified copies
Bundle it with proprietary software
Include it in a paid product

If you just want to run the plugin, none of this affects you — install it and enjoy.

Contributions

Pull requests are welcome. By submitting a contribution you agree that your changes will be licensed under the same MIT terms. Keep contributions focused (one feature or fix per PR) and include a short description of what changed and why in the PR body.

Third-party attributions

  • Jellyfin (GPL-2.0) — this plugin is a third-party extension for Jellyfin and is not affiliated with or endorsed by the Jellyfin project. At build time it references Jellyfin.Controller and Jellyfin.Model NuGet packages, which remain under their own GPL-2.0 license.
  • Xbox-style unlock toast — the animation style is inspired by Adam Cosman's Xbox One Achievement codepen and was reimplemented from scratch. No original assets from that codepen ship with this plugin.
  • Material Icons (Apache 2.0) — icon glyphs referenced in the UI are provided by Jellyfin's own web client and are licensed by Google.
  • Background video assets (v2.0) — sourced from public-domain / royalty-free stock and transcoded to H.264 1080p loops for embedding. No external CDN; all assets ship inside the plugin DLL.

See LICENSE for the full license text and third-party notices.


⭐ If you use this plugin, consider starring the repository.

achievements
csharp
dotnet
gamification
jellyfin
jellyfin-plugin
jellyfin-web
media-server
self-hosted

Languages

HTML

49.4%

C#

34.9%

JavaScript

9.0%

CSS

6.7%