[2026-09-26] eagle: family/how-to/jellyfin-config.md personal/plans/balda-mm-pr-plan.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 14:21:50 +06:00
1 parent 8589765eb8
commit 9f300da7b6
3 files changed
+242 -22

No files matched your search

+162 -15
View File
@@ -18,9 +18,124 @@ related:
---
# Music Tags Repair — TrueNAS (7906 файлов)
> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26). CSV передан Alex на проверку. Правки файлов НЕ начаты.**
> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26), затем ПЕРЕДЕЛАНЫ после замечаний Alex. CSV v2 передан на проверку. Правки файлов НЕ начаты.**
> Следующий шаг — апрув Alex по CSV, затем бэкап + применение.
## 🔴 ПЕРЕДЕЛКА ЭТАПА 1–2 (2026-09-26, после замечаний Alex)
Alex отклонил первую версию CSV: **«А чё за probe failed на большинстве? Походу ты чего-то не доделал. ДДТ — город без окон — явная хуйня предложена. Ты бы хоть постеснялся не проверив мне файл сувать!»**
Замечания оказались **верны по всем пунктам**. Разбор и что сделано:
| # | Замечание Alex | Что было не так | Фикс |
|---|---|---|---|
| 1 | «probe_failed на большинстве» | 359 файлов отданы **пустыми заглушками** `action=probe_failed` вместо разбора. Я выяснил, что там валидный ID3v2.3, и **остановился** | Написан прямой ID3-парсер (`id3_direct.py`), интегрирован в инвентарь как fallback |
| 2 | «ДДТ — явная хуйня» | Ранний `if verdict == "probe_failed": continue` **до** логики вывода → файлы `ДДТ - Любовь.mp3` не получили artist/title из имени | Ранний выход **убран**, вывод из имён применяется ко всем |
| 3 | «не проверив мне файл сувал» | Отдал CSV, не проверив сэмплы проблемных групп | Проверки прогнаны до выдачи (см. ниже) |
### 🔑 ПРИЧИНА `probe_failed`: битый фрейм `comment='eng'`
Все 343 файла — **валидный ID3v2.3**, у которого `ffprobe` молча возвращает **пустой набор тегов**.
Причина — нестандартный/битый фрейм `comment` со значением `eng`. `ffprobe` спотыкается на нём
и отдаёт пустоту, **не сообщая об ошибке** (`exit=0`, пустой JSON).
**Теги там БЫЛИ и оказались обычным mojibake:** `Âëàäèìèð Âûñîöêèé` → `Владимир Высоцкий`.
То есть 343 файла — не «особый случай», а пропущенный массовый mojibake.
**Решение:** парсить ID3 напрямую из байтов файла, минуя `ffprobe`.
```python
# id3_direct.py — читает ID3v2 фреймы из файла
# ENCODINGS = {0: latin-1, 1: utf-16 (BOM), 2: utf-16-be, 3: utf-8}
# FRAMES = TIT2→title, TPE1→artist, TPE1→album_artist, TALB→album,
# TCON→genre, TDRC/TYER→date, TRCK→track, TPOS→disc
```
> 🔴 **МЕТОДИЧЕСКИЙ ВЫВОД: `ffprobe` — НЕ единственный источник. Молчаливый пустой
> результат ≠ «тегов нет».** Всегда иметь fallback-парсер для ID3. Это уже вторая
> ловушка `ffprobe` в проекте (первая — `wget`/SOCKS в доке TrueNAS).
## Инвентаризация v2 — `tag_inventory2.py`
Двухступенчатое чтение: сначала `ffprobe`, при пустом результате — прямой ID3-парсер.
| Источник тегов | Файлов |
|---|---:|
| `ffprobe` | 6703 |
| `no_tags` (тегов нет реально) | 860 |
| **`id3_direct`** (был `probe_failed`) | **343** |
| `realmedia` | 0 (оказались `.mp3` с тегом внутри) |
**Результат v2 (7906 файлов):**
| Вердикт | v1 | v2 | Δ |
|---|---:|---:|---|
| `partial` | 2919 | 2920 | +1 |
| `mojibake` | 2419 | **2759** | **+340** ← те самые «probe_failed» |
| `empty` | 1010 | 1026 | +16 |
| `lost` | 662 | 662 | — |
| `clean` | 537 | 537 | — |
| `corrupt` (новый) | — | 2 | +2 |
| `probe_failed` | 359 | **0** | ✅ **устранён** |
> ✅ **`probe_failed` полностью устранён.** Новый вердикт `corrupt` — управляющие байты
> в тегах (например `\x1d56=>ABL...` у `!Классика/Пахмутова`).
## CSV v2 — итоговые цифры
| Действие | v1 | v2 |
|---|---:|---:|
| `fix` | 4601 | **4961** |
| `no_change` | 2946 | 2945 |
| `probe_failed` | 359 | **0** |
| `fixed_lost` (восстановлено из имён) | 0 | **662** |
| `fixed_corrupt` | 0 | 2 |
**Контрольные проверки перед выдачей (все пройдены):**
| Проверка | Результат |
|---|---|
| U+FFFD в выходных значениях | **0** ✅ |
| `fix` с пустым title | **0** ✅ |
| `fix` с пустым artist | 8 (плоские папки, источника нет — законно) |
| `TrackNN` в title | **0** ✅ |
| Регресс-тесты | **32/32 PASS** ✅ |
## ⚠️ РАЗБОР СЛУЧАЯ ДДТ (проверка «это не мой баг»)
Файлы `!Retro/ДДТ - *.mp3` предложены с `album = Просвистела` для всех пяти.
**Проверено сырыми тегами — это НЕ баг скрипта:**
```
ДДТ - Любовь.mp3 album='Ïðîñâèñòåëà' track='06' date='1999'
ДДТ - Просвистела.mp3 album='Ïðîñâèñòåëà' track='1' date='1999'
ДДТ - Что такое осень.mp3 album='Ïðîñâèñòåëà' track='5' date='1999'
ДДТ - Дождь.mp3 album='Ïðîñâèñòåëà' track='3' date='1999'
ДДТ - В последнюю осень album='Ïðîñâèñòåëà' track='04' date='1999'
```
Альбом `Просвистела` **реально записан в тегах** всех пяти файлов — это сборник 1999 года
(треки 1, 3, 4, 5, 6). Скрипт прочитал факт. Если Alex захочет — можно поставить album из
имени папки вместо тега, но по умолчанию тег приоритетнее.
**Было исправлено по ДДТ:** `Ëþáîâü` → `Любовь`, `ÄÄÒ` → `ДДТ` (раньше отдавались пустыми).
### Проверка через `check_ddt_album.py`
Скрипт печатает полный набор тегов каждого файла — так отличим «баг скрипта» от «так в файле».
**Вывод: всегда проверять сырые теги конкретного файла прежде чем заявлять о баге.**
### Разбор транслит-имён (добавлено)
| Пример | Результат |
|---|---|
| `detskiy_hor_benassi_satisfaction.mp3` | `Detskiy Hor Benassi Satisfaction` |
| `29 Benny_benassi_feat._farenheit-free_your_mind__on_the_floor.mp3` | artist=`Benny Benassi Feat. Farenheit`, title=`Free Your Mind (On The Floor)` |
| `01-benassi_bros-make_me_feel.mp3` | artist=`Benassi Bros`, title=`Make Me Feel` |
Приём `prettify_translit`: подчёркивания → пробелы + заглавные буквы. **Кириллицу не выдумывает**,
применяется только когда других источников нет.
## Контекст
Задача началась как «добавить папку музыки в Jellyfin», но разведка тегов показала, что папка
@@ -137,16 +252,20 @@ ssh truenas_admin@mallexxx.duckdns.org \
**2377 из 2419 (98.3%) конвертируются без потерь.** Остальные 42 — требуют ручного просмотра.
### `probe_failed` расшифрован (359 файлов)
### `probe_failed` — РАЗОБРАН И УСТРАНЁН (v2)
**Итог: `probe_failed` больше не существует (0 файлов).** Все 343 файла прочитаны прямым
ID3-парсером (см. раздел «ПЕРЕДЕЛКА» выше). Результат разбора v1:
| Что | Кол-во | Деталь |
|---|---:|---|
| Валидный **ID3v2.3** | 343 | Заголовок целый (`hdr_ok=True`), теги есть — `ffprobe` их не отдал по иной причине |
| **RealMedia** (`00051607`) | 15 | Это не mp3 — `video/x-pn-realvideo` |
| Валидный **ID3v2.3** | 343 | Заголовок целый, теги есть. `ffprobe` молчит из-за битого фрейма `comment='eng'`. **Прочитаны через `id3_direct`** |
| **RealMedia** (`00051607`) | 15 | Помечались как RealMedia по заголовку, но оказались `.mp3` с ID3 внутри → в v2 ушли в `id3_direct` |
| Без заголовка | 1 | — |
> Вывод: `probe_failed` ≠ «файл битый». 343 файла структурно валидны, но остаются
> без извлечённых тегов. **Не блокируют ремонт остальных** — разбираются отдельно.
> 🔴 Изначальный вывод «343 файла структурно валидны, но остаются без тегов» был
> **недоработкой**: файлы надо было прочитать другим способом, а не оставлять заглушкой.
> **`probe_failed` ≠ «файл битый» И ≠ «нет тегов».**
### Защита от порчи: доля кириллицы в `fix_encoding`
@@ -186,9 +305,9 @@ if cyr < 2: # одна буква — совпадение
| # | Шаг | Статус |
|---|---|---|
| 1 | **Инвентаризация** read-only: `ffprobe` по 7906 файлам → CSV `путь, теги, классификация` | ✅ **ГОТОВО** — [[#✅ РЕЗУЛЬТАТ ЭТАПА 1 инвентаризация завершена (2026-09-26)]] |
| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО** — `~/Downloads/music-tags/tag-fix-plan.csv` |
| 3 | **Апрув Alex по CSV** → только после этого правки | ⏳ **ЖДЁМ** |
| 1 | **Инвентаризация** read-only: `ffprobe` по 7906 файлам → CSV | ✅ **ГОТОВО (v2)** — `tag_inventory2.py`, `probe_failed`=0 |
| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v2)** — `~/Downloads/music-tags/tag-fix-plan.csv` |
| 3 | **Апрув Alex по CSV** → только после этого правки | ⏳ **ЖДЁМ** (v1 отклонён, v2 отдан) |
| 4 | **Бэкап** — копировать **только перезаписываемые** файлы в `Music/backup/` | ⏳ |
| 5 | **Ремонт** — перекодировка mojibake + восстановление из имён путей + чистка мусора | ⏳ |
| 6 | **Приёмка** — Alex слушает выборку | ⏳ |
@@ -224,18 +343,24 @@ if cyr < 2: # одна буква — совпадение
| Файл | Что делает |
|---|---|
| `tag_inventory.py` | **Этап 1.** Обход `Music`, `ffprobe` по каждому файлу, классификация тегов (`clean`/`mojibake`/`lost`/`empty`/`partial`/`probe_failed`) → CSV. Путь к `ffprobe` через `FFPROBE` env (по умолчанию `ffprobe`) |
| `tag_inventory.py` | **Этап 1 (v1, устарел).** Обход `Music`, `ffprobe` по каждому файлу → CSV. Только `ffprobe`, без fallback → дал 359 `probe_failed` |
| `tag_inventory2.py` | **Этап 1 (v2, актуальный).** `ffprobe` + **прямой ID3-парсер как fallback**. Добавлена колонка `source` (`ffprobe`/`id3_direct`/`no_tags`), новый вердикт `corrupt`. `probe_failed` = 0 |
| `id3_direct.py` | **Прямой парсер ID3v2** из байтов файла (минуя `ffprobe`). Поддержка ID3v2.2/2.3/2.4, кодировок 0–3, extended header. Используется как fallback и как отладочный инструмент |
| `analyse_inventory.py` | Анализ CSV инвентаря: доли вердиктов, проверка обратимости mojibake, разбор `lost`/`probe_failed` |
| `build_fix_plan.py` | **Этап 2.** Строит CSV с колонкой «предлагаемое исправление». Содержит `fix_encoding`, `clean_value`, `repair`, `split_artist_title`, `title_from_name`, `derive_from_path` |
| `check_fix_plan.py` | Контроль качества плана: сколько U+FFFD на выходе (должно быть 0), сколько без title, сэмплы «до/после» по каждой категории |
| `build_fix_plan.py` | **Этап 2 (v1, устарел).** Содержал ранний выход по `probe_failed` — из-за него ДДТ отдавались пустыми |
| `build_fix_plan2.py` | **Этап 2 (v2, актуальный).** Ранний выход убран, `prettify_translit` добавлен. Содержит `fix_encoding`, `clean_value`, `repair`, `split_artist_title`, `title_from_name`, `derive_from_path`, `prettify_translit` |
| `check_fix_plan.py` | Контроль качества плана: U+FFFD на выходе, пустые title/artist, сэмплы «до/после» по категориям |
| `find_bad_titles.py` | Ищет недоработки: `TrackNN` в title, голые номера, пустой artist |
| `test_encoding_guard.py` | **32 регресс-теста.** Защита от порчи корректного UTF-8 (`Björk`, `Saint-Saëns`, `Noël`) + проверка `split_artist_title` |
| `check_ddt_album.py` | Печатает **полный набор сырых тегов** файлов по маске — для проверки «баг скрипта vs так в файле» |
| `diagnose_probe_failures.py` | Анализ заголовков файлов, упавших в `probe_failed`: ID3-версия, `hdr_ok`, размеры |
| `test_encoding_guard.py` | **32 регресс-теста.** Защита от порчи корректного UTF-8 (`Björk`, `Saint-Saëns`, `Noël`) + `split_artist_title` |
| Артефакт | Где | Что |
|---|---|---|
| CSV-инвентарь | `/mnt/RED_2TB/storage/tmp/tag-inventory.csv` (NAS) → `~/Downloads/music-tags/tag-inventory.csv` (Mac) | 7906 строк, результат этапа 1 |
| **CSV-план правок** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac) | **Отдан Alex 2026-09-26.** 4601 `fix` / 2946 `no_change` / 359 `probe_failed` |
| CSV-инвентарь v2 | `/mnt/RED_2TB/storage/tmp/tag-inventory2.csv` (NAS) → `~/Downloads/music-tags/tag-inventory.csv` (Mac) | 7906 строк, колонка `source`, `probe_failed`=0 |
| **CSV-план правок v2** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, 2.2 МБ) | **Отдан Alex 2026-09-26.** 4961 `fix` / 2945 `no_change` / 0 `probe_failed` |
| Копия БД Jellyfin | `/tmp/jf-copy.db` (Mac) | Для чтения API-ключа (см. `[[family/how-to/jellyfin-config]]`) |
| Копия плана на NAS | `/tmp/tag-fix-plan.csv` (NAS) | Для запуска `id3_direct.py`/`check_ddt_album.py` с хостовым Python |
### Колонки `tag-fix-plan.csv`
@@ -269,6 +394,28 @@ if cyr < 2: # одна буква — совпадение
- ⚠️ `alpine` без `ffmpeg` → инвентарь падает молча. Ставить `apk add --no-cache ffmpeg` первой строкой.
- 🔴 **`execute_code` требует апрува** — Alex явно сказал: **«Без аппрува команды используй! Скрипты пиши»**. Писать исполняемое в файл (`write_file`) → запускать через `terminal`. Не инлайн.
- 🔴 **Наивная перекодировка CP1251→Latin-1 портит корректный UTF-8** (`Saint-Saëns` → `Saint-Saлns`). Обязателен guard по доле кириллицы (см. выше).
- 🔴 **`ffprobe` молча отдаёт ПУСТЫЕ теги** при битом фрейме внутри (напр. `comment='eng'`) — при `exit=0` и валидном JSON. Нельзя считать «пусто от `ffprobe`» доказательством отсутствия тегов. Нужен fallback-парсер ID3 (`id3_direct.py`).
- 🔴 **Ранний `continue` по «нераспознанному» вердикту = потеря файлов.** Именно так 359 файлов ушли заглушками, а ДДТ — без artist/title. **Неизвестный вход обрабатывать общей логикой, а не пропускать.**
- 🔴 **Нельзя отдавать пользователю вывод, не проверив сэмплы проблемных групп.** CSV v1 ушёл Alex без проверки `probe_failed` и `!Retro` — и был справедливо раскритикован. Проверка = прогон `check_fix_plan.py` + `find_bad_titles.py` + выборочный `check_ddt_album.py`.
- ⚠️ **Правка скриптов только локально → `scp` заново.** `sed -i` на NAS применён быть не должен (была такая ошибка, исправлена).
- ⚠️ **Кириллица в аргументах `ssh`-команд** может поднять tirith `confusable_text`. Держать русские пути внутри файла-скрипта.
- ⚠️ **Долгий инвентарь — запускать в фоне** (`docker run -d`) и поллить статус, а не держать `for`-цикл ожидания дольше 600 с (таймаут foreground).
### 🧠 ГЛАВНЫЙ ПРОЦЕССНЫЙ УРОК ЭТОЙ СЕССИИ
Alex: **«Ты бы хоть постеснялся не проверив мне файл сувать!»**
Порядок работы, который себя оправдал **после** исправления:
1. Написать скрипт **в файле** (`write_file`) — не инлайн, не `execute_code`.
2. Прогнать **автотесты** на краевых случаях (латиница с умляутами, дефисы, номера).
3. Прогнать **контрольные проверки** на реальном выводе: U+FFFD, пустые поля, оставшийся мусор.
4. **Посмотреть сэмплы «до/после» глазами** по каждой проблемной группе.
5. Проверить **сырые данные конкретного файла**, прежде чем спорить «это баг / не баг».
6. **Только потом** отдавать пользователю.
Три бага были пойманы пунктами 3–4 **до** выдачи. Пункт 5 отличает «баг скрипта» от «так в файле» (случай ДДТ).
**Заглушка `needs investigation` в отчёте — это не результат, а моя недоработка.**
### 🐞 Три бага, пойманные ДО выдачи CSV (файлы не пострадали)