nsm/README.en.md
2026-07-13 09:31:20 +03:00

12 KiB
Raw Blame History

nsm

A terminal music player written in Rust — something like cmus, but built from scratch, with an equalizer, cue sheet support, internet radio, and SMB shares.

Читать по-русски

Features

  • Library: recursive scanning of nested folders, fuzzy search (title/artist/album), grouping by album
  • Playback: open a file directly (plays the whole album in order) or add to a playlist; multiple playlists with reordering and deletion
  • Transport: shuffle, repeat (off/all/single track), playback speed (0.25×1.75×), mute, real seeking with position tracking
  • .cue sheets: a single physical file (e.g. a whole album as one FLAC) is correctly recognized as multiple tracks with proper titles and boundaries
  • Equalizer: 8 bands, custom biquad filters
  • Cover art: rendered directly in the terminal (kitty/sixel/iTerm2 protocols, falling back to block graphics)
  • Spectrum analyzer: real-time FFT (not a fake animation) — toggle cover/spectrum with v, expand to fullscreen with z (transport controls — play/pause, next/prev, seek, mute — still work while fullscreen)
  • Fade-out at track end: the last 4 seconds smoothly fade to silence — toggle with F, off by default
  • Loudness normalization: ReplayGain tag takes priority if present in the file, otherwise an adaptive leveler (AGC) adjusts volume on the fly — toggle with G, off by default
  • Internet radio: add your own stations by URL, a curated built-in list (SomaFM, Radio Paradise), station search via the open Radio Browser API (auto-fails over between several mirrors if one is down), ICY metadata ("now playing" straight from the stream)
  • SMB shares: connect to network folders (including anonymous/guest access) via the system's gvfs — no SMB protocol implemented inside the player itself
  • Listening history: asks on first launch whether to remember what you play; stats (artist/track/date) as a chart — hourly for today, daily for the last week/month; can be toggled on/off anytime, resettable with confirmation
  • New releases: checks for new albums/EPs/singles from artists already in your library, via the open MusicBrainz database (no API keys) — not a global chart, but specifically "what's new from the artists you actually listen to"
  • Keyboard layout independent: hotkeys work the same on both English and Russian (or other Cyrillic) keyboard layouts

Supported formats

Anything symphonia can decode: MP3, FLAC, ALAC/AAC (in .m4a), WAV, AIFF, OGG (Vorbis), MKV/WebM.

Not supported: Opus — symphonia has no native Opus decoder in any feature set (only third-party libopus bindings, which aren't used here). .opus files are skipped by the library scanner.

Building

System dependencies

Linux — ALSA headers are required (for audio output via cpal):

# Debian/Ubuntu
sudo apt-get install libasound2-dev pkg-config

# ALT Linux
sudo apt-get install libalsa-devel

# Fedora
sudo dnf install alsa-lib-devel

macOS (untested — this project has only been built and tested on Linux) — no separate audio dependency needed, cpal talks to CoreAudio directly. Just the standard build tools:

xcode-select --install

Windows (untested — this project has only been built and tested on Linux) — no separate audio dependency needed either (cpal uses WASAPI); a normal Rust install via rustup (MSVC toolchain) should be enough. For cover art to render well you'll want a modern terminal with true color support — Windows Terminal is recommended; the image protocols (kitty/sixel/iTerm2) are unlikely to be supported there, so cover art will fall back to block-graphics rendering.

The SMB feature (c in Library) only works on Linux — it's built entirely on gio/gvfs (the GNOME stack), which doesn't exist on macOS or Windows. On those systems, trying to connect an SMB share through nsm will just return a clear error ("is gvfs installed?") rather than crash — network shares there are normally mounted through Finder/Explorer instead.

No extra system dependencies are needed on any OS for internet radio (SomaFM, Radio Paradise, etc.) or MusicBrainz — TLS is handled by rustls (pure Rust, no OpenSSL).

Build

git clone <this-repo-url>
cd nsm
cargo build --release

The binary will be at target/release/nsm (on Windows — target\release\nsm.exe).

Cross-compiling a Windows binary from Linux

# 1. Add the Windows target (needs rustup — the normal Rust install via rustup.rs)
rustup target add x86_64-pc-windows-gnu

# 2. Install mingw-w64 — the Windows cross-compiler/linker
sudo apt-get install gcc-mingw-w64-x86-64  # Debian/Ubuntu; use your own package manager otherwise

# 3. Tell cargo which linker to use for this target
mkdir -p .cargo
cat >> .cargo/config.toml <<EOF
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
EOF

# 4. Build
cargo build --release --target x86_64-pc-windows-gnu

The binary will be at target/x86_64-pc-windows-gnu/release/nsm.exe.

Partially verified: the linker itself (mingw-w64) was actually tested — compiled a test C program with it and got a genuine Windows PE executable. Couldn't verify a full end-to-end nsm build for the Windows target (that needs rustup to fetch the Windows std library, which this environment can't reach) — but along the way found and fixed a real bug: the code used a Unix-specific way to detect the terminal's cell size for cover art, which would have made the Windows build fail to compile at all. Non-Unix platforms now use a sensible default instead.

Run

./target/release/nsm /path/to/music

If no path is given, it defaults to ~/Music.

Controls

