From 7d71a558d9c1857c2f96c3c1d97ffbe723f147b7 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 27 Sep 2026 11:57:32 +0600 Subject: [PATCH] [2026-09-27] eagle: personal/plans/music-artist-albumartist-repair.md personal/tech/id3-tag-repair-skill.md personal/tech/music-tags-repair-truenas.md --- .../plans/music-artist-albumartist-repair.md | 116 +++++++++++++++++- personal/tech/id3-tag-repair-skill.md | 78 ++++++++++++ personal/tech/music-tags-repair-truenas.md | 3 + 3 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 personal/tech/id3-tag-repair-skill.md diff --git a/personal/plans/music-artist-albumartist-repair.md b/personal/plans/music-artist-albumartist-repair.md index 21949fb6..2bb08501 100755 --- a/personal/plans/music-artist-albumartist-repair.md +++ b/personal/plans/music-artist-albumartist-repair.md @@ -324,26 +324,129 @@ NEED_LOOKUP: 0 `АЛЬБОМ-мульти(?)` (30 папок) разобраны вручную — почти все НЕ сборники, а альбомы с фитами. +## ✅ ЭТАП ЗАВЕРШЁН: ЖАНРЫ — `genre-plan-FINAL.csv` (7861 строка, 51 пустой) + +Alex: «Ещё проверь» → разбор жанров по всей библиотеке. + +### Исходное состояние (до чистки) + +``` +genre ЗАПОЛНЕН: 4911 (62%) +genre ПУСТ: 2950 (38%) +уникальных: 154 +``` + +### 🔴 Дефекты жанров, найденные аудитом + +| # | Дефект | Файлов | Пример | +|---|---|---:|---| +| 1 | **Мойибаке CP1251 в жанрах** | 29 | `Psy Årance` / `Psy Еrance` (Е — **кириллическая**) → `Psy-Trance` | +| 2 | **Разнобой регистра одного жанра** | ~1800 | `Rock` | `ROCK` | `Рок` | `General Rock` — четыре варианта | +| 3 | **Синонимы-дубли** | 453 | `Metal`→`Heavy Metal`, `Punk Rock`→`Punk`, `Russia Rap`→`Rap`, `Поп`→`Pop`, `Рэп и хип-хоп`→`Hip-Hop` | +| 4 | **Мусорные значения** | 365 | `Other` 261 · `(17)` 32 · `General Rock` 31 · `Разное` 22 · `Unknown` 19 | +| 5 | **Детские/сказки без жанра вообще** | 570 | `ДЕТСКИЕ` 0 файлов с жанром, `СКАЗКИ` 11 (10 `Other`, 1 `Blues` у сказки — мусор) | +| 6 | **Спам в тегах** | 4 | `www.mp3-ogg.ru`, `.:::artMkiss 2009:::.`, `ÿþElectronic` | + +### ✅ Ручной разбор мусора (по решению Alex — НЕ батчем по папке) + +> Alex: «По other/unknown — не уверен про что речь, **каждую смотри конкретно и ставь правильный +> жанр, не батчем по папке**.» + +`(17)` и `General Rock` расшифрованы фактом — это **не жанры вообще, а артисты**: + +| Значение | Факт | Решение | +|---|---|---| +| `(17)` | **все 32 файла — ДДТ**, попал номер трека/плейлиста | → `Rock` | +| `General Rock` | **все 31 файл — Ленинград** (Дачники, Бабаробот) | → `Rock` | +| `Other` (261) | DJ-сборники `!Radio`, Пушной, Vanessa Mae, Каста, Трофим | разобраны **по исполнителю** | + +### Ручные правила заполнения 2715 файлов (по Alex: «Вроде все так») + +| Папка/артист | Ф. | Жанр | +|---|---:|---| +| `!Барды/Владимир Высоцкий/*` | 223 | `Бардовская песня` | +| `!A Not sorted/Sbornik_Novij_Rok` | 200 | `Rock` | +| `!Retro` | 94 | `Pop` | +| `!Radio/*` (DJ-сборники) | ~200 | `Dance` | +| `Захар Май` | ~60 | `Rock` | +| `Александр Пушной` | 23 | `Rock` | +| `Vanessa Mae` | 26 | `Classical Crossover` | +| `The Tiger Lillies` | 24 | `Dark Cabaret` | +| `Каста` / `Eminem` | 46 | `Hip-Hop` | +| `Трофим` | 50 | `Chanson` | +| `Король и Шут` | 21 | `Punk` | +| `Rammstein` | — | `Industrial` | + +**Полные правила — 130 key→genre в `fill_genres.py` (RULES), первое совпадение выигрывает.** + +### 📊 Итог + +``` +было: 154 уникальных жанра, 2950 пустых +стало: 82 уникальных жанра, 51 пустой + +мойибаке исправлено: 29 +синонимов схлопнуто: 453 +заполнено правилами: 2715 → 0 пустых, затем дочистка 173 + 453 +``` + +**Финальное распределение:** + +``` +3071 Rock 882 Pop 385 Punk + 344 Детские 276 Бардовская песня + 248 Сказки 226 Chanson 218 Hip-Hop + 211 Heavy Metal 188 Rap 139 House + 127 Electronic 127 Psy-Trance 121 Industrial + 107 Blues 101 Dark Cabaret 95 Alternative +``` + +**51 пустой — осознанно.** Это `unknown`/`неизвестно`/`other`/`другое`/`разное`/`наши`, +`(12)`, `www.mp3-ogg.ru` — заполнять нечем. Navidrome покажет файлы без жанра, +они просто не попадут в жанровые фильтры. + +### 🔴 ПИТФОЛЛ: `clean_genres.py` чистит по списку, а не итеративно до сходимости + +Первый прогон схлопнул 173 значения, но **на выходе тут же обнаружились новые остатки** +(`Russia Rap`, `Рэп и хип-хоп`, `Поп`, `Pop/Rock`) — потому что нормализация дала +значения, которых не было в исходном `FIX`-словаре. Понадобился **третий проход** +(`final_genres.py`: +мойибаке-декодер + ещё 453 синонима). +**Правило: нормализацию гонять до нуля изменений, а не один раз.** Проверка — +`Counter(new_genre)` на выходе: если появились неизвестные значения, словарь неполон. + +### 🔴 ПИТФОЛЛ: `Å`/`Е` в `Psy Årance` — латиница vs кириллица + +`Psy Årance` и `Psy Еrance` выглядят идентично, но `Å` (U+00C5) и `Е` (U+0415) — разные символы +из разных алфавитов. Словарь синонимов обязан содержать **оба варианта** явно; +`str.strip()`/`lower()` их не схлопывает. + ## ⏭ СЛЕДУЮЩИЕ ШАГИ 1. **Alex проверяет `fix-plan-FINAL.csv`** — три выборки: 57 строк «из скобок», 1518 строк `Various Artists`, 248 строк `Сказки` -2. **Скрипт-писатель `apply_tags.py` — написать заново** (старый не годен, см. §Разбор): +2. **Свести `fix-plan-FINAL.csv` + `genre-plan-FINAL.csv`** в один CSV для скрипта-писателя + (сейчас это два независимых файла, колонка `path` — ключ связи) +3. **Скрипт-писатель `apply_tags.py` — написать заново** (старый не годен, см. §Разбор): - `delall` — **вне** `if`, `add` — только при непустом значении - **+`TPE2`** (`albumartist`) в `_frames()` - декодер мойибаке CP1251→UTF-8 - фильтр `.mp3` — **обязателен** (иначе `ID3()` поверх не-MP3 → нет `moov atom`, непоправимо) - `save(path, v2_version=3)` **в тот же путь**, не в `.tmp` через `ID3(path)` -3. **Сухой прогон на 20 файлах** + пост-сверка (прочитать тег ПОСЛЕ, сравнить с ожиданием) -4. **Только после «go»** — массовая правка с бэкапом +4. **Сухой прогон на 20 файлах** + пост-сверка (прочитать тег ПОСЛЕ, сравнить с ожиданием) +5. **Только после «go»** — массовая правка с бэкапом ## 📁 ФАЙЛЫ РАБОТЫ (Mac) ``` /Users/admin/milestones/music-csv-2026-09-26/ -├── fix-plan-FINAL.csv ⭐ 7861 стр, ИТОГОВЫЙ план правок +├── fix-plan-FINAL.csv ⭐ 7861 стр, ИТОГОВЫЙ план artist/albumartist/album +├── genre-plan-FINAL.csv ⭐ 7861 стр, ИТОГОВЫЙ план жанров (82 уник., 51 пусто) ├── fix-plan-final.csv промежуточный (NEED_LOOKUP=121) ├── fix-plan-final2/3/4.csv промежуточные после каждой волны lookup +├── genre-plan.csv / 2 / 3 промежуточные жанров (нормализация в 3 прохода) +├── genre-junk.csv 365 строк с мусорным жанром (Other и т.п.) +├── bad-genres.csv 3315 строк (мусор + пустые) +├── genre-empty-folders.txt 291 папка без жанра ├── raw_tags_full.tsv 7861 стр, сырые теги (md5 0c3647fa...) ├── tag-inventory3.csv 7861 стр, 13 колонок, +albumartist ├── tag-inventory2.csv.bak-20260927 бэкап @@ -359,7 +462,10 @@ NEED_LOOKUP: 0 ├── need-lookup.txt / lookup-still.txt списки на поиск в интернете └── *.py analyze / build_inventory3 / build_folders_plan / build_fix_plan / resolve_composers / apply_lookup1-3 / - build_final / verify_manual / audit_categories + build_final / verify_manual / audit_categories / + check_genres / dump_bad_genres / split_genres / + normalize_genres / fill_genres / clean_genres / + final_genres ``` ### Питфолл: субагент пишет в корень vault diff --git a/personal/tech/id3-tag-repair-skill.md b/personal/tech/id3-tag-repair-skill.md new file mode 100644 index 00000000..e9eaa3b4 --- /dev/null +++ b/personal/tech/id3-tag-repair-skill.md @@ -0,0 +1,78 @@ +--- +title: ID3 Tag Repair — скилл (жанры, мойибаке, безопасная пакетная правка) +created: '2026-09-27' +updated: '2026-09-27' +type: tech +namespace: personal +tags: + - music + - id3 + - metadata + - genre + - mojibake + - skill +related: + - '[[personal/plans/music-artist-albumartist-repair]]' + - '[[personal/tech/music-tags-repair-truenas]]' +--- + +# Скилл `id3-tag-repair` — зеркало знаний + +**Путь скилла:** `~/.hermes/hermes-whale/skills/media/id3-tag-repair/SKILL.md` +**Обновлён:** 2026-09-27 — добавлен раздел **Genre normalization** (5 правил G1–G5). + +## Что в скилле (карта) + +| Раздел | О чём | +|---|---| +| Core rules | дампить БАЙТЫ до гипотезы; `ffprobe` врёт (пусто при валидном ID3v2.3) | +| Step 2 — три случая `enc_byte` | 🔴 BOM-ловушка: `enc=0` + `FF FE` → **снять BOM, читать как 8-bit**, НЕ `decode('utf-16')` | +| Step 3 — guard мойибаке | 🔴 конвертировать **по РУНАМ**, не по строке (иначе `Ïîëç¸ò(Birthday mix)` не чинится) | +| Step 4 — трек-номера | `01 ` без пунктуации не матчился → **2125 файлов**; `N-MM` → disc/track; `159_Css` не трогать | +| CSV worklist | 🔴 НЕ писать в источник: `w`-режим обрезает файл (7906→4640). Схема `.tmp` → revalidate → `os.replace` | +| Rule 4/4b | идемпотентность: цель считать от ИСХОДНОГО (кейс Disc4 10→60→110); `was == now` не идемпотентна | +| Rule 6 | `action=no_change` — **инструкция**, а не статус; валидировать по ДИСКУ (353 ложных) | +| Rule 7/8 | не-MP3 в MP3-плане = MISSING, не потеря; `backup/` из плана покрывает только его форматы | +| Unicode | NFC vs NFD (`Принц и нищий (Диск 2)`, combining breve U+0306) | +| Summary CSVs | инвариант суммы: `single + multi + skip == total` (теряли 562, потом 335 строк) | +| Sentinel | группировать по КОРНЮ сборника, не по ближайшей папке (165 файлов в 9 фальшивых «артистов») | +| 🆕 **Genre normalization** | 🔴 **G1–G5**, см. ниже | + +## 🆕 Genre normalization (G1–G5) — квинтэссенция + +**Контекст:** 7861 файл, было **154 уникальных жанра**, 2950 пустых. Стало **82 / 51**. + +| # | Правило | +|---|---| +| **G1** | **Чистить итеративно до фиксированной точки.** Нормализация РОЖДАЕТ новые значения (`Metal`→`Heavy Metal`, но всплывает `Russia Rap`). Один проход дал 173, понадобилось ещё 453. Тест: прогон с 0 изменений. | +| **G2** | **Гомоглифы — оба варианта в словарь явно.** `Å` (U+00C5, латиница) vs `Е` (U+0415, кириллица) выглядят одинаково; `lower()` их не схлопывает, а `.encode('latin-1')` падает на кириллической → декодер мойибаке **молча пропускает половину строк**. | +| **G3** | **Мусорный жанр может быть мисридом поля.** `(17)` = все 32 файла **ДДТ** (номер плейлиста в кадре жанра); `General Rock` = все 31 **Ленинград**. Признак: `Counter(genre) → set(artist)` даёт **1:1** = это не жанр. Alex: «каждую смотри конкретно, не батчем по папке». | +| **G4** | **Заполнять по артисту, порядок specific → general.** Таблица `подстрока → жанр` по `folder + artist`, первое совпадение выигрывает. Правила артистов **перед** папкой-ловушкой, иначе `!rock&folk` съест всех внутри. | +| **G5** | **Незаполнимое — оставить ПУСТЫМ.** `unknown`/`неизвестно`/`разное`/`наши`/`(12)` не несут информации; подстановка по папке = **выдумывание метаданных**. 51 пустой — осознанное решение. | + +### Порядок работ по жанрам (7 шагов) + +``` +1. check — перепись: заполнено/пусто, Counter(genre), дыры по папкам +2. split — три корзины: норма / МУСОР / пусто; мусор — отдельным CSV +3. группировка мусора по АРТИСТУ (G3) → 1:1 разбирать руками +4. normalize — регистр + синонимы + декодер мойибаке (G1, G2) +5. fill — таблица правил по артисту, specific→general (G4) +6. перепись → добавить правила → повтор до 0 изменений +7. реально-неизвестное оставить пустым (G5) +``` + +## Артефакты сессии + +``` +/Users/admin/milestones/music-csv-2026-09-26/ +├── genre-plan-FINAL.csv ⭐ 7861 стр, 82 жанра, 51 пусто +├── genre-junk.csv 365 строк мусора +├── bad-genres.csv 3315 (мусор + пустые) +└── check_genres.py → split_genres.py → normalize_genres.py → + fill_genres.py → clean_genres.py → final_genres.py +``` + +**Питфолл пайплайна:** `clean_genres.py` чистит **по списку**, а не до сходимости — после +прогона на выходе тут же обнаружились новые остатки. Понадобился третий проход +(`final_genres.py`: + детектор мойибаке + ещё 453 синонима). **Правило G1 выведено отсюда.** diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index 8e08a348..5d605043 100755 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -38,6 +38,9 @@ related: > - мойибаке — **исходное состояние**, НЕ следствие волны 38 (доказано `diff` v1 vs v2 инвентаря) > - **сборник ≠ «>1 артиста»** — это ловит альбомы с фитами; Alex дал прямые примеры > - процессный запрет Alex: **сначала ручные списки, скриптом — только обновление CSV** +> - ✅ `fix-plan-FINAL.csv` — 7861 стр, **0 пустых `artist`**, 7233 изменения +> - ✅ `genre-plan-FINAL.csv` — 7861 стр, **82 уникальных жанра** (было 154), 51 пустой, +> мойибаке `Psy Årance` → `Psy-Trance` (29 файлов, `Å`/`Е` — латиница vs кириллица) > ## ✅ ТРИДЦАТЬ ВОСЬМАЯ ВОЛНА (2026-09-26) — ФИНАЛЬНЫЙ АУДИТ + 4 ПРАВКИ + ПРИМЕНЕНИЕ НА NAS >