Contributing
Issues and pull requests are welcome on GitHub. For how the code is organised, read the architecture overview. The bot lives in the djsnake/ package; bot.py is the entry point.
Development setup
Section titled “Development setup”git clone https://github.com/selfhostr/djsnake.git && cd DJ_Snakepython3 -m venv venv && source venv/bin/activatepip install -r requirements-dev.txtcp .env.example .env # set DISCORD_TOKEN and POT_BASE_URLYou need ffmpeg and deno on your PATH.
Checks
Section titled “Checks”CI runs these on Python 3.10 to 3.15, and a change must pass them:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statisticsblack --check --diff .python -c "import bot"pytest -qBlack uses its default 88-column limit.
Run pytest after installing requirements-dev.txt. The suite in tests/ needs no network, Discord or YouTube: voice, channels and FFmpeg are faked, and the few tests that need the real ffmpeg binary are skipped without it. Add tests for any behaviour you change.
A plain pytest leaves out the slower end-to-end tests (see below).
End-to-end tests
Section titled “End-to-end tests”The tests marked e2e (in tests/e2e/) run the real pipeline as far as it goes without Discord or the network, and are skipped by a plain pytest. They generate short test media with ffmpeg at run time (a sine sweep whose pitch encodes the position, a quiet steady tone, a test-pattern video), then:
- Audio (
test_e2e_audio.py): the automix analysis (silence trim, loudnorm first pass), then the realplay_next()chain withdiscord.FFmpegPCMAudioand the crossfade mixer, read frame by frame by a fake voice client. They check the frame counts, the trim, the loudness normalisation, the fades, a crossfade overlap, where a seek lands and the volume gain. - Web (
test_e2e_web.py): the real aiohttp app on a free port with a session playing that media, andweb/index.htmlin headless Chromium through Playwright. They check that the page loads, that the video stays in sync with the server clock, live updates, the next, pause, seek and add controls, the view-only link, the theme menu, the lyrics panel (with a fake LRCLIB) and the 320 px layout.
They need ffmpeg on your PATH and Playwright’s Chromium:
pip install -r requirements-dev.txtpython -m playwright install chromium # add --with-deps on a bare Linux boxpytest -m e2e # about a minuteWithout ffmpeg, all of them are skipped; without Playwright or its Chromium, only the web ones are. CI runs them in a separate e2e job.
Manual test on a real Discord server
Section titled “Manual test on a real Discord server”The automated tests cannot reach Discord, so the voice connection, slash command sync and the real downloads still need a hand test before a release. Run the bot against a test server with a second account (or a friend) in the voice channel, keep the clip page open in a browser, and tick the list below. It takes about 20 minutes. Use short tracks.
Playing
-
!play <YouTube video link>from a voice channel: the bot joins, posts the status message with the clip page links, and you hear the track; the page shows the clip in sync with the sound. -
/play query:<YouTube video link>: same, with a private ✅ instead of a reaction. -
!play <YouTube playlist link>: one “Loading N tracks” message, edited at the end; tracks play while the rest downloads. -
!play <radio or mix link>(awatch?v=...&list=RD...link): the one-track-or-whole-list buttons appear and both choices work. -
!play daft punk one more time(plain text): the best YouTube match is queued; nonsense text gives “No YouTube match found”. -
!play <Spotify track link>and a Spotify playlist or album: matched on YouTube, with a “not found on YouTube” count if any. -
!play <SoundCloud track link>(and a Bandcamp track if you can): an audio-only track shows its artwork on the page. -
!play <Deezer or Apple Music link>: matched on YouTube like Spotify. -
!play https://example.com/x: refused with “link not accepted”.
Queue and playback commands (try a few as slash commands too)
-
!listshows the queue;!nextskips;!pauseand!resumepause and resume the sound and the page. -
!seek 1:00jumps there, on Discord and on the page. -
!crossfade on, then let a track end: the next one fades in over it and the page cross-dissolves the clips. -
!loop trackreplays the track at its end;!loop queuesends finished tracks to the end of the queue;!loopcycles;!shufflereorders the queue. -
!volumeshows 100%;!volume 40lowers the sound without restarting it; the page’s slider follows; restart the bot and!volumestill says 40. -
!autoplay onwith one track queued: a related track plays after it;!autoplay off. - Let the queue run out: the status says “Queue empty — leaving in …”, and a track added from the page during that time plays at once.
-
!stopstops, empties the queue and leaves;!playagain reuses the same clip page link.!resetreconnects cleanly.
Saved playlists
-
!playlist save test,!playlist list,!playlist show test,!stop, then!playlist load testqueues the tracks again;!playlist add testand!playlist delete testwork; another member cannot delete your playlist. - The page’s save and load playlist controls do the same.
Clip page (control link)
- Previous, next, pause, the seek bar and the add box work; a queued track can be played now, removed and dragged to a new place.
- The crossfade, loop, shuffle, autoplay and volume controls change the session (the status message follows).
- The Lyrics button shows synced lyrics for a well-known song, the sung line following the sound.
- The theme menu switches presets and the choice survives a reload; the Stats card opens and shows plays.
- The page works on a phone (no horizontal scroll) and keeps up after the phone is locked for a minute.
Permissions and links
- The view-only link shows the page with a “View only” badge and no controls.
-
!djrole <role>as an admin: the status message now shows only the view-only link, the old control link stops working, a member without the role gets refused on!next(but may skip a track they queued), and!weblinkDMs the links only to DJs.!djrole offrestores it. - A member without Manage Server cannot run
!theme,!djroleor!language.
Server settings
-
!theme nightchanges the page’s default;!theme editDMs an editor link; a custom theme with a logo saves and shows to every viewer;!theme reset. -
!language fr: replies, the status message,!helpand the page switch to French (check accents and spacing before:and!);!language reset.
Features still on their own branches (audio filters, 24/7 stay, queue limits, vote skip, Last.fm, backups) get their lines here when they are merged.
Building these docs
Section titled “Building these docs”The documentation is written in Markdown under docs/source/ and built with Sphinx, MyST-Parser and the Furo theme. It is published on Read the Docs. CI builds it with warnings treated as errors, so build it locally before you push a docs change:
python3 -m venv docs-venv && source docs-venv/bin/activatepip install -r docs/requirements.txtsphinx-build -W --keep-going -b html docs/source docs/_build/htmlThen open docs/_build/html/index.html. The changelog page includes CHANGELOG.md directly, so do not copy it.
Pull requests
Section titled “Pull requests”Keep them focused, one change each, and update the docs (README.md, docs/source/, CLAUDE.md) when behaviour or configuration changes. History uses gitmoji-style prefixes (for example a sparkles emoji for features), then a short imperative summary.
Never commit .env, a token file or any real token, hostname or IP.
Releasing
Section titled “Releasing”Move the [Unreleased] notes in CHANGELOG.md under a new version, commit, then tag vX.Y.Z and push the tag. The release workflow builds and pushes the multi-arch ghcr.io/theroxat/dj_snake image and creates the GitHub Release.
After changing web/index.html (or docs/demo/shim.js), run python3 docs/demo/build.py and commit the regenerated docs/demo/player.html.