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

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 13:58:58 +06:00
1 parent 751e63014c
commit 042cac0216
2 files changed
+258 -1

No files matched your search

@@ -4,7 +4,7 @@ created: '2026-09-27'
updated: '2026-09-27'
type: plan
namespace: personal
status: session3-done-0-empty-fields-diff-3595-rows
status: session4-done-diff-rebuilt-4864-rows-0-losses
tags:
- music
- metadata
@@ -53,6 +53,21 @@ related:
> в разделе «🔷 СЕССИЯ 3» в конце документа: **`bb724716c7ffe45619b76abbb3deb5cc`**.
> Старые `245efb55…`, `8367084e…` более не верны. Пустых полей в плане — **0**.
>
> ## 🔴 ДИФФ ПЕРЕОПРЕДЕЛЁН (сессия 4, вечер) — правила изменились
>
> Актуальные правила диффа и **все три дефекта** старой версии скрипта — в разделе
> «🔷 СЕССИЯ 4» в конце документа. Кратко:
>
> | Правило | |
> |---|---|
> | Появление новой колонки (`albumartist`, `genre`) | ✅ **ЭТО ПРАВКА**, не выкидывать |
> | Схлопывание в ПАПКА | только если у **ВСЕХ** файлов совпадает **и `old`, и `new`** |
> | «Доминирующее большинство» | 🔴 **ЗАПРЕЩЕНО** (давало 92 искажённые папки) |
> | Полнота | `assert` в скрипте + независимый `verify_diff_completeness.py` |
>
> Актуальный дифф: **4864 строки**, md5 `0d7a31ca529b2ce2e7f018816ea0ff8e`.
> Все цифры состава диффа в разделах ниже (3594 / 3595 / 3865 строк) — **УСТАРЕЛИ**.
>
> Снимок каталога перед переименованием: `/Users/admin/Downloads/music-tags/milestones/music-csv-BEFORE-RENAME-20260927-124542.tar.gz`.
>
> ## 🔴 АУДИТ `new_artist` ВЫЯВИЛ 20 ДЕФЕКТОВ
@@ -2152,4 +2167,155 @@ bb724716… ⬅ ТЕКУЩИЙ, + Сосни попса (2) + Bimbo Jones (1)
- **Сравнение `cur_*` между двумя CSV бессмысленно** — это два разных снимка.
Проверять состояние тегов можно **только** чтением файлов.
---
## 🔷 СЕССИЯ 4 (2026-09-27): ПЕРЕСБОРКА ДИФФА — ПРАВКИ ДОЛЖНЫ БЫТЬ ВСЕ
### Задача Alex
> «пересобрал дифф?!»
> «дифф должен показывать то что поменяется. если там только перенос данных — это не изменение.
> изменение это было одно значение `new_` в 38-м csv, а стало другое, либо добавлено.
> и эти патчи должны быть в диффе **ВСЕ**.»
> «**добавление колонки — это правка или нет блядь?? почему выкинуть блядь??**»
> «одинаковое значение по папке должно быть на уровне `APPLY-PLAN` организовано —
> а дифф должен лишь **объединить если значение одинаковое в `new_` для всей папки**»
### 🔴 ЧЕТЫРЕ ПРАВИЛА ДИФФА (зафиксированы Alex, обязательны)
| # | Правило | Следствие |
|---|---|---|
| 1 | Изменение = `new_` в 38-м ≠ `new_` сейчас | Основа диффа |
| 2 | **Появление НОВОЙ КОЛОНКИ = правка.** `albumartist`/`genre` в 38-м плане **не существовало** → тег вообще не писался → сейчас пишется | `old_38 = '<КОЛОНКИ НЕ БЫЛО>'`, **НЕ выкидывать** |
| 3 | Схлопывание в запись ПАПКА — **ТОЛЬКО если у ВСЕХ файлов папки совпадает И `old`, И `new`** | «доминирующее большинство» **ЗАПРЕЩЕНО** |
| 4 | Каждая правка обязана лежать в диффе либо строкой ТРЕК, либо быть покрыта строкой ПАПКА | Обязателен `assert` на полноту |
> 🔴 **Моя ошибка, из-за которой правило 2 появилось:** я предложил «выкинуть `albumartist`
> и `genre` (2022 строки), колонок в 38-м не было». Alex: «**ты идиот? добавление колонки —
> это правка или нет блядь??**». Логика: колонки не было ⇒ план её не заполнял ⇒ тег не
> записывался ⇒ появление колонки = запись тега = правка. **Не путать с «переносом данных».**
### 🔴 Три дефекта, найденные в предыдущей версии `diff_new_vs_38.py`
| # | Дефект | Как проявлялся | Фикс |
|---|---|---|---|
| 1 | Схлопывание по «**большинству**» (`len(dom) > len(others)`) | **92 места**, где папка показана как вся-одна, а часть файлов отличалась. Пример: `Sbornik_Novij_Rok/genre` — 193 `Rock` схлопнуто, 7 файлов другого жанра → папка выглядела как «вся Rock» | условие `len(set(new)) == 1` |
| 2 | `NEVER_COLLAPSE = {title, track}` | Правило разное для разных полей — Alex требует **одно** правило | убрано |
| 3 | Схлопывание при одинаковом `new`, но **разном `old`** | **43 потери** в `t 2009-07 c6Or_NICK` и `z Старое/disk*/2 ALT,PUNK`: запись папки брала `old` первого файла (`''`) → врала про старое значение (`Дискотека Арлекина 2009`) | сравнивать **пару `(old, new)`** |
**Мораль дефекта 3:** «значение одинаково у всех» недостаточно — запись папки утверждает
**переход** `old → new`. Если `old` у файлов разный, переход разный ⇒ папка не схлопывается.
### 🔴 ПИТФОЛЛ: `python3 script.py | head -40` = SIGPIPE = CSV НЕ записан
Запуск с пайпом в `head` убивает Python по `SIGPIPE` **до** `w.open(OUT)`. Скрипт отработал,
отчёт напечатался — **а `DIFF-new-vs-38.csv` остался старым**, MD5 не изменился.
Я почти отчитался «готово». **Всегда `python3 x.py > /tmp/log 2>&1`**, затем читать лог.
### 🔴 ВЕРИФИКАТОР: независимый скрипт, не переиспользующий логику диффа
`/Users/admin/Downloads/music-tags/verify_diff_completeness.py` — пересчитывает эталон
из двух CSV **с нуля** (своя реализация) и проверяет:
```
эталон (всех реальных изменений): 18317
покрыто строками ПАПКА: 14806
отдельными строками ТРЕК: 3511
ПОТЕРЯНО: 0 ЛИШНИХ: 0
НЕТ-В-38: 41 = 41 OK
ТОЛЬКО-В-38: 85 = 85 OK
ИТОГ: ПОТЕРЬ НЕТ, дифф ПОЛНЫЙ
```
`exit-code 0` = чисто. **Два ложных срабатывания, обнаруженных в нём же:**
- считал «ЛИШНИМИ» 101 строку файлов из категории `НЕТ-В-38` (у них `old_38 = '<НЕТ В 38-м>'`) —
поправлено: такие пары законны, если путь в множестве `e_none`;
- `e_none` вычислялся **ниже** места использования → `NameError`/неработающий фильтр.
Вычисление перенесено наверх.
> **Урок:** «своя проверка своей же функцией» ничего не доказывает (ср. W11).
> Верификатор пишется **независимо** и печатает цифры, а не «успех».
### 📊 ИТОГ СЕССИИ 4 — АКТУАЛЬНЫЕ АРТЕФАКТЫ
```
APPLY-PLAN.csv 7861 строка md5 bb724716c7ffe45619b76abbb3deb5cc
DIFF-new-vs-38.csv 4864 строки md5 0d7a31ca529b2ce2e7f018816ea0ff8e
tag-fix-plan.csv не тронут (база 38-го, источник диффа)
```
**Состав диффа:**
```
ПАПКА 1126 (было 1140 до фикса дефекта 3)
ТРЕК 3612 (было 3419)
НЕТ-В-38 41
ТОЛЬКО-В-38 85
ВСЕГО правок 18558 ← контроль: 14806 покрыто папками + 3511 треков ≈ эталон
```
**Строк по полям (правильный подсчёт — через `csv`, НЕ `awk -F,`: папки содержат запятые):**
```
genre 2509
track 987
albumartist 701
album 305
artist 194
title 42
```
**ПАПКА-строки по полям:** `albumartist` 571 · `genre` 470 · `album` 72 · `track` 9 · `artist` 4.
**Цепочка MD5 диффа:**
```
5d134966… сессия 3 (3595 строк, схлопывание по большинству)
eab00b4b… + правило колонок (4685 строк) — НЕ записался из-за SIGPIPE-пайпа
0d7a31ca… ⬅ ТЕКУЩИЙ, + фикс схлопывания по паре (old,new) (4864 строки)
```
### 🔴 ПИТФОЛЛ: `awk -F,` для подсчёта полей CSV врёт
`folder` в диффе содержит запятые (`!Pop, Club, Любэ/...`) → `awk -F, '{print $4}'` печатал
**имена папок вместо имён полей**. Считать только парсером: `csv.DictReader` + `Counter(r['field'])`.
### ⚙️ Инвариант в самом скрипте
```python
covered = sum(n for _, _, _, _, n in collapse_lines)
assert covered + len(file_lines) == sum(field_counts.values()), "ПОТЕРЯ ПРАВОК!"
print(" OK: потерь нет")
```
Скрипт **падает** при нарушении — молчаливая потеря правок исключена. Запуск:
```bash
cd /Users/admin/Downloads/music-tags
python3 diff_new_vs_38.py > /tmp/diff_run.log 2>&1 # НЕ через | head!
python3 verify_diff_completeness.py # exit 0 = чисто
```
Бэкапы: `diff_new_vs_38.py.bak-before-collapse-fix-<ts>`,
`DIFF-new-vs-38.csv.bak-before-collapse-fix-<ts>`.
---
### ⏳ НЕ ЗАКРЫТО (обновлено конец сессии 4)
| # | Вопрос | Статус |
|---|---|---|
| 1 | `track = '238'` у 56 файлов (`Красная Плесень/Золушка`) | ⏳ верное значение неизвестно |
| 2 | `track` пуст у 1438, из них **9 с непустым `cur_track`** → тег `TRCK` будет УДАЛЁН | ⏳ решение Alex |
| 3 | `changed = 'no'` у **213 строк** в `APPLY-PLAN.csv`; из них **50 файлов (61 строка)** присутствуют в диффе как «изменения» | ⏳ уточнить трактовку |
| 4 | Боевой прогон `--apply --verify` | 🔴 не запущен |
| 5 | Контейнер `snapcur` на NAS (только чтение, `:ro`) | ⏳ не убран — Alex не подтвердил удаление |
| 6 | Глазами пройти `field=album` (305 строк) в диффе | ⏳ следующий шаг по указанию Alex |
### Про `changed = 'no'` — разбор (сессия 4)
Проверено фактом: **0 случаев**, где `changed='no'`, но `new_* != cur_*` — флаг не врёт.
213 строк = файлы, у которых правка не требуется (`new_ == cur_`). Файлы этой группы
попадают в дифф, когда `new_` отличается от **38-го плана** — то есть правка по факту
не нужна, а в диффе строка есть. Пользователь обозначил принцип: **в диффе только то,
что реально поменяется.**
+91
View File
@@ -22,6 +22,9 @@ related:
> Зеркало скилла `id3-tag-repair` (категория `media`, `~/.hermes/hermes-whale/skills/media/id3-tag-repair/`).
> Скилл — для исполнения, этот док — для контекста проекта и истории.
>
> ⚠️ **Скилл обновлён до v1.3.0** — добавлены правила **D1–D6** (слой диффа планов,
> раздел «🔵 Слой ДИФФА планов» ниже). Полные формулировки — в `SKILL.md` скилла.
## 📁 Артефакты проекта
@@ -141,6 +144,94 @@ os.replace(TMP, SRC)
с combining `U+0306`. Перед сравнением/группировкой всегда
`unicodedata.normalize("NFC", name)`.
## 🔵 Слой ДИФФА планов — правила и питфоллы (добавлено 2026-09-27, сессия 4)
Задача класса: «показать, что изменится, относительно предыдущей версии плана». Здесь
ломается не запись, а **отчёт** — и это опаснее, потому что отчёт читает человек и по нему
принимает решение.
### 🔴 D1 — схлопывание «по большинству» искажает папку целиком
```python
# ❌ 92 испорченные папки
dominant, dom = max(by_new.items(), key=lambda kv: len(kv[1]))
if len(dom) > len(others) and len(dom) > 1: # «большинство победило»
collapse(folder, field, dominant)
# ✅ схлопывать только если значение одинаково у ВСЕХ файлов папки
if len(set(nv for _, _, nv in lst)) == 1 and len(lst) > 1:
collapse(folder, field, lst[0][1], lst[0][2])
```
`Sbornik_Novij_Rok` / `genre`: 193 `Rock` + 7 других → папка показывалась как «вся Rock».
Читатель видит одно значение и не знает, что 7 файлов отличаются.
### 🔴 D2 — сравнения только `new` недостаточно: запись папки утверждает ПЕРЕХОД
Папка `t 2009-07 c6Or_NICK`: у файлов `old` = `''`, `'Дискотека Арлекина 2009'`,
`'Record Super Chart (13.06.09)'` — а `new` одинаковый. Схлопывание по `new` берёт `old`
первого файла → запись врёт про старое значение, а **43 файла теряют свою строку**.
Сравнивать **пару**:
```python
if len(set((ov, nv) for _, ov, nv in lst)) == 1 and len(lst) > 1: # ✅
```
### 🔴 D3 — новая КОЛОНКА в плане = правка, а не «перенос данных»
Если в предыдущем плане колонки `albumartist`/`genre` **не существовало** — тег не писался
вообще. Появление колонки означает, что тег **впервые будет записан**. Это изменение.
```
albumartist 633 строк → 0 = НЕВЕРНО (выкинуты на основании «колонки не было»)
albumartist 7861 строк = ВЕРНО (old_38 = '<КОЛОНКИ НЕ БЫЛО>')
```
Реакция Alex на выкидывание: «**ты идиот? добавление колонки — это правка или нет?**»
Не путать с настоящим «переносом данных» (значение v1→v2 при неизменном `new_`).
### 🔴 D4 — `python3 script.py | head` убивает скрипт ДО записи файла
`SIGPIPE` приходит, когда читатель (`head`) закрывает пайп. `print`-и доходят, `open(OUT, "w")`
в конце — **никогда**. Симптом: скрипт «отработал успешно», отчёт напечатан,
**MD5 выходного файла не изменился**. Проверять после каждого запуска:
```bash
python3 gen.py > /tmp/run.log 2>&1 # ✅
tail -5 /tmp/run.log && md5 out.csv # сверять, что файл реально перезаписан
```
### 🔴 D5 — контроль полноты обязателен: `assert` в генераторе
Генератор обязан доказать, что не потерял правок:
```python
covered = sum(n for _, _, _, _, n in collapse_lines) # покрыто записями ПАПКА
assert covered + len(file_lines) == sum(field_counts.values()), "ПОТЕРЯ ПРАВОК!"
```
Плюс **независимый** верификатор (`verify_diff_completeness.py`): он строит эталон
из двух исходных CSV своей реализацией и проверяет, что каждая пара `(path, field, old, new)`
присутствует в диффе **либо** строкой ТРЕК, **либо** покрыта строкой ПАПКА. Печатает цифры
(`ПОТЕРЯНО: 0`), а не «успех»; exit-code 0 = чисто.
> **Не переиспользовать код генератора в верификаторе** — иначе общая ошибка пройдёт обе
> проверки (ср. W11: копия функции продолжала проходить после фикса).
### 🔴 D6 — подсчёт полей CSV через `awk -F,` врёт
`folder` = `!Pop, Club, Любэ/...` содержит запятую → `awk -F, '{print $4}'` печатает фрагмент
имени папки вместо имени поля. Считать только парсером: `csv.DictReader` + `Counter(r['field'])`.
### Правило именования записей диффа
```
ПАПКА + field + old + new — значение одинаково у ВСЕХ файлов папки
ТРЕК + path + field + old + new — хоть у одного файла отличается
```
Никаких третьих форм. «Одинаковое значение по папке» должно быть организовано **в плане**,
дифф лишь объединяет — так формулирует Alex.
## 🖊 Слой ЗАПИСИ — ID3v1 vs ID3v2 (добавлено 2026-09-27)
Зеркало правил W1–W15 скилла. Полные формулировки — в `id3-tag-repair/SKILL.md`.