From 3f48512ff4810e49cb249deb5fef409ad8fe2ec2 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sat, 26 Sep 2026 16:53:40 +0600 Subject: [PATCH] [2026-09-26] eagle: family/how-to/jellyfin-config.md personal/tech/id3-tag-repair.md personal/tech/music-tags-repair-truenas.md --- family/how-to/jellyfin-config.md | 13 ++- personal/tech/id3-tag-repair.md | 117 +++++++++++++++++++++ personal/tech/music-tags-repair-truenas.md | 117 +++++++++++++++++++-- 3 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 personal/tech/id3-tag-repair.md diff --git a/family/how-to/jellyfin-config.md b/family/how-to/jellyfin-config.md index 0d24210d..43acc99c 100755 --- a/family/how-to/jellyfin-config.md +++ b/family/how-to/jellyfin-config.md @@ -152,11 +152,14 @@ sqlite3 jf-copy.db "SELECT Name, AccessToken FROM ApiKeys;" > потеряно 343 файла (Высоцкий и др.). Читать ID3 **напрямую из байтов** (`id3_direct.py`). > Детали — [[personal/tech/music-tags-repair-truenas]]. -> 📋 **ДОКУМЕНТЫ РЕВЬЮ ДЛЯ ALEX (2026-09-26)** — CSV с предлагаемыми исправлениями выдан не только -> файлом, но и нотами в vault (`personal/plans/music-tags-review/`): обзор + 3 блока -> (требует решения / средняя / высокая уверенность), каждый файл = чекбокс с `старое → новое` -> и источником. **Это вход для апрува (шаг 3 плана ремонта).** Генератор — -> `render_grouped_doc.py` в `~/Developer/personal-os/tools/music-tags/`. +> 📋 **ДОКУМЕНТЫ РЕВЬЮ ДЛЯ ALEX (2026-09-26, редакция v7)** — CSV с предлагаемыми исправлениями выдан +> не только файлом, но и нотами в vault (`personal/plans/music-tags-review/`): обзор + 3 блока. +> **Формат v7 (утверждён Alex):** `1-needs-decision` — папки + чекбоксы (**осталось 4 файла**); +> `2-medium-confidence` (**5811**) и `3-high-confidence` (**958**) — **плоские списки** +> `- **файл.mp3** → artist — title`, без заголовков и оглавлений. Один файл = одна строка. +> **Это вход для апрува (шаг 3 плана ремонта).** Генератор — `render_grouped_doc.py` +> в `~/Developer/personal-os/tools/music-tags/`. Залито коммитом `4464e64`, синкнуто. +> Разбор волн 5–8 и питфоллы — [[personal/tech/music-tags-repair-truenas]]. > ✅ **Порядок работ (решение Alex 2026-09-26):** ремонт тегов → валидация Alex → удаление > `Music/backup/` → **только потом** создание Music-библиотеки. Бэкап кладётся в `Music/backup/` diff --git a/personal/tech/id3-tag-repair.md b/personal/tech/id3-tag-repair.md new file mode 100644 index 00000000..78c9e42b --- /dev/null +++ b/personal/tech/id3-tag-repair.md @@ -0,0 +1,117 @@ +--- +title: ID3 Tag Repair — техника и питфоллы +created: '2026-09-26' +updated: '2026-09-26' +type: tech +namespace: personal +tags: + - id3 + - mp3 + - metadata + - encoding + - mojibake + - cp1251 + - jellyfin + - repair +related: + - '[[personal/tech/music-tags-repair-truenas]]' + - '[[family/how-to/jellyfin-config]]' +--- + +# ID3 Tag Repair — техника и питфоллы + +> Зеркало скилла `id3-tag-repair` (категория `media`, `~/.hermes/hermes-whale/skills/media/id3-tag-repair/`). +> Скилл — для исполнения, этот док — для контекста проекта и истории. + +## 📁 Артефакты проекта + +**Путь:** `~/Developer/personal-os/tools/music-tags/` + +| Файл | Назначение | +|---|---| +| `id3_direct.py` | **Парсер ID3 напрямую из байтов.** Функция `decode_text()` — здесь живёт фикс BOM. Обходит `ffprobe` | +| `dump_id3.py` | Дамп сырых байтов кадров (`enc_byte`, hex, repr). **Главный диагностический инструмент** | +| `scan_bom.py` | Поиск `ÿþ` по всем полям CSV-плана — отделяет реальный BOM от середины слова | +| `audit_leftover_mojibake.py` | Ищет значения, которые конверсия **пропустила** (3+ latin-1 → кириллица). Гонять после каждой перегенерации | +| `fix_leftover_mojibake.py` | Точечная посимвольная конвертация run'ами (`--dry-run`) | +| `apply_manual_decisions.py` | Перенос правок человека из нот в CSV + `ARTIST_CANON` | +| `render_grouped_doc.py` | Генератор нот ревью из CSV | +| `build_fix_plan2.py` | Построение плана: `split_track_number`, `normalise_case`, `has_word`, `find_separator` | +| `audit_plan.py`, `final_gate.py` | Аудит и финальный гейт качества плана | +| `test_*.py` | 121 тест (track, регистр, разделители, guard кодировок) | + +## 🔴 Четыре питфолла, которые стоили больше всего времени + +### 1. BOM при `enc_byte=0` + +Заголовок кадра объявляет latin-1, а payload начинается с `FF FE`. Текст за BOM — **обычный +8-битный**, не UTF-16. Правильно: отбросить BOM, декодировать остаток как 8-бит. + +``` +TPE1 enc=0 payload = 00 FF FE 53 74 65 65 6C 20 44 65 6C 75 78 65 + └BOM┘ └────── "Steel Deluxe" ASCII ──────┘ +``` + +🔴 `raw.decode("utf-16")` **ломает** это: `Steel Deluxe` → `瑓敥\u206c敄畬數`. Проверено на живом файле. + +### 2. Guard «есть латиница → не перекодировать» тестировал всю строку + +Смешанные значения оставались битыми: + +``` +Ïîëç¸ò(Birthday mix) ← "Birthday mix" блокировала конверсию +ÑÀÂËÎ&LIL'DEN vs. NTL ← "LIL'DEN", "NTL" +Четыре ñëîâà(Plazmatix trance) ← "Plazmatix" +``` + +Правильно — конвертировать **непрерывными run'ами** (`convert_runs()`), ASCII не трогать. + +### 3. `ffprobe` молча отдаёт пустые теги + +Для валидного ID3v2.3 с битым кадром (напр. `comment='eng'`): `exit=0`, пустой JSON, **без ошибки**. +Потеряно 343 файла. → Читать ID3 **напрямую из байтов** (`id3_direct.py`). + +### 4. Ноты из CSV — односторонний канал + +Правки человека в сгенерированной ноте **не возвращаются** в CSV. Следующая перегенерация их +**затирает**. Лечится только явным переносом (`apply_manual_decisions.py`, список `DECISIONS`). + +## ⚪ Что НЕ является багом + +- **Рукописная транслитерация:** `КняZz`, `EMINЭM`, `Flёur`, `изBESTняк`, `Скаzки`, `Глюкоzа`, + `Cо`, `WWWаленки`, `Закoлебал` — оригинальное написание 2000-х / реальные псевдонимы. + Заменять = выдумывать. **Alex сам решает**, когда сводить варианты (`Fl¸ur`→`Flëur` попросил, + `КняZz` оставил). +- **`ÿþ` в середине слова:** `Òåðÿþ ñâåò → Теряю свет`, `Ïîçäðàâëÿþ → Поздравляю` — корректная + CP1251-конверсия. Ложное срабатывание grep. Различать **по позиции**: начало значения ≠ середина слова. +- **`album == artist`** (101 файл) — одноимённые альбомы (`Рубль/Рубль`, `Би-2/2001 Би-2`). +- **`numeric_title`** (15 файлов) — `007` у Ленинграда, `48` у Пилота. Законно. + +## 🚫 Запреты (жёсткие, от Alex) + +- **Не лезть в ID3** до апрува — работа только в md-файлах Obsidian. Применение тегов = отдельный + шаг после «go» + бэкап. +- **Не удалять файлы** без явного разрешения — даже очевидный мусор. +- **Не перекладывать работу на Alex.** Разбор artist/title из имени файла — **моя** задача. + Ему показывать только то, где в имени названия нет. +- **Не дробить ревью на десятки файлов** — он не будет проставлять 2000 чекбоксов. + +## Ключевые решения + +| Решение | Обоснование | +|---|---| +| Mojibake CP1251→Latin-1 — точно обратимо | 98.3% без потерь, проверено | +| `U+FFFD` — данные уничтожены | восстановление **только** из имён путей | +| Поиск стоп-слов **по словам**, не подстроке | `"va" in "Ivan"` ломало `DJ Ivan Scratchin'` | +| Track-номер ≤ 2 цифр (1–99) | трёхзначные = id плейлиста/архива | +| Бэкап только изменяемых файлов | не всю библиотеку; снести после валидации, до скана Jellyfin | +| Ноты: плоские списки для medium/high | Alex не нужна навигация по 5800 строкам | + +## Проверка + +```bash +python3 audit_leftover_mojibake.py # ожидание: still mojibake: 0 +python3 dump_id3.py --file test.mp3 # ожидание: artist 'Steel Deluxe', не 'ÿþSteel Deluxe' +``` + +**Приёмка Alex = видно в UI Jellyfin**, не «API отвечает». diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index 76e6faf2..da46b128 100755 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -19,16 +19,112 @@ related: # Music Tags Repair — TrueNAS (7906 файлов) -> **Статус: ЭТАПЫ 1–3 ЗАВЕРШЕНЫ (2026-09-26). CSV v6 + ноты в НОВОМ формате отданы Alex. +> **Статус: ЭТАПЫ 1–3 ЗАВЕРШЕНЫ (2026-09-26). CSV v7 + ноты v7 отданы Alex. > Правки файлов НЕ начаты.** -> Следующий шаг — апрув Alex по нотам ревью, затем бэкап + применение тегов. -> Прогонялись **семь волн** проверок: полный аудит всех строк (`audit_plan.py` + `final_gate.py`) → 6 багов, +> Следующий шаг — апрув Alex по блокy «требует решения» (осталось **4 файла**), +> затем бэкап + применение тегов. +> Прогонялись **восемь волн** проверок: полный аудит всех строк (`audit_plan.py` + `final_gate.py`) → 6 багов, > третья волна по замечаниям Alex → 3 бага (en-dash + подстроковый поиск стоп-слов), > **четвёртая волна (ручной проход) → 10 багов (track-номера, disc-track, восклицательные знаки, регистр)**, > **пятая волна (ручной разбор блока 1 глазами) → 28 файлов, 10 артистов вписано вручную**, > **шестая волна (Alex правит мои md + ACL-фикс синка) → найден и ПОЧИНЕН баг `ÿþ`-BOM в парсере ID3**, -> **седьмая волна (правки Alex из нот → CSV, новый формат нот) → 17 правок перенесено, 1 потеряна и восстановлена**. -> Итог v6: блок «требует решения» 28 → **15**, ноты 2/3 — плоские списки, 121 тест PASS, mojibake = 0. +> **седьмая волна (правки Alex из нот → CSV, новый формат нот) → 17 правок перенесено, 1 потеряна и восстановлена**, +> **восьмая волна (28 правок в CSV, фикс остаточного mojibake, канон `Flëur`) → блок «требует решения» 28 → 4,** +> **`audit_leftover_mojibake.py` = 0, ноты v7 залиты в vault и синкнуты.** +> Итог v7: «требует решения» **4**, medium **5811**, high **958**, mojibake = 0, остаточный mojibake = 0. + +## 🖐 ВОСЬМАЯ ВОЛНА: 28 ПРАВОК В CSV + ОСТАТОЧНЫЙ MOJIBAKE + КАНОН `Flëur` (2026-09-26) + +Alex: **«Да. И Flëur.»** — предварил «Реальные данные. Заголовки с оглавлением выкини…», +**«Needs decision — если ничего не поменялось то будет пусто, так?»** + +### ✅ Итог волны: «требует решения» 28 → **4** + +| Файл | Почему остался | +|---|---| +| `НАШЕСТВИЕ 2009 - ДЕНЬ ВТОРОЙ.mp3` | Alex **вычеркнул** (`~~…~~`) — не трогать | +| `Kazantip 2008 - Beat (zvukoff.ru).mp3` | Kazantip — **событие**, не артист; artist → пусто | +| `i am instructor.mp3` | Alex: «artist не найден, какой нибудь yechno beat поставь» ← **не выяснено** | +| `Из чего же.mp3` | artist `Неизвестный исполнитель` → пусто | + +### 📐 ФОРМАТ НОТ v7 (утверждён Alex) + +`render_grouped_doc.py`, `note_body(..., flat=False)`: + +| Блок | Формат | Размер v7 | +|---|---|---| +| `1-needs-decision` | **папки + чекбоксы** (каждому файлу нужно решение) | 4 файла, 1.5 КБ | +| `2-medium-confidence` | **плоский**: `- **файл.mp3** → artist — title` | 5811 строк, 572 КБ | +| `3-high-confidence` | тот же плоский | 958 строк, 104 КБ | + +**Убрано окончательно:** заголовки `## <папка>`, `## Оглавление`, разбиение по категориям +(56 файлов **отменены** — Alex: «Я не собираюсь столько вручную тебе проставлять! Это твоя работа!»). +Один файл = одна строка. Номер трека — как `` `#N` `` в конце строки. +**Запуск:** `python3 render_grouped_doc.py ` — 3 файла + индекс. + +### 🔴 ОСТАТОЧНЫЙ MOJIBAKE: guard «рядом латиница» пропускал конверсию — 9 файлов + +Alex: **«Какого хуя ты мне подсовываешь мусор со сбитой кодировкой?!»** — после v6 в нотах +осталось **9 значений** с непереведённой CP1251. Причина: guard против ложной перекодировки +(`Flёur`, `КняZz`) проверял **всю строку** — если в значении была латиница, конверсия +**пропускалась целиком**, и кириллица оставалась битой. + +``` +Ïîëç¸ò(Birthday mix) ← латиница "Birthday mix" блокировала конверсию +ÑÀÂËÎ&LIL'DEN vs. NTL ← "LIL'DEN", "NTL" +Четыре ñëîâà(Plazmatix trance) ← "Plazmatix" +``` + +> 🔴 **ПИТФОЛЛ: guard «есть латиница → не перекодировать» должен работать ПОСИМВОЛЬНО, не по всей строке.** +> Иначе смешанные значения (`Ползёт(Birthday mix)`) остаются битыми. Правильно — конвертировать +> **максимальные непрерывные run'ы** кириллических байтов, ASCII не трогать (`convert_runs()`). +> Тот же приём уже применялся для `Глюк'OZA`. + +**Скрипт:** `fix_leftover_mojibake.py` (`--dry-run`) — правит **только 9 перечисленных файлов**, +посимвольная конвертация run'ами, бэкап CSV, добавляет `;moji-per-run` в `source`. +Результат: `Ñîñóíkillaz` → `Сосунkillaz`, `Ïîëç¸ò(Birthday mix)` → `Ползёт(Birthday mix)` +(латиница сохранена). + +**Аудит-скрипт:** `audit_leftover_mojibake.py` — ищет 3+ подряд latin-1 символа, декодируемых +в кириллицу. **После правки: `still mojibake: 0`** на всех 7906 строках. Прогонять после любой +перегенерации CSV. + +### 🎨 КАНОН `Flëur` — 12 полей приведены к одному написанию + +В библиотеке три варианта одного исполнителя: `Fl¸ur` (типографская замена `ё`→`¸` **в самом теге**), +`Fleur` (латиница), `Flёur` (правильно). Alex: **«Да. И Flëur.»** + +`apply_manual_decisions.py`, словарь `ARTIST_CANON` + функция `canon_artist()` — применяется +ко **всем** строкам CSV (не только ручным), по полям `new_artist` **и** `cur_artist`: + +```python +ARTIST_CANON = {"Fl¸ur": "Flëur", "Fleur": "Flëur"} +``` + +> ⚠️ **Это осознанная нормализация, а не авто-ремонт.** `Fl¸ur` — не порча байтов, а рукописная +> замена (как `КняZz`, `EMINЭM`). Разница: `КняZz` Alex оставил как есть, а `Fl¸ur` попросил +> свести к `Flëur`. **Спросить, прежде чем объединять варианты написания.** + +### 📊 Итог восьмой волны + +| Метрика | v6 | **v7** | +|---|---:|---:| +| Блок «требует решения» | 15 | **4** | +| `2-medium-confidence` | 5800 | **5811** | +| `3-high-confidence` | 958 | **958** | +| Остаточный mojibake | 9 | **0** | +| К правке (`fix`) | 6773 | **6773** | +| Без изменений | 1133 | **1133** | + +**Скрипты восьмой волны:** + +| Файл | Что делает | +|---|---| +| `audit_leftover_mojibake.py` | Ищет значения, которые конверсия пропустила (3+ latin-1 → кириллица) | +| `fix_leftover_mojibake.py` | Точечная посимвольная конвертация оставшихся 9 файлов (`--dry-run`) | +| `apply_manual_decisions.py` | **Расширен:** 28 правок + `ARTIST_CANON` | + +**Залито в vault и синкнуто:** коммит `4464e64`, `git push` + `bash ~/scripts/sync-vault.sh` → `Pushed ✓`. ## 🖐 СЕДЬМАЯ ВОЛНА: ПРАВКИ ALEX → CSV + НОВЫЙ ФОРМАТ НОТ (2026-09-26) @@ -87,8 +183,10 @@ Alex ранее вписал `artist = Детский хор + Benny Benassi`, ` Перегенерация нот **затёрла** это — правка не была в списке `DECISIONS`. В v6 CSV файл стоит с `artist = —`, `title = detskiy hor benassi satisfaction` (транслит имени). -> ⚠️ **Статус: НЕ восстановлено, ждёт подтверждения Alex.** Это иллюстрация главного питфолла: -> любая правка в ноте живёт ровно до следующей перегенерации, пока не перенесена в CSV. +> ⚠️ **Статус: ВОССТАНОВЛЕНО в восьмой волне.** Файл стоит с +> `artist = Детский хор + Benny Benassi`, `title = Satisfaction` (внесён в `DECISIONS` скрипта +> `apply_manual_decisions.py`). Это иллюстрация главного питфолла: любая правка в ноте живёт +> ровно до следующей перегенерации, пока не перенесена в CSV. ### 📐 НОВЫЙ ФОРМАТ НОТ (запрос Alex) @@ -125,10 +223,13 @@ Alex ранее вписал `artist = Детский хор + Benny Benassi`, ` | Файл | Проблема | Как чинить | |---|---|---| -| `Би-2 и Агата Кристи - Теряю свет` (3 шт) | **битая кодировка внутри** title: `Теряю свет (feat.Áè-2)` | правило чистит только поля **целиком** битые; нужна пословная чистка | | `Ундервуд - Мама, Я нашел нефть` | title case на каждое слово: `Мама, Я Нашел Нефть` | нужен нижний регистр для середины фразы | | `KaZantip - Трэк обалденный!!!!` | `!!!!` срезаны | проверить, намеренно ли | +> ✅ **Закрыто в восьмой волне:** `Би-2 и Агата Кристи - Теряю свет (feat.Áè-2)` (3 файла) — +> посимвольная конвертация run'ами (`fix_leftover_mojibake.py`) убрала битую кодировку внутри +> значения. Плюс ещё 6 файлов того же класса. `audit_leftover_mojibake.py` = **0**. + ## 🔴 ШЕСТАЯ ВОЛНА: ALEX ПРАВИТ МОИ НОТЫ + ACL-ФИКС СИНКА (2026-09-26) Alex: **«Needs decision — забирай из syncthing. Medium confidence — там у тебя явный мусор,