[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:
1 parent
4464e648cf
commit
3f48512ff4
3 files changed
+234
-13
No files matched your search
@@ -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/`
|
||||
|
||||
@@ -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 отвечает».
|
||||
@@ -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 — там у тебя явный мусор,
|
||||
|
||||
Reference in new issue
Block a user