[2026-09-26] eagle: personal/plans/music-tags-review/music-tags-review.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 18:55:15 +06:00
1 parent 82f27a69da
commit 31644d1010
2 files changed
+397 -18

No files matched your search

+354 -18
View File
@@ -4,7 +4,7 @@ created: '2026-09-26'
updated: '2026-09-26'
type: tech
namespace: personal
wave: 17
wave: 20
tags:
- music
- jellyfin
@@ -20,23 +20,32 @@ related:
# Music Tags Repair — TrueNAS (7906 файлов)
> **Статус: ВОСЕМНАДЦАТАЯ ВОЛНА (2026-09-26). CSV ГОТОВ К ПРИМЕНЕНИЮ.
> `Рэпер Сява`→`Сява`; РЕГРЕССИЯ 18 БИТЫХ СТРОК ПОЧИНЕНА (0 битых); ДВЕ СВОДНЫЕ CSV СОБРАНЫ.
> NAS ДОСТУПЕН, применение НЕ начато — ждём «go».**
> ✅ **НОВОЕ (18-я волна):** исправлена **собственная регрессия** — при вырезании техпапок из
> `new_album` в `z Старое/disk*` (17-я волна) терялись запятые → 18 битых строк. Все починены.
> **Урок: после каждого батча считать ЧИСЛО ПОЛЕЙ, не только кавычки и длину файла.**
> Диагностика: `python3 ~/Downloads/music-tags/diag_fields.py` (гистограмма полей, строки ≠ 11).
> 📊 **Две сводные CSV** (`build_summaries.py`, только чтение): `artists-single-track.csv`
> (**778** строк: artist/title/album/path) + `artists-multi-track.csv` (**7128** строк:
> artist/album/track/title/path). **778 + 7128 = 7906 — покрыты все строки.** Сортировка
> artist→album→track. Пустой artist → sentinel `(без исполнителя)` (иначе Excel схлопнет).
> 🖐 **188 строк без artist — НЕ косяк:** 186 из них `!Детские/Сказки/*` (аудиосказки без
> единого исполнителя), + `НАШЕСТВИЕ 2009`, + `Голубой щенок`. Так было с самого начала.
> Итог: **0 битых, 7907 строк, `fix` 6931 / `no_change` 976, ручных правок ~640.**
> ⏭ Осталось ТОЛЬКО применение: бэкап `Music/backup/` → применение → приёмка в UI →
> Music-библиотека Jellyfin. Плюс (отдельно) merge-конфликт в `1-needs-decision.md`.
> **Бэкапы:** `...bak-20260926-184122` (последний до регрессии), `bak-20260926-164432` (самый ранний).
> **Статус: ДВАДЦАТАЯ ВОЛНА (2026-09-26). CSV ГОТОВ, 0 БИТЫХ СТРОК, 7907 строк.
> Ждём решения по `NSK.mp3` + «go» на применение.**
> ✅ **НОВОЕ (20-я волна):** `Перец.mp3` (2 копии в `disk*`) → `Перцы`/`Валдай` (решение Alex
> из `2-medium-confidence.md` — было пропущено); все сводные CSV пересобраны (777 + 7129).
> 🔴 **ГЛАВНЫЙ УРОК ВОЛНЫ: ПЕРЕД ОТВЕТОМ ОБ ARTIST/ПРОБЛЕМНОЙ СТРОКЕ — ОТКРЫТЬ НОТЫ РЕВЬЮ.**
> Alex: «Перец я тебе в доку выписывал! В чем трабл??» — решение **уже было в
> `personal/plans/music-tags-review/2-medium-confidence.md`**, ассистент ответил по памяти
> и оставил `Перец`/пусто. Проверять `personal/plans/music-tags-review/*.md` grep'ом по имени
> файла, не полагаться на CSV `new_artist`.
> 🔴 **ВТОРОЙ УРОК: строка-НАДГРОБИЕ ≠ строка к обработке.** `НАШЕСТВИЕ 2009` помечен
> `no_change` + `source = dropped (file removed by user)` — файла нет. Выносить такие строки
> в списки «требуют решения» нельзя — Alex: «Нашествие мы блядь снесли откуда он опять взялся?!».
> Фильтровать `source` начинающийся с `dropped` при любых отчётах.
> 🔴 **ТРЕТИЙ УРОК: нет данных → СПРАШИВАТЬ, не оставлять молча пустым.** `NSK.mp3` (2 копии)
> — artist пустой, вопрос Alex'у не задан → Alex: «Почему ты не спросил меня что с ним делать?!».
> Открытый вопрос теперь вынесен явно (см. ниже).
> 📊 **Сводные CSV (20-я волна):** `artists-single-track.csv` **777** + `artists-multi-track.csv`
> **7129** = **7906 — все строки покрыты.**
> 🖐 **190 строк без artist (было 188 — +2 из-за `Перец` не считается, см. разбор):** 186
> `!Детские/Сказки/*` (artist не существует), `Голубой щенок/01 - Хоттабыч!`,
> `НАШЕСТВИЕ` (надгробие), **`NSK.mp3` ×2**. Живой разбор `NSK.mp3` на NAS: ID3v2.3 заголовок
> 1014 Б, **0 кадров** (тело — нули), аудио валидно: 128 kbps / 44100 Hz / **~29 сек**.
> Итог: **0 битых, 7907 строк, ручных правок ~641.**
> ⏭ Осталось: **решение по `NSK.mp3`** (A/B/C ниже) → бэкап `Music/backup/` → применение →
> приёмка в UI → Music-библиотека Jellyfin. Плюс merge-конфликт в `1-needs-decision.md`.
> **Бэкапы:** `...bak-20260926-185339` (последний), `bak-20260926-184122`, `bak-20260926-164432` (самый ранний).
>
> ---
>
@@ -92,6 +101,333 @@ related:
> Прогонялись **девять волн** проверок: см. таблицу волн ниже.
> Итог v7: «требует решения» **4**, medium **5811**, high **958**, mojibake = 0, остаточный mojibake = 0.
## 🖐 ДЕВЯТНАДЦАТАЯ ВОЛНА: СЯВА, РЕГРЕССИЯ, ИМЕНА ДИСКОВ, СВОДНЫЕ CSV (2026-09-26)
> Триггер: Alex — **«Закончи правку имен песен в csv. Вручную»** → далее **«Доделывай»**,
> затем **«Откуда строки с пустым артистом? Это косяк»** → **«Что за NSK? Ты что туда
> вписал как артист/трек? Больше нет треков без артиста?»**
### ✅ 1. `Рэпер Сява` → `Сява` (2 строки)
Alex: **«Рэпер сява - поправь на просто "Сява"»**. Обе строки в папке
`!Pop, Club, Любэ/!Radio/v 2009-03 mIr.vEsNЫ/2 y.MATno.E/`:
- `Нука нака.mp3` → `new_artist = Сява` (было `Рэпер Сява`)
- `Отдыхаем хорошо(Ida Corr cocktail).mp3` → artist `DJ Иван Silver & Сява`
> 🔴 **Ошибка при правке:** `patch mode=replace` дал fuzzy-match и **съел первую строку** —
> `old_string` совпал частично. Строк стало 7906 вместо 7907. **Обнаружено** сверкой
> `diff <(sed 's/Рэпер Сява/Сява/g' bak-184122) tag-fix-plan.csv`. Восстановлено вручную
> (вернул строку `Ленинград - Ласточка.mp3`, потерянную при починке).
> **Правило: `patch mode=replace` — только по УНИКАЛЬНОМУ тексту; после — сразу `wc -l`.**
### 🔴 2. РЕГРЕССИЯ 18 БИТЫХ СТРОК — ПОЧИНЕНА (главное этой волны)
Alex: **«Откуда строки с пустым артистом? Это косяк»**.
**Диагностика по бэкапам (гистограмма числа полей по волнам):**
| Бэкап | 3 поля | 10 | 11 | 12 |
|---|---:|---:|---:|---:|
| `bak-164432` (самый ранний, 15:15) | 2 | 2 | 7904 | 0 |
| `bak-171423` | 2 | 2 | 7904 | 0 |
| `bak-180619` (после 17-й волны) | 2 | **10** | 7886 | **10** |
| `bak-184122` | 2 | 10 | 7886 | 10 |
**Вывод: 4 битые строки были ИЗНАЧАЛЬНО** (Пахмутова — многострочный mojibake с `"` внутри,
разваливал парсинг). **Остальные 18 — регрессия 17-й волны:** при вырезании техпапок из
`new_album` в `z Старое/disk*` (`disk`, `disk_v2`, `5 POPSA`, `2 ALT,PUNK`, `3 SHANSON`)
**терялась ровно одна пустая запятая-плейсхолдер**.
**Три подкласса фикса:**
| Строк | Дефект | Фикс |
|---:|---|---|
| 8 | 10 полей — потерян пустой `new_album` | `NSK,,,fix` → `NSK,,,,fix` |
| 9 | 12 полей — лишняя пустая запятая | `Nickelback,,,7,fix` → `Nickelback,,7,fix` |
| 2 (Трофим) | 12 полей — **запятая в `Короче, дело к ночи` без кавычек** | `,"Короче, дело к ночи",Трофим,,4,fix` |
| 2 (Пахмутова) | 3/10 полей — многострочный mojibake с `"` внутри | значение заменено на читаемое `"Пахмутова — Нежность"` / `"Нежность"` |
**Итог: 22 битых → 0.** Проверено: `diag_fields.py` + `wc -l` = 7907 + CRLF = 7907 + BOM на месте.
> 🔴 **ПРАВИЛО (закрепить навсегда): после КАЖДОГО батча — считать ЧИСЛО ПОЛЕЙ, не кавычки.**
> Проверка кавычек (`awk -F'"' 'NF%2==0'`) и длины файла **НЕ ловит** потерю пустого поля:
> 10-полевая строка имеет чётное число кавычек и проходит все старые чекеры.
> ```bash
> python3 ~/Downloads/music-tags/diag_fields.py # гистограмма: должно быть {11: 7906}
> ```
### 🛠 3. БУКВЕННЫЕ/ЦИФРОВЫЕ ИМЕНА ДИСКОВ В `new_album` — ВЫЧИЩЕНЫ (200 строк)
**Главный новый класс дефекта.** Папка `!Детские/Сборка Песни для детей/` разбита на
подпапки-«диски» (`1`, `2`, `C`, `K`, `L`, `M`, `P`, `Y`, `Les`), и скрипт-граббер записал
имя подпапки в `new_album`. **Это часть пути, а не альбом.**
| Значение `new_album` | Строк | Реальный альбом |
|---|---:|---|
| `1`, `2`, `C`, `K`, `L`, `M`, `P`, `Y`, `Les` | **155** | `Сборка Песни для детей` |
| `misc` (Пилот, Rammstein, Красная Плесень, Eminem) | ~40 | пусто (нет реального альбома) |
| `new` (НТЛ) | 3 | пусто |
| `Album` (Пилот `Братишка.mp3`) | 1 | пусто |
Диагностический детектор:
```bash
awk -F',' 'NR>1 {n=NF; na=(n==11)?8:((n==13)?10:((n==12)?9:0));
if(na && length($na)<=4 && $na ~ /^[A-Za-z0-9_]+$/ && $na !~ /[а-яА-Я]/) print NR"|"$na}' tag-fix-plan.csv
```
> ⚠️ **Пойман побочный дефект:** в `Сборка Песни для детей` строка `19. Песенка смелого моряка`
> содержала `new_album = 1` и `new_track = 20` — при вырезании диска номер трека съехал.
> Проверять `new_track` рядом с вырезаемым `new_album`.
### 📊 4. ДВЕ СВОДНЫЕ CSV (запрос Alex)
Alex: **«подготовить рядом ещё пару csv: все исполнители с одним треком + путь; все
исполнители с более 1 трека, альбом, треки в альбоме, путь. С сортировкой
исполнитель/альбом/номер трека. (Имеются в виду исправленные названия для записи)
Эти две csv должны включать все строки из оригинальной csv.»**
Скрипт: `~/Downloads/music-tags/build_summaries.py` (**только чтение** исходника, пишет 2 файла).
| Файл | Строк | Колонки |
|---|---:|---|
| `artists-single-track.csv` | **778** | `artist, title, album, path` |
| `artists-multi-track.csv` | **7128** | `artist, album, track, title, path` |
- **778 + 7128 = 7906 — покрыты ВСЕ строки** (проверка встроена в скрипт, печатает `TOTAL COVERED`).
- Значения берутся из `new_*`, при пустом — fallback на `cur_*`.
- Сортировка: artist → album → track (числовая, `track_key()` с regex `^(\d+)`).
- Пустой artist → sentinel **`(без исполнителя)`**, иначе Excel схлопывает группу в невидимую.
> 🔴 **ПЕРВАЯ ВЕРСИЯ СКРИПТА БЫЛА БИТАЯ:** писала `e["artist"]` (пустой) вместо ключа группировки
> `e["key"]` → **189 строк с пустым artist** в `multi`. Alex поймал сразу. Фикс: писать `key`.
> **Урок: sentinel бесполезен, если в вывод идёт исходное поле, а не ключ.**
### 🖐 5. 188 СТРОК БЕЗ ARTIST — РАЗОБРАНЫ ЖИВЬЁМ (не косяк)
Alex: **«Больше нет треков без артиста?»**
| Источник | Кол-во | Вердикт |
|---|---:|---|
| `!Детские/Сказки/*` | 186 | аудиосказки — исполнителя **не существует** (сборник, не песня). Оставить пустым |
| `5 POPSA` нарезки (NSK, Перец и др.) | ~12 | ID3-кадры пустые — определять не из чего |
| `НАШЕСТВИЕ 2009 - ДЕНЬ ВТОРОЙ.mp3`, `Голубой щенок/01 - Хоттабыч!.mp3` | 2 | фестивальная нарезка / сказка |
**Проверка по самому раннему бэкапу `bak-164432` подтвердила: так было ИЗНАЧАЛЬНО**, не регрессия.
### 🔬 6. `NSK.mp3` — ЖИВОЙ РАЗБОР ФАЙЛА НА NAS
Alex: **«Что за NSK? Ты что туда вписал как артист/трек?»**
Строка: `new_title = NSK`, `new_artist` = пусто, `new_album` = пусто.
**Метод (питфоллы окружения):**
- `ffprobe` в контейнере/на хосте **молча отдаёт пусто** при битом/пустом ID3 — известный питфолл.
- `mutagen` на хосте NAS **НЕ установлен** (`ModuleNotFoundError`).
- `xxd` на хосте NAS **НЕТ** (`command not found`).
- **Вложенные кавычки в `ssh 'python3 -c "..."'` ломаются** (bash/zsh на NAS). **Рабочий путь:
написать скрипт локально → `scp` в `/tmp/` → `ssh … 'python3 /tmp/script.py'`** (совпадает
с каноном из `family/how-to/truenas-access.md`).
**Результат разбора (байты, `python3` на хосте NAS):**
```
ID3 v2.3 rev0 flags=0x0 size=1014 ← заголовок есть
кадров: 0 (тело — нули) ← теги ПОЛНОСТЬЮ пустые
audio starts at 1024, first sync 0xFFFB9240 → 128 kbps, 44100 Hz
файл 475954 Б ≈ 29 секунд ← валидный MP3, короткая нарезка
```
**Вывод: `NSK.mp3` — 29-секундная нарезка из чарт-сборника с пустыми тегами.** `NSK` в title —
это **имя файла** (первые буквы, вероятно от названия сборника/«Новосибирск»), **НЕ название
песни**. Раньше в artist лежало `disk_v2`/`5 POPSA` (мусорные папки) — вычищено, artist оставлен
**пустым**, а не выдуман.
> 📌 **Канон `id3_direct.py`:** запускать на **ХОСТЕ** NAS (`truenas_admin@mallexxx.duckdns.org`,
> python3 3.11.9 есть) — в контейнере `jellyfin` python3 НЕТ (см. `family/how-to/truenas-access.md`).
### ⏭ ОТКРЫТЫЙ ВОПРОС К ALEX (ждём ответа)
По ~12 нарезкам из `5 POPSA` (NSK, Перец, Саундтрек Простоквашено и др.) — три варианта:
**A)** вписать artist/title вручную (нужен ID по звуку); **B)** оставить `(без исполнителя)`;
**C)** пометить `no_change` — файлы не трогать. Сказки (186) — предлагается оставить как есть.
### 📊 СОСТОЯНИЕ НА КОНЕЦ ДЕВЯТНАДЦАТОЙ ВОЛНЫ
| Показатель | Значение |
|---|---:|
| Строк в CSV | 7906 (+1 заголовок = 7907 в файле) |
| **Битых строк** | **0** |
| CRLF-потерь | **0** |
| Ручных правок (`source = manual (user decision)`) | **~640** |
| `fix` / `no_change` | **6931 / 976** |
| Сайтов-источников / иероглифов в `new_*` | **0 / 0** |
| Техпапок и букв-дисков в `new_album` | **0** |
| Сводные CSV | ✅ 778 + 7128 = 7906 |
| Применение на NAS | **НЕ начато (ждём «go»)** |
**Бэкапы:** `bak-20260926-164432` (самый ранний), `bak-20260926-171423`, `bak-20260926-180619`,
`bak-20260926-183432`, `bak-20260926-183541`, `bak-20260926-184029`, `bak-20260926-184122`.
**Рабочие скрипты в `~/Downloads/music-tags/` (все read-only, кроме `build_summaries.py`):**
`diag_fields.py` (гистограмма полей), `build_summaries.py` (2 сводные CSV),
`repair_broken.py` / `repair_min.py` (черновики починки регрессии — **не применялись**,
регрессия починена вручную), `probe_nsk.py` (живой разбор ID3), `check_artist_number.py`,
`check_file_vs_csv.py`, `verify_group_a.py`, `dump_block.py`, `find_defects.py`, `probe_real.py`.
### ⏭ ЧТО ДЕЛАТЬ ПЕРВЫМ ДЕЛОМ В СЛЕДУЮЩЕЙ СЕССИИ
> ⚠️ **УСТАРЕЛО — см. «ДВАДЦАТАЯ ВОЛНА» выше.** Из 12 нарезок `5 POPSA` решён только `Перец`
> (`Перцы`/`Валдай`, 20-я волна). Реальный остаток — **`NSK.mp3` ×2** (A/B/C в 20-й волне).
1. Получить от Alex решение по `NSK.mp3` (A/B/C) — см. 20-ю волну.
2. **Бэкап `Music/backup/` → применение тегов по «go» → приёмка в UI Alex** → Music-библиотека Jellyfin.
3. Отдельно: **чистка merge-конфликта** в `personal/plans/music-tags-review/1-needs-decision.md`.
## 🖐 ДВАДЦАТАЯ ВОЛНА: ПЕРЕЦ, НАДГРОБИЯ, NSK (2026-09-26)
> Триггер: Alex — **«Закончи правку имен песен в csv. Вручную»** → **«Доделывай»** →
> **«Откуда строки с пустым артистом? Это косяк»** → **«Что за NSK? Ты что туда вписал как
> артист/трек? Больше нет треков без артиста?»** → **«Рэпер сява - поправь на просто "Сява"»** →
> **«Почему ты не спросил меня что с ним делать?! Это единственный такой трек непонятный?
> Все остальное не вызывает никаких сомнений? Перец я тебе в доку выписывал! В чем трабл??
> Нашествие мы блядь снесли откуда он опять взялся у тебя в списке?!»**
### ✅ 1. `Рэпер Сява` → `Сява` (2 строки)
Обе в `!Pop, Club, Любэ/!Radio/v 2009-03 mIr.vEsNЫ/2 y.MATno.E/`:
- `Нука нака.mp3` → `new_artist = Сява`
- `Отдыхаем хорошо(Ida Corr cocktail).mp3` → artist `DJ Иван Silver & Сява`
> 🔴 **Ошибка при правке:** `patch mode=replace` дал **fuzzy-match** и съел первую строку —
> стало 7906 вместо 7907. Обнаружено сверкой `diff <(sed 's/Рэпер Сява/Сява/g' bak-184122) tag-fix-plan.csv`.
> Затем при починке случайно удалилась строка `Ленинград - Ласточка.mp3` — тоже восстановлена.
> **Правило: после `patch` ВСЕГДА `wc -l`, и сверять с бэкапом через `sed`-подстановку.**
**Второй дефект той же правки:** удаляя `cur_album`/`new_album`, легко потерять **пустое поле** —
`,"Сява",,,fix` (11 полей) vs `,"Сява",,fix` (10). Считать поля, не запятые.
### 🔴 2. `Перец.mp3` — решение Alex БЫЛО В ДОКЕ, ассистент его пропустил
Alex: **«Перец я тебе в доку выписывал! В чем трабл??»**
**Реальное решение (в `personal/plans/music-tags-review/2-medium-confidence.md`):**
```
- **Перец.mp3** → Валдай — Перцы
```
⇒ `new_artist = Валдай`, `new_title = Перцы`.
**Почему пропущено:** `Перец.mp3` существует в **трёх** папках:
| Путь | Было | Стало |
|---|---|---|
| `z Старое/Перец.mp3` | ✅ `Перцы`/`Валдай` | (не трогалось, уже верно) |
| `z Старое/disk_v2/5 POPSA/Перец.mp3` | ❌ `Перец`/пусто | ✅ `Перцы`/`Валдай` |
| `z Старое/disk/5 POPSA/Перец.mp3` | ❌ `Перец`/пусто | ✅ `Перцы`/`Валдай` |
Ассистент чистил `disk*` и **не сверил с нотами ревью** — искал только по CSV.
> 🔴 **ПРАВИЛО (закрепить):** перед ответом по ЛЮБОМУ artist/title — **грепнуть
> `personal/plans/music-tags-review/*.md` по имени файла**, а не полагаться на текущее
> состояние CSV. Нота ревью — первичный источник решения Alex.
### 🔴 3. `НАШЕСТВИЕ 2009` — строка-НАДГРОБИЕ, а не задача
Alex: **«Нашествие мы блядь снесли откуда он опять взялся у тебя в списке?!»**
Строка 883: `no_change` + `source = dropped (file removed by user)`. **Файла нет** — Alex снёс.
Метка стоит с 16:57 (`bak-165750`), присутствует во всех бэкапах (строка не удалена из CSV
намеренно — чтобы не потерять `path`).
**Промах:** ассистент вынес её в список «строки без artist» как будто она к обработке.
Это сбило с толку.
> 🔴 **ПРАВИЛО:** при любых отчётах/списках **фильтровать `source`, начинающийся с `dropped`**.
> Такие строки — история, не работа. Отдельный вопрос Alex'у: убирать ли надгробия из CSV вообще.
### 🖐 4. ПОЛНЫЙ РАЗБОР строк без artist — 190 (не 188)
Скрипт: `~/Downloads/music-tags/list_noartist.py` (read-only).
| Источник | Кол-во | Вердикт |
|---|---:|---|
| `!Детские/Сказки/*` | **186** | у аудиосказок artist **не существует** (сборник, не песня). Решение Alex: оставить как есть, дубли разберёт сам |
| `Голубой щенок/01 - Хоттабыч!.mp3` | 1 | часть сказки «Голубой щенок» — artist не нужен |
| `!A Not sorted/НАШЕСТВИЕ 2009…` | 1 | **надгробие**, файла нет |
| `…/disk_v2/5 POPSA/NSK.mp3` | 1 | ❓ нарезка, теги пустые |
| `…/disk/5 POPSA/NSK.mp3` | 1 | ❓ та же нарезка, копия |
**Проверено по самому раннему бэкапу `bak-164432`:** так было ИЗНАЧАЛЬНО, это **не регрессия**.
### 🔬 5. `NSK.mp3` — живой разбор на NAS (артефакт для будущих сессий)
Alex: **«Что за NSK? Ты что туда вписал как артист/трек?»**
Строка: `new_title = NSK`, `new_artist = пусто`, `new_album = пусто`.
**Результат байтового разбора (`python3` на ХОСТЕ NAS):**
```
ID3 v2.3 rev0 flags=0x0 size=1014 ← заголовок есть, syncsafe = 0x000776
кадров: 0 (тело — полностью нули) ← теги ПУСТЫЕ
audio starts at 1024, first sync 0xFFFB9240 → 128 kbps, 44100 Hz
file size 475954 Б ≈ 29 секунд ← валидный MP3, короткая нарезка
```
**Вывод:** 29-секундная нарезка из чарт-сборника с пустыми тегами. `NSK` в title — **имя файла**
(вероятно «Новосибирск» или имя сборника-источника), **НЕ название песни**. Раньше в artist
лежал мусор `disk_v2`/`5 POPSA` — вычищен, artist оставлен **пустым** (не выдуман).
**Открытый вопрос Alex'у (A/B/C):**
- **A)** вписать artist/title вручную по звуку (нужен ID)
- **B)** оставить `(без исполнителя)`, title = `NSK`
- **C)** пометить `no_change` — файл не трогать
**Питфоллы окружения (проверено в этой волне):**
- `ffprobe` на хосте/в контейнере **молча отдаёт пусто** при пустом ID3 — не доказательство.
- `mutagen` на хосте NAS **НЕ установлен** (`ModuleNotFoundError`).
- `xxd` на хосте NAS **НЕТ** (`command not found`).
- 🔴 **Вложенные кавычки в `ssh 'python3 -c "..."'` ломаются** (zsh/bash на NAS съедает их,
скрипт получает пустоту). **Рабочий путь: написать скрипт локально → `scp` в `/tmp/` →
`ssh … 'python3 /tmp/script.py'`.** Совпадает с каноном `family/how-to/truenas-access.md`.
- Питфолл `sed`: `\r\n`-split в Python даёт сдвиг номеров строк после правок — брать строки
по **тексту**, не по номеру (`grep -n`, потом `sed -n` для просмотра).
### 📊 6. Сводные CSV пересобраны после правок
`python3 ~/Downloads/music-tags/build_summaries.py` (read-only исходника, пишет 2 файла):
| Файл | Строк | Колонки |
|---|---:|---|
| `artists-single-track.csv` | **777** | `artist, title, album, path` |
| `artists-multi-track.csv` | **7129** | `artist, album, track, title, path` |
**777 + 7129 = 7906 — покрыты ВСЕ строки** (проверка `TOTAL COVERED` встроена в скрипт).
Пустой artist → sentinel **`(без исполнителя)`**; для сказок ключ `(без исполнителя) — <album>`.
### 📊 СОСТОЯНИЕ НА КОНЕЦ ДВАДЦАТОЙ ВОЛНЫ
| Показатель | Значение |
|---|---:|
| Строк в CSV | 7906 данных + заголовок = **7907** |
| **Битых строк** | **0** |
| CRLF-потерь | **0** |
| Ручных правок | **~641** |
| Строк без artist | **190** (186 сказки + 4 прочие) |
| Сводные CSV | ✅ 777 + 7129 = 7906 |
| Применение на NAS | **НЕ начато (ждём «go»)** |
**Бэкапы:** `bak-20260926-185339` (последний, 20-я волна), `bak-20260926-184122`,
`bak-20260926-164432` (самый ранний).
**Рабочие скрипты `~/Downloads/music-tags/`:**
`diag_fields.py` (гистограмма полей), `build_summaries.py` (2 сводные CSV),
`list_noartist.py` (**новый, 20-я волна** — полный список строк без artist по папкам),
`probe_nsk.py` (живой разбор ID3 — переиспользован для дампа кадров),
`repair_broken.py` / `repair_min.py` (черновики, **не применялись** — регрессия починена вручную).
### ⏭ ЧТО ДЕЛАТЬ ПЕРВЫМ ДЕЛОМ В СЛЕДУЮЩЕЙ СЕССИИ
1. **Получить от Alex решение по `NSK.mp3`** (A/B/C выше) — 2 строки.
2. Спросить, убирать ли **строку-надгробие** `НАШЕСТВИЕ 2009` из CSV.
3. **Бэкап `Music/backup/` → применение тегов по «go» → приёмка в UI Alex** → Music-библиотека Jellyfin.
4. Отдельно: **чистка merge-конфликта** в `personal/plans/music-tags-review/1-needs-decision.md`.
## 🖐 СЕМНАДЦАТАЯ ВОЛНА: ЧИСТКА `no_change` — САЙТЫ, ИЕРОГЛИФЫ, ВОДА (2026-09-26)
> Триггер: Alex — **«Продолжай до конца»**, затем **«Можешь точечно использовать батч правки