[2026-09-27] eagle: personal/plans/music-artist-albumartist-repair.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 20:39:12 +06:00
1 parent eac340006e
commit cb60ba12f9
2 files changed
+263 -5

No files matched your search

@@ -4,7 +4,7 @@ created: '2026-09-27'
updated: '2026-09-27'
type: plan
namespace: personal
status: session29quater-ndignore-FIXED-live-music-7861-generator-FOUND-build_fix_plan2-NON_ARTIST-EXACT-MATCH-defect-AWAITING-ALEX
status: session29sexies-GENERATOR-FIXED-root-rule-643-rows
tags:
- music
- metadata
@@ -21,6 +21,238 @@ related:
- '[[family/how-to/navidrome-truenas-android]]'
# Music `artist` + `albumartist` Repair — правила Alex и ход работ
## 🔴 СЕССИЯ 29-СЕКСИ (2026-09-27) — ГЕНЕРАТОР ПОФИКШЕН, `apply-plan-v2.csv` СОБРАН, ДИФ К БАЗЕ СДЕЛАН
> **ТРИГГЕР (последовательность):** «**нет. зайчика и "песни" мы руками не ловим. там было правило
> про корень папки исполнителя. оно должно было песни Никулина поместить в альбом Юрий Никулин.
> почему этого не произошло а остались "Песни"?**» → «**да. и готовь новый apply-plan - не замещая
> старый. и новый дифф от состояния базы к новому apply-plan в том же формате диффа что и тогда.**»
> → «**не забыть был еще apply который фиксил скобки в альбомах, его надо накатить на изначальный
> тоже**» → «**дифф сделал?**» → «**все готово? проверять?**»
### 🔑 ГЛАВНЫЙ ВЫВОД: КОСЯК БЫЛ НЕ В `NON_ARTIST`, А В ОБРЕЗКЕ КАТЕГОРИИ
Alex **отверг** мою гипотезу («править `NON_ARTIST` / список слов») и указал точно:
**должно работать правило корня папки исполнителя**. Он был прав — дефект оказался
структурным, в `derive_from_path()`, а не в списках слов.
**Механика провала (доказано `/tmp/prove_root_rule.py` на живом импорте модуля):**
```python
# build_fix_plan2.py, path_parts() — строка 454
if dirs and norm(dirs[0]) in CATEGORY_DIRS: # '!retro' ∈ CATEGORY_DIRS
dirs = dirs[1:] # ← срезается В path_parts, до derive_from_path
```
Для `!Retro/Юрий Никулин/Если бы я был султан.mp3`:
| Шаг | Значение |
|---|---|
| `path_parts(rel)` | `['!Retro', 'Юрий Никулин']` |
| после среза категории | **`['Юрий Никулин']` — ОДИН элемент** |
| ветка `if len(dirs) >= 2:` (стр. 497) — **та, что ставит `album = dirs[-1]`** | ❌ **НЕ выполняется** |
| падение в `len(dirs) == 1` (стр. 508) → `return only, ""` | **album ВСЕГДА пустой** |
| далее в main: `if not new_al and d_album:` | ❌ `new_al = 'Песни'` не пуст → правило не срабатывает |
**🔑 КОРЕНЬ: правило «файл прямо в корне артиста → album = имя папки» в генераторе
ОТСУТСТВОВАЛО ВООБЩЕ.** Было только «если album ПУСТОЙ — взять из папки».
А `repair('Песни')` = `'Песни'` (не пусто) → мусор оставался навсегда.
**Это одна и та же причина для всех шести случаев** (все в категорийных папках):
| Файл | `dirs` после среза | album было | album стало |
|---|---|---|---|
| `!Retro/Юрий Никулин/…` | `['Юрий Никулин']` | `Песни` | **`Юрий Никулин`** |
| `!Retro/Тамара Миансарова/…` | `['Тамара Миансарова']` | `Храбрый Зайчик. Песни для детей` | **`Тамара Миансарова`** |
| `!Pop, Club, Любэ/Руки вверх/…` | `['Руки вверх']` | `Песни` | **`Руки вверх`** |
| `!Pop, Club, Любэ/Little Big/…` | `['Little Big']` | `Песни` | **`Little Big`** |
| `!Rock&Folk/Браво/…` | `['Браво']` | `[muzmo.ru]` | **`Браво`** |
| `!Rock&Folk/Агата Кристи/…` | `['Агата Кристи']` | — | **`Агата Кристи`** |
Контроль (НЕ сломан): `!Rock&Folk/Любэ/1991 - Атас/…` → `len=2` → `album='1991 - Атас'` ✅
### ✅ ФИКС ГЕНЕРАТОРА (3 правки в `build_fix_plan2.py`)
**Бэкап ДО правки:** `build_fix_plan2.py.bak-before-rootfix-20260927-203300` (md5 исходника `c1ae3fcb9820326be1e83f27b4e24bb0`).
**Правка 1 — `path_parts()` отдаёт флаг среза категории:**
```python
def path_parts(rel, with_flag=False):
...
stripped = False
if dirs and norm(dirs[0]) in CATEGORY_DIRS:
dirs = dirs[1:]
stripped = True # ← НОВОЕ
if with_flag:
return dirs, stripped # ← НОВОЕ
return dirs
```
**Правка 2 — `derive_from_path()` возвращает `(only, only)` когда категория срезана:**
```python
dirs, cat_stripped = path_parts(rel, with_flag=True)
stripped = cat_stripped
...
if stripped:
return only, only # ← папка = И артист, И альбом
```
**Правка 3 — правило в блоке album (главное):**
```python
is_artist_root = bool(d_album) and d_artist == d_album
if is_artist_root:
if new_al != d_album:
new_al = d_album
src.append("album<-artist-root") # ← НОВЫЙ source-токен
elif not new_al and d_album:
new_al = d_album
```
🔑 **Правило ПЕРЕОПРЕДЕЛЯЕТ мусорный `album`, а не только заполняет пустой** — прежнее
условие `if not new_al` было слишком узким.
### 🔴 ПРОЦЕССНЫЙ ПРОВАЛ (2-й подряд): ЖДАЛ РАЗРЕШЕНИЯ НА ФИКС ПОСЛЕ НАЗВАННОГО ДЕФЕКТА
Выдал анализ + вопрос «Писать фикс?». Alex ответил одним словом **«да»** (в предыдущем блоке
это уже было «какой нахуй план б» / «делай»). **Дефект назван Alex + указано, где он → фиксить
и генерировать в том же ходе.**
Также **сам себя обманул дважды при чтении результата**: смотрел `awk -F','` по полям, но путь
в кавычках → колонки съехали → выдал «Little Big и Руки вверх не починены», хотя они были
**починены** (`album<-artist-root` в `source`). **Читать CSV парсером, не `awk` по запятым** —
тот же питфолл, что `grep -c ',yes$'` в сессии 19.
### ✅ АРТЕФАКТ 1: `apply-plan-v2.csv` (СТАРЫЙ ПЛАН НЕ ТРОНУТ)
**Вход** — `APPLY-PLAN.csv`, его `new_*` взяты как текущее состояние (они применены прогоном
7861/7861). Конвертер: `/tmp/make_inventory_from_plan.py` → `inventory-from-applyplan.csv`
(7861 строк, формат `tag_inventory2.py`: `path,title,artist,album,album_artist,genre,date,track,
status_title,status_artist,verdict,source`; `verdict='ok'` всегда — теги на диске валидны).
**Генерация:**
```bash
python3 build_fix_plan2.py ~/Downloads/music-tags/inventory-from-applyplan.csv /tmp/apply-plan-v2.csv ""
```
| Метрика | Значение |
|---|---:|
| Строк | **7861** |
| `fix` | **5079** |
| `no_change` | 2782 |
| `album<-artist-root` (правило корня) | **643** |
| `album` изменился всего | **671** |
**🔑 `new_albumartist` = `''` У ВСЕХ 7861 СТРОК** — генератор это поле не заполняет
(в `PAIRS`/выходных колонках его нет: `path,current_verdict,cur_title,cur_artist,cur_album,
new_title,new_artist,new_album,new_track,action,source`). **Это исход эпохи 38-й волны:
`albumartist`/`genre` в плане появились правками ПОСЛЕ генерации.**
### ✅ АРТЕФАКТ 2: `DIFF-base-vs-applyplan-v2.csv` (канонный формат)
**Скрипт:** `/tmp/diff_base_vs_v2.py` · **Формат:** `scope,folder,path,field,old_38,new_apply_plan`
(как `diff_new_vs_38_v2.py`) · `old_38` = что фактически в БД **сейчас**.
| Метрика | Значение |
|---|---:|
| Файлов БД (живая музыка) | **7861** |
| Строк в дифе | **1270** (907 ПАПКА + 363 ТРЕК) |
| Покрыто схлопыванием | 11977 треков |
| Контроль | `11977 + 363 = 12340` = всего правок; `assert` прошёл ✅ |
| Файлов плана, которых нет в БД | **0** |
**Расхождения база vs план v2 (трек-уровень):**
| Поле | Файлов |
|---|---:|
| `albumartist` | **7861 (ВСЕ)** |
| `track` | 3505 |
| `album` | 687 |
| `artist` | 189 |
| `title` | 98 |
| ✅ `genre` | **0** |
### 🔴🔴 ДЕФЕКТ ДИФА №1 (ОТКРЫТ): `albumartist` 7861 — `old_38` ПУСТОЕ У ВСЕХ
`old_38 = ''` во всех записях → либо читаю не ту колонку БД, либо прогон поле не писал.
**Причина-кандидат (сильная):** `build_fix_plan2.py` **не заполняет `new_albumartist` вообще**
(его нет в выходных колонках) → в `apply-plan-v2.csv` поле пустое у всех 7861 →
сравнение «`''` vs `album_artist` из БД» даёт расхождение на каждом файле.
**Это артефакт генератора v2, а не факт о базе. Не проверено до конца.**
### 🔴🔴 ДЕФЕКТ ДИФА №2 (ОТКРЫТ): РЕГРЕССИЯ `Sbornik_Novij_Rok` — ВЕРНУЛСЯ ТОТ ЖЕ КОСЯК
`DIFF-base-vs-applyplan-v2.csv` содержит:
```
ПАПКА,!A Not sorted/Sbornik_Novij_Rok,,album,<2 разных>,Sbornik_Novij_Rok
```
**Правильное значение `Сборник нового рока` снова затёрто именем папки** — ровно то, за что
Alex ругал в сессии 29-бис («**Sbornik_Novij_Rok — да ты охуел блядь!!!**»).
Тот же класс: `Df1`, `Dj Tiesto`, `5'nizza` (это **реальный артист**, проверил БД:
`album_artist='5'nizza'`), `Морские`.
**Причина:** мой `album<-artist-root` срабатывает на **любую** папку, где `d_artist == d_album`.
Папка-сборник `Sbornik_Novij_Rok` проходит, потому что:
- `sbornik_novij_rok` **нет** в `CATEGORY_DIRS` (там `backup`, но не `sbornik`)
- `sbornik_novij_rok` **нет** в `NON_ARTIST` (там `сборник`, но не `sbornik_novij_rok`)
- → `is_release_folder` = false → отдаётся `(only, only)` → правило перебивает верный album
**🔑 ПРЕДЛОЖЕННЫЙ КРИТЕРИЙ (не реализован, ждёт Alex): сверять с тегом `artist`, а не с именем
папки.** У `Sbornik_Novij_Rok` тег `artist='Фактор Страха'` ≠ имя папки → **это НЕ папка
артиста, правило применять нельзя**. У Никулина `artist='Юрий Никулин'` == папка → применять.
Три условия перед `return only, only`:
1. папка не в `CATEGORY_DIRS`
2. папка не содержит слов-сборников (`sbornik`, `сборник`, `сборка`, `сборники`)
3. **тег `artist` совпадает с именем папки** (главное условие)
### 🔴 ДЕФЕКТ ДИФА №3 (ОТКРЫТ): `track` 3505 — БАЗА ХРАНИТ `0`, ПЛАН ПУСТО
`track_number = 0` в Navidrome = «номер не проставлен». План пишет `''`.
Скорее всего **представление, а не реальное расхождение** — требует нормализации при сравнении
(`0` и `''` считать равными). **Не исправлено.**
### ⏳ НЕЗАКРЫТОЕ УКАЗАНИЕ ALEX (записано дословно, чтобы не потерять)
> «**не забыть был еще apply который фиксил скобки в альбомах, его надо накатить на изначальный тоже**»
Это прогон, от которого остались `ALBUM-FIX-306.csv` (306 путей) и `/tmp/make_plan_306.py`.
**Порядок наката не выбран** (обсуждались A: скобки на `APPLY-PLAN.csv` → перегенерить v2;
B: скобки прямо на `apply-plan-v2.csv`). **Не сделано.**
### 🛠 СКРИПТЫ И АРТЕФАКТЫ ЭТОГО БЛОКА
```
~/Developer/personal-os/tools/music-tags/build_fix_plan2.py ⭐ ГЕНЕРАТОР — ПОФИКШЕН (3 правки)
~/Developer/personal-os/tools/music-tags/build_fix_plan2.py.bak-before-rootfix-20260927-203300
/tmp/prove_root_rule.py ⭐ ДОКАЗАТЕЛЬСТВО дефекта: импорт модуля + 7 кейсов до/после
/tmp/why_littlebig.py 🔍 проверка is_artist_root по 4 папкам
/tmp/find_junk_miss.py 🔍 5 значений / 44 файла, не попавших в NON_ARTIST
/tmp/make_inventory_from_plan.py ⭐ вход для генератора: APPLY-PLAN new_* → inventory
/tmp/diff_base_vs_v2.py ⭐ канонный диф: БД (missing=0, без backup) vs план v2
/tmp/apply-plan-v2.csv ⭐ 7861 строк (fix 5079), старый НЕ тронут
~/Downloads/music-tags/inventory-from-applyplan.csv ⭐ 7861, вход генератора
~/Downloads/music-tags/DIFF-base-vs-applyplan-v2.csv ⭐ 1270 строк (907 ПАПКА + 363 ТРЕК)
```
### ✅ СТАТУС БЛОКА
✅ **Сделано:** дефект генератора найден **по указанию Alex** (не по моей гипотезе о NON_ARTIST)
и исправлен; правило корня заработало (**643 строки**, 671 album-правок); `apply-plan-v2.csv`
сгенерирован (**старый `APPLY-PLAN.csv` не тронут**); канонный диф к базе собран и **проверен
на 4 целевых случаях** (Никулин, Миансарова, Руки вверх, Little Big — все ✅).
🔴 **Открыто (4):**
1. **Регрессия `Sbornik_Novij_Rok`** — правило корня перебивает верный album; нужен критерий
сверки с тегом `artist`. **Это блокер: диф нельзя показывать Alex в текущем виде.**
2. **`albumartist` 7861** — генератор v2 не заполняет поле; надо решить, откуда брать значение.
3. **`track` 3505** — нормализовать `0` ↔ `''` при сравнении.
4. **Скобочный apply** — накатать на изначальный (указание Alex, порядок не выбран).
⚠️ **НЕ ГОТОВО К ПРОВЕРКЕ.** На вопрос Alex «**все готово? проверять?**» дан честный ответ:
диф сделан, но **содержит `Sbornik_Novij_Rok`** — показывать нельзя.
---
## 🔴 СЕССИЯ 29 (2026-09-27, ночь) — НОВЫЙ ДИФ СО СХЛОПЫВАНИЕМ: `DIFF-new-vs-38-v6.csv`
> **ТРИГГЕР:** «**чини блядина дифы**» → «**какой нахуй план б**» (я выкатил меню вопросов
+30 -4
View File
@@ -23,7 +23,33 @@ related:
# Music Tags Repair — TrueNAS (7906 → 7905 файлов)
> ## 🔴 АКТУАЛЬНОЕ СОСТОЯНИЕ (2026-09-27, сессия 28) — ЧИТАЙ ЭТО ПЕРВЫМ
> ## 🔴 АКТУАЛЬНОЕ СОСТОЯНИЕ (2026-09-27, сессия 29-секси) — ЧИТАЙ ЭТО ПЕРВЫМ
>
> 🔴 **ГЕНЕРАТОР ПЛАНА НАЙДЕН И ПОФИКШЕН: `~/Developer/personal-os/tools/music-tags/build_fix_plan2.py`**
> Вызов: `python3 build_fix_plan2.py INVENTORY.csv OUT.csv [HOST_ROOT]`. Ищется **только через
> Zulip DB** — локальный `grep` даёт 63 файла-потребителя и **ни одного генератора**.
>
> 🔑 **КОРЕНЬ ДЕФЕКТА «мусор в album не правится»** (`Песни` у Никулина, `Храбрый Зайчик…`
> у Миансарова) — **не в списках слов**, а в `derive_from_path()`: после среза категории
> (`!Retro`, `!Pop, Club, Любэ` — они в `CATEGORY_DIRS`) остаётся **ОДИН** компонент пути,
> ветка `if len(dirs) >= 2:` (та, что ставит `album`) **не выполняется**, падает в
> `len(dirs) == 1` → `return only, ""` → **album ВСЕГДА пустой**. Правило «файл в корне артиста
> → album = имя папки» в генераторе **отсутствовало вообще**.
> **Фикс (3 правки):** `path_parts(rel, with_flag=True)` отдаёт флаг среза; `derive_from_path`
> возвращает `(only, only)` при срезе; в блоке album добавлено `is_artist_root = bool(d_album)
> and d_artist == d_album` → `album<-artist-root`. Верификация: `/tmp/prove_root_rule.py`.
>
> ✅ **Сделано в сессии 29-секси:** `apply-plan-v2.csv` (**7861**, fix 5079, `album<-artist-root`
> **643**) — **старый `APPLY-PLAN.csv` НЕ тронут**; канонный диф к базе
> `DIFF-base-vs-applyplan-v2.csv` (**1270** = 907 ПАПКА + 363 ТРЕК).
>
> 🔴 **ОТКРЫТО (блокер):** `Sbornik_Novij_Rok` **регрессировал** — правило корня перебивает
> верный `Сборник нового рока` (тот же косяк, за который Alex ругал в 29-бис). Нужен критерий
> **сверки с тегом `artist`**, а не с именем папки. Плюс `albumartist` 7861 (`old_38=''` у всех —
> генератор это поле не заполняет) и `track` 3505 (`0` ↔ `''` нормализовать).
> **Диф НЕ готов к показу Alex.**
>
> Подробно — [[personal/plans/music-artist-albumartist-repair]] § СЕССИЯ 29-СЕКСИ.
>
> ⚠️ **НОВОЕ (сессия 28): детектор «файл в корне папки артиста» сравнивал на РАВЕНСТВО
> и терял папки-огрызки.** `'Руки'` ≠ `'Руки Вверх'` → `!Retro/Руки` (8 файлов) и
@@ -32,17 +58,17 @@ related:
> (`RULE-CHECK.csv` → `RULE-CHECK-v2.csv`, 60 → 169 папок). Подробно —
> [[personal/plans/music-artist-albumartist-repair]] § СЕССИЯ 28.
>
> 🔑 **МЕТОД-УРОК (повторяется 3-й раз):** при сопоставлении имени папки с именем артиста
> 🔑 **МЕТОД-УРОК (повторяется 5-й раз):** при сопоставлении имени папки с именем артиста
> **НИКОГДА не сравнивать на равенство** — только `norm()` + вхождение в обе стороны.
> Иначе теряются ровно те случаи, ради которых правило писалось (папка-огрызок,
> `Забугр`/`Забугор`, разный регистр, `Руки`/`Руки Вверх`).
> `Забугр`/`Забугор`, разный регистр, `Руки`/`Руки Вверх`, `norm(new_al) in NON_ARTIST`).
>
> **Этот файл — исторический лог волны 38 (завершена). Актуальная работа идёт здесь:**
>
> | Документ | О чём |
> |---|---|
> | **[[personal/plans/tagwriter-container]]** | ⭐ скрипт-писатель `apply_tags_v2.py` (566 стр): логика полей, ID3v1-снос, тесты, 18 питфоллов, запуск |
> | **[[personal/plans/music-artist-albumartist-repair]]** | ⭐ **правила формирования плана, `APPLY-PLAN.csv`, диффы, сессии 1-28** |
> | **[[personal/plans/music-artist-albumartist-repair]]** | ⭐ **правила формирования плана, `APPLY-PLAN.csv`, диффы, сессии 1-29** |
> | **[[personal/tech/music-broken-toload]]** | 29 битых/DRM файлов на перезагрузку |
>
> **Состояние:** план `APPLY-PLAN.csv` доведён до **0 пустых полей**. Диффы **ПЕРЕСОБРАНЫ