Bumblebee is a full-scale project with two major components:
bumblebee-bootstrap/— Automated Jellyfin library bootstrap for disorganized MP3 collectionsbumblebee/— Interactive "speak like Bumblebee" engine (CLI + Web UI) for lyric-based song search, audio snippet playback, and MP3 export
Take a folder of disorganized MP3s (unknown filenames, missing/wrong metadata, no album art) and produce a fully structured, metadata-enriched library ready for Jellyfin with the lyrics plugin.
- Audio fingerprinting (AcoustID/Chromaprint) — identifies songs regardless of filename/tags
- Metadata enrichment from MusicBrainz + lrclib.net
- Album art fetching and embedding
- Directory restructuring:
Artist/Album/01 - Title.mp3 - LRC lyrics download from lrclib.net (same source as the Jellyfin plugin)
- Jellyfin Docker Compose setup with auto-install of lyrics plugin
- One-command operation:
uv run bootstrap.py /path/to/mp3s
bumblebee-bootstrap/
├── pyproject.toml # uv project, dependencies
├── uv.lock # pinned deps
├── README.md
├── docker-compose.yml # Jellyfin server + plugin setup
├── bootstrap.py # main entry point (argparse CLI)
├── bootstrap/
├── __init__.py
├── fingerprint.py # Chromaprint/AcoustID integration
├── metadata.py # MusicBrainz metadata enrichment
├── lyrics.py # lrclib.net LRC download
├── organize.py # Directory restructuring + file ops
├── jellyfin.py # Docker compose, plugin install, library scan
├── console.py # Rich-based progress UI
└── utils.py # helpers, path sanitization, etc.
# models.py — shared dataclasses
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass
class TrackInfo:
source_path: Path # original file location
fingerprint: str # chromaprint fingerprint
acoustid_id: Optional[str] # AcoustID track ID
mb_recording_id: Optional[str] # MusicBrainz recording ID
mb_release_id: Optional[str] # MusicBrainz release (album) ID
title: Optional[str]
artist: Optional[str]
album: Optional[str]
album_artist: Optional[str]
track_number: Optional[int]
disc_number: Optional[int]
year: Optional[int]
genre: Optional[str]
duration_ms: int
album_art_url: Optional[str]
has_lyrics: bool = False
dest_path: Optional[Path] = None # computed destination
@dataclass
class LibraryConfig:
source_dir: Path
output_dir: Path # organized library root
jellyfin_data_dir: Path # Jellyfin config/data volume
musicbrainz_enabled: bool = True
lyrics_enabled: bool = True
album_art_enabled: bool = True
copy_mode: bool = True # True=copy, False=move- Function: Generate Chromaprint fingerprints, query AcoustID API
- Dependencies:
pyacoustid(or subprocessfpcalc) - Key functions:
fingerprint_file(path: Path) -> str— generate fingerprintlookup_fingerprint(fingerprint: str, duration: float) -> dict— query AcoustID
- AcoustID API:
https://api.acoustid.org/v2/lookup - Rate limiting: built-in, max 3 req/sec (free tier)
- Function: Enrich TrackInfo from MusicBrainz
- Dependencies:
musicbrainzngs - Key functions:
enrich_from_musicbrainz(track: TrackInfo) -> TrackInfofetch_album_art(mbid: str) -> Optional[bytes]embed_metadata(path: Path, track: TrackInfo, art: Optional[bytes])
- Uses: mutagen (ID3/MP3 metadata writing), requests (album art download)
- Function: Download synced lyrics (.lrc) from lrclib.net
- API:
https://lrclib.net/api/search?q={artist}+{title}or?track_name={}&artist_name={}&album_name={}&duration={} - Key functions:
search_lyrics(artist: str, title: str, duration: int, album: Optional[str]) -> Optional[dict]download_lrc(track: TrackInfo, output_dir: Path) -> Optional[Path]
- Output:
.lrcfile saved alongside MP3 (Jellyfin convention: same name,.lrcextension)
- Function: Directory restructuring and file operations
- Key functions:
compute_destination(track: TrackInfo, root: Path) -> Pathsanitize_filename(name: str) -> str— remove/replace illegal charsorganize_tracks(tracks: list[TrackInfo], config: LibraryConfig) -> list[TrackInfo]
- Naming:
Artist Name/Album Name/01 - Track Title.mp3 - Compilations:
Various Artists/Album Name/...
- Function: Docker Compose management, plugin setup, library scanning
- Key functions:
ensure_docker_compose(config: LibraryConfig)— write docker-compose.ymlstart_jellyfin(config: LibraryConfig)— docker compose up -dinstall_lyrics_plugin(config: LibraryConfig)— API call to install plugintrigger_library_scan(config: LibraryConfig)— API call
- Docker Image:
jellyfin/jellyfin:latest - Plugin Repo:
https://raw.githubusercontent.com/Felitendo/jellyfin-plugin-lyrics/master/manifest.json - Ports: 8096 (HTTP)
- Function: Rich-based TUI for progress display
- Dependencies:
rich - Components: Progress bars, status panels, file count stats, spinner
def main():
# argparse: source_dir, output_dir, --jellyfin-data, --move, --skip-fingerprint, etc.
# 1. Discover all MP3 files (recursive)
# 2. Fingerprint each file
# 3. Lookup AcoustID -> MusicBrainz metadata
# 4. Download lyrics from lrclib.net
# 5. Download album art, embed metadata
# 6. Restructure into Artist/Album/ format
# 7. Write docker-compose.yml, start Jellyfin
# 8. Install lyrics plugin, trigger scandependencies = [
"mutagen>=1.47",
"pyacoustid>=1.3",
"musicbrainzngs>=0.7",
"requests>=2.31",
"rich>=13.0",
"pillow>=10.0",
]Interactive "speak like Bumblebee" engine. Search your music library by lyrics, find the exact line, play or export audio snippets. Uses the LRC files and MP3s from Part 1.
bumblebee/
├── pyproject.toml # uv project
├── uv.lock
├── README.md
├── bumblebee-cli.py # CLI entry point
├── bumblebee/
│ ├── __init__.py
│ ├── __main__.py
│ ├── search.py # Full-text lyric search engine
│ ├── lrc_parser.py # LRC file parser (timestamps -> lyrics)
│ ├── audio.py # pydub-based audio slicing + playback
│ ├── db.py # SQLite FTS index builder + querier
│ ├── tui.py # Rich-based interactive TUI
│ ├── models.py # dataclasses
│ └── utils.py
└── web/ # Vite/React/Tailwind web app
├── package.json
├── vite.config.ts
├── tsconfig.json
├── tailwind.config.js
├── index.html
└── src/
├── main.tsx
├── App.tsx
├── index.css
├── types.ts
├── components/
│ ├── SearchBar.tsx
│ ├── ResultsList.tsx
│ ├── LyricViewer.tsx
│ ├── AudioPlayer.tsx
│ ├── SnippetExporter.tsx
│ └── Navbar.tsx
└── hooks/
├── useAudio.ts
└── useSearch.ts
# models.py
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass
class LyricLine:
timestamp_ms: int # e.g., 12340 for 00:12.340
text: str
@property
def timestamp_str(self) -> str:
minutes = self.timestamp_ms // 60000
seconds = (self.timestamp_ms % 60000) // 1000
centis = (self.timestamp_ms % 1000) // 10
return f"{minutes:02d}:{seconds:02d}.{centis:02d}"
@dataclass
class Song:
id: str # sha256 of file path
file_path: Path
title: str
artist: str
album: str
duration_ms: int
lrc_path: Optional[Path]
@dataclass
class Match:
song: Song
matched_line: LyricLine
context_before: list[LyricLine] # 2 lines before
context_after: list[LyricLine] # 2 lines after
match_score: float # 0.0 - 1.0- Function: Parse
.lrcfiles into structured LyricLine objects - Format:
[mm:ss.xx]Lyric textor[mm:ss.xxx](3-digit centis) - Key functions:
parse_lrc(path: Path) -> list[LyricLine]get_line_at_timestamp(lines: list[LyricLine], ts_ms: int) -> Optional[LyricLine]timestamp_to_ms(ts: str) -> int—[01:23.45]-> 83450
- Function: SQLite FTS5 full-text search index
- Key functions:
build_index(library_paths: list[Path])— scan all LRC files, populate FTSsearch(query: str, limit: int = 20) -> list[Match]— FTS queryget_song_lyrics(song_id: str) -> list[LyricLine]
- Schema:
CREATE VIRTUAL TABLE lyrics_fts USING fts5( lyric_text, artist, title, album, song_id ); CREATE TABLE songs ( id TEXT PRIMARY KEY, path TEXT, title TEXT, artist TEXT, album TEXT, duration_ms INT );
- Function: Fuzzy lyric search with fallbacks
- Dependencies:
rapidfuzz(faster than thefuzz) - Key functions:
search(query: str, db_path: Path) -> list[Match]search_with_fallbacks(query: str, db_path: Path) -> list[Match]- Try exact FTS match
- Try fuzzy match on individual lines (rapidfuzz.partial_ratio)
- Try word-by-word narrowing (match any word, score by coverage)
- Return best matches sorted by score
- Scoring: Combines FTS rank + fuzzy ratio + word coverage
- Function: Audio snippet extraction and playback
- Dependencies:
pydub,ffmpeg(system dependency) - Key functions:
play_snippet(file_path: Path, start_ms: int, end_ms: int)— playbackexport_snippet(file_path: Path, start_ms: int, end_ms: int, output: Path)— MP3 exportget_preview_path(file_path: Path) -> Path— cached preview
- Playback: Uses
pydub.playback(simpleaudio/pyaudio) for CLI - Export: pydub
.export(format="mp3")with proper bitrate
- Function: Rich interactive terminal UI
- Dependencies:
rich,textual(optional upgrade) - Screens:
- Search: Input prompt for lyric query
- Results: Table with Song, Artist, Matched Line, Score
- Lyric View: Full lyrics with line numbers, highlight match
- Action Menu: Play snippet / Export snippet / Select range
- Interactive flow:
> Enter lyrics to search: gonna be okay Results: # Song Artist Matched Line Score ───────────────────────────────────────────────────────────────────────── 1 Just Dance Lady Gaga "Gonna be okay" 98% 2 Poker Face Lady Gaga "...gonna be alright..." 72% > Select result (number): 1 Just Dance (feat. Colby O'Donis) — Lady Gaga ───────────────────────────────────────────────── 38 [01:23.45] Gonna be okay 39 [01:25.12] Da-da-da-dance, dance, dance 40 [01:28.00] Just, j-j-just dance > Select line number or range (e.g., 38 or 38-40): 38-39 [P]lay snippet | [E]xport MP3 | [B]ack | [Q]uit > Action: e > Export filename [bumblebee_snippet.mp3]: ✓ Exported: ./bumblebee_snippet.mp3 (3.2s, 128kbps)
SearchBar.tsx
- Large centered input with autocomplete
- Debounced 300ms search
- History dropdown
ResultsList.tsx
- Expandable cards per result
- Shows matched line + context
- Click to expand full lyrics
LyricViewer.tsx
- Full lyric display with line numbers
- Matched line highlighted in amber
- Click line to set start, shift+click to set end
- Visual range indicator
AudioPlayer.tsx
- HTML5
<audio>element with custom controls - Waveform visualization (Web Audio API + Canvas)
- Playhead synced to lyrics (auto-scroll)
- Play selected range only
SnippetExporter.tsx
- Export dialog: filename, quality (128/192/320 kbps)
- Progress indicator
- Download link on completion
- Backend: The web UI communicates with a local Python FastAPI server (or uses the CLI as a subprocess)
- Endpoints:
GET /api/search?q={query}-> list of matchesGET /api/song/{id}/lyrics-> full lyricsPOST /api/play-> stream audio snippetPOST /api/export-> download MP3 snippet
CLI/Python:
dependencies = [
"pydub>=0.25",
"rich>=13.0",
"rapidfuzz>=3.0",
"requests>=2.31",
"fastapi>=0.100", # for web API server
"uvicorn>=0.23", # ASGI server
]Web:
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0",
"tailwindcss": "^3.4.19",
"wavesurfer.js": "^7.0.0",
"lucide-react": "^0.400.0"
},
"devDependencies": {
"vite": "^7.2.4",
"typescript": "^5.3.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
}
}# 1. Install uv (one-liner)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. Clone bumblebee
git clone https://github.com/CommanderTurtle/bumblebee bumblebee
cd bumblebee
# 3. Run bootstrap — organizes MP3s, sets up Jellyfin
uv run bumblebee-bootstrap/bootstrap.py \
--source ~/Downloads/my_mp3s \
--output ~/Music/Library \
--with-jellyfin
# 4. Jellyfin is now running at http://localhost:8096
# Lyrics plugin installed, library scanned
# 5. Run Bumblebee CLI
uv run bumblebee/bumblebee-cli.py \
--library ~/Music/Library
# 6. Or run Bumblebee Web UI
uv run --package fastapi uvicorn bumblebee.web_api:app --port 8000
cd bumblebee/web && npm install && npm run dev
# Open http://localhost:5173# Skip bootstrap, just point to existing library with LRC files
uv run bumblebee/bumblebee-cli.py --library ~/Music> Search lyrics: gonna be okay
Result: Just Dance — Lady Gaga
Line 38: "Gonna be okay" (01:23)
Line 39: "Da-da-da-dance, dance, dance" (01:25)
> Select range: 38-39
> Action: export
> Filename: gonna_be_okay.mp3
Exported: 2.1s snippet at ~/bumblebee_exports/gonna_be_okay.mp3
# Chain multiple snippets into a message:
> Chain mode: gonna be okay + don't stop believin + we are the champions
[exports concatenated MP3 with crossfade]
- Portability:
uvfor Python (no pip, no venv activation),bunoptional for web - Minimal commands: Every workflow is ≤3 commands
- Fingerprint-first: Never rely on existing metadata for identification
- Rich interactive UI: Both CLI (Rich TUI) and Web (React) are first-class
- Professional export: High-quality MP3 exports with proper encoding