8.7 KiB
Jellystat in Magent Beta
Magent's My Stats page (/insights) reads personal viewing history from an existing Jellystat instance. Jellystat owns playback collection, history and retention. Magent does not install Jellystat, collect sessions or keep a second playback database.
Setup
- Run Jellystat and connect it to the same Jellyfin server Magent uses. Let its initial sync finish.
- Create an API key in Jellystat's settings.
- In Magent, open Configuration → Jellystat, enter its internal URL and API key, save, and test the connection. Include any reverse-proxy base path in the URL.
- Sign in using Jellyfin. Existing Jellyfin accounts can also be linked by Configuration → Jellyfin → Import Jellyfin users. First use of My Stats resolves an existing Jellyfin account against Jellyfin's user directory using its exact username.
Alternatively, set these backend environment variables:
JELLYSTAT_URL=http://jellystat:3000
JELLYSTAT_API_KEY=your-jellystat-api-key
JELLYSTAT_BASE_URL is also accepted. Docker deployments already load the backend environment through .env. These are server settings; no NEXT_PUBLIC_ variables or browser credentials are needed. Saved Configuration values override environment values.
What users see
- Past 7, 30, 90 or 365 days of watch time, distinct movies and episodes played, and total plays.
- Watch-time chart, current/longest streak within the chosen period, active days, favourite titles, players and streaming methods.
- Latest 20 plays in the chosen period and personal request totals from Magent's Seerr cache.
- Clear setup, account-link, no-history and temporary-unavailability states.
The page is personal for admins as well as ordinary users. There is no arbitrary user-ID parameter or server-wide history endpoint in this version. Monthly reports are available from My Stats; admin reporting and newsletters can build on this integration later.
Data semantics and boundaries
History comes from Jellystat's POST /api/getUserHistory, with the backend's linked Jellyfin ID in userid, and a fixed date filter. GET /api/getLibraries supplies movie-library classification and the connection test. Authentication uses the x-api-token header. The adapter follows the upstream API routes and playback model; the installed instance exposes its API at /swagger.
- Playback duration is in seconds and displayed as minutes. Positive-duration history entries count as plays, including unfinished watches. Repeat plays add time without inflating distinct movie/episode counts.
- Episodes are identified by
EpisodeId. Movies are identified by their movie library. Mixed libraries or deleted library metadata may leave an item classified as other media; that time still contributes to totals. - Ranges cover a rolling number of days. Charts and streaks use UTC and Jellystat's
ActivityDateInserted, so the first/last chart days can be partial. A streak day requires at least one minute. Streaks are bounded by the selected period. Long charts group days for readability. - Requests use their creation date and the authenticated account's canonical Seerr ID. Exact usernames are only used for legacy requests without an owner ID; conflicting IDs never fall back to a name.
- Pages are fetched at 200 rows per request, up to 50 pages, with a 30-second total timeout. Excess history asks the user to choose a shorter period; it is never presented as a complete partial total.
- A normalized, per-identity response is cached in memory for up to 60 seconds, with a 128-entry bound. The cache is separated by Jellystat URL/key, Jellyfin URL, user ID and period. HTTP responses are marked
no-store. - Browser output excludes raw Jellystat responses, usernames from playback data, user/device IDs, IP addresses, tokens and media stream details. Unexpected account IDs in upstream history are rejected.
- The only new database table is the stable Magent-to-Jellyfin identity mapping. It is scoped to the configured Jellyfin URL and does not automatically transfer ownership after account replacement. A changed Jellyfin URL needs identity resolution again.
Validation
Backend coverage is in backend/tests/test_insights.py and backend/tests/test_insights_media.py. It checks API contracts, pagination, ownership, credential masking, cache separation, time units, dates, repeat plays, media classification, artwork authorization, transcoding attribution and empty/error states.
After building the frontend, scripts/review_insights_ui.cjs checks the page and configuration using fixture-only requests. Set REVIEW_BASE, REVIEW_PLAYWRIGHT, and optionally REVIEW_DIR to save screenshots outside the repository. Live Jellystat verification requires configuring the actual instance.
Monthly reports
Open My Stats → Monthly reports (/insights/reports). The default is the most recent complete calendar month. The month picker covers the current month and the previous 23 months. Reports include viewing and request totals, changes against the preceding month, daily viewing, active days, longest streak, favourite titles, players, transcoding and the latest 20 plays in that month. The chart and streaming cards are shared with the Stats overview.
- Completed months compare full UTC calendar months, even when their lengths differ. The current month compares the same elapsed time in the previous month, capped at that month's end when it is shorter. The page identifies reports that are still in progress.
- Periods include their start and exclude their end. Midnight activity belongs to exactly one month; leap years and December/January boundaries use calendar arithmetic. Missing prior activity has no percentage increase, rather than an infinite or invented percentage.
- Reports use the same stored Jellyfin identity resolution as My Stats, including administrator-confirmed links. They accept only a month, never a browser-supplied user ID or server scope. Requests use the authenticated account's Seerr ID under the existing ownership rules.
GET /insights/reports/monthlyreturns the report;GET /insights/reports/monthly.csvdownloads its summary, comparisons, daily totals, leading titles, players, streaming methods, transcoding and request counts. Both require authentication and returnCache-Control: no-store. CSV text cells are escaped and formula-like values are prefixed to prevent spreadsheet execution. Exports omit account IDs, artwork tokens and upstream credentials.- Reports are generated on demand from retained Jellystat history and Magent's available request cache. They are not immutable historical snapshots. Request statuses are current, and historical totals can change with retention or library metadata. No report database, email delivery or scheduler is added.
- One bounded history read covers the selected and comparison months. Playback summaries are cached for 60 seconds in at most 128 entries, separated by identity, connection and month. Request totals are refreshed independently. Upstream errors or history limits fail the report without presenting a partial result.
backend/tests/test_monthly_reports.py covers calendar boundaries, matched partial periods, ownership, cache isolation, comparisons and safe CSV exports. scripts/review_monthly_reports_ui.cjs checks the report controls and layouts with fixtures only.
Artwork and transcoding
Recently watched uses Jellystat's NowPlayingItemId, which identifies the movie or series, to load a Jellyfin primary poster. Magent proxies the image through an authenticated endpoint using a short-lived signature bound to the viewer, item and Jellyfin connection. Jellyfin credentials stay on the backend. Missing or deleted artwork falls back to a media tile. Thumbnail responses are privately cached.
How you streamed includes audio transcoding minutes and hardware-assisted video transcoding minutes. These are playback durations attributed to the recorded transcode flags, not GPU busy time or encoder runtime. Audio and video durations can overlap. Video must actually be transcoded for hardware-assisted minutes to count; a hardware label on an audio-only conversion does not count as GPU video work. Direct-play records ignore residual transcoding metadata, and missing details remain unknown.
Jellyfin exposes separate video/audio passthrough flags and hardware type, with hardware type names. The existing Jellystat history does not contain GPU utilization or GPU busy-time samples, so Magent does not calculate those figures.