Global keys (any tab)

Key Action
Space play/pause
N / P next / previous track
s toggle shuffle
r repeat (off → all → single track → off)
m mute
[ / ] speed down / up
/ seek 5s / +5s
e focus the equalizer
h toggle listening history tracking
v toggle cover art / spectrum analyzer
z expand cover art / spectrum to fullscreen
F toggle fade-out at track end
G toggle loudness normalization
I cycle cover art rendering protocol (if auto-detection got it wrong)
Tab next tab
q quit

Equalizer (after e)

Key Action
/ select band
/ ±1dB on selected band
0 reset current band
R reset all bands
Esc / e leave equalizer focus

Library

Key Action
j/k, / navigate
Enter expand album / play
Esc collapse album
/ search
a add to playlist
c connect an SMB share
L add another folder to the library
C clear the library (doesn't touch files on disk)

Playlists

Key Action
j/k navigate
Enter open playlist / play from here
n new playlist
J/K move track down/up
d remove track from playlist
Esc back to playlist list

Queue

Key Action
j/k navigate
Enter play from selected item

Radio

Key Action
j/k navigate
Enter play station
u add your own station by URL
o search stations online
d remove a (custom) station

Stats

Key Action
p cycle period (day → week → month)
x reset history (press twice in a row to confirm)

New Releases

Key Action
p cycle period (day → week → month)
f check new releases for all artists in your library (can take a while, see below)
a manually check one artist by name (doesn't need to be in your library) — adds to the already-shown list

In text-input fields — Enter confirms, Esc cancels, Backspace deletes.

Architecture

A Cargo workspace of four crates:

crates/
├── player/    — playback engine: decoding (symphonia), audio output (cpal),
│                equalizer, speed resampling, queue, internet radio
├── library/   — library scanning, tags (lofty), cue sheets, playlists,
│                radio stations, SMB
├── tui/       — ratatui-based interface: tabs, cover art, equalizer, event loop
└── nsm/       — thin binary tying everything together

player and library don't know about each other — tui wires them together via player::QueueItem.

Listening history

Stored locally in ~/.config/nsm/history.jsonl (one JSON line per entry: artist, track, date/time). Never sent anywhere. Asked for permission on first launch; can be toggled on/off anytime with h, and the Stats tab shows a chart by hour/day, a list of exactly what was played, and lets you wipe it all (press x twice to confirm).

Mojibake in old tags

Many old mp3 files (especially with 2000s-era tags) store Cyrillic text in ID3v1 using CP1251, but this isn't formally declared anywhere — most libraries (including ours, by default) read such tags as Latin-1, producing "Ïåñíÿ" instead of "Песня". nsm detects this heuristically (code points ≤0xFF, then checks the Cyrillic-letter ratio after re-decoding as CP1251) and fixes it automatically, without touching text that's already proper UTF-8.

Cover art

The rendering protocol (kitty/sixel/iTerm2/halfblocks) is auto-detected based on your terminal. If detection gets it wrong (e.g. showing low resolution on a terminal that actually supports kitty graphics) — press I to manually cycle through protocols; your choice is remembered for next time.

New releases

The New Releases tab explicitly shows releases only for artists already in your library — this is not a global "what's trending" chart (rateyourmusic.com actively blocks bots, and honestly scraping it wasn't feasible — see discussion). Instead it uses the open MusicBrainz database, no API keys needed.

Because of MusicBrainz's rate limit (no more than one request per second), checking a large library can take a while — the progress line shows which artist is currently being checked. For very prolific artists (many singles/reissues over the years), this may take several requests per artist — MusicBrainz doesn't guarantee results are sorted by date, so the full discography is paged through rather than just the first 100 entries (otherwise a new release could randomly be missed). Results are cached to ~/.config/nsm/new_releases_cache.json, so relaunching nsm shows the last result immediately without hitting the network; refresh manually with f.

Important scope note: the tab only shows releases from the last day/week/month (toggle with p) — it's literally "what came out recently", not the artist's whole discography. If your favorite artist hasn't released anything in the last 30 days (true for most artists most of the time), their list will be empty — that's expected, not a bug.

Loudness normalization

Toggle with G. If the file has a ReplayGain tag (REPLAYGAIN_TRACK_GAIN and equivalents — read universally via lofty across FLAC/MP3/OGG/MP4), that exact value is used. If there's no tag, an adaptive leveler (AGC) kicks in instead: it continuously measures the loudness of the last 1-3 seconds and smoothly adjusts gain toward a target. This is not the same as true ReplayGain (computed in advance over the whole track) — the first few seconds of a track won't be perfectly leveled, but it converges quickly after that.

Known limitations

  • Only built and tested on Linux — macOS/Windows builds haven't been verified; the SMB feature fundamentally won't work there (see above)

  • Opus is not decoded (see "Supported formats" above)

  • Playlists don't store cue track ranges — adding a cue track to a playlist saves the whole underlying file, not the specific virtual track

  • Minimum terminal size is 70×18; below that a warning is shown instead of a broken layout

  • Connecting to SMB shares is a blocking call on the UI thread; the interface will briefly freeze during the operation (radio search and new-releases checks already run in the background and don't block)

  • Multi-file cue sheets (a rare case) are only supported using the first FILE entry in the sheet