notes

spofy manual

First published: Last updated: 9104 words · 3 lines of code

Overview

spofy is a full-featured Spotify client that runs entirely inside Emacs. It communicates with the Spotify Web API to provide playback control, search, browsing, playlist management, and library management without leaving the editor.

The package targets Emacs 30 or later and depends on the transient library (available on GNU ELPA) for its command popup. Optional integrations are provided for consult (minibuffer completion), embark (contextual actions), org-mode (link storage and export), and Wikipedia (album and musical work lookup).

spofy is organized as a multi-file package. The main entry point is the spofy command, which opens a dashboard buffer showing the current track, recently played tracks, and the user’s playlists. From there, the user can search for content, browse albums and artists, manage playlists, and control playback. A transient popup (Transient popup) provides quick access to all commands from any buffer.

The core capabilities are:

  • Authentication: OAuth2 authorization code flow with a local HTTP callback server and persistent token storage with optional GPG encryption (Authentication).
  • Playback control: Play, pause, skip, seek, adjust volume, toggle shuffle and repeat. Timer-based polling keeps the UI in sync with the player state (Playback control).
  • Search: Dedicated commands for searching tracks, albums, artists, and playlists, each displaying results in a tabulated-list buffer (Search).
  • Browsing: Detail views for albums, artists, and playlists with contextual keybindings (Browsing).
  • Playlist management: Full CRUD operations on playlists: create, rename, delete, add, remove, and reorder tracks (Playlist management).
  • Library management: View and manage saved tracks and albums; follow and unfollow artists (Library management).
  • Timeline: A unified buffer showing recently played tracks, the now-playing track, and the upcoming queue in one view, with row actions for each (Timeline).
  • Mode-line display: A configurable mode-line segment showing the currently playing track (Mode-line display).
  • Playback tracking: Jump to the currently playing track in any buffer, optional cursor-follows-playback mode, and a custom buffer mode-line showing entity name, playback state, and track position (Tracking the playing position).
  • Tab-bar display: A configurable tab-bar segment showing the currently playing track (Tab-bar display).
  • Wikipedia lookup: Opens the Wikipedia article for the currently playing album or, for classical music, the underlying musical work. Classical detection uses Spotify’s genre metadata; work identification uses an LLM via gptel (Wikipedia integration).

Getting started

Before using spofy, you must register a Spotify application:

  1. Go to the Spotify Developer Dashboard and create a new application.
  2. In the application settings, add http://127.0.0.1:8080/spofy/callback as a redirect URI (adjust the port if you change spofy-redirect-port).
  3. Note the Client ID and Client Secret.

Then configure spofy in your init file:

(setq spofy-client-id "your-client-id")
(setq spofy-client-secret "your-client-secret")

Run M-x spofy-authenticate to authorize spofy with your Spotify account. This opens your browser to the Spotify authorization page. After granting access, the browser redirects to the local server and spofy stores the tokens and their expiry time in spofy-token-file (defaults to spofy-tokens.el in the user Emacs directory, with restrictive file permissions). Subsequent sessions reuse the stored tokens automatically, refreshing only when the persisted expiry time indicates the access token has lapsed.

Once authenticated, run M-x spofy to open the dashboard.

The development repository is on GitHub.

User options

Authentication

The user option spofy-client-id holds the Client ID of your registered Spotify application. It defaults to nil. Without this value, spofy cannot communicate with the Spotify API. Obtain it from the Spotify Developer Dashboard (Getting started).

The user option spofy-client-secret holds the Client Secret of your registered Spotify application. It defaults to nil. Like the client ID, this is required for the OAuth2 token exchange.

The user option spofy-redirect-port controls the port on which spofy’s temporary OAuth2 callback server listens. It defaults to 8080. Change this if port 8080 is already in use on your machine. The redirect URI registered in the Spotify Developer Dashboard must match the port you configure here.

The user option spofy-token-file specifies where spofy stores OAuth tokens and the access-token expiry time. It defaults to spofy-tokens.el in the user Emacs directory. Tokens are stored as a Lisp alist and the file is written with restrictive permissions (mode 600) so that only the file owner can read it. For additional security, use a .gpg extension (e.g. spofy-tokens.el.gpg) to have Emacs encrypt the file automatically via EasyPG; this requires a configured GPG key. When writing to a .gpg file, spofy binds epa-file-select-keys to silent to suppress the interactive GPG recipient selection dialog.

Playback

The user option spofy-poll-interval sets the number of seconds between player state polls. It defaults to 5. Lower values make the UI more responsive to changes (e.g. track transitions) at the cost of more API requests. The Spotify API does not provide push notifications, so polling is the only way to detect state changes.

The user option spofy-seek-seconds controls how many seconds spofy-seek-forward and spofy-seek-backward jump. It defaults to 10.

The user option spofy-volume-step sets the percentage increment for spofy-volume-up and spofy-volume-down. It defaults to 5. Valid volume values range from 0 to 100.

Mode-line

The user option spofy-mode-line-format is a format string that controls what the mode-line segment displays. It defaults to "▶ %t — %a". The following specifiers are available:

SpecifierMeaning
%tTrack name
%aArtist name
%bAlbum name
%pPlay/pause icon (⏸ when playing, ▶ when paused)
%sShuffle indicator (⇌ when on, empty when off)
%rRepeat indicator (↻ for context, ↻₁ for track, empty)

The user option spofy-mode-line-max-length sets the maximum character length of the mode-line string. It defaults to 40. Longer strings are silently truncated. Increase this if you have a wide frame and want to see more of the track and artist names.

The user option spofy-mode-line-separator is the string inserted before the spofy segment in the mode line. It defaults to a single space.

Tab-bar

The user option spofy-tab-bar-format is a format string that controls what the tab-bar segment displays. It defaults to "%t %a %s%r %G", which shows the track and artist, shuffle and repeat indicators, and a progress bar. The same format specifiers as spofy-mode-line-format are supported (Mode-line), plus two additional specifiers: %G inserts a progress bar, and %T inserts the timestamps (e.g. “1:23 / 3:45”).

The user option spofy-tab-bar-progress-width sets the width in characters of the progress bar in the tab bar. It defaults to 15. This option is only relevant when the format string contains %G.

The tab-bar segment dynamically truncates the text portion (track, artist, album names) to fit the available frame width. The segment measures the combined width of all other tab-bar items (tabs, alignment spacers, global mode string) and allocates the remaining columns to the text; status indicators and the progress bar are never truncated. When the text is truncated, a fade-out gradient progressively blends the last three characters toward the tab-bar background, matching the truncation style used in spofy list buffers.

