README
¶
ostui (OpenSubsonic TUI)
ostui is a terminal client for *sonic music servers, inspired by ncmpcpp and musickube.
★ Releases ★
★ Changelog ★Software metrics ★ Issues ★ Mailing List ★
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 (viagetSongsByGenre/
. 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
Browser
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
: Quit1
: Folder view2
: Queue view3
: Playlist view4
: Search view5
: Log (errors, etc.) viewEscape
/Return
: Close modal if open
Playback Controls
These controls are accessible from any view:
p
: Play/pauseP
: Stop>
: Next song-
/=
: Volume down/volume up,
/.
: Seek -10/+10 secondsr
: Add 50 random songs to the queuec
: Start a server library scan
Browser Controls
Enter
: Play song (clears current queue)a
: Add album or song to queuey
: Toggle star on song/albumA
: Add song to playlistR
: Refresh the list (if in artist directory, only refreshes that artist)/
: Search artistsn
: Continue search forwardN
: Continue search backwardS
: Add similar artist/song/album to playlist
Queue Controls
d
/Delete
: Remove currently selected song from the queueD
: Remove all songs from queuey
: Toggle star on songi
: Toggle song info panelk
: Move song up in queuej
: Move song down in queues
: Save the queue as a playlistS
: Shuffle the songs in the queuel
: 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 playlistd
: Delete playlista
: Add playlist or song to queueR
: 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:
- Go
- markdownlint for running the markdown linting test
- revive for linting the Go code
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 tofile
-memprofile=<file>
: Write memory profile tofile
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".
- 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, notgetMusicDirectory
,getArtist
, orgetAlbum
. - OSA does not provide
getMusicFolder
(singlular) - 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 withgetMusicDirectory
. 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
¶
There is no documentation for this package.