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

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 12:53:10 +06:00
1 parent 8ce2b1e6a4
commit 6912c41874
3 files changed
+294 -5

No files matched your search

@@ -4,7 +4,7 @@ created: '2026-09-27'
updated: '2026-09-27'
type: plan
namespace: personal
status: script-v13-id3v1-logic-tested-awaiting-go
status: csv-audited-20-artist-defects-awaiting-alex
tags:
- music
- metadata
@@ -23,6 +23,27 @@ related:
# Music `artist` + `albumartist` Repair — правила Alex и ход работ
> ## 🔴 ИМЕНА CSV ПЕРЕИМЕНОВАНЫ (2026-09-27) — актуальная раскладка
>
> Все упоминания `APPLY-PLAN-FINAL.csv` / `APPLY-PLAN-FINAL2.csv` **ниже по тексту устарели**.
> Актуально (в `~/milestones/music-csv-2026-09-26/` и `~/Downloads/music-tags/`):
>
> | Было | Стало | Роль |
> |---|---|---|
> | `APPLY-PLAN-FINAL2.csv` | **`APPLY-PLAN.csv`** | ✅ **БОЕВОЙ, единственный источник истины** (7861 стр, 16 колонок, md5 `245efb55ab1de26cb5172f0754192c3d`) |
> | `APPLY-PLAN-FINAL.csv` | `APPLY-PLAN-RAW.csv` | сырой план (track не нормализован) — **НЕ ПРИМЕНЯТЬ** |
> | `APPLY-PLAN.csv` (12:02) | `archive/APPLY-PLAN-v0-20260927-1202.csv` | устаревший (нет колонки `track`) |
> | `APPLY-PLAN2.csv` (12:03) | `archive/APPLY-PLAN-v0-20260927-1203.csv` | устаревший |
>
> Снимок каталога перед переименованием: `~/milestones/music-csv-BEFORE-RENAME-20260927-124542.tar.gz`.
>
> ## 🔴 АУДИТ `new_artist` ВЫЯВИЛ 20 ДЕФЕКТОВ — ПРАВКА ЖДЁТ ALEX
>
> `'Maria Makarova (organ)'` ×2 (исполнитель вместо композитора — должен быть **Бах**),
> `'Классика в современной обработке'` ×3, `'Сказочный сборник'` ×15.
> Подробно: [[personal/plans/tagwriter-container]] § 3.5.
> **Боевой прогон заблокирован до правки этих 20 файлов.**
**Задача Alex (2026-09-27):**
> «Артиста всем трём где он не указан или указан неправильно, включая детские песни ставишь
> в реального артиста. Что не знаешь — ищешь в интернете. Для _всех_ треков.
+98 -4
View File
@@ -4,7 +4,7 @@ created: '2026-09-26'
updated: '2026-09-27'
type: plan
namespace: personal
status: v13-csv-renamed-single-source-of-truth-awaiting-go
status: v14-csv-audited-20-defects-awaiting-alex
tags:
- music
- truenas
@@ -19,11 +19,12 @@ related:
---
# tagwriter — применение тегов к MP3 (TrueNAS)
> 🔴 **СТАТУС: скрипт готов и протестирован. БОЕВОЙ ПРОГОН НЕ ЗАПУЩЕН — ЖДЁТ «go» от Alex.**
> 🔴 **СТАТУС: скрипт готов и протестирован. Аудит CSV выявил 20 дефектов в `new_artist`
> (§ 3.5) — правка НЕ начата, ждёт решения Alex. БОЕВОЙ ПРОГОН НЕ ЗАПУЩЕН.**
## 1. Что делает скрипт
Применяет `APPLY-PLAN-FINAL2.csv` (7861 строка) к MP3 на `/storage/Music`.
Применяет `APPLY-PLAN.csv` (7861 строка) к MP3 на `/storage/Music`.
Пишет через `mutagen`, **только ID3v2.3-блок** — MPEG-поток не пересжимается.
### Логика полей — граница по КОЛОНКАМ CSV
@@ -119,6 +120,83 @@ changed=no: 213
MD5: `245efb55ab1de26cb5172f0754192c3d` — сверен между Mac и NAS.
## 3.5 🔴 АУДИТ CSV — 5 классов дефектов (2026-09-27)
Alex: «**ты нихуя не проверил csv. давай бэкапь ее и проверяй всю.**»
Бэкап снят: `APPLY-PLAN.csv.bak-chk-20260927-124918`.
Скрипт аудита: `~/Automation/music-tags/audit_artist.py` (8 классов A–H).
Результат — **2 реальных класса дефектов, 20 файлов**; остальные 6 классов оказались
ложными срабатываниями детектора.
### A. 🔴 Исполнитель вместо композитора — 2 файла
```
'Maria Makarova (organ)' Бах — Toccata and Fugue in D minor.mp3 ×2
```
**Мария Макарова — органистка, это ИСПОЛНИТЕЛЬ.** Должно быть `Иоганн Себастьян Бах`.
Это ровно тот кейс, из-за которого в прошлой сессии искали источник бага Jellyfin #18162 —
тогда нашли `Maria Makarova (organ)` в теге и не поправили.
### B. 🔴 Название сборника в поле артиста — 18 файлов
```
'Классика в современной обработке' Моцарт — Турецкий марш.mp3 ×3 → по композитору из title
'Сказочный сборник' Чебурашка, Котёнок Гав, Айболит, Щелкунчик … ×15
```
`'Сказочный сборник'` — не артист вообще. Для сказок нужно согласованное значение
(кандидаты: `Сказки` — как `albumartist`; пусто; автор конкретной сказки).
### C / D — чисто ✓
```
служебные значения (Various/Unknown/NA/…): 0
URL/спам в артисте: 0
```
### E, F, G, H — ЛОЖНЫЕ срабатывания (CSV корректен)
| класс | n | почему НЕ дефект |
|---|---:|---|
| E «артист и альбом пересекаются» | 204 | `artist='Винни-Пух'` + `album='Винни-Пух (2)'` — норма |
| F «несколько артистов в одном поле» | 267 | фиты: `'Би-2 feat. Диана Арбенина'` — норма |
| G «смешаны алфавиты» | 96 | `'Иванушки International'`, `'Flёur'` — норма |
| H «artist == title» | 25 | `'Винни-Пух.mp3'` в сборнике «Винни-Пух» — норма |
🔴 **Правило: детектор по regex даёт много ложных срабатываний. Каждый класс разбирать
по примерам вручную, прежде чем объявлять дефектом.** E/F/G/H — шум, править нельзя.
### Попутно: дубликаты файлов
`Моцарт — Турецкий марш.mp3` лежит в **двух** папках (`!Классика` и
`z Другое, Enigma, Ленинград/Классика`). Проверено `cmp_audio.py` (хеш аудио без тег-блока):
```
размер: 5993976 = 5993976
ID3v2-блок: 87040 = 87040
аудио-MD5: ac0f10ba114e196f9e49b99913bb1c27 (одинаков)
→ АУДИО ИДЕНТИЧНО — это дубликат
```
Файлы различались только наличием `TRCK` в v2 (у одного 3 фрейма, у другого 4).
План даёт им **разные** `album`/`genre` — для дубликата это дефект плана.
### Скрипт `cmp_audio.py` — сравнение аудио в обход тегов
Считает MD5 от аудио-потока: пропускает ID3v2-блок (`10 + synchsafe size`),
отрезает ID3v1 (последние 128 байт при `TAG`). Отвечает на вопрос «это дубликат
или разные записи», когда размеры совпадают, а MD5 файла различается.
## ⏳ ОТКРЫТЫЙ ВОПРОС: 20 файлов ждут решения Alex
| # | Что | Предложено |
|---|---|---|
| 1 | `Maria Makarova (organ)` ×2 | `Иоганн Себастьян Бах` (или `Бах`) |
| 2 | `Классика в современной обработке` ×3 | по композитору из title: Моцарт ×2, Вивальди ×1 |
| 3 | `Сказочный сборник` ×15 | `Сказки` / пусто / автор сказки — **нужно решение** |
| 4 | `title='Вивальди А. - Зима (рок-обработка)'` | срезать префикс `Вивальди А. - `? |
Правка боевого CSV **не начата** — ждёт ответа по всем четырём пунктам.
## 4. Что скрипт НЕ трогает
`APIC` (обложка) · `TDRC` (год) · `TCOM` · `TXXX` · `USLT` · `TPUB` · `TLEN` ·
@@ -215,13 +293,29 @@ docker start -a tw && docker rm -f tw
| # | Шаг | Статус |
|---|---|---|
| 1 | Боевой `--apply --verify` по 7861 | 🔴 **ЖДЁТ «go»** |
| 0 | 🔴 **Аудит `new_artist` → 20 дефектов, правка CSV** | ⏳ **ЖДЁТ решения Alex (4 вопроса в § 3.5)** |
| 1 | Боевой `--apply --verify` по 7861 | 🔴 **ЖДЁТ «go»** (после шага 0) |
| 2 | Приёмка Alex в UI (Navidrome/Jellyfin) | ⏳ |
| 3 | Откат 40 из `backup_conv40/` | ⏳ |
| 4 | Удаление 29 битых/DRM ([[personal/tech/music-broken-toload]]) | ⏳ |
| 5 | Удаление бэкапов (`backup/` `backup_raw/` `backup_fix/` `backup_conv40/`) | ⏳ после приёмки |
| 6 | Lidarr + Navidrome | ⏳ не начато |
### Артефакты этой сессии
| Файл | Назначение |
|---|---|
| `~/Automation/music-tags/apply_tags_v2.py` | писатель тегов (566 стр, ID3v1-логика) |
| `~/Automation/music-tags/normalize_track_csv.py` | вход `APPLY-PLAN-RAW.csv` → выход `APPLY-PLAN.csv` |
| `~/Automation/music-tags/audit_artist.py` | аудит 8 классов дефектов в CSV |
| `~/Automation/music-tags/test_logic.py` | тесты нормализатора и инвариантов |
| `~/Automation/music-tags/snapshot.py` | снимок фреймов файла (ДО/ПОСЛЕ) |
| `~/Automation/music-tags/cmp_audio.py` | сравнение аудио-потоков (поиск дубликатов) |
| `~/Automation/music-tags/dump_v1.py` | дамп ID3v1 + ID3v2 для сравнения |
| `~/Automation/music-tags/find_v1.py` | поиск mp3 с ID3v1 |
| `~/Automation/music-tags/Dockerfile.v2` | python:3.11-slim + mutagen 1.47.0 |
| `~/Downloads/music-tags/tagwriter/rename_apply_plan.py` | раскладка имён CSV (одноразовый) |
## 10. Правило процесса (выведено на этой задаче)
> **Договорённость, не записанная в файл, для агента не существует.**
+174
View File
@@ -141,6 +141,180 @@ os.replace(TMP, SRC)
с combining `U+0306`. Перед сравнением/группировкой всегда
`unicodedata.normalize("NFC", name)`.
## 🖊 Слой ЗАПИСИ — ID3v1 vs ID3v2 (добавлено 2026-09-27)
Зеркало правил W1–W15 скилла. Полные формулировки — в `id3-tag-repair/SKILL.md`.
### 🔴 W1 — «управляемый» фрейм, который писатель не умеет СОЗДАТЬ = гарантированная потеря
Если `TRCK` стоит в списке на удаление (`_managed_frame_ids()`), но ветки его создания
в `_frames()` нет — `delall` выполнится, `add` никогда. **Номер трека исчезнет у всех файлов,
и источника для восстановления нет** (колонки `track` в плане тоже не было).
`delall` + отсутствующий `add` **не даёт ошибки**: apply рапортует успех, verify — `MISMATCH=0`.
```python
assert sorted(_managed_frame_ids()) == sorted(fid for _, fid, _ in FIELD_MAP)
```
### 🔴 W2 — из трёх способов `save()` два уничтожают аудио
```python
tags = ID3(path); tags.save(path, v2_version=3) # ✅ в ТОТ ЖЕ путь
m = MP3(path); m["TIT2"] = ...; m.save() # ✅ контейнер-осознанный
tags = ID3(path); tags.save("/tmp/out.mp3"); os.replace(...) # 🔴 АУДИО СТЁРТО
```
`ID3()` — парсер **тег-блока**, не файла: `save(other_path)` сериализует только фреймы.
Файлы 2–10 МБ → ~1 КБ. Поймано на 40 файлах, отчёт был «40/40 записано, 0 ошибок».
### 🔴 W3 — ID3-писатель по не-MP3 разрушает контейнер
`ID3()` на `.m4a`/`.wma` → `ID3NoHeaderError` → `except: tags = ID3()` → запись ID3 **поверх**
контейнера: первые байты `49 44 33` вместо `ftyp`/ASF → `moov atom not found`. Непоправимо
(в `backup_raw/` те же байты — битые до сессии). Испорчено 16 файлов. Фильтровать `.mp3`.
### 🔴 W4 — `delall` ВНЕ проверки «значение не пусто»
```python
# ❌ «очистить поле» ничего не делает при пустом новом значении
if (r.get("new_artist") or "").strip(): out.append(TPE1(...))
for fr in _frames(r): tags.delall(...); tags.add(fr) # пустой список → цикл не идёт
# ✅ удаление безусловно, запись условна
for fid in _managed_frame_ids(): tags.delall(fid)
for fr in _frames(r): tags.add(fr)
```
### 🔴 W5 — ID3v1 невидим для `ID3()` и переживает `save()`, перекрывая v2
`ID3()` читает **только** v2-блок в начале. ID3v1 — отдельная запись 128 байт в **конце**
(`TAG` + title/artist/album по 30 + year 4 + comment 30 + genre 1). `save()` пишет v2 и
**v1 не трогает**. В библиотеке — **1099 из 2004 файлов** с v1, её payload — latin-1 с обрезкой:
```
v1 title = '????? ????????? ??????? (?????' ← кириллица уничтожена
v2 TIT2 = 'Танец маленьких лебедей (Лебединое озеро)'
v1 title = 'J.S. Bach: Toccata and Fugue (' ← обрезано на 30 байтах
```
Плеер, читающий v1 первым, показывает мусор. **Фикс: `save(path, v1=0, v2_version=3)`.**
### 🔴 W6 — probe инварианта обязан проходить собственные валидаторы
```python
probe = {k: "x" for k, _, _ in FIELD_MAP} # 🔴 "x" невалиден для номера трека
# normalize_track("x") -> "" -> TRCK не создаётся -> ЛОЖНЫЙ ABORT
```
Ложный ABORT отключают — и защита исчезает. Для числовых полей probe = `"1"`.
### 🔴 W7 — граница «из CSV» / «из файла» проходит по КОЛОНКАМ CSV
```
колонки CSV → значение ТОЛЬКО из CSV. Пусто ⇒ УДАЛИТЬ тег. НЕ подставлять старое.
фреймы вне CSV → из ID3v2; если там нет — из ID3v1
ID3v1 → удалить после записи
```
ID3v1 хранит 6 полей; **4 дублируют управляемые колонки** и игнорируются. Переносить стоит
только **year → TDRC** и **comment → COMM**, и лишь когда в v2 их нет.
### 🔴 W8 — год в v1 бывает бинарным мусором
Реальное значение: `year = '\x00\x00\x000'`. Без валидации уедет в `TDRC` как есть.
Чистить NUL/пробелы **с двух сторон**, отбрасывать управляющие символы,
требовать `re.fullmatch(r"\d{4}", year)`.
### 🔴 W9 — нормализатору нужны тесты на ведущие нули
`^[1-9]\d{0,3}$` выглядит разумно и **молча отвергает `095`** → 133 файла получили бы
**пустой** TRCK. Отбрасывать также `0`, `03/`, `/10`, `N/M` с нулём, спам-строки
(в колонке трека был найден `P@X@Tu*HCK*u`). Верно: `^\d{1,4}(/\d{1,4})?$` + `int()`.
### 🔴 W10–W13 — процесс
| # | Правило |
|---|---|
| **W10** | Скрипт-фиксер НЕ должен перезаписывать файл, который сам же правит — повторный прогон затрёт правки. Разделять: CSV-нормализатор + точечные `patch`. |
| **W11** | Тест обязан **импортировать** код, а не повторять его — копия `normalize_track()` продолжала проходить после фикса (2 ложных FAIL). |
| **W12** | Образ содержит **свою** копию скрипта: `docker cp` в смонтированный каталог не обновляет `/app/script.py` внутри образа. Сигнатура: `AttributeError: no attribute '_read_id3v1'` после успешной заливки. |
| **W13** | Договорённость только в чате **не существует** — вердикт Alex: «это неоднократно». Флаг, колонка, решение → в файл **в том же ходе**. |
### 🔴 W14 — MD5 всего файла бесполезен для поиска дубликатов; хешировать АУДИО
Две копии трека в разных папках: размер одинаков, **MD5 файла разный** (блоки тегов разные —
у одной есть `TRCK`). Хешировать аудио-поток: пропустить v2-блок в начале, отрезать v1 в конце.
```python
def audio_hash(path):
size = os.path.getsize(path)
with open(path, "rb") as f:
head = f.read(10); start = 0
if head[:3] == b"ID3": # пропуск v2 (synchsafe size)
s = ((head[6] & 0x7F) << 21) | ((head[7] & 0x7F) << 14) | \
((head[8] & 0x7F) << 7) | (head[9] & 0x7F)
start = 10 + s
end = size
f.seek(size - 128)
if f.read(3) == b"TAG": end = size - 128 # отрезать v1
f.seek(start); h, rem = hashlib.md5(), end - start
while rem > 0:
chunk = f.read(min(1 << 20, rem))
if not chunk: break
h.update(chunk); rem -= len(chunk)
return h.hexdigest()
```
Проверено: `ac0f10ba114e196f9e49b99913bb1c27` для **обеих** копий → истинный дубликат.
**Для плана это важно:** дубликат, получающий **разные** `album`/`genre` из плана — дефект плана.
### 🔴 W15 — аудит `artist` требует РУЧНОГО разбора по классам
Regex-детектор по `new_artist` дал 8 классов / 628 попаданий; **606 — ложные**. Реальных — два:
| Класс | Реально? | Пример |
|---|---|---|
| Исполнитель вместо композитора | ✅ | `'Maria Makarova (organ)'` на файле Баха → должно быть `Иоганн Себастьян Бах` |
| Название сборника в `artist` | ✅ | `'Сказочный сборник'` ×15, `'Классика в современной обработке'` ×3 |
| `artist` и `album` пересекаются | ❌ 204 | `artist='Винни-Пух'` + `album='Винни-Пух (2)'` — норма |
| Несколько имён в поле | ❌ 267 | `'Би-2 feat. Диана Арбенина'` — фит |
| Смешаны алфавиты | ❌ 96 | `'Иванушки International'`, `'Flёur'` — норма |
| `artist == title` | ❌ 25 | `'Винни-Пух.mp3'` внутри сборника «Винни-Пух» |
**Печатать значения по классу и смотреть глазами.** Счётчик класса — не список дефектов:
объявить 628 дефектов при 20 реальных = потеря доверия (реакция Alex: «ты нихуя не проверил csv»).
🔴 **Паттерн «инструмент в скобках» — самая ценная проверка:** `'(organ)'`, `'(piano)'`,
`'(vocals)'`, `'(guitar)'` в `artist` означает, что поле держит **исполнителя**; композитора
брать из title/имени файла. Это ровно тот дефект, из-за которого Jellyfin показывал мусор.
### Приёмка записи
`snapshot.py` ДО и ПОСЛЕ + diff. Критерий: управляемые фреймы совпадают с CSV, **каждый
неуправляемый фрейм на месте** (`APIC`, `TDRC`, `COMM`, `TCOM`, `TSSE`, `WCOP`, `WXXX`, `TKEY`,
`TCOP`…), `ID3v1` исчез, **размер в байтах не изменился** (равный размер = MPEG-поток не тронут).
Отчёт `ok=3 err=0` не доказывает ничего из этого.
### Запуск длинных задач без апрува и без сети
```bash
docker create --name tw <image> # без промпта
docker cp script.py tw:/app/script.py # АКТУАЛЬНЫЙ скрипт внутрь
docker start -a tw && docker rm -f tw # foreground, видно stdout
```
### 🔴 W16 — имена рабочих CSV: один боевой файл
Раскладка, к которой пришли (после путаницы `APPLY-PLAN-FINAL` vs `APPLY-PLAN-FINAL2`):
```
APPLY-PLAN.csv — БОЕВОЙ, единственный источник истины
APPLY-PLAN-RAW.csv — сырой (до нормализации), НЕ применять
archive/APPLY-PLAN-v0-*.csv — устаревшие версии
```
Суффикс `2` в имени файла-плана — источник ошибок: непонятно, какой применять. Один файл
без суффикса, остальные явно помечены. Перед переименованием — `tar czf` снимок каталога.
## ⚪ Что НЕ является багом
- **Рукописная транслитерация:** `КняZz`, `EMINЭM`, `Flёur`, `изBESTняк`, `Скаzки`, `Глюкоzа`,