The user option spofy-tab-bar-max-length provides an optional upper bound. It defaults to nil (dynamic only). When set to an integer, the segment is capped at that many columns even when more space is available.

The user option spofy-tab-bar-alignment controls whether the spofy segment appears on the left or right side of the tab bar. It accepts the symbols left (default) and right. When set to left, the segment is inserted before any tab-bar-format-align-right entry, keeping it on the left side. When set to right, the segment is placed after tab-bar-format-align-right, inserting that alignment entry automatically if it is not already present. Set this option before enabling spofy-tab-bar-mode (Tab-bar display).

Global key and mode-line activation

The user option spofy-global-key sets the prefix key that spofy-global-mode binds to spofy-menu. It defaults to "C-c s". Change this if the default conflicts with other bindings.

The user option spofy-enable-mode-line controls whether spofy-global-mode automatically enables spofy-mode-line-mode. It defaults to t. Set it to nil if you prefer a clean mode line and want to access now-playing information only through the dashboard or transient popup.

The user option spofy-enable-tab-bar controls whether spofy-global-mode automatically enables spofy-tab-bar-mode. It defaults to nil. Set it to t to display the currently playing track in the tab bar (Tab-bar display).

The two display modes are mutually exclusive: when spofy-enable-tab-bar is non-nil, it takes precedence and the mode-line display is not enabled, regardless of spofy-enable-mode-line. This avoids duplication because Emacs renders global-mode-string (where the mode-line segment lives) in the tab bar via tab-bar-format-global, which would otherwise cause the track information to appear twice.

The user option spofy-dashboard-sections controls which sections appear on the dashboard and in what order. It defaults to (recently-played playlists). Each element is a symbol naming a section. Available sections:

recently-played
Last 10 tracks played.
playlists
First 10 user playlists.
top-tracks-short
Top tracks from the last 4 weeks.
top-tracks-medium
Top tracks from the last 6 months.
top-tracks-long
Top tracks from the last year.
top-artists-short
Top artists from the last 4 weeks.
top-artists-medium
Top artists from the last 6 months.
top-artists-long
Top artists from the last year.
new-releases
New album releases (not personalized).

The user option spofy-dashboard-album-art controls whether album artwork is displayed in the now-playing section. It defaults to t. When enabled, the album cover appears between the track information (track, artist, album) and the player controls (progress bar, shuffle/repeat state). Album art is fetched asynchronously from the Spotify API and cached on disk under XDG_CACHE_HOME/spofy/album-art/ (defaulting to ~/.cache/spofy/album-art/). The image updates automatically when the track changes. This option has no effect in terminal frames, since Emacs cannot display images there.

The now-playing section always appears at the top regardless of this option. Each section is fetched asynchronously and includes a “View all” link that opens a full tabulated-list buffer. Track and artist names in every section are truncated to the window width with a fade-out gradient when they would otherwise overflow. The truncation accounts for the scaled font size of the now-playing faces so that text does not wrap even at larger display heights.

Column widths

Variable-width columns are sized dynamically so that all columns together fill the window. The user option spofy-columns controls the proportional weights of these columns. Each entry maps a view symbol to a list of weights; columns are allocated window space in proportion to their weight. Fixed-width columns such as indicators, duration, year, and track counts are not affected. Column widths are recomputed automatically when the window is resized. The default weights are:

ViewColumns (weights)
library-trackName (35), Artist(s) (25), Album (25)
library-albumName (35), Artist(s) (25)
search-trackName (35), Artist(s) (25), Album (25)
search-albumName (35), Artist(s) (25)
search-artistName (35), Genres (30)
search-playlistName (35), Owner (20)
album-trackName (40), Artist(s) (25)
artist-albumName (40)
artist-top-trackName (35), Album (25)
playlist-trackName (30), Artist(s) (20), Album (20)
playlist-listName (35), Owner (20)

The function spofy-ui-compute-format takes a view symbol and a list of column specs, where each spec is (NAME WIDTH SORT . PROPS). When WIDTH is the keyword :flex, the column width is computed proportionally from spofy-columns. The convenience function spofy-ui-set-format calls spofy-ui-compute-format and stores the recipe so that column widths are recomputed on window resize. It also installs the custom buffer mode-line (Buffer mode-line). The function spofy-ui-col returns the computed width for a given view and flex column index.

Commands

Authentication

The command spofy-authenticate initiates the Spotify OAuth2 authorization flow. It starts a temporary HTTP server on spofy-redirect-port, opens the user’s default browser to Spotify’s authorization page, and waits for the redirect callback. The server silently ignores non-OAuth requests (such as the browser’s favicon.ico request) and only processes callbacks that carry the expected query parameters. Once the user grants access, spofy exchanges the authorization code for access and refresh tokens, stores them along with the expiry time in spofy-token-file, and shuts down the temporary server.

After successful authentication, spofy runs spofy-authenticated-hook. You only need to authenticate once; subsequent sessions reuse the stored refresh token to obtain new access tokens automatically.

Any spofy command that requires an active session will prompt for authentication if no valid tokens are found, so you do not need to call spofy-authenticate or spofy explicitly before using other entry points such as the transient menu.

Dashboard and global control

The command spofy is the main entry point. It ensures that the user is authenticated (prompting if not), starts player state polling, and opens the *spofy* dashboard buffer. The dashboard always shows a now playing section at the top (track name, artist, and album with release year each on its own line, then a progress bar, then shuffle/repeat state on the left with elapsed/total timestamps right-aligned to the progress bar). The artist and album lines are clickable: clicking an artist name opens that artist via spofy-view-artist, and clicking the album line opens that album via spofy-view-album. Below it, a configurable list of sections is displayed according to spofy-dashboard-sections (Global options). By default, the dashboard shows recently played tracks and user playlists. Each section includes a “View all” link that opens the full list in a tabulated-list buffer. Use the transient popup (Transient popup) to discover available keybindings.

The now-playing section refreshes automatically when the player state changes. A 1-second timer interpolates the progress bar between polls so it animates smoothly. The recently-played section refreshes automatically when the current track changes. Because the Spotify API takes a few seconds to register a finished track in the me/player/recently-played endpoint, the refresh is delayed by spofy-poll-interval seconds to avoid fetching stale data. The command spofy-dashboard-refresh forces a full refresh of the entire dashboard.

The command spofy-stop stops player state polling and disables spofy-global-mode. Use it when you are done with spofy and want to stop all background activity.

The dashboard keymap provides these bindings:

KeyCommand
SPCspofy-play-pause
nspofy-next
pspofy-previous
/spofy-menu
gspofy-dashboard-refresh
qquit-window

Transient popup

