This commit is contained in:
Magnus Root 2026-08-17 16:52:46 +03:00
parent 4fb1ea911f
commit 8457ccc448
7 changed files with 37 additions and 37 deletions

2
Cargo.lock generated
View file

@ -1700,7 +1700,7 @@ dependencies = [
]
[[package]]
name = "nm"
name = "nmp"
version = "0.1.0"
dependencies = [
"anyhow",

View file

@ -4,7 +4,7 @@ members = [
"crates/player",
"crates/library",
"crates/tui",
"crates/nm",
"crates/nmp",
"crates/textfix",
"crates/smbfs",
]

View file

@ -1,4 +1,4 @@
# nsm
# nmp
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.
@ -69,11 +69,11 @@ No extra system dependencies are needed on any OS for internet radio (**SomaFM**
```bash
git clone <this-repo-url>
cd nsm
cd nmp
cargo build --release
```
The binary will be at `target/release/nsm` (on Windows — `target\release\nsm.exe`).
The binary will be at `target/release/nmp` (on Windows — `target\release\nmp.exe`).
### Cross-compiling a Windows binary from Linux
@ -94,14 +94,14 @@ 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`.
The binary will be at `target/x86_64-pc-windows-gnu/release/nmp.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.
**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 `nmp` 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
```bash
./target/release/nsm /path/to/music
./target/release/nmp /path/to/music
```
If no path is given, it defaults to `~/Music`.
@ -225,18 +225,18 @@ crates/
├── 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
└── nmp/ — 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).
Stored locally in `~/.config/nmp/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.
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 "Песня". `nmp` 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
@ -246,7 +246,7 @@ The rendering protocol (kitty/sixel/iTerm2/halfblocks) is auto-detected based on
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`.
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/nmp/new_releases_cache.json`, so relaunching `nmp` 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.

View file

@ -1,4 +1,4 @@
# nsm
# nmp
Терминальный музыкальный плеер на Rust — что-то вроде [cmus](https://cmus.github.io/), но с нуля, с эквалайзером, поддержкой cue-листов, интернет-радио и SMB-шар.
@ -70,11 +70,11 @@ xcode-select --install
```bash
git clone <URL-этого-репозитория>
cd nsm
cd nmp
cargo build --release
```
Бинарник появится в `target/release/nsm` (на Windows — `target\release\nsm.exe`).
Бинарник появится в `target/release/nmp` (на Windows — `target\release\nmp.exe`).
### Сборка Windows-бинарника с Linux (кросс-компиляция)
@ -95,14 +95,14 @@ EOF
# 4. Собрать
cargo build --release --target x86_64-pc-windows-gnu
```
Бинарник появится в `target/x86_64-pc-windows-gnu/release/nsm.exe`.
Бинарник появится в `target/x86_64-pc-windows-gnu/release/nmp.exe`.
**Проверено частично**: сам линкер (`mingw-w64`) реально протестирован — собрал им тестовую C-программу, получился настоящий Windows PE-исполняемый файл. Полную сборку `nsm` под Windows-таргет от начала до конца проверить не удалось (нужен `rustup` для загрузки Windows std-библиотеки, а в этом окружении к нему нет доступа) — но по ходу проверки нашёлся и уже исправлен реальный баг: код использовал Unix-специфичный способ определения размера ячейки терминала для обложки, из-за чего сборка под Windows не прошла бы вообще. Теперь на не-Unix платформах используется разумное значение по умолчанию.
**Проверено частично**: сам линкер (`mingw-w64`) реально протестирован — собрал им тестовую C-программу, получился настоящий Windows PE-исполняемый файл. Полную сборку `nmp` под Windows-таргет от начала до конца проверить не удалось (нужен `rustup` для загрузки Windows std-библиотеки, а в этом окружении к нему нет доступа) — но по ходу проверки нашёлся и уже исправлен реальный баг: код использовал Unix-специфичный способ определения размера ячейки терминала для обложки, из-за чего сборка под Windows не прошла бы вообще. Теперь на не-Unix платформах используется разумное значение по умолчанию.
### Запуск
```bash
./target/release/nsm /путь/к/музыке
./target/release/nmp /путь/к/музыке
```
Если путь не указан, по умолчанию используется `~/Music`.
@ -243,18 +243,18 @@ crates/
├── library/ — сканирование библиотеки, теги (lofty), cue-листы, плейлисты,
│ радиостанции, SMB
├── tui/ — интерфейс на ratatui: вкладки, обложка, эквалайзер, event loop
└── nsm/ — тонкий бинарник, связывающий всё вместе
└── nmp/ — тонкий бинарник, связывающий всё вместе
```
`player` и `library` не знают друг о друге — `tui` связывает их через `player::QueueItem`.
## История прослушиваний
Хранится локально в `~/.config/nsm/history.jsonl` (одна запись — одна строка JSON: артист, трек, дата/время). Никуда не отправляется. При первом запуске спрашивается разрешение; в любой момент можно включить/выключить клавишей `h`, а на вкладке **Stats** — посмотреть диаграмму по часам/дням, список конкретных прослушанных треков и сбросить всё (двойным нажатием `x`).
Хранится локально в `~/.config/nmp/history.jsonl` (одна запись — одна строка JSON: артист, трек, дата/время). Никуда не отправляется. При первом запуске спрашивается разрешение; в любой момент можно включить/выключить клавишей `h`, а на вкладке **Stats** — посмотреть диаграмму по часам/дням, список конкретных прослушанных треков и сбросить всё (двойным нажатием `x`).
## Кракозябры в старых тегах
Многие старые mp3 (особенно с тегами из 2000-х) хранят кириллицу в ID3v1 в кодировке CP1251, но она формально не объявлена — большинство библиотек (включая нашу) по умолчанию читают такие теги как Latin-1, получая "Ïåñíÿ" вместо "Песня". `nsm` определяет этот случай эвристически (по code points ≤0xFF и доле кириллических букв после перекодировки в CP1251) и чинит автоматически, не трогая уже нормальный UTF-8 текст.
Многие старые mp3 (особенно с тегами из 2000-х) хранят кириллицу в ID3v1 в кодировке CP1251, но она формально не объявлена — большинство библиотек (включая нашу) по умолчанию читают такие теги как Latin-1, получая "Ïåñíÿ" вместо "Песня". `nmp` определяет этот случай эвристически (по code points ≤0xFF и доле кириллических букв после перекодировки в CP1251) и чинит автоматически, не трогая уже нормальный UTF-8 текст.
## Обложка
@ -264,7 +264,7 @@ crates/
Вкладка **New Releases** явно показывает релизы **только для артистов, уже имеющихся в твоей библиотеке** — это не глобальный чарт новинок (RYM активно блокирует ботов, и честно скрапить его не вышло — см. обсуждение). Вместо этого используется открытая база [MusicBrainz](https://musicbrainz.org), без ключей API.
Из-за ограничения MusicBrainz (не больше одного запроса в секунду) проверка большой библиотеки может занять время — по прогресс-бару видно, какой артист сейчас проверяется. Для очень плодовитых артистов (много синглов/переизданий за много лет) может понадобиться несколько запросов подряд на одного артиста — MusicBrainz не гарантирует сортировку результатов по дате, поэтому вычитывается вся дискография постранично, а не только первые 100 записей (иначе новый релиз мог случайно не попасть в выборку). Результаты кэшируются в `~/.config/nsm/new_releases_cache.json`, повторный запуск `nsm` сразу покажет последний результат без обращения к сети; обновление — вручную клавишей `f`.
Из-за ограничения MusicBrainz (не больше одного запроса в секунду) проверка большой библиотеки может занять время — по прогресс-бару видно, какой артист сейчас проверяется. Для очень плодовитых артистов (много синглов/переизданий за много лет) может понадобиться несколько запросов подряд на одного артиста — MusicBrainz не гарантирует сортировку результатов по дате, поэтому вычитывается вся дискография постранично, а не только первые 100 записей (иначе новый релиз мог случайно не попасть в выборку). Результаты кэшируются в `~/.config/nmp/new_releases_cache.json`, повторный запуск `nmp` сразу покажет последний результат без обращения к сети; обновление — вручную клавишей `f`.
**Важно понимать масштаб**: вкладка показывает релизы **только за последний день/неделю/месяц** (переключение `p`) — это буквально "что вышло недавно", а не вся дискография артиста. Если твой любимый артист не выпускал ничего последние 30 дней (для большинства артистов это норма), список для него будет пустым — это ожидаемое поведение, не баг.

View file

@ -164,49 +164,49 @@ pub fn cover_for(path: &Path) -> Option<Vec<u8>> {
scanner::read_cover(path)
}
/// Путь по умолчанию для хранения плейлистов: `~/.config/nm/playlists`.
/// Путь по умолчанию для хранения плейлистов: `~/.config/nmp/playlists`.
pub fn default_playlists_dir() -> PathBuf {
dirs_config_dir().join("nm").join("playlists")
dirs_config_dir().join("nmp").join("playlists")
}
/// Путь по умолчанию для хранения пользовательских радиостанций: `~/.config/nm`.
/// Путь по умолчанию для хранения пользовательских радиостанций: `~/.config/nmp`.
pub fn default_radio_dir() -> PathBuf {
dirs_config_dir().join("nm")
dirs_config_dir().join("nmp")
}
/// Путь по умолчанию для истории прослушиваний: `~/.config/nm/history.jsonl`.
/// Путь по умолчанию для истории прослушиваний: `~/.config/nmp/history.jsonl`.
pub fn default_history_path() -> PathBuf {
dirs_config_dir().join("nm").join("history.jsonl")
dirs_config_dir().join("nmp").join("history.jsonl")
}
/// Путь по умолчанию для настроек приложения: `~/.config/nm/settings.json`.
/// Путь по умолчанию для настроек приложения: `~/.config/nmp/settings.json`.
pub fn default_settings_path() -> PathBuf {
dirs_config_dir().join("nm").join("settings.json")
dirs_config_dir().join("nmp").join("settings.json")
}
/// Путь для кэша соответствий "артист -> MusicBrainz ID".
pub fn default_mbid_cache_path() -> PathBuf {
dirs_config_dir().join("nm").join("mbid_cache.json")
dirs_config_dir().join("nmp").join("mbid_cache.json")
}
/// Путь для кэша последнего результата проверки новых релизов.
pub fn default_new_releases_cache_path() -> PathBuf {
dirs_config_dir().join("nm").join("new_releases_cache.json")
dirs_config_dir().join("nmp").join("new_releases_cache.json")
}
/// Путь для списка подписок на подкасты.
pub fn default_podcasts_store_path() -> PathBuf {
dirs_config_dir().join("nm").join("podcasts.json")
dirs_config_dir().join("nmp").join("podcasts.json")
}
/// Путь для кэша последнего разобранного состояния каждого фида подкаста.
pub fn default_podcasts_cache_path() -> PathBuf {
dirs_config_dir().join("nm").join("podcasts_cache.json")
dirs_config_dir().join("nmp").join("podcasts_cache.json")
}
/// Путь для персистентного состояния аудиокниг (прогресс, ручные правки метаданных).
pub fn default_audiobooks_path() -> PathBuf {
dirs_config_dir().join("nm").join("audiobooks.json")
dirs_config_dir().join("nmp").join("audiobooks.json")
}
/// Папка по умолчанию, куда скачиваются эпизоды подкастов.

View file

@ -1,10 +1,10 @@
[package]
name = "nm"
name = "nmp"
version.workspace = true
edition.workspace = true
[[bin]]
name = "nm"
name = "nmp"
path = "src/main.rs"
[dependencies]