nmp/README.md
2026-08-17 16:52:46 +03:00

284 lines
31 KiB
Markdown
Raw Permalink 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.

# nmp
Терминальный музыкальный плеер на 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 полос, свои биквадратные фильтры
- **Раздельная скорость и высота голоса**: изменение скорости не меняет тембр (pitch-preserving time-stretch с overlap-add, без щелчков на стыках кусков — проверено численно), высоту можно двигать отдельно и независимо
- **Усиление громкости**: до 300% сверх номинальной, с мягким (не жёстким) ограничением пиков — для тихих записей
- **Умный откат после паузы**: чем дольше пауза, тем больше откат назад при возобновлении — не теряется нить разговора
- **Пропуск тишины**: автоматически проматывает затянувшуюся тишину, короткие паузы в речи не трогает
- **Техническая информация о треке**: кодек, частота дискретизации, каналы, битность, битрейт (честно посчитанный из размера файла и длительности — корректно и для VBR), размер файла — прямо под эквалайзером
- **Обложка**: отображается прямо в терминале (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
- **Подкасты**: подписка по RSS-ссылке, список эпизодов, стриминг напрямую по ссылке (с перемоткой — через настоящие HTTP Range-запросы, если сервер их честно поддерживает) или скачивание на диск. Честно уважает заявленную в фиде кодировку (`<?xml encoding="windows-1251"?>`) — многие старые русскоязычные подкасты именно так и оформлены
- **Аудиокниги**: отдельная библиотека — сама распознаёт структуру Автор/Книга (и глубже, любую вложенность), автоматически объединяет части книги (CD1/CD2, Part 1/Part 2 и т.п.) в одну, но не путает с этим разные книги в одной папке автора. Прогресс (файл+позиция) сохраняется между запусками; вкладки Новая/Слушаю/Прослушано переключаются автоматически по проценту прослушанного, без ручной пометки. Пересканирование идемпотентно — повторное указание той же папки обновляет уже известные книги, а не плодит дубли; книги с пропавшими папками убираются из списка сами. Главы — через уже существующие cue-листы. Автора/название можно поправить вручную (частая проблема аудиокниг — в тегах путают чтеца с автором)
- **История прослушиваний**: при первом запуске спрашивает, запоминать ли прослушанное; статистика (артист/трек/дата) — диаграммой по часам за сегодня, по дням за неделю/месяц; включается/выключается в любой момент, сбрасывается с подтверждением
- **Новые релизы**: проверка новых альбомов/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 nmp
cargo build --release
```
Бинарник появится в `target/release/nmp` (на Windows — `target\release\nmp.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/nmp.exe`.
**Проверено частично**: сам линкер (`mingw-w64`) реально протестирован — собрал им тестовую C-программу, получился настоящий Windows PE-исполняемый файл. Полную сборку `nmp` под Windows-таргет от начала до конца проверить не удалось (нужен `rustup` для загрузки Windows std-библиотеки, а в этом окружении к нему нет доступа) — но по ходу проверки нашёлся и уже исправлен реальный баг: код использовал Unix-специфичный способ определения размера ячейки терминала для обложки, из-за чего сборка под Windows не прошла бы вообще. Теперь на не-Unix платформах используется разумное значение по умолчанию.
### Запуск
```bash
./target/release/nmp /путь/к/музыке
```
Если путь не указан, по умолчанию используется `~/Music`.
## Управление
### Глобальные клавиши (любая вкладка)
| Клавиша | Действие |
|---|---|
| `Space` | play/pause (с "умным откатом" — чем дольше была пауза, тем больше откат назад при возобновлении) |
| `N` / `P` | следующий / предыдущий трек |
| `s` | shuffle вкл/выкл |
| `r` | repeat (выкл → всё → один трек → выкл) |
| `m` | mute |
| `[` / `]` | скорость воспроизведения вниз/вверх (сохраняет высоту голоса) |
| `{` / `}` | высота голоса вниз/вверх (независимо от скорости) |
| `b` / `B` | усиление громкости вниз/вверх (до 300%, с мягким ограничением пиков) |
| `S` | пропуск затянувшейся тишины вкл/выкл |
| `←` / `→` | перемотка 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` | удалить (только свою) станцию |
### Podcasts
| Клавиша | Действие |
|---|---|
| `j`/`k` | навигация |
| `Enter` | открыть подкаст (список подписок) / стримить эпизод (список эпизодов, с перемоткой, если сервер поддерживает Range-запросы) |
| `u` | подписаться по URL RSS-фида |
| `f` | обновить фид (подтянуть новые выпуски) |
| `d` | отписаться (список подписок) / скачать эпизод (список эпизодов) |
| `Esc` | вернуться к списку подписок |
Скачанный эпизод сохраняется в `~/Music/Podcasts/<название подкаста>/<название эпизода>.<расширение>` и сразу добавляется в библиотеку как обычный локальный файл. Стриминг (`Enter` без скачивания) теперь тоже поддерживает перемотку — через настоящие HTTP Range-запросы (`Range: bytes=...`) напрямую к серверу эпизода, без скачивания на диск. Честно проверяем поддержку сервером по коду ответа (`206 Partial Content`, а не `200 OK` с игнорированием заголовка) — если конкретный сервер Range не поддерживает, перемотка для этого эпизода просто не сработает, без притворства. Скачивание остаётся самым надёжным вариантом для длинных выпусков или медленных серверов.
### Audiobooks
| Клавиша | Действие |
|---|---|
| `j`/`k` | навигация |
| `Enter` | продолжить книгу с сохранённой позиции |
| `o` | открыть список файлов/глав книги (без немедленного проигрывания) |
| `p` | переключить фильтр статуса (New → Listening → Finished → All) |
| `/` | поиск по названию/автору |
| `L` | добавить папку для сканирования |
| `f` | пересканировать все добавленные папки |
| `t` | вручную поправить название/автора книги |
| `x` | отметить книгу для объединения / объединить с ранее отмеченной |
| `Esc` | вернуться к списку книг (из списка файлов) |
Библиотека собирается из явно указанных папок (`L`) — никакого неявного доступа к остальной файловой системе. Понимает структуру `Автор/Книга` и произвольную вложенность глубже; если книга разбита на части (`CD1`/`CD2`, `Part 1`/`Part 2` и т.п. — с числом в имени папки), части автоматически объединяются в одну книгу, но папки с разными названиями книг под одним автором не путаются друг с другом. Прогресс (файл + позиция внутри него) сохраняется на диск каждые несколько секунд, переживает перезапуск и повторное сканирование. Статусы New/Listening/Finished вычисляются автоматически по проценту прослушанного — вручную не выставляются. Повторное указание той же папки не создаёт дублей (сверяется по набору папок книги), а книги, чьи папки пропали с диска, автоматически убираются из списка при следующем пересканировании. Главы читаются из `.cue`-листов рядом с аудиофайлом, если они есть.
### 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
└── nmp/ — тонкий бинарник, связывающий всё вместе
```
`player` и `library` не знают друг о друге — `tui` связывает их через `player::QueueItem`.
## История прослушиваний
Хранится локально в `~/.config/nmp/history.jsonl` (одна запись — одна строка JSON: артист, трек, дата/время). Никуда не отправляется. При первом запуске спрашивается разрешение; в любой момент можно включить/выключить клавишей `h`, а на вкладке **Stats** — посмотреть диаграмму по часам/дням, список конкретных прослушанных треков и сбросить всё (двойным нажатием `x`).
## Кракозябры в старых тегах
Многие старые mp3 (особенно с тегами из 2000-х) хранят кириллицу в ID3v1 в кодировке CP1251, но она формально не объявлена — большинство библиотек (включая нашу) по умолчанию читают такие теги как Latin-1, получая "Ïåñíÿ" вместо "Песня". `nmp` определяет этот случай эвристически (по code points ≤0xFF и доле кириллических букв после перекодировки в CP1251) и чинит автоматически, не трогая уже нормальный UTF-8 текст.
## Обложка
Протокол отображения обложки (kitty/sixel/iTerm2/halfblocks) определяется автоматически по терминалу. Если автоопределение угадало неверно (например, показывает низкое разрешение на терминале, который на самом деле поддерживает kitty-графику) — жми `I`, чтобы вручную перебрать протоколы; выбор запоминается и применяется при следующих запусках.
## Новые релизы
Вкладка **New Releases** явно показывает релизы **только для артистов, уже имеющихся в твоей библиотеке** — это не глобальный чарт новинок (RYM активно блокирует ботов, и честно скрапить его не вышло — см. обсуждение). Вместо этого используется открытая база [MusicBrainz](https://musicbrainz.org), без ключей API.
Из-за ограничения MusicBrainz (не больше одного запроса в секунду) проверка большой библиотеки может занять время — по прогресс-бару видно, какой артист сейчас проверяется. Для очень плодовитых артистов (много синглов/переизданий за много лет) может понадобиться несколько запросов подряд на одного артиста — MusicBrainz не гарантирует сортировку результатов по дате, поэтому вычитывается вся дискография постранично, а не только первые 100 записей (иначе новый релиз мог случайно не попасть в выборку). Результаты кэшируются в `~/.config/nmp/new_releases_cache.json`, повторный запуск `nmp` сразу покажет последний результат без обращения к сети; обновление — вручную клавишей `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` в листе