nm/README.md
2026-07-25 21:00:32 +03:00

244 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# nsm
Терминальный музыкальный плеер на Rust — что-то вроде [cmus](https://cmus.github.io/), но с нуля, с эквалайзером, поддержкой cue-листов, интернет-радио и SMB-шар.
*[Read this in English](README.en.md)*
## Возможности
- **Библиотека**: рекурсивное сканирование вложенных папок, поиск (нечёткий, по названию/исполнителю/альбому), группировка по альбомам
- **Воспроизведение**: открыть файл напрямую (весь альбом подряд) или добавить в плейлист; несколько плейлистов с возможностью переставлять и удалять треки
- **Транспорт**: shuffle, повтор (выкл/один трек/всё), изменение скорости воспроизведения (0.25×1.75×), mute, честный seek (перемотка) с сохранением позиции
- **`.cue`-листы**: один физический файл (например, целый альбом одним FLAC) корректно распознаётся как несколько треков с правильными названиями и границами
- **Эквалайзер**: 8 полос, свои биквадратные фильтры
- **Обложка**: отображается прямо в терминале (kitty/sixel/iTerm2-протоколы с fallback на блочную графику)
- **Анализатор спектра**: настоящий FFT в реальном времени (не анимация) — переключение обложка/спектр клавишей `v`, разворот на весь экран клавишей `z` (транспорт — пробел/next/prev/seek/mute — работает и в полноэкранном режиме)
- **Затухание в конце трека**: последние 4 секунды перед концом плавно уводятся в тишину — включается клавишей `F`, по умолчанию выключено
- **Выравнивание громкости**: тег ReplayGain в приоритете (если есть в файле), иначе — адаптивный левеллер (AGC), подстраивающий громкость на лету — клавиша `G`, по умолчанию выключено
- **Интернет-радио**: свои станции по URL, встроенный список (SomaFM, Radio Paradise), поиск станций через открытый [Radio Browser API](https://api.radio-browser.info) (с автопереключением между несколькими зеркалами, если одно недоступно), ICY-метаданные ("сейчас играет" прямо из потока)
- **SMB-шары**: подключение к сетевым папкам (в т.ч. анонимно/гостевой доступ) — свой SMB-клиент на чистом Rust (крейт `smb`), без системных зависимостей вроде `gio`/gvfs
- **История прослушиваний**: при первом запуске спрашивает, запоминать ли прослушанное; статистика (артист/трек/дата) — диаграммой по часам за сегодня, по дням за неделю/месяц; включается/выключается в любой момент, сбрасывается с подтверждением
- **Новые релизы**: проверка новых альбомов/EP/синглов **у артистов, уже имеющихся в твоей библиотеке**, через открытую базу [MusicBrainz](https://musicbrainz.org) (без ключей API) — не глобальный чарт, а именно "что вышло нового у тех, кого ты слушаешь"
- **Раскладка**: хоткеи работают одинаково что на английской, что на русской раскладке
## Форматы
Поддерживается всё, что умеет декодировать [symphonia](https://github.com/pdeltuvia/symphonia): **MP3, FLAC, ALAC/AAC (в `.m4a`), WAV, AIFF, OGG (Vorbis), MKV/WebM**.
**Не поддерживается: Opus**у symphonia нет собственного Opus-декодера ни в одном наборе фич (только сторонние биндинги к libopus, которых в проекте нет). `.opus`-файлы не индексируются сканером библиотеки.
## Сборка
### Системные зависимости
**Linux** — нужны заголовки ALSA (для аудио-вывода через [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** *(не проверено — собиралось и тестировалось только на Linux)* — отдельных аудио-зависимостей не нужно, `cpal` использует системный CoreAudio напрямую. Нужны стандартные инструменты сборки:
```bash
xcode-select --install
```
**Windows** *(не проверено — собиралось и тестировалось только на Linux)* — отдельных аудио-зависимостей тоже не нужно (`cpal` использует WASAPI), достаточно обычной установки Rust через [rustup](https://rustup.rs) (MSVC toolchain). Для нормального отображения обложки в терминале нужен современный эмулятор с поддержкой true color — рекомендуется Windows Terminal; протоколы отображения картинок (kitty/sixel/iTerm2) там, скорее всего, не поддерживаются, так что обложка будет через block-графику как fallback.
**SMB-фича (`c` в Library)** реализована собственным SMB-клиентом на чистом Rust (крейт [`smb`](https://crates.io/crates/smb)) — не зависит от системного `gio`/gvfs и его особенностей на конкретном дистрибутиве. Подключение и сканирование шары идут в фоновом потоке — интерфейс не подвисает даже если сеть медленная.
**Требует Rust 1.85+ (edition 2024)** — на момент написания в Ubuntu/Debian это отдельные версионированные пакеты (`rustc-1.91`/`cargo-1.91` и т.п.), а не то, что стоит по умолчанию (там обычно 1.75). Если у тебя старый `rustc` — либо поставь такие версионированные пакеты через `apt`, если они есть в твоём дистрибутиве, либо используй `rustup`.
**Честная оговорка по тестированию**: сама SMB-логика (подключение, рекурсивный листинг папок, чтение файлов с точным `seek`, декодирование аудио через SMB) проверена на реальном Samba-сервере и работает. Но в песочнице, где это разрабатывалось, не было настоящей звуковой карты — `cpal` (аудио-библиотека) там гарантированно падает при старте, и в этом конкретном состоянии (плюс активный поток аудио-движка) обнаружилось зависание при листинге SMB-папки. Причина не докопана до конца (что-то на стыке ALSA/cpal и tokio-реактора при **неудачной** попытке cpal открыть устройство) — и не исключено, что это специфично именно для отсутствия звуковой карты, а не проявится там, где `cpal` открывает поток нормально. Само SMB-подключение теперь работает в фоновом потоке, так что даже если это зависание всё-таки проявится — интерфейс не замёрзнет целиком, зависнет только конкретная попытка подключения. Если у тебя это тоже случится — дай знать, будем разбираться дальше уже на работающем звуке, что сильно сузит круг подозреваемых.
Для интернет-радио (**SomaFM**, **Radio Paradise** и т.д.) и MusicBrainz дополнительных системных зависимостей не нужно ни на одной ОС — TLS обеспечивает `rustls` (чистый Rust, без OpenSSL).
### Сборка проекта
```bash
git clone <URL-этого-репозитория>
cd nsm
cargo build --release
```
Бинарник появится в `target/release/nsm` (на Windows — `target\release\nsm.exe`).
### Сборка Windows-бинарника с Linux (кросс-компиляция)
```bash
# 1. Добавить Windows-таргет (нужен rustup — обычная установка Rust с rustup.rs)
rustup target add x86_64-pc-windows-gnu
# 2. Поставить mingw-w64 — кросс-компилятор/линкер под Windows
sudo apt-get install gcc-mingw-w64-x86-64 # Debian/Ubuntu; на других — свой пакетный менеджер
# 3. Указать cargo, каким линкером пользоваться для этого таргета
mkdir -p .cargo
cat >> .cargo/config.toml <<EOF
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
EOF
# 4. Собрать
cargo build --release --target x86_64-pc-windows-gnu
```
Бинарник появится в `target/x86_64-pc-windows-gnu/release/nsm.exe`.
**Проверено частично**: сам линкер (`mingw-w64`) реально протестирован — собрал им тестовую C-программу, получился настоящий Windows PE-исполняемый файл. Полную сборку `nsm` под Windows-таргет от начала до конца проверить не удалось (нужен `rustup` для загрузки Windows std-библиотеки, а в этом окружении к нему нет доступа) — но по ходу проверки нашёлся и уже исправлен реальный баг: код использовал Unix-специфичный способ определения размера ячейки терминала для обложки, из-за чего сборка под Windows не прошла бы вообще. Теперь на не-Unix платформах используется разумное значение по умолчанию.
### Запуск
```bash
./target/release/nsm /путь/к/музыке
```
Если путь не указан, по умолчанию используется `~/Music`.
## Управление
### Глобальные клавиши (любая вкладка)
| Клавиша | Действие |
|---|---|
| `Space` | play/pause |
| `N` / `P` | следующий / предыдущий трек |
| `s` | shuffle вкл/выкл |
| `r` | repeat (выкл → всё → один трек → выкл) |
| `m` | mute |
| `[` / `]` | скорость воспроизведения вниз/вверх |
| `←` / `→` | перемотка 5с / +5с |
| `e` | фокус на эквалайзер |
| `h` | включить/выключить запись истории прослушиваний |
| `v` | переключить обложка/спектр |
| `z` | развернуть обложку/спектр на весь экран |
| `F` | затухание в конце трека вкл/выкл |
| `G` | выравнивание громкости вкл/выкл |
| `I` | переключить протокол отображения обложки (если автоопределение ошиблось) |
| `Tab` | следующая вкладка |
| `q` | выход |
### Эквалайзер (после `e`)
| Клавиша | Действие |
|---|---|
| `←` / `→` | выбор полосы |
| `↑` / `↓` | ±1dB на выбранной полосе |
| `0` | сброс текущей полосы |
| `R` | сброс всех полос |
| `Esc` / `e` | выйти из фокуса |
### Library
| Клавиша | Действие |
|---|---|
| `j`/`k`, `↓`/`↑` | навигация |
| `Enter` | раскрыть альбом / сыграть |
| `Esc` | свернуть альбом / сбросить поиск и показать всю библиотеку |
| `/` | поиск |
| `t` | переключить вид: по альбомам ↔ плоский список всех треков (Enter на любом треке — играть всё оттуда) |
| `a` | добавить в плейлист |
| `c` | подключить SMB-шару |
| `L` | добавить ещё одну папку к библиотеке |
| `C` | очистить библиотеку (не трогает файлы на диске) |
### Playlists
| Клавиша | Действие |
|---|---|
| `j`/`k` | навигация |
| `Enter` | открыть плейлист / играть с этого места |
| `n` | новый плейлист |
| `J`/`K` | переместить трек вниз/вверх |
| `d` | удалить трек из плейлиста |
| `Esc` | назад к списку плейлистов |
### Queue
| Клавиша | Действие |
|---|---|
| `j`/`k` | навигация |
| `Enter` | играть с выбранного места |
### Radio
| Клавиша | Действие |
|---|---|
| `j`/`k` | навигация |
| `Enter` | играть станцию |
| `u` | добавить свою станцию по URL |
| `o` | поиск станций онлайн |
| `d` | удалить (только свою) станцию |
### Stats
| Клавиша | Действие |
|---|---|
| `p` | переключить период (день → неделя → месяц) |
| `x` | сбросить историю (нужно нажать дважды подряд для подтверждения) |
### New Releases
| Клавиша | Действие |
|---|---|
| `p` | переключить период (день → неделя → месяц) |
| `f` | проверить новые релизы у всех артистов из библиотеки (может занять время, см. ниже) |
| `a` | вручную проверить одного артиста по имени (не обязательно есть в библиотеке) — добавляется к уже показанному списку |
В полях ввода текста — `Enter` подтверждает, `Esc` отменяет, `Backspace` стирает.
## Архитектура
Cargo-workspace из четырёх крейтов:
```
crates/
├── player/ — движок воспроизведения: декодирование (symphonia), вывод звука (cpal),
│ эквалайзер, ресемплинг для скорости, очередь, интернет-радио
├── library/ — сканирование библиотеки, теги (lofty), cue-листы, плейлисты,
│ радиостанции, SMB
├── tui/ — интерфейс на ratatui: вкладки, обложка, эквалайзер, event loop
└── nsm/ — тонкий бинарник, связывающий всё вместе
```
`player` и `library` не знают друг о друге — `tui` связывает их через `player::QueueItem`.
## История прослушиваний
Хранится локально в `~/.config/nsm/history.jsonl` (одна запись — одна строка JSON: артист, трек, дата/время). Никуда не отправляется. При первом запуске спрашивается разрешение; в любой момент можно включить/выключить клавишей `h`, а на вкладке **Stats** — посмотреть диаграмму по часам/дням, список конкретных прослушанных треков и сбросить всё (двойным нажатием `x`).
## Кракозябры в старых тегах
Многие старые mp3 (особенно с тегами из 2000-х) хранят кириллицу в ID3v1 в кодировке CP1251, но она формально не объявлена — большинство библиотек (включая нашу) по умолчанию читают такие теги как Latin-1, получая "Ïåñíÿ" вместо "Песня". `nsm` определяет этот случай эвристически (по code points ≤0xFF и доле кириллических букв после перекодировки в CP1251) и чинит автоматически, не трогая уже нормальный UTF-8 текст.
## Обложка
Протокол отображения обложки (kitty/sixel/iTerm2/halfblocks) определяется автоматически по терминалу. Если автоопределение угадало неверно (например, показывает низкое разрешение на терминале, который на самом деле поддерживает kitty-графику) — жми `I`, чтобы вручную перебрать протоколы; выбор запоминается и применяется при следующих запусках.
## Новые релизы
Вкладка **New Releases** явно показывает релизы **только для артистов, уже имеющихся в твоей библиотеке** — это не глобальный чарт новинок (RYM активно блокирует ботов, и честно скрапить его не вышло — см. обсуждение). Вместо этого используется открытая база [MusicBrainz](https://musicbrainz.org), без ключей API.
Из-за ограничения MusicBrainz (не больше одного запроса в секунду) проверка большой библиотеки может занять время — по прогресс-бару видно, какой артист сейчас проверяется. Для очень плодовитых артистов (много синглов/переизданий за много лет) может понадобиться несколько запросов подряд на одного артиста — MusicBrainz не гарантирует сортировку результатов по дате, поэтому вычитывается вся дискография постранично, а не только первые 100 записей (иначе новый релиз мог случайно не попасть в выборку). Результаты кэшируются в `~/.config/nsm/new_releases_cache.json`, повторный запуск `nsm` сразу покажет последний результат без обращения к сети; обновление — вручную клавишей `f`.
**Важно понимать масштаб**: вкладка показывает релизы **только за последний день/неделю/месяц** (переключение `p`) — это буквально "что вышло недавно", а не вся дискография артиста. Если твой любимый артист не выпускал ничего последние 30 дней (для большинства артистов это норма), список для него будет пустым — это ожидаемое поведение, не баг.
## Выравнивание громкости
Клавиша `G`. Если в файле есть тег ReplayGain (`REPLAYGAIN_TRACK_GAIN` и аналоги — читается универсально через `lofty` для FLAC/MP3/OGG/MP4) — используется точное значение из тега. Если тега нет — включается адаптивный левеллер (AGC): он постоянно измеряет громкость последних 1-3 секунд и плавно подстраивает усиление к цели. Это **не то же самое**, что настоящий ReplayGain (посчитанный заранее по всему треку) — первые секунды трека выравниваются не идеально, дальше подстройка сходится.
## Известные ограничения
- Минимальный размер терминала — 70×18; на меньшем показывается предупреждение вместо сломанного интерфейса
- **Собиралось и тестировалось только на Linux** — сборки под macOS/Windows не проверялись (SMB теперь на чистом Rust и в теории кросс-платформенна, но не тестировалась вне Linux)
- **Opus не декодируется** (см. раздел «Форматы»)
- **Плейлисты не хранят диапазоны cue-треков** — добавление cue-трека в плейлист сохраняет файл целиком, а не конкретный виртуальный трек
- Подключение к SMB, поиск радиостанций и проверка новых релизов — все работают в фоне и не блокируют интерфейс
- Мульти-файловые cue-листы (редкость) поддерживаются только по первому `FILE` в листе