[2026-09-26] eagle: family/how-to/jellyfin-config.md personal/tech/id3-tag-repair.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 16:53:40 +06:00
1 parent 4464e648cf
commit 3f48512ff4
3 files changed
+234 -13

No files matched your search

+8 -5
View File
@@ -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/`
+117
View File
@@ -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 отвечает».
+109 -8
View File
@@ -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 <plan.csv> <outdir>` — 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 — там у тебя явный мусор,