The command spofy-menu opens a transient popup that provides quick access to all spofy commands from any buffer. Before showing the popup, it ensures that spofy-global-mode is active (Global minor mode), so invoking spofy-menu for the first time in a session automatically starts polling, the mode-line display, and the tab-bar display as configured. The popup header shows the currently playing track. When spofy-global-mode is active, this command is bound to spofy-global-key (default C-c s).

The popup is organized into three columns:

  • Column 1 — Playback, Volume, Mode: play/pause, next, previous, seek forward, seek backward, seek to (t), timeline (Q); volume up/down and set; shuffle and repeat toggles.
  • Column 2 — Search and Browse: Spotify search for tracks (s t), albums (s l), playlists (s p), artists (s a); library search for saved tracks (l t), saved albums (l a), playlists (l p), context (l c); browse saved tracks (b t), saved albums (b a), playlists (b p), current context (b c).
  • Column 3 — View and Now playing: dashboard (/), devices (d), jump to playing track (.); “Now playing” actions — save track (L), unsave track (U), copy URL (c), Wikipedia lookup (w); quit (q).

Volume up/down, seek forward/backward, shuffle, and repeat all keep the menu open so they can be used repeatedly.

Global minor mode

The command spofy-global-mode toggles a global minor mode. When enabled, it:

  • Binds spofy-global-key to spofy-menu.
  • Starts player state polling.
  • Enables the tab-bar display (if spofy-enable-tab-bar is non-nil), or the mode-line display (if spofy-enable-mode-line is non-nil). Tab-bar takes precedence; the two are mutually exclusive (Global options).

When disabled, it reverses all of these effects. Both spofy and spofy-menu enable this mode automatically when they are invoked.

In addition, spofy auto-enables the mode at package load time when a display feature (spofy-enable-mode-line or spofy-enable-tab-bar) is configured and a non-expired access token is found on disk. This check is entirely passive: it reads the token file but never makes network requests (no token refresh, no API calls). If the stored access token has expired, auto-start is silently skipped and the user must invoke spofy-menu or spofy to start the mode. Loading the package eagerly (e.g. with :demand t in a use-package form) is sufficient for the tab-bar or mode-line display to appear at Emacs startup, provided the token has not yet expired.

Playback control

The command spofy-play-pause toggles playback between playing and paused. It checks the current state and sends the appropriate API call. On success, the local player state is updated immediately so that the mode line and tab bar reflect the change without waiting for the next poll cycle. When pausing, the interpolated progress is snapshotted so the displayed position does not jump backwards. When resuming, the interpolation anchor is reset and spofy issues an immediate re-poll, which picks up any track change that Spotify may have made (for example, advancing to the next track in the queue).

The commands spofy-next and spofy-previous skip to the next or previous track, respectively. Both commands re-poll after a short delay on success so the UI reflects the new track without waiting for the next poll cycle. If the user was playing when they skipped, the playing state is preserved through Spotify’s brief transitional pause.

The commands spofy-seek-forward and spofy-seek-backward move the playback position by spofy-seek-seconds seconds (Playback options). The command spofy-seek-to prompts for an arbitrary timestamp and jumps to that position. It accepts plain seconds ("90"), M:SS ("1:30"), or H:MM:SS ("1:05:30") format. The prompt shows the track duration as a guide; the value is clamped to the range [0, duration].

The commands spofy-volume-up and spofy-volume-down adjust the volume by spofy-volume-step percent. The command spofy-volume-set prompts for an exact volume level between 0 and 100. All three commands update the local player state optimistically so that repeated keypresses accumulate without waiting for the API response. The optimistic value is protected from being overwritten by the next poll cycle, so there is no regression even if Spotify has not yet processed the change.

The command spofy-toggle-shuffle toggles shuffle mode on or off. The command spofy-toggle-repeat cycles through three repeat states: off, context (repeat the current album/playlist), and track (repeat the current track). Both commands update the local player state immediately on success, so the mode line, tab bar, and transient menu reflect the new state without waiting for the next poll. In the transient popup (Transient popup), the current state is shown with the transient-value face, and both suffixes remain transient so that the user can toggle repeatedly without reopening the menu.

The command spofy-play-track plays a specific track by its Spotify URI. If an optional context URI is provided (an album or playlist URI), playback starts within that context at the specified track. The command spofy-play-context plays an entire context (album, playlist, or artist) from the beginning. Both commands are used internally by the browse and search interfaces.

The command spofy-save-current-track saves the currently playing track to the user’s library; spofy-unsave-current-track removes it. Both are available from the transient popup under the “Now playing” column (L and U).

The command spofy-copy-url copies the https://open.spotify.com URL of the entity at point in a spofy list buffer, or of the currently playing track when invoked elsewhere. The URL is placed on the kill ring. It is also bound to c in the transient popup.

Tracking the playing position

When listening to a long playlist—especially in shuffle mode—it is easy to lose track of which item in the list is currently playing. spofy provides two complementary mechanisms to address this.

Cursor follows playback

The command spofy-cursor-follows-playback-mode toggles a global minor mode that automatically scrolls to the playing track in all visible spofy windows whenever the track changes. The mode is off by default. It hooks into spofy-player-track-changed-hook at depth 10, which ensures it runs after spofy-ui--refresh-track-highlights has reprinted the entries with updated playing indicators.

Enable it from the transient popup (Transient popup) with F, or in your init file:

(spofy-cursor-follows-playback-mode 1)

Buffer mode-line

All spofy tabulated-list buffers and the dashboard use a custom mode-line-format that replaces the default line-and-column indicator with information relevant to a media player. The mode-line shows:

  • The mode name (e.g. “spofy Playlist”, “spofy Album”).
  • The entity name, when the buffer displays a specific album, artist, or playlist (e.g. “spofy Album: Thriller”, “spofy Playlist: Discover Weekly”). List views such as the playlists browser show only the mode name.
  • Playback state icons: shuffle (⇌ when on), repeat (↻ for context, ↻₁ for track). These are omitted in the dashboard, which already displays shuffle and repeat state in its now-playing section.
  • The playing track’s position in the current buffer (e.g. “47/213”), displayed with the spofy-muted face. This element is empty when the playing track is not in the buffer.

The mode-line updates automatically on player state changes (shuffle, repeat) and on track changes. The constant spofy-ui-mode-line-format holds the format list; it is set buffer-locally by spofy-ui-set-format in tabulated-list modes and by spofy-dashboard-mode in the dashboard.

Polling

