Contributing to finstats¶
Thanks for wanting to help. finstats is a small project with a narrow idea of itself, and this page tells you how to report a problem, suggest something, or send a change that is likely to be merged. Everyone taking part is expected to follow the code of conduct.
Ways to help¶
You do not have to write Rust. Useful contributions, roughly in order of how often they are needed:
- A good bug report. Especially from setups unlike the maintainer's: another Jellyfin version, ARM hardware, a reverse proxy, rootless Docker, a NAS, a big library, unusual clients.
- Telling us where the docs lost you. If these pages or the README made you guess, that is a bug in them: open an issue on the docs.
- A fix for something in the issue list.
- A feature, after talking about it first (see below).
- Answering a question in Discussions, or showing how you run it. Other people's setups are the best documentation.
A question about installing or running finstats belongs in Discussions → Q&A, not in an issue.
What finstats is, and what it will not become¶
Knowing this saves you from building something that cannot be merged.
- Read-only towards Jellyfin. finstats never changes anything on your server and never starts a scan. The one write it ever makes is creating its own API key during setup.
- Light. One binary, a bundled SQLite database, under 100 MB of memory. No database server, no queue, no cache service. A change that needs a second container will not be merged.
- Private. No telemetry, no accounts, nothing loaded from other hosts. By default the only request it makes to anything but your Jellyfin is an anonymous "what is my IP" lookup, made once and switchable off; the one other, downloading a geolocation database for the Security map, stays off until the owner asks for it, and addresses are always looked up locally. Notifications are the one thing finstats sends rather than reads, and only to destinations the owner enters, only the events ticked for each, and without addresses unless that destination asked for them. Settings → System → Outbound connections lists every destination, so a new one cannot be added quietly: it shows up there. The privacy page, the README and the security model state these things as promises; a change that would make one of those sentences untrue has to change the sentence too, and will be looked at very hard.
- No front-end build step. The web UI is plain ES modules with no dependencies. Please do not add a framework, a bundler or a CDN.
- Few dependencies. Every crate is something to audit and keep alive. Adding one needs a reason, and a
GPL-3.0-compatible licence. It also needs its licence recorded: run
python3 tools/make-third-party.py(aftercargo fetch) and commit the updatedTHIRD-PARTY.jsonwith the change.cargo testfails while that file does not cover every package inCargo.lock, and the app shows the result under Settings → System → Licences. - Personal where it matters. The recap is one person's year; the server's year (for administrators) ranks and names nobody, and no card ever names anybody else. Permissions are enforced on the server, never only in the UI.
Reporting a bug¶
Open a bug report. The form asks for what is needed; the two things that matter most:
- The finstats version (bottom right of every page, or the Patch notes tab) and how you run it (which image tag, or from source).
- The log around the moment it went wrong:
docker logs finstats. For more detail, start it with-e RUST_LOG=finstats=debug.
Look at what you paste before you post it. Logs and screenshots contain user names, titles, device names
and IP addresses of the people on your server. Blur or replace them. Never attach finstats.db, a
finstats backup or a Jellystat export: each is a complete viewing history, and the database also contains
your Jellyfin API key. If a maintainer needs data to reproduce something, they will ask for the smallest
piece that shows it.
Suggesting a feature¶
Open a feature request and describe the question you could not answer ("which of my users still use the old Android app?") rather than the screen you imagine. There is often a smaller way to answer it, sometimes with data finstats already has.
Not sure yet whether it is a feature? Think out loud in Discussions → Ideas first. For anything bigger than a bug fix, please talk before you build. It is no fun to review, or to write, a large pull request that does not fit.
Reporting a security problem¶
Please do not open a public issue for a vulnerability. See reporting a vulnerability for how to report it privately.
Your first change¶
# fork on GitHub, then:
git clone https://github.com/<you>/finstats.git && cd finstats
git config core.hooksPath .githooks # refuses databases, backups and secrets; do this once per clone
git switch -c fix/what-you-are-fixing
FINSTATS_DATA_DIR=./data FINSTATS_BIND=127.0.0.1:8088 cargo run
Open http://127.0.0.1:8088 and point it at a Jellyfin server. You need a current stable Rust (edition 2024,
so 1.85 or newer); SQLite is bundled; Node.js is only used for a syntax check. There is nothing to
npm install.
With cargo run, changes to the web UI (web/) need only a browser refresh: debug builds read it from disk.
Rust changes need a restart. Release builds and the Docker image compile the UI and CHANGELOG.md in, so
they need a rebuild. To start over, stop finstats and delete the data directory. RUST_LOG=finstats=debug
shows what the collector and the syncs are doing.
CLAUDE.md explains how the code is organised and why; the HTTP API is the
HTTP contract.
A local Docker setup¶
finstats only reads from Jellyfin, so developing against your real server is safe. A throwaway one is still nicer: you can create users, break things and reset it without anyone noticing. This runs your working tree as an image next to a throwaway Jellyfin, on a private network, with its own data folder. Nothing here touches a production container or its data. Compose is not needed.
# 1. a network, so the containers find each other by name
docker network create finstats-dev
# 2. a throwaway Jellyfin (two or three short media files are plenty)
docker run -d --name jellyfin-dev --network finstats-dev -p 8097:8096 \
-v jellyfin-dev-config:/config -v jellyfin-dev-cache:/cache \
-v /path/to/a/few/media/files:/media:ro \
jellyfin/jellyfin
# 3. your working tree as an image
docker build -t finstats:dev .
# 4. run it: another port, another data folder, debug logging
docker run -d --name finstats-dev --network finstats-dev -p 8089:8080 \
-e TZ=Europe/London -e RUST_LOG=finstats=debug \
-v "$PWD/data-dev:/data" \
finstats:dev
- Open http://localhost:8097 and click through Jellyfin's own first-run wizard: create an administrator,
add
/mediaas a library. - Open http://localhost:8089. In finstats' setup the Jellyfin address is
http://jellyfin-dev:8096: the container's name and Jellyfin's internal port, notlocalhostand not 8097. Inside a container,localhostis the container itself; this is the most common way to get "Could not connect". - Play something in the Jellyfin web client. It appears on the finstats dashboard within five seconds. Pause, skip and switch subtitles to get a timeline.
After a change: docker build -t finstats:dev . && docker rm -f finstats-dev, then step 4 again, and
docker logs -f finstats-dev. Dependencies are cached in a layer of their own, so a rebuild compiles only
finstats unless you touched Cargo.toml or Cargo.lock.
Things that trip people up:
- Jellyfin accepts connections a few seconds before it is ready. "Did not answer like a Jellyfin server" right after starting it means: wait a moment.
data-dev/is created by Docker as root; the container takes it over on start and then runs as an ordinary user. Start it with-e PUID=$(id -u) -e PGID=$(id -g)if you want the files to be yours. It is not in.gitignore: add it to.git/info/excludeif you keep it inside the repository folder.- When you change the image or its entrypoint, test on a data folder that does not exist yet. A folder that has had the right owner for weeks hides exactly the bugs strangers will hit.
- Without
TZ, "per day" and "hour of day" are in UTC. - An empty install is a poor place to work on charts. A Jellystat export imports headless too:
docker run --rm -v "$PWD/data-dev:/data" -v /path/to/backup.jsonl:/backup.jsonl:ro finstats:dev import-jellystat /backup.jsonl. Never commit such a file.
Tear down: docker rm -f finstats-dev jellyfin-dev && docker network rm finstats-dev, plus
docker volume rm jellyfin-dev-config jellyfin-dev-cache and data-dev/ for a clean slate.
Rules for a change¶
Short, because each one exists for a reason that has already cost somebody an evening:
- Do not run
cargo fmt. The code is deliberately not rustfmt-formatted; it would rewrite every file and bury your change. Match the style around you. - Tests pass, and you add one when you fix a bug.
cargo test, andfor f in $(find web/assets/js -name "*.js"); do node --check "$f"; done.cargo clippy --all-targetshas about 20 style warnings; add no new ones. Every commit that touches Rust builds and passes on its own. - Look at it like a user. UI changes: at desktop width and around 390 px, as an administrator and as
a user without permissions. Anything a browser downloads: test it the way a browser asks
(
curl -H 'Accept-Encoding: gzip'), not with barecurl. Anything about the image: on a data folder that does not exist yet, not on one that has had the right owner for weeks. - Permissions are decided on the server. Hiding a button is cosmetic. Statistics go through
Scope→Condinstats.rs; IP addresses needsee_network, file pathssee_server, other people's rowssee_everyone. - Released migrations are immutable. Add a new entry to
MIGRATIONS; never edit or reorder one. A new table holding something Jellyfin cannot give back also goes intobackup::TABLES. - Library reads must ask for real items (
CollapseBoxSetItems=false,ExcludeLocationTypes=Virtual). Whatever a read does not return is marked as removed, so a query that hides items loses data. - Text reaches the page through
h()ortextContent, neverinnerHTML. No inline styles in markup (the Content-Security-Policy forbids them). New tables go throughtables.js. Reuse the existing controls before inventing one. - An endpoint and its HTTP API page change together. The page lives in
the docs repository (
docs/finstats/api.md): open a pull request there beside the one here, and link the two. - Invented data only. Code, tests, docs, screenshots and commit messages never contain anything from a
real server. Use "alice", "Big Buck Bunny",
Europe/London,192.168.1.10, and the documentation ranges (203.0.113.0/24,198.51.100.0/24) for public addresses. This repository is public and its history is permanent. - Leave
CHANGELOG.mdand the version inCargo.tomlalone. A test ties the two together, and the changelog is the in-app Patch notes, written at release time. Say in your pull request what a user will notice, in one or two plain sentences; that becomes the patch note. - Documentation lives in its own repository, published as this site.
A change that a page here describes comes with a pull request there; finstats itself keeps only its README, this
project's short
CONTRIBUTING.mdandSECURITY.md, and the patch notes.
Commit messages¶
Conventional commits, with a scope where one fits:
fix(import): stop counting Live TV channels as movies
Jellystat has no item type, so imported channels were guessed to be films and
showed up in "top movies". A video that is not in the library and has no
container is a channel.
- Types in use:
feat,fix,perf,refactor,docs,test,build,ci,chore. - The subject says what changed, in the imperative, without a full stop. The body says why: what was wrong, what you found, what you decided against. The diff already says how.
- One logical change per commit, however many files it touches. A fix you noticed on the way is its own
commit. Documentation may follow as a separate
docs:commit. - No attribution lines for tools or models (
Co-Authored-By, "Generated with …"). You are the author of what you send, however you wrote it, and you answer for it.
Opening a pull request¶
- Push your branch to your fork and open the pull request against
main. The template has a short checklist. - Say what a user will notice and why the change is right, and link the issue. Screenshots for UI changes (with invented or blurred data).
- CI runs the unit tests and the JavaScript check, then builds the image for amd64 and arm64. Nothing is published from a pull request.
- Expect questions, and possibly a request to split or rebase. The history is kept readable on purpose: it is how the next person finds out why something is the way it is. Small, focused pull requests are reviewed first.
- Maintainers cut releases (version, patch notes, tag); you do not need to.
Licence¶
finstats is licensed under the GNU General Public License v3.0 (GPL-3.0-only). By contributing
you agree that your contribution is licensed under the same terms, and you confirm that you wrote it or
otherwise have the right to submit it under that licence. You keep the copyright to your work.