# 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-запросы, если сервер их честно поддерживает) или скачивание на диск. Честно уважает заявленную в фиде кодировку (``) — многие старые русскоязычные подкасты именно так и оформлены - **Аудиокниги**: отдельная библиотека — сама распознаёт структуру Автор/Книга (и глубже, любую вложенность), автоматически объединяет части книги (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 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 </<название эпизода>.<расширение>` и сразу добавляется в библиотеку как обычный локальный файл. Стриминг (`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` в листе