The command spofy-player-start-polling starts a timer that polls the Spotify player state at spofy-poll-interval second intervals. Polling is necessary because the Spotify Web API does not support push notifications. The timer uses a self-rescheduling design: each poll is a one-shot timer that only schedules the next poll after the HTTP response callback completes. This prevents request accumulation under network latency—a fixed-interval repeating timer would fire new requests even while previous ones were still in flight, causing unbounded buffer growth. A generation counter ensures that stale in-flight callbacks from a previous polling loop do not accidentally reschedule after a stop or restart.

When the player state changes, spofy runs spofy-player-state-changed-hook. When the track changes specifically, it also runs spofy-player-track-changed-hook. When the API reports no active player session (e.g. playback ended or the device went idle), the response may contain null fields such as item; spofy normalizes these to nil, clears the player state, and runs spofy-player-state-changed-hook so that the mode line and tab bar reflect that nothing is playing.

The command spofy-player-stop-polling cancels the polling timer. Polling is started automatically by the spofy command and spofy-global-mode, and stopped by spofy-stop. All HTTP response buffers created by API requests are cleaned up via unwind-protect, so transient errors cannot leak buffers.

When Spotify’s autoplay continues beyond an album, the API may still report the original album as the playback context even though the current track belongs to a different album. During state extraction, spofy detects this mismatch by comparing the context album ID with the track’s actual album ID and corrects the context URI to point to the track’s own album. This ensures that all context-dependent commands (spofy-library-browse-context, spofy-library-search-context) navigate to the correct album.

Device management

The Spotify Web API can control playback on any device logged into the user’s account: desktop apps, mobile phones, web players, smart speakers, and more. The command spofy-select-device fetches the list of available devices, presents them in a completing-read prompt, and transfers playback to the selected device.

Any playback command that requires an active device will automatically call spofy-select-device if none is found, so you never need to select a device manually before issuing a command.

Search

spofy provides four search commands, each targeting a specific entity type:

  • spofy-search-tracks searches for matching tracks.
  • spofy-search-albums searches for matching albums.
  • spofy-search-artists searches for matching artists.
  • spofy-search-playlists searches for matching playlists.

When spofy-consult is loaded, these commands use incremental search via consult--dynamic-collection: the minibuffer appears immediately and candidates update live as you type. Without spofy-consult, they fall back to prompting for a query string and displaying results in a tabulated list.

Each command creates a *spofy Search: TYPE* buffer using tabulated-list-mode. The columns vary by entity type:

TypeColumns
TracksPlay indicator, Name, Artist(s), Album, Duration
AlbumsName, Artist(s), Year, Tracks
ArtistsName, Genres, Followers
PlaylistsName, Owner, Tracks, Public

If the currently playing track appears in the search results, it is highlighted with the spofy-playing face across all columns and prefixed with a ▶ icon. The highlight updates automatically when the track changes, so there is no need to press g to see which track is now playing.

Navigating search results

All search result buffers share the spofy-search-mode keymap:

KeyCommandAction
RETspofy-search-play-itemPlay the item at point
aspofy-search-view-albumView album (Album view)
Aspofy-search-view-artistView artist (Artist view)
pspofy-search-add-to-playlistAdd track to a playlist
sspofy-search-saveSave item to library
Sspofy-search-unsaveRemove item from library
SPCspofy-play-pauseToggle play/pause
gspofy-search-refreshRe-run the search
qquit-windowClose the buffer

The Spotify API returns search results in pages. Results load automatically as the user scrolls (Pagination).

Browsing

Album view

The command spofy-view-album displays the tracks of a Spotify album in a *spofy Album: NAME* buffer. The buffer has a header showing the album name, artist, year, and total track count. Tracks are listed with their track number, name, artist(s), and duration. For multi-disc albums, the track number column switches to left-aligned disc.track notation (e.g. 1.1, 1.10, 2.1) so that the period stays in a fixed position; single-disc albums show a right-aligned track number. The currently playing track is highlighted with the spofy-playing face and the highlight updates automatically when the track changes. Column widths are controlled by the spofy-columns user option (Column widths).

KeyCommandAction
RETspofy-album-play-trackPlay track in album context
aspofy-album-playPlay the entire album
Aspofy-album-view-artistView the artist
sspofy-album-saveSave album to library
SPCspofy-play-pauseToggle play/pause
gspofy-album-refreshRefresh
qquit-windowClose

Artist view

The command spofy-view-artist displays an artist’s albums in a *spofy Artist: NAME* buffer. The header shows the artist name, genres, and follower count. Albums are listed with their name, year, type (album or single), and track count. Column widths are controlled by the spofy-columns user option (Column widths).

KeyCommandAction
RETspofy-artist-view-albumView album (Album view)
tspofy-artist-top-tracksView top tracks
aspofy-artist-play-albumPlay the album at point
Fspofy-follow-artistFollow this artist
Uspofy-unfollow-artistUnfollow this artist
SPCspofy-play-pauseToggle play/pause
gspofy-artist-refreshRefresh
qquit-windowClose

The function spofy-view-artist-top-tracks fetches the artist’s top tracks (as determined by Spotify’s algorithm) and displays them in a *spofy Top Tracks: ARTIST* buffer. Tracks are listed with their name, album, and duration. The currently playing track is highlighted and the highlight updates automatically when the track changes. Column widths are controlled by the spofy-columns user option (Column widths).

KeyCommandAction
RETspofy-artist-top-tracks-playPlay the track
aspofy-artist-top-tracks-view-albumView album
sspofy-artist-top-tracks-saveSave track to library
SPCspofy-play-pauseToggle play/pause
gspofy-artist-top-tracks-refreshRefresh
qquit-windowClose

Playlist detail view

The command spofy-view-playlist displays the tracks of a playlist in a *spofy Playlist: NAME* buffer. The header shows the playlist name, owner, description, and total track count. Columns include the track number (1-based position in the playlist), track name, artist(s), album, and duration. The currently playing track is highlighted with the spofy-playing face and the highlight updates automatically when the track changes. Column widths are controlled by the spofy-columns user option (Column widths).

KeyCommandAction
RETspofy-playlist-view-play-trackPlay track in playlist context
aspofy-playlist-view-albumView album
Aspofy-playlist-view-artistView artist
dspofy-playlist-view-remove-trackRemove track from playlist
sspofy-playlist-view-save-trackSave track to library
fspofy-playlist-view-followFollow playlist
SPCspofy-play-pauseToggle play/pause
gspofy-playlist-view-refreshRefresh
qquit-windowClose

Playlist management

The command spofy-library-browse-playlists displays all of the user’s playlists in a *spofy Playlists* buffer. Each row shows the playlist name, owner, track count, and public/private status.

