A CLI audio looper with a real-time FFT visualizer, startup screen, default history browser, favorites, fullscreen mode, and online URL support.
looper plays audio in a terminal UI built with ratatui.
It supports:
- local audio files
- YouTube URLs
- SoundCloud URLs
- HypeM URLs
- single tracks and playlists
- infinite looping for single tracks
- whole-playlist looping for playlists
- pause / resume
- fullscreen visualizer
- centered ASCII startup/loading screen with cheeky boot logs
- default no-arg startup into playlist history
- SQLite-backed playback history and favorites
- remote download/loading UI with progress, speed, and ETA
- small source badges in the TUI for supported services (
YT,SC,HM) - animated terminal/tab title with playback, pause, and loading status
Fresh install:
brew tap program247365/tap
brew install looperUpgrade an existing install:
brew update
brew upgrade program247365/tap/loopergit clone https://github.com/program247365/looper.git
cd looper
make installRequires Rust. Install via rustup if needed.
Remote URL playback depends on:
yt-dlpffmpeg
Install them with Homebrew:
brew install yt-dlp ffmpegIf YouTube playback starts failing with 403 errors, update yt-dlp first.
looperThis opens the playlist history browser with no active playback. Press Enter on a row to start playing it.
If you want to skip the browser and jump straight into playback, use looper play --url ....
looper play --url "/path/to/your/song.mp3"looper play --url "https://www.youtube.com/watch?v=xAR6N9N8e6U"looper play --url "https://soundcloud.com/odesza/line-of-sight-feat-wynne-mansionair"looper play --url "https://hypem.com/track/2gq0d/CHVRCHES+-+Clearest+Blue"looper play --url "https://www.youtube.com/playlist?list=PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI"- startup opens the local SQLite database, runs embedded migrations, and then begins loading playback
yt-dlpextracts track metadata and media URLs- remote audio is cached locally (see Data and Cache Locations)
- uncached remote tracks show a full-screen loading scene before playback
- single tracks loop forever
- playlists play each track once, then loop the entire playlist
- background prefetch caches upcoming playlist tracks when possible
Current behavior is intentionally pragmatic:
- YouTube uses a download-first cached path for reliability
- SoundCloud and HypeM prefer a stream-first path and fall back to cached download when needed
Remote tracks are cached locally after download:
| Platform | Cache directory |
|---|---|
| macOS | ~/Library/Caches/sh.kbr.looper/ |
| Linux | ~/.cache/looper/ |
Playback history and favorites live in a SQLite database:
| Platform | Database path |
|---|---|
| macOS | ~/Library/Application Support/sh.kbr.looper/looper.sqlite3 |
| Linux | ~/.local/share/looper/looper.sqlite3 |
- startup applies pending embedded migrations automatically
- bare
looperloads this history first and lets you replay from it - history is tracked per playable URL or canonical local file path
- each track stores title, platform, favorite state, last played timestamp, play count, and cumulative time played
| Key | Action |
|---|---|
Enter |
Replay the selected track from the default history browser |
Space |
Pause / Resume |
f |
Toggle fullscreen visualizer |
s |
Toggle favorite for the currently playing track |
p |
Toggle the played-songs panel |
Cmd-P |
Attempt to toggle the played-songs panel when the terminal forwards the modifier |
q / Ctrl-C |
Quit |
Bare looper opens directly into playlist history. During playback, the played-songs panel is hidden by default and opens over the minimal UI.
| Key | Action |
|---|---|
j / k |
Move selection down / up |
h / l |
Change sort field |
r |
Reverse sort direction |
s |
Toggle favorite for the selected row |
Enter |
Replay the selected track |
p / Esc |
Close the panel |
Sort fields:
- time played
- last played
- platform
- title
- times played
make run # play fixture file (tests/fixtures/sound.mp3)
make test # run tests
make build # debug build
make build-release # optimized release binaryUseful direct commands:
cargo build
cargo build --release
cargo test- Public online URLs work best. Private, age-restricted, or members-only content may still fail depending on
yt-dlpaccess. - If a YouTube watch URL includes both
v=andlist=,loopercurrently normalizes it toward single-video playback unless you use the playlist URL directly. - The remote loading UI is designed to hand off into playback cleanly rather than waiting on a full silent download.
- The startup screen and loading copy are intentionally a little cheeky.
make release-patch
make release-minor