215 lines
11 KiB
Markdown
215 lines
11 KiB
Markdown
# nsm
|
||
|
||
A terminal music player written in Rust — something like [cmus](https://cmus.github.io/), but built from scratch, with an equalizer, cue sheet support, internet radio, and SMB shares.
|
||
|
||
*[Читать по-русски](README.md)*
|
||
|
||
## 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](https://api.radio-browser.info) (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](https://musicbrainz.org) 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](https://github.com/pdeltuvia/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](https://github.com/RustAudio/cpal)):
|
||
|
||
```bash
|
||
# 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:
|
||
```bash
|
||
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](https://rustup.rs) (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
|
||
|
||
```bash
|
||
git clone <this-repo-url>
|
||
cd nsm
|
||
cargo build --release
|
||
```
|
||
|
||
The binary will be at `target/release/nsm` (on Windows — `target\release\nsm.exe`).
|
||
|
||
### Run
|
||
|
||
```bash
|
||
./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](https://musicbrainz.org) 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
|