KeyCommandAction
RETspofy-playlists-viewView playlist tracks (detail view)
pspofy-playlists-playPlay the playlist
cspofy-create-playlistCreate a new playlist
rspofy-rename-playlistRename the playlist at point
dspofy-delete-playlistDelete (unfollow) the playlist
fspofy-follow-playlistFollow a playlist by ID or URI
vspofy-playlist-set-publicToggle public/private
SPCspofy-play-pauseToggle play/pause
gspofy-playlists-refreshRefresh
qquit-windowClose

The command spofy-create-playlist prompts for a name and description, then creates a new private playlist. The command spofy-rename-playlist prompts for a new name with the current name pre-filled. The command spofy-delete-playlist asks for confirmation with y-or-n-p before unfollowing the playlist (Spotify models deletion as unfollowing).

The command spofy-playlist-add-track adds a track to a playlist. When called interactively, it attempts to get the track URI from the entity at point, then presents the user’s playlists in completing-read for target selection. This command is available from search results (Navigating search results), library views (Library management), and browse views.

The command spofy-playlist-remove-track removes the track at point from the current playlist. It only works in playlist detail buffers (Playlist detail view).

The command spofy-playlist-reorder moves the track at point to a new position within the playlist. It prompts for the target position (1-indexed for the user interface, converted internally to the 0-indexed API format).

The command spofy-playlist-set-public toggles the public/private visibility of the playlist at point.

The command spofy-follow-playlist prompts for a playlist ID or URI and follows it. The command spofy-unfollow-playlist unfollows the playlist at point (equivalent to spofy-delete-playlist).

Library management

The command spofy-library-browse-tracks displays the user’s saved tracks in a *spofy Saved Tracks* buffer. Columns include a play indicator, track name, artist(s), album, and duration. The currently playing track is highlighted with the spofy-playing face across all columns, and the highlight updates automatically when the track changes. When the library cache is warm (Cache management), all items render instantly; otherwise the command falls back to paginated API requests. Column widths are controlled by the spofy-columns user option (Column widths).

KeyCommandAction
RETspofy-library-tracks-playPlay the track
aspofy-library-tracks-view-albumView album
Aspofy-library-tracks-view-artistView artist
Sspofy-library-tracks-unsaveRemove from library
pspofy-library-tracks-add-to-playlistAdd to a playlist
SPCspofy-play-pauseToggle play/pause
gspofy-library-tracks-refreshRefresh
qquit-windowClose

The command spofy-library-browse-albums displays saved albums in a *spofy Saved Albums* buffer with columns for name, artist(s), year, and track count. Like spofy-library-browse-tracks, it uses the library cache when available for instant rendering.

KeyCommandAction
RETspofy-library-albums-viewView album tracks
pspofy-library-albums-playPlay the album
Aspofy-library-albums-view-artistView artist
Sspofy-library-albums-unsaveRemove from library
SPCspofy-play-pauseToggle play/pause
gspofy-library-albums-refreshRefresh
qquit-windowClose

The command spofy-library-browse-context opens the currently playing album or playlist in a standard browse buffer. For album contexts it opens the album view (Album view); for playlist contexts it opens the playlist detail view (Playlist detail view). It signals an error when the context is unsupported (e.g., Spotify-generated radio or daily mixes).

See Timeline for the recently-played and queue views, which live in a single buffer.

The command spofy-list-top-tracks displays the user’s top tracks in a *spofy Top Tracks* buffer. It accepts an optional TIME-RANGE argument: "short_term" (4 weeks), "medium_term" (6 months, the default), or "long_term" (1 year). The columns are the same as saved tracks, and the playing highlight updates automatically when the track changes.

KeyCommandAction
RETspofy-top-tracks-playPlay the track
aspofy-top-tracks-view-albumView album
Aspofy-top-tracks-view-artistView artist
SPCspofy-play-pauseToggle play/pause
gspofy-top-tracks-refreshRefresh
qquit-windowClose

The command spofy-list-top-artists displays the user’s top artists in a *spofy Top Artists* buffer with columns for name and genres. It accepts the same TIME-RANGE argument as spofy-list-top-tracks.

KeyCommandAction
RETspofy-top-artists-viewView artist
SPCspofy-play-pauseToggle play/pause
gspofy-top-artists-refreshRefresh
qquit-windowClose

The command spofy-list-new-releases displays new album releases in a *spofy New Releases* buffer with columns for album name, artist(s), year, and track count. This data is not personalized.

KeyCommandAction
RETspofy-new-releases-viewView album
Aspofy-new-releases-view-artistView artist
SPCspofy-play-pauseToggle play/pause
gspofy-new-releases-refreshRefresh
qquit-windowClose

The command spofy-library-save saves a Spotify entity (track or album) to the user’s library. It accepts a Spotify URI and determines the entity type from it. The command spofy-library-unsave removes an entity from the library. Both commands are available through keybindings in search, browse, and library buffers.

The command spofy-follow-artist follows the artist at point (in artist views) or prompts for an artist ID. The command spofy-unfollow-artist is its counterpart. Both are bound to F and U in the artist view (Artist view), and to f and u in the embark artist map (Embark integration).

Timeline

The command spofy-view-timeline opens a *spofy Timeline* buffer that combines three chronologically-ordered sections in one view: “Recently played” at the top (oldest first), “Now playing” in the middle, and “Up next” at the bottom. Row actions (RET, Q, a, A, s, g) work in every section. The buffer refreshes automatically on every track change while it is visible, and recenters the window on the now-playing row.

The user option spofy-timeline-history-limit controls how many past tracks to fetch for the history section. It defaults to 20.

KeyCommandAction
RETspofy-timeline-play-at-pointPlay the track at point
aspofy-timeline-view-album-at-pointView album
Aspofy-timeline-view-artist-at-pointView artist
sspofy-timeline-save-at-pointSave to library
Qspofy-timeline-queue-at-pointAdd to queue
SPCspofy-play-pauseToggle play/pause
gspofy-view-timelineRefresh
qquit-windowClose

RET is section-aware. In the “Up next” section, it plays the track inside the current playback context so the remainder of the queue stays intact. In “Recently played” and on the now-playing row, it plays the bare track URI, since historical tracks are often unrelated to the current context.

The command spofy-add-to-queue adds a track or podcast episode to the Spotify playback queue. When called interactively, it uses the URI at point (in tabulated-list and timeline buffers) or prompts for one. Non-queueable URIs (albums, artists, playlists) are rejected with a user-error. The command is bound to Q in every track-list keymap and in the embark track map, and is available from the transient menu (which opens the timeline under Q).

Cache management

