A fast keyboard-driven Spotify client for the terminal, built with Textual.
Search Spotify, browse your library and playlists, control playback, follow synced lyrics, manage your queue and devices, all from the keyboard.
Status: v0.2.0 (early release). The application includes a responsive multi-panel search dashboard, reliable library loading, automatic table sizing, synchronized lyrics and an extensive regression test suite. APIs and behaviour may still change before 1.0.
- Features
- What's New in v0.2.0
- Requirements
- Installation
- Spotify Setup
- Running
- Search
- Keyboard Shortcuts
- Tests
- Architecture
- Caching
- Limitations
- Security
- Contributing
- License
- Multi-panel search dashboard with dedicated Songs, Artists, Albums and Playlists sections.
- Type-specific search for tracks, artists, albums, playlists, podcasts and episodes.
- Responsive search layout that adapts to large, medium and narrow terminals.
- Playlists with a persistent disk cache, instant startup rendering and safe background refresh.
- Library with liked songs, recently played tracks, saved albums, followed artists, saved podcasts and saved episodes.
- Favorites for liking tracks and following or saving albums, artists, podcasts and episodes directly from the interface.
- Unified saved-state indicators using consistent heart symbols throughout the application.
- Playback control with play, pause, next, previous, seek, volume, mute, repeat and shuffle.
- Synced lyrics with current-line highlighting, background loading and persistent caching.
- Bounded lyrics cache with automatic pruning by entry count and total file size.
- Queue for viewing the current queue and adding selected tracks.
- Devices for listing Spotify devices and transferring playback.
- Responsive tables with automatic column sizing and no manual mouse-driven column resizing.
- Context-aware refresh that updates the active view instead of repeating stale searches.
- Keyboard-driven navigation throughout the interface.
- Built-in help view with keyboard scrolling.
- Responsive welcome screen with layouts adapted to the available terminal size.
- Offline regression suites covering navigation, search, library loading, playback, lyrics, caching and responsive layouts.
Searches without a type prefix now use a categorized dashboard:
- Songs
- Artists
- Albums
- Playlists
Each category has its own panel, selection state and navigation behaviour.
The search dashboard uses a two-level navigation model:
- Select a result category with the arrow keys.
- Press
Enterto enter the selected panel. - Use
UpandDownto navigate its results. - Use
LeftorRightto return to panel selection.
Searches with explicit prefixes still use full single-type result tables.
- Immediate visual feedback when a search begins.
- Existing results remain visible while a new search is running.
- A visible frame title indicates the active search query.
- Long-running searches display an additional waiting state.
- Older results cannot overwrite a newer search.
- Empty results are distinguished from request failures.
- Partial search failures preserve successful result categories.
- Search workers cannot repaint the interface after leaving Search.
- Saved Albums now tolerate unavailable Spotify entries.
- Saved Episodes handle unavailable tombstone objects without displaying fake durations.
- Saved Artists use the same reliable background-loading infrastructure as the other saved-library views.
- Recently Played provides clear loading feedback.
- Errors no longer silently appear as empty libraries.
- Valid cached data remains visible during temporary failures.
- Library responses are protected against stale worker updates.
- Cursor and scroll position are preserved during refreshes where possible.
- Improved disk-cache safety.
- Atomic cache writes.
- Invalid or incomplete responses no longer erase valid playlists.
- Older workers cannot replace newer playlist data.
- Playlist refreshes preserve existing content during temporary API errors.
- Reduced risk of the playlist list disappearing during refresh.
- Fixed liked-state rendering for tracks opened from albums.
- Track hearts are resolved in the background and matched by Spotify track ID.
- Favorite mutations update the relevant row without resetting the table.
- Saved-state visuals now use hearts consistently.
- Removed the old
Ssaved-state column header. - Removed floppy-disk saved indicators.
- Track, album, artist, podcast and episode semantics remain independent internally.
- Redesigned responsive column-width calculation.
- Removed unnecessary horizontal scrolling in normal terminal sizes.
- Removed manual column resizing with the mouse.
- Added automatic width profiles for different table types.
- Improved width distribution for titles, artists, albums, owners, durations and dates.
- Long text is truncated predictably without hiding compact fields.
- Table resize operations preserve cursor, selection and vertical scroll.
- Removed the Source column from standard content views.
- Queue retains Source where it still provides useful context.
- Improved table borders and content framing.
- Removed unnecessary spacer rows.
- Search, Help, the main content area and Now Playing now use a tighter layout.
- Now Playing remains visible across supported terminal sizes.
- Added a hard maximum number of cached lyrics entries.
- Added a maximum cache-file size.
- Older entries are automatically removed when limits are exceeded.
- Lyrics cache files can no longer grow indefinitely.
- Improved metadata updates when the current track changes.
- Previous lyrics no longer remain associated with a newly selected track.
Ctrl+R is now context-aware:
- Lyrics refreshes the current lyrics.
- Search repeats the active search.
- Playlists refreshes the current playlist.
- Library views refresh the active library section.
- Queue and Devices refresh their current data.
- Welcome refreshes the visible playlist list.
- Text left in the Search input no longer hijacks refresh actions elsewhere.
- Python 3.9+. Developed and tested primarily on Python 3.14.
- A Spotify account.
- A Spotify Premium account for playback control, including play, pause, skip, seek, volume, shuffle and device transfer. This is a Spotify Web API restriction.
- A Spotify Developer application for the Client ID, Client Secret and Redirect URI.
- An active Spotify device, such as the desktop application, a phone or a compatible speaker, for playback operations.
Browsing, searching and some library operations may work with a free Spotify account, but playback-control endpoints require Premium.
# 1. Clone the repository
git clone https://github.com/naarvent/Spotify-TUI.git
cd Spotify-TUI
# 2. Create a virtual environment
python -m venv .venv
# 3. Activate it on Linux or macOS
source .venv/bin/activate
# Or activate it on Windows PowerShell
.venv\Scripts\Activate.ps1
# 4. Install the project and its dependencies
pip install -e .pip install -e . installs Spotify-TUI and its dependencies using pyproject.toml.
Alternatively, install only the runtime dependencies:
pip install -r requirements.txtInstalling only requirements.txt does not install the spt-tui console entry point.
- Open the Spotify Developer Dashboard.
- Create a Spotify application.
- Copy its Client ID and Client Secret.
- Add a Redirect URI in the application settings.
- Use the same Redirect URI when configuring Spotify-TUI.
A loopback URI such as the following generally works well:
http://127.0.0.1:8888/callback
Environment variables take precedence over the local configuration file.
Linux and macOS:
export SPOTIPY_CLIENT_ID="your_client_id"
export SPOTIPY_CLIENT_SECRET="your_client_secret"
export SPOTIPY_REDIRECT_URI="http://127.0.0.1:8888/callback"Windows PowerShell:
$env:SPOTIPY_CLIENT_ID = "your_client_id"
$env:SPOTIPY_CLIENT_SECRET = "your_client_secret"
$env:SPOTIPY_REDIRECT_URI = "http://127.0.0.1:8888/callback"You may also enter the credentials inside the application on first launch. They are then stored in the local configuration directory described in the Security section.
On first launch, Spotify-TUI opens the standard Spotify OAuth authorization flow in the browser. After authorization, the resulting token is cached locally and refreshed automatically when possible.
Run the package directly:
python -m spt_tuiOr use the console entry point after installing with pip install -e .:
spt-tuiEnter a normal query without a prefix:
radiohead
Results are displayed in the multi-panel search dashboard:
- Songs
- Artists
- Albums
- Playlists
The dashboard can display up to 20 results in each panel.
Use a prefix to request a single result type:
/TRK radiohead
/ART radiohead
/ALB radiohead
/PLY radiohead
/PDC radiohead
/EPS radiohead
Type-specific searches use a full-width result table and can display more items than the combined dashboard.
These shortcuts reflect the application's current bindings.
| Key | Action |
|---|---|
Up / Down |
Move through sections, lists or rows |
Left / Right |
Move between the menu and the active view |
Enter |
Open, select or play the focused item |
/ |
Focus the search input |
Escape |
Return to the main menu |
? / F1 |
Open or close Help |
Ctrl+Q |
Quit |
The search dashboard has two navigation levels.
| Key | Action |
|---|---|
| Arrow keys | Select Songs, Artists, Albums or Playlists |
Tab / Shift+Tab |
Cycle through available result panels |
Enter |
Enter the selected panel |
Left from a left-side panel |
Return to the main menu |
Escape |
Return to the main menu |
| Key | Action |
|---|---|
Up / Down |
Move through results |
Enter |
Play or open the selected result |
Left / Right |
Return to panel selection |
f |
Toggle favorite for a supported result |
Escape |
Return to the main menu |
| Key | Action |
|---|---|
Space |
Play or pause |
n |
Next track |
p |
Previous track or restart |
r |
Toggle repeat |
Ctrl+S |
Toggle shuffle |
- / + |
Volume down or up |
m |
Mute or unmute |
Ctrl+Left / Ctrl+Right |
Seek backward or forward |
< |
Open seek and volume settings |
| Key | Action |
|---|---|
f |
Like, unlike, follow or save the selected item |
l |
Toggle the lyrics view |
c |
Add the selected track to the queue |
Ctrl+C |
Open Queue |
d |
Open Devices |
Ctrl+L |
Toggle multi-add mode |
Ctrl+Shift+P |
Add selected tracks to a playlist |
Ctrl+T |
Import a playlist |
Ctrl+D |
Delete or remove the selected item |
Ctrl+R |
Refresh the active view |
While Help is open:
| Key | Action |
|---|---|
Up / Down |
Scroll |
PageUp / PageDown |
Scroll by page |
Home / End |
Jump to the beginning or end |
Escape |
Close Help |
The test suites are standalone Python scripts. No external test framework is required.
Each test script exits with status 0 only when all its checks pass.
Run a single suite:
python tests/test_spotify_client.pyRun all suites on Linux or macOS:
for f in tests/test_*.py; do
python "$f" || echo "FAILED: $f"
doneRun all suites on Windows PowerShell:
Get-ChildItem tests/test_*.py | ForEach-Object {
python $_.FullName
}The regression suites use fake Spotify clients and deterministic Textual test pilots. Network operations are mocked, allowing the primary behaviour to be tested offline.
Some concurrency tests depend on event-loop and thread scheduling and may occasionally require a retry under unusually heavy CPU load.
Current coverage includes:
- Spotify client behaviour
- Search loading and result ordering
- Multi-panel search navigation
- Search limits and partial failures
- Library pagination and cache behaviour
- Playlist refresh protection
- Favorites and saved-state synchronization
- Album-track liked state
- Lyrics lookup and bounded caching
- Queue and device views
- Responsive tables
- Table titles and content frames
- Recently Played loading feedback
- Welcome responsiveness
- Now Playing spacing
- Application teardown
- Worker error handling
Spotify-TUI started as a single approximately 6,700-line Python file containing one large application class.
The project was later refactored, with behaviour preserved, into a package organized by responsibility:
Spotify-TUI/
├── .gitignore
├── LICENSE
├── README.md
├── pyproject.toml
├── requirements.txt
├── spt_tui/
│ ├── __init__.py Package metadata
│ ├── __main__.py python -m spt_tui and spt-tui entry point
│ ├── config.py Paths, logging and credentials
│ ├── constants.py Welcome content, glyphs and library entries
│ ├── spotify_client.py Spotify API wrapper and rate limiter
│ ├── widgets.py Help scrolling and multi-panel search widgets
│ └── app/
│ ├── __init__.py SptPy composition and Textual CSS
│ ├── core.py Layout, mounting and top-level event routing
│ ├── library.py Playlists, saved library and recently played
│ ├── lyrics_view.py Synced lyrics, rendering and persistent cache
│ ├── navigation.py Keyboard, focus and section navigation
│ ├── playback.py Playback controls, favorites and Now Playing
│ ├── queue_devices.py Queue and device management
│ ├── search.py Search workers, feedback and result dashboard
│ └── tables.py Table construction, widths and saved-state columns
└── tests/
├── test_album_likes.py
├── test_devices.py
├── test_favorites_p5.py
├── test_favorites_p6.py
├── test_favorites_p7.py
├── test_help_scroll.py
├── test_library_load.py
├── test_lyrics.py
├── test_lyrics_cache.py
├── test_lyrics_cleanup.py
├── test_narrowed_exceptions.py
├── test_now_playing_spacing.py
├── test_playback_sync.py
├── test_playlist_refresh.py
├── test_recent_and_titles.py
├── test_refresh_routing.py
├── test_responsive_tables.py
├── test_saved_hearts.py
├── test_search_feedback.py
├── test_search_grid.py
├── test_search_limits.py
├── test_search_loading.py
├── test_spotify_client.py
├── test_table_scroll.py
├── test_teardown.py
├── test_tui_flows.py
├── test_welcome_responsive.py
└── test_worker_error_handling.py
SptPy is composed through multiple inheritance from focused mixins. At runtime, the application is still a single Textual App subclass, while its implementation is separated into modules by responsibility.
The 28 standalone regression suites protect application behaviour introduced during and after the refactor.
Spotify-TUI uses multiple cache strategies depending on the type of data.
- Stored on disk.
- Painted immediately during startup.
- Refreshed in the background.
- Written atomically.
- Protected against invalid, stale or unexpectedly empty responses.
- Cached in memory for the current session.
- Refreshed in the background.
- Protected by view tokens and worker generations.
- Treated as volatile data.
- Refreshed directly instead of being persisted as a long-lived cache.
- Shows immediate loading feedback while new data is being retrieved.
- Stored in a persistent local cache.
- Bounded by entry count.
- Bounded by total serialized size.
- Older entries are removed automatically when limits are exceeded.
- Temporary network failures are not stored as permanent missing results.
- Spotify Web API access and a valid Spotify Developer application are required.
- Playback control requires Spotify Premium and an active device.
- Lyrics depend on a third-party lyrics provider and may not be available for every track.
- Spotify may return unavailable or incomplete library objects for removed albums, episodes or regional content.
- Behaviour may be affected by upstream changes in Spotify, Spotipy or Textual.
- Spotify-TUI is still a
0.xproject. Interfaces and keyboard bindings may change before1.0.
- Never commit the Spotify Client Secret, OAuth tokens or token-cache files.
- The repository
.gitignoreexcludes common credentials, token caches, logs and local workspace files. - Runtime credentials, token caches and logs are stored outside the repository under:
~/Documents/naarvent's projects/Spotify_TUI/
The directory may contain:
spt_config.json
.cache_spotify_token
spt_py_textual_spotify.log
lyrics_cache.json
playlists_cache.json
spt_config.jsonstores the Spotify Client ID, Client Secret and Redirect URI when credentials are entered through the interface..cache_spotify_tokenstores the Spotify OAuth token.- Log files may contain technical diagnostics.
- Cache files may contain Spotify metadata.
- Keep the configuration directory private.
- Prefer environment variables when possible.
Issues and pull requests are welcome.
Before opening a pull request:
- Keep changes focused.
- Preserve existing keyboard and navigation behaviour unless the change explicitly modifies it.
- Add regression coverage for bug fixes and new behaviour.
- Run every standalone test suite.
- Run the static checks.
Recommended static checks:
python -m compileall -q spt_tui
python -m pyflakes spt_tui
python -m vulture spt_tui --min-confidence 80
git diff --checkReleased under the MIT License.
Copyright (c) 2026 naarvent_
