A high-performance, self-hosted Discord music bot with a hybrid ML recommendation engine (ALS + Item2Vec + Audio Features), native
yt-dlpYouTube extraction, Spotify integration, persistent queues, zero-downtime deployments via GitHub Actions CI/CD, and a 3-tier queue buffering engine optimized for ARM64 cloud VMs.
- Overview
- Key Features
- Architecture
- File Structure
- Module Reference
- Commands Reference
- Recommendation System (RecSys)
- Queue Persistence System
- Graceful Drain (Zero-Downtime Restarts)
- Performance & Buffering Engine
- Environment Variables
- Deployment
- Local Development
- Cookies Setup
- Security Notes
Tooodles is an asynchronous Python Discord music bot designed for maximum speed, low CPU consumption, and absolute playback stability. It features:
- Direct native
yt-dlpextraction with Netscapecookies.txtauthentication and HTTP 403/429 error resilience - Instant Spotify playlist queuing with < 400ms startup
- Low-latency FFmpeg Opus passthrough (< 0.5% CPU on 1 vCPU) with
-headersforwarding - Hybrid ML Recommendation Engine — Implicit ALS (matrix factorization) + Item2Vec (Word2Vec) + Audio Feature vectors for personalized autoplay
- Persistent queue recovery across bot restarts and redeployments
- Zero-downtime CI/CD via GitHub Actions with manual
workflow_dispatchtrigger - Database-backed favorites, dislikes, playback telemetry, interactive saved playlists, and per-guild configuration
- Centralized logging with automatic rotation (5 MB × 3 backups) and dual-stream output to console + disk
The bot runs inside a Docker container (Python 3.11-slim) and is fully optimized for Oracle Cloud Free Tier (ARM64 Ampere) and any Linux VM.
- 🎵 Multi-Source Playback — YouTube (URLs or search queries), YouTube Music, Spotify (tracks, playlists, albums & top artist tracks), SoundCloud.
- ⚡ Instant Spotify Playlist Loader — Queues 100+ song Spotify playlists in < 400ms by resolving track 1 immediately and loading remaining tracks into queue memory.
- 🚀 FFmpeg Opus Passthrough & CDN Anti-Block — Streams WebM/Opus audio natively without CPU-heavy re-encoding (< 0.5% CPU on 1 vCPU). Forwards
yt-dlphttp_headers(User-AgentandReferer) directly into FFmpeg to eliminate YouTube 403 CDN errors. - 🧠 Hybrid ML Recommendation Engine — Trains Implicit ALS (256-dim matrix factorization) and Item2Vec (256-dim Word2Vec) models on user listening history. Combines both with 8 acoustic audio features (danceability, energy, valence, tempo, etc.) for multimodal recommendations. Auto-trains every 6 hours with
threadpoolctlCPU contention limits. - 🤖 ML Autoplay Mode —
td?autoplaygenerates continuous recommendations using the hybrid engine with temperature-controlled softmax sampling, anti-repetition filtering, and dislike-aware re-ranking. - 📻 Auto-Radio & Group Radio —
td?radiogenerates 5 contextual recommendations using Spotify's genre/artist graph when the queue ends;td?favradiobuilds collective playlists from channel members' favorites. - 💾 Queue Persistence — When any disconnect or restart occurs, the pending queue + current song are serialized as JSON into SQLite/PostgreSQL. Upon reconnecting with
td?j, the full queue is restored automatically — zero songs lost across restarts. - 🔄 Zero-Downtime Graceful Drain — On
SIGTERM/SIGINT, the bot saves the queue, disables radio mode, and waits for the current song to finish before shutting down cleanly (up to 10 min timeout). - 🤖 GitHub Actions CI/CD — Automated deploy on push to
mainor manual trigger viaworkflow_dispatch. SSHs into server, pulls latest code, rebuilds Docker image, and triggers graceful restart. - 🧠 3-Tier Hybrid Queue Buffering:
- Track 1 (Playing): Plays smoothly with zero network interference.
- Track 2 (Next in Queue): Full audio download to
/tmp/cache_{id}.webmwith atomic rename (.tmp→.webm) for 0ms transition delay. Audio features (BPM, energy, brightness) extracted from the cached file. - Track 3+ (Queued): Background URL pre-resolution so streams are pre-fetched before reaching the front of the queue.
- Automated Cache Cleanup: Periodic loop purges
/tmpaudio files older than 1 hour every 30 minutes.
- 📊 Now Playing with Progress Bar —
td?npshows elapsed time, total duration, and a visual scrubber (▬▬▬▬🔘▬▬▬▬▬▬). - ❤️ User Favorites & Feedback — Save liked songs (
td?like), dislike songs (td?dislike), view personal favorites (td?liked), and check global stats (td?top). - 📥 Playlist Preloader —
td?preload <URL> [@user]bulk-imports a Spotify/YouTube playlist into the database as likes + play logs for a target user, then retrains the ML model immediately. - 📚 Interactive Saved Playlists —
td?playlistsprovides a rich UI with dropdown select menus, pre-filled edit modals, deletion confirm dialogs, and automatic authentic playlist title extraction (get_playlist_title). - 🛡️ Trusted Users System — Bot owner can grant
Trustedstatus to users, enabling access to admin/diagnostic commands via DM. - 🎛️ Ergonomic 2-Row Discord UI — Redesigned control panel with interactive buttons for Pause/Resume, Skip, Stop, Radio toggle, Shuffle (
🔀), and Queue (📜) popups. - 🔐 Cookie Management & Error Classification — Reads
cookies.txtfrom disk or database with Playwright Stealth fallback. Classifies extraction errors intoRateLimitError(HTTP 429) andForbiddenBlockError(HTTP 403) with user alerts. - 🏠 Per-Guild Configuration & SQLite Hardening — Custom prefix, queue persistence toggle, restricted command channel, default radio mode. Database uses WAL journal mode and
busy_timeout=5000for concurrency safety. - 📋 Centralized Logging —
sznLogger.pyprovides hierarchical loggers, rotating file logs (5 MB × 3 backups), colored console output, UTF-8 stream handling, and dual-streamstdout/stderrcapture. - 💤 Smart Auto-Disconnect — Disconnects after 60 seconds of inactivity (empty queue, not playing), and after 30 seconds if left alone in a voice channel.
┌──────────────────────────────────────────────────────────────────────────┐
│ Discord Gateway │
│ (discord.py Bot, default prefix: td?) │
└───────────────────────────┬──────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌─────────┐
│ sznDB │ │ sznMusic/ │ │ sznUI │
│ (MusicDB)│ │ (MusicCore) │ │(MusicUI)│
└────┬─────┘ └──┬───────┬──┘ └────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────┐ ┌─────────┐ ┌──────────────────────────────┐
│database.py│ │recsys/ │ │ sznUtils.py │
│(SQLAlchemy│ │(Hybrid │ │ yt-dlp + Queue Persistence │
│SQLite/PG) │ │ML RecSys│ └──────────────┬───────────────┘
└──────────┘ │Engine) │ │
└─────────┘ ▼
┌─────────────────────┐
sznLogger.py │ YouTube Audio │
(Rotating logs │ (Direct Opus or │
+ Dual stream) │ Local Cache) │
└─────────────────────┘
| Cog | Class | Responsibility |
|---|---|---|
sznDB |
MusicDB |
Database operations: user likes/dislikes, playback history, fuzzy title matching, favradio, top, historial |
sznMusic |
MusicCore |
Queue state, yt-dlp extraction, FFmpeg playback, Spotify integration, hybrid buffering, queue persistence, radio, autoplay, RecSys training loop, disk cache purge loop |
sznUI |
MusicUI |
2-row player controls, now-playing embeds, progress bar, paginated queue view, settings panel, help, logs, trusted users, interactive playlists (edit/delete modals), debug diagnostics |
Tooodles/
├── main.py # Bot startup, graceful drain, signal handling, cog loading
├── database.py # SQLAlchemy models (Song, AppConfig, UserLike, UserDislike,
│ # PlayLog, SavedPlaylist, TrustedUser) & WAL/busy_timeout setup
├── sznUtils.py # Audio extraction engine, get_playlist_title, error classification
│ # (HTTP 403/429), queue persistence functions
├── sznLogger.py # Centralized logging: RotatingFileHandler (5MB×3), colored
│ # console formatter, DualStreamWriter for stdout/stderr capture
├── sznMusic/ # Modular MusicCore package: queue, FFmpeg, Spotify, buffering
│ ├── __init__.py # MusicCore Cog, setup(bot), all user-facing commands,
│ │ # background loops (inactivity, cache cleanup, RecSys training)
│ ├── player.py # GuildPlayer state, prefetch_chunk_throttled, audio extraction,
│ │ # Spotify/YouTube resolvers, play_next loop, FFmpeg header passthrough
│ └── radio.py # Radio algorithm: 3-layer Spotify genre expansion, GENRE_EXPANSION
│ # map (40+ genre mappings), ML autoplay via RecSysEngine
├── recsys/ # Hybrid ML Recommendation System
│ ├── __init__.py # Package exports
│ ├── engine.py # RecSysEngine: inference engine for ALS + Item2Vec + Audio
│ │ # Features with cosine similarity, re-ranking, and temperature
│ │ # controlled softmax sampling. Responds in < 10ms.
│ └── train.py # Offline training pipeline: builds user-item interaction matrix
│ # (5 signal types), trains Implicit ALS (256d, 50 iters) with
│ # threadpoolctl and Gensim Item2Vec (256d, 100 epochs). Exports .npz.
├── sznDB.py # MusicDB cog: likes, dislikes, top songs, history, group radio
├── sznUI.py # MusicUI cog: 2-row interactive player controls (Shuffle/Queue modals),
│ # paginated queue, settings panel, help, interactive playlists CRUD,
│ # trusted users, logs viewer, debug diagnostics, RecSys train trigger
├── alembic/ # Database migration scripts (Alembic)
│ ├── env.py # Migration environment configuration
│ ├── script.py.mako # Migration template
│ └── versions/ # Incremental schema change scripts
├── data/ # RecSys trained artifacts (generated, git-ignored)
│ └── recsys_artifacts.npz # Exported model weights, embeddings, and mappings
├── logs/ # Application runtime logs
│ └── tooodles.log # Auto-rotated (5 MB × 3 backups)
├── requirements.txt # Python dependencies
├── Dockerfile # Container definition (Python 3.11-slim + FFmpeg + Node.js 22.x)
├── Procfile # Process file for PaaS deployments
├── start.sh # One-click deployment script (git pull, docker build, docker run)
├── alembic.ini # Alembic configuration
├── .github/
│ └── workflows/
│ └── deploy.yml # GitHub Actions CI/CD — auto-deploy on push to main or workflow_dispatch
└── .gitignore # Excludes cookies.txt, .env, *.db, data/, logs/, /tmp cache
Bot entry point executed by Docker's CMD.
Startup Workflow:
- Executes
setup_database()to create/migrate database tables with WAL mode andbusy_timeout=5000. - Checks for
cookies.txtlocally; falls back to database config, then Playwright Stealth auto-generation. - Sets
os.environ["cookies"]for global availability. - Loads extensions:
sznDB,sznMusic,sznUI. - Registers
SIGTERM/SIGINTsignal handlers pointing tograceful_shutdown(). - Logs into Discord using
token_priv.
graceful_shutdown(bot) Flow:
- Sets
bot.is_draining = True(prevents double-execution and blocks queue overwrite events). - Saves pending queues to database for all guilds with persistence enabled.
- Clears in-memory queues and disables radio mode.
- Polls every 2 seconds until all voice clients finish playing (max 10 minutes).
- Force-disconnects any remaining voice clients.
- Closes the Discord connection cleanly.
Handles database persistence via SQLAlchemy. Enforces SQLite WAL mode (PRAGMA journal_mode=WAL;) and PRAGMA busy_timeout=5000; on connection to prevent database lock issues under high concurrency. Supports PostgreSQL (DATABASE_URL).
| Model | Table | Fields |
|---|---|---|
Song |
songs |
id, title, url, artist, duration, played_count, spotify_id, genres, popularity, danceability, energy, valence, tempo, acousticness, instrumentalness, liveness, speechiness |
AppConfig |
config |
key, value (stores queue JSON, cookies, prefix, persist flags, channel locks, default radio) |
UserLike |
likes |
id, user_id, song_id, timestamp |
UserDislike |
dislikes |
id, user_id, song_id, timestamp |
PlayLog |
play_logs |
id, user_id, username, song_id, guild_id, played_at, listened_duration (s), completed (0/1), skipped_at (s) |
SavedPlaylist |
saved_playlists |
id, guild_id, user_id, name, url, created_at |
TrustedUser |
trusted_users |
user_id, username, added_at |
| Function | Description |
|---|---|
setup_database() |
Creates all tables and handles safe column migration for new audio feature columns. |
get_db_session() |
Context manager for thread-safe transactional sessions with auto-commit/rollback. |
log_play_event(...) |
Records playback telemetry including listen duration, completion status, and skip position. |
get_recsys_data() |
Exports play_logs, likes, dislikes, and song catalog for RecSys training. |
get_session_sequences() |
Extracts listening session sequences (30-min gap threshold) for Item2Vec training. |
get_cold_start_recommendations() |
Weighted sampling from play history when no ML model is available. |
bulk_preload_tracks(...) |
Batch-imports tracks from playlists as songs + likes + simulated play logs for a target user. |
Audio extraction engine, error handling, playlist title resolution, and persistence utilities.
| Function | Description |
|---|---|
extract_info(query) |
Primary audio stream extractor. Uses yt-dlp with player_client: ["mweb", "android_creator", "web"] and cookies. Catches HTTP 429 (RateLimitError) and 403 (ForbiddenBlockError). |
get_playlist_title(url) |
Fetches authentic playlist titles for Spotify and YouTube URLs for display in UI menus. |
extract_flat_metadata(query) |
Rapid metadata-only lookup using extract_flat=True without downloading audio streams. |
extract_playlist_metadata(url) |
Extracts all track metadata from a YouTube/YouTube Music playlist. |
get_cookie_file_path() |
Returns the absolute path to a valid cookies.txt file (checks disk root, then database/environment). |
fetch_stealth_cookies() |
Launches headless Chromium via Playwright Stealth to export fresh YouTube session cookies if missing. |
extract_local_audio_features(path) |
Analyzes a cached .webm file to extract BPM, energy, and brightness metrics. |
| Function | Description |
|---|---|
save_guild_queue(guild_id, queue) |
Serializes and saves the queue (title, url, duration, uploader, origin, user) to AppConfig as JSON. |
load_guild_queue(guild_id) |
Loads and clears the saved queue from the database, returning the deserialized list. |
is_guild_persist_enabled(guild_id) |
Returns True if queue persistence is toggled on for the given guild. |
set_guild_persist_enabled(guild_id, enabled) |
Writes the persist_queue_{guild_id} flag to the database. |
save_config(key, value) / load_config(key) |
Generic key-value config storage/retrieval from AppConfig. |
Core music player package handling queue logic, playback, Spotify resolution, and persistence. Split into three modules:
- Initializes Spotify API client, RecSys engine, and background task loops.
- Exposes all user-facing playback commands (
td?play,td?skip, etc.). - Background Loops:
inactivity_check(every 60s): Auto-disconnects if queue is empty and nothing is playing.cache_cleanup_loop(every 30 min): Removes.webm/.tmpcache files older than 1 hour.recsys_training_loop(every 6 hours): Retrains the ML model and hot-reloads artifacts.
- Voice State Event Handler: Saves queue on disconnect, auto-disconnects after 30s if left alone in a channel.
GuildPlayer: Encapsulates per-guild state (queue, current song, voice client, radio/autoplay modes, play lock, radio history).MusicPlayerMixin: Methods for audio extraction, Spotify resolution (tracks, playlists, albums, artist top tracks), YouTube playlist loading, and theplay_next()loop.prefetch_chunk_throttled(): Downloads full audio to/tmp/cache_{id}.webmusing atomic.tmp→.webmrename. Extracts real audio features (BPM, energy, brightness) from cached files. Skips songs > 10 minutes.schedule_queue_optimizations(): 30-second delayed pre-resolution and pre-caching of the next song in queue (N+1 strategy).- Queue Protection Guard:
play_next()usesasyncio.Lockand checksis_playing()to prevent race conditions. - FFmpeg Opus Passthrough: Streams native WebM/Opus with
-user_agentand-headers "Referer: https://www.youtube.com/"passed directly fromyt-dlpto prevent YouTube 403 CDN errors.
MusicRadioMixin: Methods for the 3-layer radio algorithm:- Layer 1: Same artist tracks from Spotify (up to 2 songs).
- Layer 2: Genre expansion using
GENRE_EXPANSIONmap (40+ genre mappings for Latin/Chilean/Argentine music) with popularity-weighted sampling and artist diversity enforcement. - Layer 3: Similar artist queries via Spotify search.
- Fallback: Random songs from the local database.
_fill_queue_from_recsys(): ML autoplay usingRecSysEngine.get_autoplay_recommendations()with cold-start fallback.GENRE_EXPANSION: Static dictionary mapping 40+ genres to related genres (Hip Hop, Rock, Reggaeton, Metal, Pop, Reggae, etc.) for contextual radio diversity.- Anti-Repetition: Maintains a 30-song history window and filters duplicates, audiobooks, podcasts, interludes, and numeric-only titles.
Manages song statistics, favorites, dislikes, and fuzzy matching.
td?like/td?unlike: Toggle favorites for the currently playing song.td?dislike/td?undislike: Register/remove negative feedback (used by RecSys for filtering).td?liked: Display the user's saved favorite tracks.td?favradio [temp]: Queries favorites of all members in the current voice channel to build a collective personalized radio.td?top: Shows the most-played tracks per server (or globally as fallback).td?historial: Shows the most recently played tracks per server.
Provides interactive UI controls, configuration panels, and admin tools.
PlayerControlsView: 2-row interactive player control panel:- Row 1: Pause/Resume (
⏯️), Skip (⏭️), Radio Toggle (📻), Stop (⏹️), Command Channel (📍) - Row 2: Shuffle (
🔀), Queue (📜) interactive popup modal
- Row 1: Pause/Resume (
notify_now_playing(): Sends now-playing embeds with interactive buttons and auto-deletes after 5 minutes.- Progress Bar in
td?np: Calculates elapsed time fromcurrent_song_start_timeand renders a visual scrubber bar. td?q: Paginated view of the queue with 10 songs per page and navigation buttons.td?controls: Resends the 2-row interactive playback control buttons.td?help: Context-aware help — shows server commands in guilds or admin/diagnostic commands in DMs.td?playlists: Interactive manager for saved server playlists:- Dropdown select menu for selecting and playing saved playlists
- Pre-filled
PlaylistEditModalfor editing playlist titles and URLs - Deletion confirmation dialogs
td?settings: Interactive settings panel with buttons for prefix, persistence, channel lock, and default radio configuration.
td?persist: Toggles queue persistence. If requested by non-owner, sends DM approval buttons (✅ Authorize/❌ Reject) to the bot owner.td?channel: Locks bot commands to a specific text channel.td?trusted [add/remove] @user: Manages the Trusted Users list (owner-only).td?logs [N]: Displays the last N lines oftooodles.log(Admin/Trusted).td?debug: System diagnostics — Discord connections, log file size, cache files, RecSys status (Admin/Trusted).td?train: Manually triggers RecSys offline training (Admin/Trusted).
Hybrid ML recommendation system with offline training and real-time inference.
Real-time inference engine designed to respond in < 10ms using NumPy/SciPy.
| Method | Description |
|---|---|
load(force) |
Loads .npz artifacts. Detects file changes via mtime for atomic hot-reload. |
reload_if_updated() |
Reloads only if the artifact file on disk was modified. |
recommend_for_user(user_id, n) |
Personalized Top-N via ALS cosine similarity. |
recommend_for_group(user_ids, n) |
Group recommendations by averaging user embedding vectors. |
recommend_similar_songs(title, n) |
Similar songs via hybrid Item2Vec + Audio Feature cosine similarity. |
rerank(candidates, ...) |
Anti-repetition filter, dislike filter, temperature-controlled softmax sampling. |
get_autoplay_recommendations(...) |
High-level API combining Item2Vec (70% weight for vibe continuity) + ALS (30% weight for personalization), then re-ranking. |
Hybrid Multimodal Space:
- Item2Vec embeddings (256d) are concatenated with normalized Audio Feature vectors (8d × 0.5 weight) to create a combined multimodal embedding space for similarity search.
Designed to run every 6 hours via background loop or manually via td?train.
Pipeline Steps:
- Extract data from database (
PlayLog,UserLike,UserDislike,Song). - Sync Spotify audio features (danceability, energy, valence, tempo, etc.).
- Build user-item interaction matrix with 5 weighted signal types:
+5.0for explicit like-5.0for explicit dislike+2.0for completing ≥ 80% of a song+1.0for each play event-3.0for skipping within first 30 seconds
- Train Implicit ALS (256 factors, 50 iterations, L2 regularization = 0.05). Uses
threadpoolctlto limit OpenBLAS threads to 1 and prevent CPU contention on multi-core VPS. - Extract session sequences (30-min gap threshold) and train Item2Vec (256d, window=10, Skip-gram, 100 epochs).
- Build normalized 8-feature audio matrix (danceability, energy, valence, tempo, acousticness, instrumentalness, liveness, speechiness).
- Export all artifacts to
data/recsys_artifacts.npz.
Resource Profile: < 500MB RAM, < 2 min on ARM64 Ampere (1 vCPU).
Centralized logging system.
DualStreamWriter: Intercepts allprint()andsys.stderroutput, duplicating it tologs/tooodles.logwith timestamps and UTF-8 stream handling for Windows compatibility. Strips ANSI color codes for clean file output.RotatingFileHandler: 5 MB per file, 3 backup files (tooodles.log,tooodles.log.1,tooodles.log.2,tooodles.log.3).ColoredFormatter: ANSI-colored log levels for console output (Cyan=DEBUG, Green=INFO, Yellow=WARNING, Red=ERROR).get_logger(submodule): Returns a child logger under thetooodlesnamespace (e.g.,tooodles.recsys,tooodles.bot).
Default prefix:
td?(configurable per guild viatd?settings)
| Command | Aliases | Description |
|---|---|---|
td?j |
join, connect, conectar, unir |
Connect to your voice channel and restore any saved queue. |
td?p <query> |
play |
Play a song (YouTube search, YouTube URL, Spotify track/playlist/album/artist). |
td?s |
skip |
Skip the currently playing song. |
td?pause |
— | Pause audio playback. |
td?resume |
r, reanudar |
Resume paused audio or start playing a restored queue. |
td?stop |
disconnect, leave, exit, dc |
Stop playback, save queue to DB (if persist enabled), and disconnect. |
td?np |
nowplaying |
Show now-playing embed with progress bar and elapsed time. |
td?controls |
ctr, player |
Resend the 2-row interactive playback buttons. |
| Command | Aliases | Description |
|---|---|---|
td?q |
queue, cola |
Paginated queue view (10 songs per page with navigation buttons). |
td?shuffle |
— | Randomly shuffle all queued tracks. |
td?remove <index> |
— | Remove a track from the queue by position number. |
td?move <from> <to> |
— | Move a queued track from one position to another. |
td?clear |
clean, cq |
Clear the entire queue (in-memory + database) without stopping current song. |
td?search <query> |
— | Search YouTube and queue the best result. |
| Command | Aliases | Description |
|---|---|---|
td?radio [off] |
— | Toggle auto-radio mode. Generates 5 Spotify-based recommendations when queue ends. |
td?autoplay [off] |
— | Toggle ML autoplay mode. Uses RecSys hybrid engine (ALS + Item2Vec) for smart recommendations. |
td?favradio [temp] |
— | Build a group radio from all channel members' liked songs (temp: 0.0–1.0). |
td?like |
— | Save the current song to your personal favorites (+5.0 signal for RecSys). |
td?unlike |
— | Remove the current song from your favorites. |
td?dislike |
— | Mark the current song as disliked (-5.0 signal for RecSys). |
td?undislike |
— | Remove the dislike mark from the current song. |
td?liked |
— | View your personal favorite songs list. |
td?historial |
— | View the most recently played tracks in this server. |
td?top |
— | View the most played songs in this server. |
| Command | Aliases | Description |
|---|---|---|
td?playlists |
pl, playlist |
Interactive saved playlists manager with dropdown selection, edit modals, and delete dialogs. |
td?playlists add <URL> [Alias] |
— | Save a playlist URL with an optional alias (auto-extracts title). |
td?playlists remove <Name> |
— | Delete a saved playlist by name. |
td?preload <URL> [@user] |
— | Bulk-import a Spotify/YouTube playlist into the DB as likes + play logs for a user, then retrain RecSys. |
| Command | Aliases | Description |
|---|---|---|
td?persist |
persistencia |
Toggle queue persistence. Sends DM approval request to bot owner if requested by admin. |
td?channel |
canal |
Lock bot commands to a specific text channel. |
td?settings |
config |
Interactive settings panel with buttons for prefix, persistence, channel, default radio. |
td?help |
ayuda, h |
Context-aware help guide (server commands in guilds, admin commands in DMs). |
| Command | Aliases | Description |
|---|---|---|
td?trusted [add/remove] @user |
trust |
Manage the Trusted Users list (owner-only). |
td?logs [N] |
log |
Display the last N lines of tooodles.log (max 50). |
td?debug |
status, botstatus |
System diagnostics: Discord connections, log size, cache, RecSys status. |
td?status |
health, diag, diagnostico |
Technical health panel: disk usage, WAL mode, RecSys stats, Python/discord.py version (DM-only for Trusted). |
td?train |
recsys_train |
Manually trigger RecSys offline training pipeline. |
td?reloadrecsys |
— | Hot-reload RecSys artifacts from disk without retraining. |
Tooodles includes a full hybrid recommendation engine that learns from user listening behavior to generate personalized music suggestions.
┌──────────────────────────────────┐
│ PlayLog + UserLike + │
│ UserDislike + Song catalog │
└────────────┬─────────────────────┘
│
┌─────────────┼──────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Interaction Matrix │ │ Session Sequences │
│ R(u,i) with 5 │ │ (30-min gap split) │
│ weighted signals │ │ │
└────────┬────────────┘ └──────────┬──────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Implicit ALS │ │ Gensim Item2Vec │
│ 256 factors │ │ 256 dims, Skip-gram│
│ 50 iterations │ │ 100 epochs │
└────────┬────────────┘ └──────────┬──────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ User Embeddings │ │ Song Embeddings + │
│ (n_users × 256) │ │ Audio Features (8d) │
└────────┬────────────┘ │ → Hybrid Space │
│ └──────────┬──────────┘
│ │
└───────────┬───────────────────┘
│
▼
┌─────────────────────────┐
│ Hybrid Re-Ranking │
│ • Anti-repetition │
│ • Dislike filtering │
│ • Softmax sampling │
│ (temperature T) │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Top-5 Recommendations │
│ → Enqueued as songs │
└────────────┬────────────┘
| Signal | Weight | Description |
|---|---|---|
td?like |
+5.0 | Explicit positive feedback |
td?dislike |
-5.0 | Explicit negative feedback |
| Completed (≥ 80%) | +2.0 | Song was listened to near completion |
| Played / Requested | +1.0 | The user queued the song |
| Early skip (< 30s) | -3.0 | Song was skipped within 30 seconds |
- Item2Vec (70% weight) → Finds songs with similar "vibe" to the currently playing track using the hybrid multimodal embedding space (Item2Vec + Audio Features).
- ALS (30% weight) → Personalizes recommendations based on the users present in the voice channel (single user or group average).
- Re-ranking → Filters recently played titles, removes disliked songs, and applies temperature-controlled softmax sampling for controlled diversity.
Queue persistence allows the bot to recover pending songs after any type of disconnection or restart — no songs lost.
[Bot receives SIGTERM / td?stop / disconnects from voice]
│
▼
is_guild_persist_enabled(guild_id)?
│ YES
▼
save_guild_queue(guild_id, current_song + song_queue)
→ Serialized as JSON in AppConfig: "queue_{guild_id}"
│
▼
[Bot restarts / Graceful Drain completes]
│
▼
User types: td?j (join)
│
▼
connect_to_voice() → load_guild_queue(guild_id)
→ Deserializes JSON → pushes songs back into player.song_queue
→ Calls play_next() automatically
│
▼
📥 "Cola recuperada: Se restauraron N canciones" ✅
| Trigger | Behavior |
|---|---|
SIGTERM / SIGINT (restart/deploy) |
graceful_shutdown saves queue, waits for current song to end |
td?stop / td?dc |
Saves queue before disconnecting if persist is enabled |
| External disconnect (voice state event) | Saves queue unless bot.is_draining is set (prevents race condition) |
| Left alone in voice channel (30s) | Saves queue and auto-disconnects |
td?clear |
Clears both in-memory queue and the database record |
Enable persistence for a guild via the bot or directly in the database:
docker exec tooodles python3 -c "
from database import setup_database
from sznUtils import set_guild_persist_enabled
setup_database()
set_guild_persist_enabled(YOUR_GUILD_ID, True)
print('Persistence enabled.')
"When the server receives a deploy signal (e.g., from GitHub Actions), the bot does not cut the music mid-song. Instead:
SIGTERMis caught →graceful_shutdown()is invoked.bot.is_draining = Trueis set.- Pending queues are saved to the database per guild.
- Radio mode is disabled.
- In-memory queues are cleared.
- The bot polls every 2 seconds until all active voice clients stop playing.
- The bot disconnects cleanly and closes the Discord connection.
- Docker starts the new container with the latest code.
- Users rejoin music with
td?j— queue is restored automatically.
Maximum wait time before forced shutdown: 10 minutes.
Tooodles uses a 3-tier hybrid buffering pipeline designed for instant startup and zero playback stuttering:
[User adds Spotify Playlist (100+ tracks)]
│
├── Track 1: Resolved immediately (<400ms) ➔ FFmpegPCMAudio
│ + user-agent & Referer headers (prevents CDN throttling)
│
├── Track 2: Pushed to Queue ➔ prefetch_chunk_throttled()
│ (Full audio download to /tmp/cache_{id}.webm)
│ + Atomic .tmp → .webm rename (prevents truncated reads)
│ + Audio feature extraction (BPM, Energy, Brightness)
│
└── Tracks 3..100+: Pushed to Queue memory instantly ➔ URL pre-resolution
(30-second delayed pre-fetch as queue advances)
CPU Profile on Oracle Cloud Ampere (ARM64, 1 vCPU):
| Operation | CPU Usage |
|---|---|
| FFmpeg Opus passthrough (streaming) | < 0.5% |
yt-dlp stream extraction |
~2–4% (< 1 second) |
| Audio pre-download (full file) | < 1% |
| Spotify playlist load (100+ tracks) | ~1–2% for < 400ms |
| RecSys inference (recommendations) | < 0.1% (< 10ms) |
| RecSys training (offline) | ~50% for < 2 min (single-threaded OpenBLAS) |
| Variable | Required | Description |
|---|---|---|
token_priv |
✅ Yes | Discord Bot Token |
client_id |
Optional | Spotify API Client ID (enables Spotify playback + radio + RecSys audio features) |
client_secret |
Optional | Spotify API Client Secret |
DATABASE_URL |
Optional | PostgreSQL URI (defaults to sqlite:///tooodles.db) |
FERNET_KEY |
Optional | 32-byte Fernet key for symmetric encryption (cryptography.fernet) of session cookies stored in AppConfig. If absent, cookies are stored as plaintext. |
Tooodles ships with a GitHub Actions workflow at .github/workflows/deploy.yml that automatically deploys on every push to main or manually via workflow_dispatch.
How it works:
- You push code to
main(or click "Run workflow" in GitHub Actions). - GitHub Actions triggers, SSHs into your server using stored secrets.
- Runs
./start.shon the server:git pull→docker build→docker run. - The running bot receives
SIGTERMfrom Docker and enters Graceful Drain (60s timeout). - The current song finishes. The new container starts. Music resumes via
td?j.
Required GitHub Secrets:
| Secret | Description |
|---|---|
VPS_HOST |
Your server IP address |
VPS_USERNAME |
SSH login username (e.g., ubuntu) |
VPS_SSH_KEY |
Private SSH key (PEM format, content of ~/.ssh/id_rsa) |
Setting up secrets: Go to your GitHub repo → Settings → Secrets and variables → Actions → New repository secret.
SSH into your server and run:
chmod +x start.sh && ./start.shWhat start.sh executes:
git fetch origin main && git reset --hard origin/main— Force-syncs to latest remote.docker build -t tooodles-bot .— Rebuilds the image with latest code.docker stop -t 60 tooodles— SendsSIGTERMand waits up to 60s for graceful drain.docker rm tooodles— Removes the old container.docker run -d --name tooodles --restart always --env-file .env— Launches with persistent volume mounts fortooodles.db,cookies.txt,logs/, anddata/.
Persistent Docker Volumes:
| Host Path | Container Path | Purpose |
|---|---|---|
~/Tooodles/tooodles.db |
/app/tooodles.db |
SQLite database |
~/Tooodles/cookies.txt |
/app/cookies.txt |
YouTube session cookies |
~/Tooodles/logs/ |
/app/logs/ |
Rotating log files |
~/Tooodles/data/ |
/app/data/ |
RecSys trained artifacts (.npz) |
# Install Python dependencies
pip install -r requirements.txt
# Set required environment variables
export token_priv="YOUR_DISCORD_BOT_TOKEN"
export client_id="YOUR_SPOTIFY_CLIENT_ID" # Optional
export client_secret="YOUR_SPOTIFY_CLIENT_SECRET" # Optional
# Run the bot
python main.pyTip: Place
cookies.txtin the project root for YouTube authentication.
python -m recsys.trainThis exports trained artifacts to data/recsys_artifacts.npz. The bot auto-reloads them on the next recommendation request.
YouTube requires session cookies to serve audio streams from cloud server IPs.
- Open an Incognito Browser Window and navigate to
https://www.youtube.com/robots.txt. - Export cookies using the browser extension "Get cookies.txt LOCALLY" (Chrome/Firefox).
- Save the output file as
cookies.txtin the~/Tooodles/directory on your server. - Run
./start.sh— cookies are loaded automatically on startup.
Alternative (Automatic): If no cookies.txt is found, the bot will attempt to generate fresh cookies via Playwright Stealth (headless Chromium). This requires Playwright to be installed in the container (included in the Dockerfile).
cookies.txtis in.gitignoreand must never be committed to Git.- The
token_privDiscord token is passed via Docker environment variables (.envfile), never hardcoded. - The
FERNET_KEYsymmetrically encrypts session cookies viacryptography.fernetbefore storing them inAppConfig. Even if the database file is exposed, raw cookie strings are not readable without the key. - SSH keys used by GitHub Actions should be dedicated deploy keys with minimal permissions.
- The
TrustedUsersystem restricts diagnostic commands (logs,debug,train) to explicitly authorized users. - The
data/recsys_artifacts.npzfile is git-ignored and contains only model embeddings — no raw user data.
Built with ❤️ for music lovers.
Powered by discord.py, yt-dlp, spotipy, FFmpeg, Implicit, Gensim, and RapidFuzz.