spofy caches API responses in memory to reduce network requests and improve responsiveness. Each endpoint type has its own time-to-live, chosen to balance freshness against API rate limits: search results are cached for 5 minutes (results change slowly), user playlists for 1 minute (users edit playlists often), playlist tracks for 2 minutes (tracks change less frequently than list metadata), and album/artist details for 10 minutes (essentially static data). The player state endpoint is never cached.

The command spofy-clear-cache flushes the entire in-memory cache. Use it when you suspect stale data, for example after making changes to a playlist in the Spotify app. The cache is also cleared automatically when Emacs restarts.

In addition to the API response cache, spofy maintains a separate library cache that stores the complete contents of the user’s saved tracks, saved albums, and playlists. This cache is populated asynchronously in the background when spofy initializes (via spofy-library-warm-cache, called from spofy--ensure-global-mode). The background fetch does not block Emacs, even for large libraries with thousands of tracks.

The library cache serves two purposes: it provides instant results for library search commands (spofy-library-search-tracks, spofy-library-search-albums, spofy-library-search-playlists), and it allows library browse commands (spofy-library-browse-tracks, spofy-library-browse-albums, spofy-library-browse-playlists) to render all items immediately without pagination. When the cache is empty (e.g., at the start of a session before the background fetch completes), library search commands display a message asking the user to try again shortly, and browse commands fall back to the standard paginated API flow.

The library cache is invalidated automatically when items are saved or unsaved via spofy-library-save and spofy-library-unsave. The command spofy-library-invalidate-cache can also be called manually. The cache is not persisted to disk; a new Emacs session starts fresh.

Browse cache

In addition to the API response cache and library cache, spofy maintains a browse cache that stores the complete list of items for each album, artist, and playlist that has been opened in a browse view. The cache is keyed by context URI (e.g. spotify:playlist:ID, spotify:album:ID) or a synthetic key for artist views (e.g. spotify:artist:ID:albums, spotify:artist:ID:top-tracks).

The first time a browse view is opened, all pages of items are fetched from the API and stored in the cache. Subsequent opens serve items from the cache instantly, skipping the multi-page API fetch. This is particularly noticeable for large playlists (hundreds or thousands of tracks) where the initial fetch may take several seconds.

The cache is invalidated automatically when the user presses g (refresh) in any browse buffer, or when a playlist mutation occurs (adding, removing, or reordering tracks). It is not persisted to disk; a new Emacs session starts with an empty cache.

Functions

Player state accessors

The function spofy-player-current-track returns the current track as a cons cell (NAME . ARTIST), or nil if nothing is playing. This is useful in custom mode-line constructs or hook functions.

The function spofy-player-playing-p returns non-nil if Spotify is currently playing.

The function spofy-player-current-track-id returns the Spotify ID of the currently playing track, or nil. This is used internally by search and browse buffers to highlight the now-playing row.

The function spofy-player-interpolated-progress returns the estimated current playback position in milliseconds. When the track is playing, it adds the wall-clock time elapsed since the last poll to the last-known progress, clamping at the track duration. When paused, it returns the stored progress directly. The dashboard progress bar and tab-bar %G specifier use this function to update smoothly between poll cycles.

Entity access in buffers

The function spofy-ui-entity-at-point returns the full Spotify API alist for the entity on the current tabulated-list row in any spofy buffer. It works uniformly across browse, search, library, and playlist buffers by looking up the row ID in a shared buffer-local entity table managed by spofy-ui.el. This is useful for writing custom actions or hook functions that need to access entity metadata.

API client

The API client in spofy-api.el provides five high-level functions for interacting with the Spotify Web API:

  • spofy-api-get sends an asynchronous GET request to an endpoint with optional query parameters.
  • spofy-api-get-sync sends a synchronous GET request and returns the parsed JSON response directly, or nil on error. It is used by the consult integration (Consult integration) and other code paths that require a return value rather than a callback.
  • spofy-api-put sends a PUT request with an optional JSON body.
  • spofy-api-post sends a POST request with an optional JSON body.
  • spofy-api-delete sends a DELETE request with an optional JSON body.

