github ZL154/AchievementBadges_for_Jellyfin v1.9.8

latest releases: v2.3.1, v2.3.0, v2.2.0...
3 months ago

v1.9.8

Integrity release. No new features for end-users; the plugin's behaviour for legitimate watchers is unchanged. What this release closes is a class of playback-credit exploits that let a user game badges without actually watching.

The bug

PlaybackCompletionTracker trusted Jellyfin's PlayedToCompletion flag and used a position/runtime ratio to decide whether to credit a play. That meant three things were enough to mark an item as "watched" for badge purposes:

  1. Clicking "Mark as played" on a movie's menu (PlayedToCompletion fires true, we credited at 100%).
  2. Seeking to the end of the timeline and stopping (position/runtime hit 90%+, we credited).
  3. Spam play/stop clicks during which Jellyfin's player asynchronously sets PlayedToCompletion.

None of these required actually watching the item.

The fix

The tracker now maintains per-session accumulated play ticks. On every PlaybackProgress event it adds the position delta to a per-session counter — but only if the delta is between 0 and ~15 seconds. Seeks (large positive jumps) and rewinds (negative deltas) don't accumulate. On PlaybackStopped, the completion percent is computed from those accumulated ticks instead of the raw position/runtime ratio, then put through the existing 80% gate. PlayedToCompletion is no longer trusted.

Effect:

Scenario v1.9.7 v1.9.8
Mark-as-played, no playback credited rejected
Start playback, seek to 95%, stop credited rejected
Spam play+stop on a fresh item credited rejected
Real watch >=80% of runtime credited credited
2x playback speed, real watch credited credited
Pause / resume mid-session credited credited

A reference log line surfaces when the discrepancy fires - admins debugging legitimate "why didn't this credit?" reports can grep for the message and see exactly what the tick math reported.

Defense in depth

The credit-gate fix is the primary one. v1.9.8 also adds three layers of defense around it:

  • 60s minimum runtime - items shorter than 60 seconds (Projectionist prerolls, Intros plugins, bumpers) get rejected up front. They live in the Movie/Episode library type so without this filter every preroll play silently credited +1 movie.
  • Daily credit cap - soft cap on items credited per user per UTC day. Default 200 (real users never hit it; spammers do). Configurable in admin UI. Audit-logged as rate_cap_blocked the first time per day each user hits the cap.
  • Suspicious-rate audit flag - if a user crosses 30 credits in a rolling 60-minute window (configurable), an audit-log entry with type suspicious_rate fires. Doesn't block - just makes the pattern visible. Throttled to one entry per user per hour.

Both controls are on by default. Both are configurable in the admin UI.

Admin tools

A new "Integrity & anti-abuse" section appears in both admin surfaces (Classic plugin config page in the Jellyfin dashboard, and the in-app Revamp admin panel inside the achievements page).

  • Settings - cap + flag toggles and their thresholds.
  • Test injection - pick a user, choose a count (default 50), choose movies vs episodes, hit Run. Drives N synthetic playbacks through the real path so cap+flag fire end-to-end. Response reports Requested, Credited, Blocked, and the last block reason. Use on a throwaway test user to verify caps work before relying on them.
  • Manual badge revoke - pick a user, enter a badge id, confirm. Un-awards that specific badge, un-equips it if it was in the showcase. Audit-logged as admin_revoke_badge.

Other behaviour changes

  • PlaybackCompletionService.RecordCompletion now writes per-day credit counters and a rolling-hour timestamp queue into playbackstate.json. Old state files load cleanly with defaulted fields; new state is pruned to 30 days of date entries.
  • PluginConfiguration gains EnableDailyCreditCap, DailyCreditCap, EnableSuspiciousActivityFlag, SuspiciousRatePerHour.
  • New REST endpoints: GET/POST /admin/integrity-config, POST /admin/users/{userId}/test/inject-playbacks?count=N&isMovie=true|false, DELETE /admin/users/{userId}/badges/{badgeId}.

Upgrade

Drop-in upgrade from v1.9.7. No config migration needed. Existing badge unlocks are NOT retroactively re-evaluated against the new gate - only credits going forward are affected. If you want to manually revoke a badge a user gamed previously, use the new revoke tool.

Package checksums

  • MD5: see Jellyfin.Plugin.AchievementBadges_1.9.8.0.md5
  • SHA256: see Jellyfin.Plugin.AchievementBadges_1.9.8.0.sha256

Sigstore-signed + SLSA build-provenance attested. Verify with cosign verify-blob or gh attestation verify.

Don't miss a new AchievementBadges_for_Jellyfin release

NewReleases is sending notifications on new releases.