ostui

command module
v0.0.0-...-d5b2c6c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 7, 2025 License: GPL-3.0 Imports: 28 Imported by: 0

README

ostui (OpenSubsonic TUI)

ostui is a terminal client for *sonic music servers, inspired by ncmpcpp and musickube.

Build statusReleasesAUR versionChangelogSoftware metricsIssuesMailing ListGo Report Card

Features

  • Browse by Artist
  • Browse by genre
  • Queue songs and albums
  • Create and play playlists
  • Search music library
  • Mark favorites
  • Volume control
  • Server-side scrobbling (e.g., on Navidrome, gonic)
  • MPRIS2 control and metadata
  • A toggle-able song info panel in the playlist containing song information, lyrics, and cover art
  • Synced lyrics are shown, and synced with the music, if the server supports getLyricsBySongId/. Currently, a PR in gonic provides this through filesystem .lrc files. This version of gonic is in the main branch of @danielepintore's fork.
  • The search tab can toggle between "search for anything" (via search3/), or search-by-genre (via getSongsByGenre/. As part of this, switching to the genre search in the search tab with 'g' also shows a list of all known genres, which can be browsed.

Screenshots

These screenshots use Navidrome's demo server (config file).

Queue

Queue View

Browser

Browser View

Installation

There are four ways to install ostui:

  • With your package manager. Packages should be in Arch AUR, Alpine Testing, and Void.

  • With a binary release. If a binary for your OS/architecture is not available, let me know via the mailing list, and I will add it to the CI if at all possible.

  • The Go Way: go install ser1.net/ostui@latest

  • From source:

    # Either grab a sourcecode tarball -- pick your version
    curl -L -o ostui.tgz https://hg.sr.ht/~ser/ostui/archive/v0.1.0.tar.gz
    tar -xzf ostui.tgz
    # Or, if you have Mercurial installed, clone the repo:
    hg clone hg.sr.ht/~ser/ostui
    # And then build it:
    cd ostui
    go build .
    

Note that this project depends on libraries that have C bindings to native libraries: mpv and ncurses. Because of this, it can take a while to compile the first time. If you're used to blindingly fast Go build speeds, be patient the first time.

Runtime Dependencies
  • mpv
  • Linux (Debian/Ubuntu): apt install pkg-config libmpv libmpv-dev
  • MacOS (Homebrew): brew install pkg-config mpv (not the cask)

Development

The METRICS.md file contains detailed information about:

  • lines-of-code & complexity
  • ABC metrics
  • build library dependencies
  • licenses for every dependency

The CHANGELOG.md contains a list of all tagged releases and signficant changes, as well as a list of contributors.

Compiling

Compile ostui with go build. Cgo is needed for interfacing with libmpv.

ostui can be installed without checking out the repository by running:

  go install ser1.net/ostui@latest

Configuration

ostui looks for a configuration file named config.toml in either $HOME/.config/ostui or the directory containing the executable.

Example Configuration
[auth]
username = 'admin'
password = 'password'
plaintext = true  # Use 'legacy' unsalted password authentication (default: false)

[server]
host = 'https://your-subsonic-host.tld'
scrobble = true  # Use Subsonic scrobbling for last.fm/ListenBrainz (default: false)

[client]
random-songs = 50

[ui]
spinner = '▁▂▃▄▅▆▇█▇▆▅▄▃▂▁'

Usage

General Navigation
  • Q: Quit
  • 1: Folder view
  • 2: Queue view
  • 3: Playlist view
  • 4: Search view
  • 5: Log (errors, etc.) view
  • Escape/Return: Close modal if open
Playback Controls

These controls are accessible from any view:

  • p: Play/pause
  • P: Stop
  • >: Next song
  • -/=: Volume down/volume up
  • ,/.: Seek -10/+10 seconds
  • r: Add 50 random songs to the queue
  • c: Start a server library scan
Browser Controls
  • Enter: Play song (clears current queue)
  • a: Add album or song to queue
  • y: Toggle star on song/album
  • A: Add song to playlist
  • R: Refresh the list (if in artist directory, only refreshes that artist)
  • /: Search artists
  • n: Continue search forward
  • N: Continue search backward
  • S: Add similar artist/song/album to playlist
Queue Controls
  • d/Delete: Remove currently selected song from the queue
  • D: Remove all songs from queue
  • y: Toggle star on song
  • i: Toggle song info panel
  • k: Move song up in queue
  • j: Move song down in queue
  • s: Save the queue as a playlist
  • S: Shuffle the songs in the queue
  • l: Load a queue previously saved to the server

When ostui exits, the queue is automatically recorded to the server, including the position in the song being played. There is a single queue per user that can be thusly saved. Because empty queues can not be stored on Subsonic servers, this queue is not automatically loaded; the l binding on the queue page will load the previous queue and seek to the last position in the top song.

If the currently playing song is moved, the music is stopped before the move, and must be re-started manually.

The save function includes an autocomplete function; if an existing playlist is selected (or manually entered), the Overwrite checkbox must be checked, or else the queue will not be saved. If a playlist is saved over, it will be replaced with the queue contents.

Playlist Controls
  • n: New playlist
  • d: Delete playlist
  • a: Add playlist or song to queue
  • R: Refresh playlists from server
Search Controls

The search tab performs a server-side search for text in metadata name fields. The search results are filtered into three columns: artist, album, and song, where each entry matches the query in name or title.

In any of the columns:

  • /: Focus search field.
  • Enter / a: Adds the selected item recursively to the queue.
  • Left/right arrow keys (, ) navigate between the columns
  • Up/down arrow keys (, ) navigate the selected column list
  • g: toggle genre search

In the search field:

  • Enter: Perform the query.
  • Escape: Escapes into the columns, where the global key bindings work.

Note that the Search page is not a browser like the Browser page: it displays the search results returned by the server. Selecting a different artist will not change the album or song search results.

In Genre Search mode, the genres known by the server are displayed in the middle column. Pressing Enter on one of these will load all of the songs with that genre in the third column. Searching with the search field will fill the third column with songs whose genres match the search. Searching for a genre by typing it in should return the same songs as selecting it in the middle column. Note that genre searches may (depending on your Subsonic server's search implementation) be case sensitive.

Advanced Configuration and Features

MPRIS2 Integration

To enable MPRIS2 support (Linux only), run ostui with the -mpris flag. Ensure you have D-Bus set up correctly on your system.

MacOS Media Control

On MacOS, ostui integrates with the native MediaPlayer framework to handle system media controls. This is automatically enabled if running on MacOS. Note: This is work in progress.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests on GitHub. For major changes, please discuss first to ensure alignment with the project goals. You'll need:

Please base your PRs against the main branch.

Note that the CI has a test section, in which it runs:

  • go test ./...
  • revive --config .revive.toml
  • markdownlint --disable MD049 MD040 -- README.md

If you run these yourself before contributing patches, it will vastly increase the odds I'll pull the patch faster.

There's a shell script devup.sh which will run all of the linting tests and update CHANGELOG.md and METRICS.md. You shouldn't need to run these, as updating the changelog and metrics files are release activities.

Profiling

To profile the application, use the following flags:

  • -cpuprofile=<file>: Write CPU profile to file
  • -memprofile=<file>: Write memory profile to file

These flags are useful for performance debugging and analysis.

Debugging and Logs

View logs and error messages in the log view by pressing 4. This can help diagnose issues with server connections, playback, or other functionalities.

Challenges

Data representation

Here's the canonical, opinionated view for ostui, and one of the main reasons for the fork:

Metadata takes precidence over directory structure.

There is a fundamental issue in how music servers organize media, and it's exacerbated by different servers having strong, conflicting opinions. Many servers make an implicit assumption that music is organized on disk following a pattern of the metadata, usually ARTIST/ALBUM/SONG. This has a number of problems, not least of which is the unlikelyhood that people with large and long-lived music libraries rarely have perfectly clean directory structure. And what do we do when the music metadata tags don't match the file structure?

The crux of the issue is that the OpenSubsonic API documentation is ambiguous about the distinction between filesystem and metadata structure, and server implementations vary on how they interpret this. Subsonic, and Navidrome, assume that the directory structure matches the metadata:

musicFolder/
  artist/
    album/
      song

In practice, users often have their music only losely organized on disk. This should be a valid structure:

musicFolder/
  artist1/
    album1/
      song
  artist1-album1/
      song
  artist2/
    song
  album3/
    artist3/
      song
  album4/
    song
  song
  artist5/
    album5/
      disc 1/
        song

Some of these examples may be contrived and seem silly, but some seem (to me) entirely reasonable. If I have a Beetles anthology, the last folder (artist5) would seem utterly sane. Increasingly, there are artists who release songs only as singles and not under albums, in which case the third subfolder (artist3) makes sense. In Subsonic and Navidrom, artist1, artist1-album1, artist2, album3, album4, and artist5 would all be different artists.

However, the view you see when you use different OpenSubsonic API calls -- file system vs metadata -- can be different and worse, can vary between servers. Mistagged songs might not be reachable at all through the metadata API calls, and wild directory structures can present lies to the use if browsed through the filesystem calls. For example, I can easily imagine someone structuring their music this way simply because of the circumstances in which they live:

  musicfolder/
    Amy Grant/
      Praise the Lord/
        Sabbath Bloody Sabbath.mp3

If the song is properly tagged, the OpenSubsonic API would consider the song "Sabbath Bloody Sabbath" to belong to both the artist "Amy Grand" and "Black Sabbath".

  1. OSA defines getMusicFolders. The returned items have IDs, but in neither Navidrome nor Gonic do these IDs refer to any object that can be fetched by any other API call -- in particular, not getMusicDirectory, getArtist, or getAlbum.
  2. OSA does not provide getMusicFolder (singlular)
  3. OSA claims that getIndexes returns "a list of authors." In Navidrome, this is mostly true, in that the returned object IDs are indeed artist IDs; in gonic, this is not true, and the IDs returned can be used only with getMusicDirectory.
  4. getMusicDirectory could be either an artist, an album, or any other folder in the filesystem

stmp, and stmps, take the position that directory structure takes precedence over metadata. stmp and stmps start with getIndexes, and this information powers the main browser. getIndexes is a list of IDs which are supposed to be both directory IDs that are also artist IDs. Any directory at the top level is considered an artist, even if that directory is, in reality, an album or in fact contains songs belonging to a different artist. If this is TLD == Artist is not true, then the user is wrong.

ostui instead starts with getArtists, and it is Artists which power the main browser. Albums are under artists, and when the album is not identified for a song, it becomes [UNKNOWN ALBUM] -- this is how both Navidrome and gonic present such metadata. Additional divisions such as disk number are ignored for hierarchical purposes, and are only displayed as song metadata.

A problem with ostui's approch is that song metadata (tags) are often not clean. By depending on getArtists, we omit songs that lack an ARTIST tag -- whether because the song has no tags or because it is simply missing that one. This is an issue that ostui still needs to address, by somehow identifying untagged music and providing a virtual structure for them -- perhaps one built on filesystem hierarchy.

OpenSubsonic's Impossible Position

The OpenSubsonic API, in attempting to serve two masters (following the inventor of the API, Subsonic, and yet trying to be a formal specification) can be ambiguously interpreted, leading to different behaviors by different servers. The same API calls to Subsonic, Navidrome, and gonic can return different results. Subsonic is self-authoritative; I'm not aware they recognize OSAPI. Navidrome follows OSAPI, but adds its own extensions and tries to be compatible with Subsonic. gonic recognizes the irreconcilable issues in OSAPI, and diverges from the spec in several ways.

There are features allowed by OSAPI for which there is not yet agreement about how servers should support them. Many result from trying to satisfy different audio container formats. Examples of this are: cover art -- embeded, or external file? Should synchronized lyrics be embedded or in external files? If synchronized lyrics are embedded, what are the tags calld -- in MP3, in Opus, in OGG, in Flac? As of yet, there is no consensus between containers, let alone between servers, about how to answer these questions. ostui attempts to support these as defined by OSAPI, regardless of how servers resolve them on the backend.

ostui tries to walk the middle path of interpreting OSAPI in a way that works with all servers. This means sometimes avoiding parts of the API definition which are problematic or self-conflicting.

The User Experience

The application necessarily lazily makes calls to the server in response to user actions. This can easily result in "laggy" UI behavior, and is an issue that dates back to the stmp project.

One possible fix, which was actually in the code base for Playlists a while, was a sort of "fetch everything in the background, and just work with a local cache." This had a number of issues:

  • No information would be available until the cache was filled.
  • The user was responsible for refreshing the cache, which meant the cache could easily go out of sync.
  • It was error prone.
  • If it was followed everywhere to address all laggy UI, it would have resulted in making a copy of the entire server DB on the client. This would have had a significant impact on memory use.
  • This cache loading happened every time the app was started, which -- if extended to other metadata -- would have made restarts unacceptibly expensive.
  • The logical outcome of addressing all of these would be a persistent, local cache of the server DB, which would have needed synchronizing and constant refreshing. Since the OpenSubsonic API was not designed with this use, there are no cheap or easy ways to keep such caches in sync, making the program excessively network chatty.

Rather than going further down this path, and motivated by having experienced exactly these issues in other projects, my plan is to refactor the interface to keep a space-constrained FILO cache that's filled on demand by user actions, with some pre-emptive localized pre-fetching. This will not be a small job, and will be a substantial rewrite.

The implementation will consist of four components, two of which are in place.

  • An LRU linked list.
  • A cache front-end to expensive calls. This provides an API through which assets can be requested; if they're cached, they're immediately returned. If they aren't, a request is forked to fetch the asset, and a placeholder is returned. When the asset is fetched, a call-back is notified. The cache is not size constrained.
  • An LRU cache, combining the previous into a single asset interface, designed to allow background fetching without exhausting memory restrictions.
  • Hooking all TUI user interactions to the LRU cache, so that no user action is ever blocking.

Credits

ostui is a hard fork of stmps, which is itself a hard fork of stmp. The main justifications for the fork are:

  • stmp development is slow, and mainly targetted at Subsonic servers.
  • I want a client that specifically tries to follow the OpenSubsonic API definitions, and not any specific client behavior.
  • There are a number of issues with OSAPI servers, described in Challenges section, that osapi attempts to work around with the objective of supporting a rich feature set regardless of the server.
  • I'm willing to make major, destructive changes to the code base to address some issues. This sometimes means big change sets, and a lot of work for a maintainer like @spezifisch. This either requires a lot of active communication, collaboration, and review work, or a hard fork. The collaboration part wasn't working out well, and frankly trying to review and merge patches at the rate I was submitting PRs was overwhelming.

Licensing

ostui is licensed under the GNU General Public License v3.0 (GPL-3.0-only). This license allows you to freely use, modify, and distribute the software, provided that any distributed versions of the software, or derivative works, are also licensed under the GPL-3.0-only.

For more details, refer to the LICENSE file in the repository. For all licenses for dependencies, see METRICS.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL