github ZL154/AchievementBadges_for_Jellyfin v2.4.1

3 hours ago

v2.4.1 - What 2.4.0 got wrong, and a cap you can raise

Everything reported against 2.4.0, including two 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.

Fixed - the Jellyfin 12 package reported a version Jellyfin could not act on (#140)

Reported by @Roboatlas21, who diagnosed it correctly from the logs.

Releases ship two packages from one source tree, x.y.z.0 for Jellyfin 10.11 and x.y.z.1 for Jellyfin 12, and the manifest entry a server installs carries that version. The assembly inside both was stamped x.y.z.0. Jellyfin then read two different numbers for one install: the dashboard shows the assembly's version, while GetPlugin(id, version), which the uninstall route and the plugin image endpoint both resolve through, matches the manifest's.

So on Jellyfin 12 the plugin installed as 2.4.0.1 and displayed as 2.4.0.0, its image 404'd on the version the dashboard asked for, and uninstalling it failed outright with "An error occurred while uninstalling the plugin". AssemblyVersion and FileVersion are now stamped per target framework, so each package reports the version it was shipped as.

If you are stuck on 2.4.0.1 and cannot uninstall it: updating to 2.4.1.1 through the catalogue is enough. The update installs next to the old version, and the restart that loads it deletes the 2.4.0.1 folder.

Fixed - the page worked in a private window and nowhere else (#141)

Also @Roboatlas21. Same install, and the cause is unrelated.

A browser that cached Jellyfin's page before the plugin was installed revalidates it with If-None-Match. Jellyfin's static file handler answers 304 Not Modified from the file on disk, which means there is no page body for the plugin's injection middleware to add its scripts to, and the browser goes on using the copy it already has: one with no plugin in it. A private window has no cache, gets a full page, and works. That is the whole difference.

Nothing about the file on disk changes to break the cycle, and on his server nothing could: the web directory is read-only there, so the plugin's on-disk patch fails and the file keeps its original validators forever.

The middleware now drops those validators from the request while the on-disk patch has not taken, so the page is always rebuilt and injected into, and it strips ETag and Last-Modified from a page it has injected into, since those describe the file rather than what was actually sent. Installs whose on-disk patch works keep their conditional requests, because there the file itself carries the scripts.

Nobody needs to clear their browsing data. A normal reload after updating is enough.

Verified on a Jellyfin 12.1.0 server with a read-only web directory, the reporter's exact failure mode: on 2.4.0 a returning browser gets 304 and no scripts, on this build it gets the page with them.

Changed - a targeted badge cap you can raise (#129)

Requested by @Tschiyo in #129: an automated setup that mints one completion badge per title of a curated top list ran into MaxTargetedBadgeTargets, the ceiling of 50 distinct targets that 2.4.0 introduced with the feature, and the targets past it were only named in the log.

  • The per-play check no longer grows with the number of targets. The scoped recompute used to ask the library, once per target, whether the played item sat under it. It now reads the played item's ancestor ids once and tests each series, season or folder target against that set. Collections and playlists have no such index, so their member ids are cached per target: built on first use, dropped when Jellyfin reports the container changed (adding or removing a member updates it), and rebuilt after ten minutes regardless. The library query stays as a fallback for the rare item whose ancestors cannot be read.
  • The cap is a setting on the config page. Under Custom badges, "Targeted badge cap" takes 1 to 1000 (default unchanged at 50), backed by GET and POST /Plugins/AchievementBadges/custom-badges/targets. Values outside the range are clamped wherever the cap is read, so a hand-edited configuration file cannot turn a play into an unbounded sweep or into nothing.
  • What is past the cap is on the page. The same section shows how many distinct targets the enabled badges reference against the applied cap, and lists the names that are not computed. The list refreshes whenever the badge list does. Each cap change is recorded in the audit log.

Upgrade notes: no data migration. Existing servers keep the cap at 50 until an admin raises it; after raising it, saving any badge runs the full pass so the previously ignored targets get their progress computed for everyone.

Fixed - the Revamp style reached the rest of Jellyfin (#133)

Reported by @clarjon1, fixed by @camarigor in #134.

With UI: Revamp selected, this plugin restyled Jellyfin's own controls and every other plugin's settings page: checkboxes went invisible, text inputs lost their border and height so the floating label collapsed into the value, and text selection changed colour everywhere.

Twelve rules in the Revamp stylesheet named generic elements (input, textarea, select, ::selection, :focus-visible) keyed only on the style attribute, and that attribute is stamped on <body> so the widgets the plugin mounts outside its own pages can style themselves. The invisible checkbox was two faults stacked: the rule removes the native control with appearance: none, then takes its border and background from design tokens that are only declared on the plugin's own two surfaces, leaving a transparent 20 by 20 box covering the real control.

Those twelve rules are now anchored to the two surfaces that declare the tokens. A test refuses any new rule of that shape.

Fixed - a Custom Tab that rendered nothing (#131)

Reported by @Borededdy, fixed by @camarigor in #132.

With the Abyss theme the Achievements tab in the Home tab bar showed a blank page. The button and its content panel come from two different places: Custom Tabs creates the button in the browser, and injects the panel server-side by matching one exact string, whitespace included, inside Jellyfin's home page file. Abyss ships its own copy of that file with the same markup pretty printed across three lines, the match fails, and no panel is ever created — while the button appears regardless.

The plugin now builds the panel itself when its own tab button exists without one, identified by the plugin's marker in the Custom Tabs configuration rather than by the tab's title, so renaming the tab does not break it. Every other Custom Tabs tab is still empty on such a server; that is the same injection failing, and it is reported upstream.

Added - a top-center toast placement and a server default (#136)

Requested by @Verdancy-Rin, built by @camarigor in #137.

  • Top-center joins the five existing placements, laid out the way bottom-center is and under the header like the other top ones.
  • The admin can set the default placement for everyone who has not picked one, under the UI style default. A user's own choice still wins, and their picker now offers "Server default" first, labelled with the admin's current choice.
  • A profile saved by an earlier version stored top-right whether or not the user chose it, so a stored top-right could not be told from silence. That is read as "no choice" once, behind a migration flag, and a top-right picked afterwards stays a choice.
  • The preferences endpoint now rejects a placement it does not know instead of storing whatever string it was sent.

Two bugs found on the way there, both in the admin feature-config save, both fixed:

  • The default UI style from #43 was never stored. The admin page posted it, nothing on the server bound it, and the page still said "saved". It always reloaded as Classic, unforced.
  • Every main save switched the page integrations off. Enabling the Custom Tabs host and then editing the welcome message turned the host back off without a word.

Added - keep accounts hidden from the login screen out of other users' views (#138)

Requested by @Digital-Yeti, built by @camarigor in #139. Off by default.

When on, an account Jellyfin hides from the login screen is visible only to itself, to administrators, to other hidden accounts, and to the accounts it is already mutual friends with. It applies to the leaderboards, the activity feed, the friend search, the compare picker, simple-mode friends, and the public profile endpoints, where a hidden account now reads exactly like an id that does not exist, so none of them can be used to probe for one.

Worth knowing before you turn it on: Jellyfin ticks "hide from login screen" for accounts it creates, so on a server that keeps the default every account is hidden and the option changes nothing.

Fixed - the plugin broke the server's own API documentation

Found while testing this release against Jellyfin 12.1, not reported by anyone, and present in every version that shipped Models.MediaType - so 2.1.0 onwards, on 10.11 and 12 alike.

GET /api-docs/openapi.json answered 500 for the whole server. Jellyfin builds one OpenAPI document from itself and every installed plugin, and Swashbuckle keys each schema in it by the bare type name. Our media-type enum was called MediaType, which is also the name of one of Jellyfin's own enums, and the second registration of a name does not lose a race - it throws, and takes the entire document with it. Swagger UI, the API browser and anything that generates a client from the spec were all down, on a plugin that started cleanly and logged nothing about it.

The enum is now BadgeMediaType. Its numbers are unchanged and the property carrying it is still Media, so stored profiles and badge definitions are unaffected and 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 - previously the failure meant none of them were documented at all.

A test now walks this plugin's public types against the Jellyfin assemblies it builds against and fails on any shared name, because nothing else could catch this: the break was in the host's document, not in ours.

Dependencies

  • Microsoft.NET.Test.Sdk 18.10.1 (#135). Dependabot regenerated the test project's lock file from a single target framework, which quietly dropped the net10.0 section, the Jellyfin 12 half of the test matrix; it is restored, and locked-mode restore passes on all three projects again.
  • The same regression returned on #145, that time dropping net10.0 from both projects' lock files. Locked mode tolerates a lock file carrying fewer frameworks than its project, so every check stayed green and nothing failed until a locked restore was run by hand. A test now reads both lock files and fails if either stops locking both frameworks.

Don't miss a new AchievementBadges_for_Jellyfin release

NewReleases is sending notifications on new releases.