[2026-09-27] eagle: personal/plans/music-artist-albumartist-repair.md personal/tech/id3-tag-repair.md
This commit is contained in:
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-го плана** — то есть правка по факту
|
||||
не нужна, а в диффе строка есть. Пользователь обозначил принцип: **в диффе только то,
|
||||
что реально поменяется.**
|
||||
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in new issue
Block a user