All five accept an endpoint path (relative to https://api.spotify.com/v1/), optional data, and—for the asynchronous variants—a callback function that receives the parsed JSON response. They handle authentication headers, token refresh, rate limiting (429 responses), transient error retry, and response buffer cleanup automatically. If no valid access token is available, the request is skipped and a message suggesting M-x spofy-authenticate is shown instead of sending a malformed request.

These functions are the building blocks for all of spofy’s features. Advanced users who want to extend spofy with custom API calls can use them directly.

Authentication helpers

The function spofy-auth-access-token returns the current OAuth2 access token, or nil if unavailable or expired. Because the token expiry is persisted to disk alongside the tokens, this function can determine after an Emacs restart whether the stored access token is still valid without contacting Spotify. If the token has expired and a refresh token is available, it automatically attempts a refresh before returning nil.

The function spofy-auth-refresh-token explicitly uses the stored refresh token to obtain a new access token from Spotify. The request is synchronous with a 10-second timeout to prevent Emacs from blocking indefinitely if the Spotify token endpoint is unreachable. It requires spofy-client-id and spofy-client-secret to be set, and signals an error if either is missing. This is called automatically when needed, but can be invoked manually if you encounter authentication issues.

Formatting utilities

The function spofy-ui-format-duration-ms takes a duration in milliseconds and returns a human-readable "M:SS" string.

The function spofy-ui-format-artists takes a list of artist alists (as returned by the Spotify API) and formats them as a comma-separated string of names.

The function spofy-ui-truncate truncates a string to a given maximum display width. It accepts an optional FACE argument; when provided, the face is applied to the result and, if truncation occurred, the last three characters are marked with the spofy-fade text property (levels 1, 2, 3). After tabulated-list-print, the advice function spofy-ui--after-tabulated-list-print calls spofy-ui--apply-buffer-fades, which creates overlays on the marked characters with foreground colors blended toward the default background at 25%, 50%, and 75% respectively. This produces a smooth fade-out effect instead of a hard cut or an explicit ellipsis character. Because the color blending happens at display time via overlays rather than being baked into text properties, the fade automatically adapts when the user switches themes: spofy-ui--refresh-fades is registered on enable-theme-functions and recomputes all fade overlays in spofy buffers. It uses truncate-string-to-width internally, so column alignment is correct even with variable-width characters. To prevent tabulated-list-mode from adding its own ellipsis (which in Emacs 30 uses a display text property that overrides the fade), spofy-ui-set-format sets truncate-string-ellipsis to "" buffer-locally. All entry formatting functions in the browse, library, search, consult, and playlist modules pass the appropriate face to spofy-ui-truncate, which handles both truncation and propertizing in one call. The mode-line and tab-bar modules call it without a face, so truncation there is silent.

The function spofy-ui-progress-bar-only builds a text progress bar for a given progress and duration (both in milliseconds) at a given character width. It uses full-block characters (U+2588) for the filled portion with the spofy-progress-filled face, and light-shade characters (U+2591) for the empty portion with spofy-progress-empty. The function spofy-ui-progress-time returns a time display string (e.g. "1:23 / 3:45") for a given progress and duration. These functions are used by the dashboard and the tab-bar %G and %T format specifiers respectively.

The function spofy-ui-insert-header inserts a list of strings at the top of the current buffer, each on its own line with the spofy-header face. It is used by the album, artist, and playlist browse views to render header metadata above the track listing.

The function spofy-ui-play-pause-icon returns a play or pause icon string based on the player state alist: when playing, when paused. The function spofy-ui-shuffle-indicator returns when shuffle is on, or an empty string when off. The function spofy-ui-repeat-indicator returns for context repeat, ↻₁ for track repeat, or an empty string when repeat is off. These functions are used by the global mode-line segment, tab-bar display, and buffer mode-line.

Pagination

Browse views (album, artist, and playlist detail) fetch all pages of results asynchronously before rendering, so the buffer always shows the complete list of items with no pagination. The shared helper spofy-browse--fetch-all-pages recursively follows the Spotify API’s next URL until all pages are collected, then hands the complete list to the rendering function. This is consistent with how the library cache pre-fetches all saved tracks and albums (Cache management).

Search results use a scroll-triggered pagination mechanism instead, since the total result set can be very large. The function spofy-ui--maybe-load-more is registered on window-scroll-functions and triggers a fetch when the window end is within a few lines of the buffer end. A post-command-hook complement ensures pagination fires reliably under pixel-scroll-precision-mode, which can miss window-scroll-functions events. A guard variable (spofy-ui--loading-more) prevents duplicate requests while one is already in flight, and the URL is checked with stringp to avoid attempting a fetch when the JSON :null sentinel has not yet been normalized. Each buffer stores a buffer-local handler (set by the module that created the buffer) that knows how to extract items and the next-page URL from the API response. This keeps all pagination logic in one place in spofy-ui.el while letting each module control its own entry formatting. Both spofy-api--extract-paged-results and spofy-ui-load-more normalize JSON :null values to nil, so that a null next-page URL from the Spotify API is correctly treated as the end of results.

Entity storage and list rendering

All spofy list buffers share a common entity storage mechanism provided by spofy-ui.el. The buffer-local variable spofy-ui--entities holds a hash table mapping row IDs (typically Spotify URIs) to the full API alists returned by the Spotify Web API. The function spofy-ui-store-entity stores an entity under a given key, lazily creating the hash table on first use. The function spofy-ui-entity-at-point retrieves the entity for the current tabulated-list row by looking up its ID in the hash table. Modules such as spofy-playlist.el, spofy-browse.el, spofy-search.el, and spofy-library.el use these functions instead of maintaining their own per-module entity tables.

The function spofy-ui-extract-id extracts the bare Spotify ID from a URI of the form spotify:TYPE:ID, returning the ID portion. If the string does not match the URI pattern, it is returned unchanged, so plain IDs can be passed through safely.

The function spofy-ui-album-year extracts the four-digit release year from an album alist. It reads the release_date field and returns the first four characters if they form a year, or the raw value otherwise. All album formatting functions in the library, browse, and search modules use this instead of inlining the extraction logic.

The function spofy-ui-playing-indicator returns a now-playing indicator string for a given track URI. It compares the URI against the currently playing track ID (via spofy-player-current-track-id) and returns a play icon () propertized with the spofy-playing-icon face when they match, or an empty string otherwise. Track formatting functions in library, browse, and search buffers call this to mark the active row.

The function spofy-ui-render-list provides the standard recipe for setting up a spofy tabulated-list-mode buffer. It takes a buffer name, a mode function, a list of entries, a next-page URL, and an optional load-more handler and display function. It creates or reuses the named buffer, erases it, activates the mode, sets the pagination state and entries, prints the list, and displays the buffer (defaulting to pop-to-buffer; browse views pass switch-to-buffer instead). Modules call this function instead of duplicating the buffer-setup boilerplate.

Consult integration

The file spofy-consult.el provides consult completion sources for searching and selecting Spotify entities from the minibuffer. These require the consult package. The dependency is soft-loaded so that byte-compilation of the main spofy package succeeds even when consult is not in the load path; at runtime, consult must be installed for these commands to work.

All candidates are displayed in aligned tabular columns with faces matching the search result buffers. The user option spofy-consult-columns controls the column widths per entity type; see its docstring for the column layout of each type.

The command consult-spofy-track lets you search for tracks with live narrowing as you type (debounced at 300ms to avoid excessive API calls). Candidates show Name, Artist(s), Album, and Duration columns. Selecting a track plays it immediately.

The command consult-spofy-album provides the same experience for albums, with Name, Year, Artist(s), and number of tracks columns. Selecting an album opens the album view (Album view).

The command consult-spofy-artist searches artists, with Name, Genres, and Followers columns. Selecting an artist opens the artist view (Artist view).

The command consult-spofy-playlist searches all Spotify playlists with live narrowing, with Name, Owner, and number of tracks columns. Selecting a playlist opens the playlist detail view (Playlist detail view).

The following three commands search within the user’s own library rather than the full Spotify catalog. They rely on the library cache (Cache management), which is populated asynchronously in the background when spofy starts. All saved items are available for instant client-side filtering as you type. If the cache is not yet ready (e.g., at the start of a session), the command displays a message and starts the background fetch. These are bound under the l prefix in the transient popup (Transient popup).

The command spofy-library-search-tracks searches the user’s saved tracks with incremental narrowing. Candidates show Name, Artist(s), Album, and Duration columns. Selecting a track plays it immediately.

The command spofy-library-search-albums searches saved albums with incremental narrowing, with Name, Year, Artist(s), and number of tracks columns. Selecting an album opens the album view (Album view).

The command spofy-library-search-playlists searches the user’s playlists with incremental narrowing, with Name, Owner, and number of tracks columns. Selecting a playlist opens the playlist detail view (Playlist detail view).

The command consult-spofy-device fetches available playback devices with incremental narrowing, with Name and Type columns. Selecting a device transfers playback to it.

The command spofy-library-search-context fetches the tracklist of the currently playing album or playlist and lets you jump to any track with incremental narrowing. It is not available for Spotify-generated contexts such as radio or daily mixes. This command is bound to l c in the transient popup (Transient popup).

The Spotify search commands use consult--dynamic-collection for incremental search with synchronous API requests (via url-retrieve-synchronously), debounced to avoid excessive API calls. Each search returns up to 20 results per query; consult’s completion framework has no native pagination concept, so refining the query is the primary way to find results beyond the initial batch. To browse further, use embark-export to send the candidates to a paginated search buffer with infinite scroll (Embark integration).

The library search commands pass a pre-fetched candidate list directly to consult--read, since all items are already in memory from the library cache. In both cases, each candidate stores the full Spotify entity in a text property; the :lookup mechanism ensures the original propertized candidate is returned on selection, so the entity data is available for the action. This makes them compatible with the embark integration (Embark integration).

Embark integration

The file spofy-embark.el registers four entity categories with embark, each with a keymap of contextual actions. Like spofy-consult.el, the embark dependency is soft-loaded for byte-compilation compatibility. These actions work on Spotify entities selected through consult (Consult integration) or in tabulated-list buffers.

The spofy-track category provides actions to play the track, add it to a playlist, save it to the library, view its album, view its artist, open its Wikipedia article (w), or copy its Spotify URI.

The spofy-album category provides actions to play the album, save it, view its tracks, view its artist, open its Wikipedia article (w), or copy its URI.

The spofy-artist category provides actions to play the artist’s top tracks, view the artist’s albums, open the artist’s Wikipedia article (w), or copy the URI.

The spofy-playlist category provides actions to play the playlist, view its tracks, follow it, unfollow it, or copy its URI.

The command spofy-embark-copy-uri copies the Spotify URI of the target entity to the kill ring. This is available in all four category keymaps and is useful for sharing links or constructing org-mode links manually.

In addition to single-target actions, spofy-embark.el registers exporters for all four categories: spofy-track, spofy-album, spofy-artist, and spofy-playlist. Calling embark-export from any spofy consult command exports the visible candidates into a dedicated buffer with the usual spofy keybindings.

The behavior of the export depends on the source. When exporting from a search command (e.g., consult-spofy-track), the exporter creates a spofy-search-mode buffer with the scroll-triggered pagination handler wired up. This means you can browse past the initial 20 search results by scrolling: the buffer automatically fetches the next page from the Spotify API as you approach the bottom. This bridges the gap between consult’s fixed candidate list and the unlimited pagination available in tabulated-list buffers (Search).

When exporting from a library command (e.g., spofy-library-search-tracks), the exporter renders a standard library browse buffer instead, since all items are already in memory from the cache. This is useful when you want to filter your library with consult’s narrowing and then explore or act on the results interactively rather than selecting a single item.

Internally, the search pagination state is carried on each candidate as a spofy-search-state text property containing the query, entity type, and next-page URL. The exporter detects this property to decide whether to create a paginated search buffer or fall back to the library render path. This avoids global mutable state and ensures the correct behavior regardless of the order in which consult commands are invoked.

Org-mode integration

The file spofy-org.el registers a spotify: link type with Org mode, supporting three operations. It is loaded automatically when spofy-global-mode is enabled.

The function spofy-org-follow handles opening spotify: links. It parses the link path to determine the entity type and ID, then dispatches to the appropriate spofy command: tracks are played, while albums, artists, and playlists open their respective browse views (Browsing). This means you can write [[spotify:track:4uLU6hMCjMI75M1A2tKUQC][Never Gonna Give You Up]] in an Org file and open it to play the track.

The function spofy-org-store enables org-store-link in any spofy buffer. It extracts the URI and a descriptive label from the entity at point and stores it for later insertion with org-insert-link.

The function spofy-org-export generates appropriate links for export backends. In HTML, LaTeX, and Markdown, it produces links to open.spotify.com/TYPE/ID so readers can open the content in their browser or Spotify app.

Faces

spofy defines a set of faces in the spofy-faces customization group for controlling the appearance of its buffers:

  • spofy-track-name is used for track names in all listings. It inherits from font-lock-keyword-face.
  • spofy-artist-name is used for artist names. It inherits from font-lock-function-name-face.
  • spofy-album-name is used for album names. It inherits from font-lock-type-face.
  • spofy-now-playing-track, spofy-now-playing-artist, and spofy-now-playing-album are used in the dashboard now-playing section. They inherit from the corresponding base faces with a larger height (1.15×).
  • spofy-playing marks the currently playing track in any listing. It uses ultra-bold weight, layered on top of each column’s own face so that the original colors are preserved. All columns of the playing row use this face, including the duration column. The cursor is hidden in all table buffers. A box border is drawn around the entire playing line using the default foreground color. Due to an Emacs display limitation, the border’s vertical lines may not render after a theme change until the buffer is refreshed with g.
  • spofy-playing-icon is used for the ▶ icon on the currently playing track. It inherits from spofy-playing.
  • spofy-header is used for the header area in album, artist, and playlist detail views. It inherits from header-line.
  • spofy-progress-filled is used for the filled portion of the progress bar. It inherits from success.
  • spofy-progress-empty is used for the empty portion of the progress bar. It inherits from shadow.
  • spofy-muted is used for secondary text like durations and dates. It inherits from shadow.

All faces can be customized with M-x customize-group RET spofy-faces RET.

Hooks

The hook spofy-player-state-changed-hook runs whenever any aspect of the player state changes: track, play/pause state, shuffle, repeat, volume, or device. The mode-line display and dashboard now-playing section both use this hook to stay up to date. Add your own functions to this hook if you want to react to any state change.

The hook spofy-player-track-changed-hook runs only when the current track changes. This is a subset of state changes and is useful for notifications or logging. The dashboard recently-played section hooks into this to refresh automatically when the track changes (Dashboard and global control). All spofy list buffers (album, playlist, library tracks, search results, top tracks, recently played, and artist top tracks) also hook into this to re-render their entries so the green playing indicator moves to the new track without a manual refresh.

The hook spofy-authenticated-hook runs once after a successful OAuth2 authentication. Use it to perform setup that depends on having a valid token, such as pre-fetching the user’s playlists.

Mode-line display

The command spofy-mode-line-mode toggles a global minor mode that adds a spofy segment to the mode line. The segment shows the currently playing track formatted according to spofy-mode-line-format (Mode-line options).

When enabled, the mode hooks into spofy-player-state-changed-hook to rebuild the mode-line string whenever the player state changes, then calls force-mode-line-update to refresh all frames.

The mode is enabled automatically when spofy-global-mode is active and spofy-enable-mode-line is non-nil, provided spofy-enable-tab-bar is nil (Global options). When both options are non-nil, tab-bar display takes precedence and mode-line display is not enabled.