From 607fc8ba2a03cbaf40869cdf1ebf10b892de18af Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 27 Sep 2026 01:40:36 +0600 Subject: [PATCH] [2026-09-27] eagle: personal/plans/tagwriter-container.md personal/tech/music-broken-toload.md personal/tech/music-tags-repair-truenas.md --- personal/plans/tagwriter-container.md | 185 +++++++++++++++++++-- personal/tech/music-broken-toload.md | 10 ++ personal/tech/music-tags-repair-truenas.md | 21 ++- 3 files changed, 200 insertions(+), 16 deletions(-) diff --git a/personal/plans/tagwriter-container.md b/personal/plans/tagwriter-container.md index 56271985..a956e581 100755 --- a/personal/plans/tagwriter-container.md +++ b/personal/plans/tagwriter-container.md @@ -4,7 +4,7 @@ namespace: personal related: - '[[personal/plans/music-tags-wave38-fixes]]' - '[[personal/tech/music-tags-repair-truenas]]' -status: applied-delta-closed +status: converted-tags-lost-pending-restore tags: - music - truenas @@ -17,7 +17,7 @@ updated: '2026-09-26' --- # tagwriter — контейнер применения тегов MP3 (TrueNAS) -_Создан: 2026-09-26. **Статус (обновлён 2026-09-26, вечер): ✅ APPLY 7041 + ✅ ДЕЛЬТА-ПРАВКИ 165/165 (readback 0 расхождений) + ✅ МУСОР `._*` ВЫЧИЩЕН (29 удалено, 0 осталось) + ✅ ПРОБА 70 не-MP3 разобрана (41 ok · 13 drm · 16 corrupt) + 🔴 БИТЫЕ m4a/wma — контейнер разрушен ID3-заголовком, решения Alex: битые/DRM **удалять**, список в [[personal/tech/music-broken-toload]]. ⏳ Конвертация 41 (`convert41.py`, DRY-RUN) ждёт апрува. Решение Alex по TRCK = **B** (номера не удалять). Бэкапы: `backup/` 7041 файл 42 ГБ · `backup_raw/` 70 файлов 386 МБ · `backup_fix/` 165 файлов. Образ с ffmpeg 8.1.2.**_ +_Создан: 2026-09-26. **Статус (обновлён 2026-09-26, финал сессии): ✅ APPLY 7041 + ✅ ДЕЛЬТА-ПРАВКИ 165/165 + ✅ МУСОР `._*` ВЫЧИЩЕН (29) + ✅ КОНВЕРТАЦИЯ 41 (41/41) + ✅ ПРОБА 70 не-MP3 (41 ok · 13 drm · 16 corrupt) + ✅ ТЕГИ 40 НОВЫХ MP3 ЗАПИСАНЫ (`fix40.py`, 40/40, решение Alex «Нормально делать» → номер срезать). 🔴🔴 **КРИТИЧЕСКИЙ ДЕФЕКТ `fix40.py`: 40 MP3 СХЛОПНУЛИСЬ В ~1 КБ** — `ID3(p).save(tmp)` пишет ТОЛЬКО тег-блок, MPEG-поток не копируется. Аудио уничтожено. **ВОССТАНОВИМО:** `backup_conv40/` (40/40 целых, до записи тегов) + `backup_raw/` (41/41 оригиналов ogg/flac). Откат готов, ждёт «откат» от Alex. Решение Alex по TRCK = **B** (номера не удалять). Бэкапы: `backup/` 7041 · `backup_raw/` 70 · `backup_fix/` 165 · `backup_conv40/` 40. Образ с ffmpeg 8.1.2. ⏳ НЕ выполнено: удаление 29 битых/DRM, правка дыры в `apply_tags.py` (нет `.mp3`-фильтра), приёмка Jellyfin.**_ _Основание: [[personal/plans/music-tags-wave38-fixes]]._ ## 🆕 СОСТОЯНИЕ НА КОНЕЦ 2026-09-26 (последние шаги сессии) @@ -37,8 +37,10 @@ _Основание: [[personal/plans/music-tags-wave38-fixes]]._ | **🔴 ПРИЧИНА «corrupt»** | **ID3-заголовок записан поверх структуры контейнера** (`moov atom not found`). Файлы битые **до сессии** — в `backup_raw/` те же байты. Восстановление невозможно | | **Список битых/DRM** | ✅ **[[personal/tech/music-broken-toload]]** — 29 файлов на перезагрузку (рядом с `yandex-music-api.md`) | | **Решение Alex по битым/DRM** | ✅ **удалять** («какая разница, они все равно не играют, так что сноси») | -| **Конвертация 41 (`convert41.py`)** | ⏳ **скрипт готов, DRY-RUN ждёт апрува** (блокирован таймаутом апрува) | -| **Удаление 29 битых/DRM** | ⏳ после конвертации 41 | +| **Конвертация 41 (`convert41.py`)** | ✅ **ВЫПОЛНЕНО: 41/41, 0 ошибок** (`convert-result.json` — все 41 статус `CONVERTED`). Скрипт удаляет оригинал ogg/flac **сразу после успешной валидации** → оригиналов на диске больше нет | +| **🔴 НОВЫЙ ДЕФЕКТ: теги новых MP3 ПУСТЫЕ** | 🔴 **40 из 41 без тегов** (`check_conv_tags.py` → `conv-tags-report.json`). Причина: **ffmpeg НЕ переносит Vorbis comments (ogg) → ID3**, `-map_metadata 0` работает только для уже-ID3-источников. Только 1 файл (flac) сохранил тег | +| **Восстановление тегов 40** | ⏳ **ждёт решения Alex** по заголовкам Кирпичей (в плане ` - `, у Muse/Бутусова заголовок чистый). Скрипт `fix40.py` ещё не написан | +| **Удаление 29 битых/DRM** | ⏳ **не выполнено** — ждёт команды (список в [[personal/tech/music-broken-toload]] уже сохранён) | | **Приёмка в Jellyfin** | ⏳ ждёт Alex | | **Обновление доков** | ✅ `personal/plans/tagwriter-container.md` + `personal/tech/music-tags-repair-truenas.md` + `personal/tech/music-broken-toload.md` | @@ -208,10 +210,152 @@ docker run --rm -u 0 -v /mnt/RED_2TB/docker:/docker alpine:latest sh -c \ 17. ✅ **`probe_convert.py`** — 70 файлов разобраны: **`ok` 41 · `drm` 13 · `corrupt` 16**. 18. 🔴 **Диагноз `corrupt`** — ID3-заголовок поверх контейнера; `backup_raw/` идентичен диску → битые до сессии. 19. ✅ **Список 29 битых/DRM → [[personal/tech/music-broken-toload]]**; Alex: **удалять**. -20. ⏳ **Конвертация 41 (`convert41.py`)** — скрипт готов, DRY-RUN ждёт апрува. -21. ⏳ **Удаление 29 битых/DRM** — после конвертации 41. -22. ⏳ **Приёмка в Jellyfin** (глазами Alex). -23. ✅ **Обновить доки** — `personal/plans/tagwriter-container.md` + `personal/tech/music-tags-repair-truenas.md` + `personal/tech/music-broken-toload.md`. +20. ✅ **Конвертация 41 (`convert41.py --apply`)** — **41/41, 0 ошибок**, оригиналы ogg/flac удалены. +21. 🔴 **Проверка тегов новых MP3** — **40 из 41 ПУСТЫЕ** (см. § ПИТФОЛЛ: теги теряются при конвертации ogg → MP3). +22. ✅ **Решение Alex по Кирпичам** — «Нормально делать» = ведущий `<N> - ` **срезать**. +23. ✅ **`fix40.py` написан** (ID3v2.3 UTF-16, правило B, срезка номера, бэкап `backup_conv40/`) → **DRY-RUN 40/40, 0 ошибок**. +24. ✅ **`fix40.py --apply`** → отчёт «40/40 записано, 0 ошибок» — 🔴 **ЛОЖНАЯ ПРИЁМКА: аудио уничтожено** (см. § КРИТИЧЕСКИЙ ДЕФЕКТ). +25. 🔴 **Диагностика** (`verify40.py`, `probe40b.py`, `damage_check.py`): 40 MP3 = огрызки ~1.1 КБ; `backup_conv40/` 40/40 целых; `backup_raw/` 41/41 целых. +26. ⏳ **Откат 40 из `backup_conv40/` + перезапись тегов правильным способом** — ждёт «откат» от Alex. +27. ⏳ **Удаление 29 битых/DRM** — список в [[personal/tech/music-broken-toload]], команды не было. +28. ⏳ **Правка дыры в `apply_tags.py`** (нет `.mp3`-фильтра, `build_disk_index` индексирует всё) — Alex сказал «правь», не выполнено. +29. ⏳ **Приёмка в Jellyfin** (глазами Alex). +30. ✅ **Обновить доки** — этот файл + `personal/tech/music-tags-repair-truenas.md` + `personal/tech/music-broken-toload.md`. + +### 🔴 ОТКРЫТЫЙ ДЕФЕКТ `apply_tags.py`: НЕТ ФИЛЬТРА `.mp3` + +**Симптом (обнаружен по mtime битых m4a/wma):** 16 не-MP3 файлов получили mtime 10:20–10:32 — +время `--apply` волны 38. Причина: `build_disk_index()` (стр. 47-48) индексирует **все** файлы, +`cmd_apply` для m4a/wma: `ID3(disk)` бросает `ID3NoHeaderError` → `except: tags = ID3()` (пустой) → +`tags.save(disk)` пишет ID3 **поверх контейнера** → `moov atom not found`. + +```python +# ❌ ДЫРА (apply_tags.py, ~стр. 47-48): +for root, dirs, files in os.walk(MUSIC): + if "/backup" in root: continue + for name in files: # ← НЕТ проверки расширения! + ... + +# ✅ ФИКС: фильтровать только .mp3 + защита в записи +if not name.lower().endswith(".mp3"): continue +# и в cmd_apply перед save(): +if not disk.lower().endswith(".mp3"): continue +``` + +⚠️ Это тот же класс дефекта, что и порча 16 файлов: **запись ID3 в контейнер другого формата.** +Текущие скрипты (`apply_fix165.py`, `convert41.py`, `fix40.py`) работают по явным спискам MP3 — безопасны. +Дыра остаётся в `apply_tags.py`, без фикса повторный прогон снова испортит не-MP3. + +### 🔴🔴 ПИТФОЛЛ: ТЕГИ ТЕРЯЮТСЯ ПРИ КОНВЕРТАЦИИ OGG → MP3 (40 из 41) + +**Симптом:** конвертация прошла чисто (41/41, 0 ошибок), аудио валидно (`codec=mp3`, длительность ≥ 98 %), +но **40 новых MP3 имеют пустые `TIT2`/`TPE1`/`TALB`/`TRCK`**. Только 1 файл (источник — `.flac`) тег сохранил. + +**Причина:** `-map_metadata 0` в ffmpeg переносит метаданные **только между совместимыми контейнерами**. +У ogg метаданные лежат в **Vorbis comments** (`ARTIST=`, `TITLE=`), у MP3 — в **ID3v2** (`TPE1`, `TIT2`). +ffmpeg этот маппинг **не делает** → пишет пустой ID3-блок. У flac-источника метаданные уже в формате, +близком к ID3, поэтому единственный flac тег пронёс. + +```bash +# НЕ РАБОТАЕТ для ogg → mp3 (теги теряются): +ffmpeg -i src.ogg -vn -acodec libmp3lame -q:a 0 -map_metadata 0 -id3v2_version 3 out.mp3 +``` + +**Рабочие варианты:** +1. **Восстановить теги из плана** (выбран) — `tag-fix-plan.csv` содержит целевые `new_*` для всех 40 путей. + Матч по **basename без расширения** + NFC (в плане путь с `.ogg`/`.flac`, на диске теперь `.mp3`). +2. `ffmpeg ... -metadata title="..." -metadata artist="..."` — явный маппинг, но данные всё равно берём из плана. +3. `mutagen` — прочитать `OggVorbis`/`FLAC` до конвертации и записать `ID3` после (сложнее, не нужно: план есть). + +**🔴 ПРАВИЛО: при конвертации между форматами с разными системами тегов (`ogg`→`mp3`, `flac`→`mp3`) +`-map_metadata 0` НЕ гарантирует перенос. Проверять теги у выхода ОБЯЗАТЕЛЬНО — +и предпочтительно писать теги из плана, а не надеяться на ffmpeg.** +Проверка: `check_conv_tags.py` → `conv-tags-report.json` (ключ `empty` — список пустых). + +### 🔴🔴🔴 КРИТИЧЕСКИЙ ДЕФЕКТ `fix40.py`: АУДИО УНИЧТОЖЕНО (`ID3().save()` пишет только тег-блок) + +**Симптом:** `fix40.py --apply` отработал «успешно» — **40/40 записано, 0 ошибок**. Но независимая +проверка (`verify40.py`, `probe40b.py`) показала: `mutagen.mp3.MP3()` падает +`can't sync to MPEG frame`, `ffprobe` → `Failed to find two consecutive MPEG audio frames`, +`Invalid data found when processing input`. Файлы на диске стали **~1.1 КБ** вместо 2–10 МБ. + +**Причина — корневая, ловится один раз на всю жизнь:** + +```python +# ❌ СЛОМАНО (fix40.py): +tags = ID3(p) # ID3-объект, НЕ привязанный к файлу +tags.save(tmp, v2_version=3) # mutagen пишет ТОЛЬКО ID3-заголовок → MPEG-поток НЕ копируется +os.replace(tmp, p) # оригинал перезаписан огрызком +``` + +`ID3(path)` — это **парсер тег-блока**, он не знает про аудио-поток. `save()` у него сериализует +только фреймы ID3. Чтобы аудио сохранилось, нужен объект файла, знающий про MPEG-поток: + +```python +# ✅ ПРАВИЛЬНО (вариант 1 — тег пишется в тот же файл, поток сохраняется): +from mutagen.id3 import ID3 +tags = ID3(p) +tags.save(p, v2_version=3) # save() В СУЩЕСТВУЮЩИЙ путь → поток перекопируется корректно + +# ✅ ПРАВИЛЬНО (вариант 2 — через MP3-контейнер): +from mutagen.mp3 import MP3 +m = MP3(p) +m["TIT2"] = TIT2(encoding=1, text=[title]) +m.save(v2_version=3) # MP3.save() сохраняет и поток, и тег + +# ✅ ПРАВИЛЬНО (вариант 3 — копируем оригинал целиком, потом правим тег): +shutil.copy2(p, tmp) # сначала полная копия (аудио на месте) +t = ID3(tmp); ...; t.save(tmp, v2_version=3) # правим тег в копии +os.replace(tmp, p) +``` + +⚠️ **Разница незаметна на MP3, который ещё не читался:** `ID3(p)` + `save(tmp)` создаёт валидный +ID3-заголовок, mutagen/`ffprobe` не ругаются на **запись**, ошибка вылезает только на **чтении аудио**. +Поэтому «40/40, 0 ошибок» в отчёте — **ложная приёмка**. Проверять надо `MP3(p).info.length`, а не факт `save()`. + +**Ключевое правило: `save()` у mutagen-ID3 без привязки к потоку = перезапись файла одним тег-блоком.** + +### ✅ УЩЕРБ ЛОКАЛИЗОВАН И ОТКАТЫВАЕТСЯ В НОЛЬ (40 файлов) + +| Источник | Состояние | +|---|---| +| 40 MP3 на диске (`fix40-result.json`, все `WROTE`) | 🔴 огрызки ~1.1–1.2 КБ, `Failed to find two consecutive MPEG audio frames` | +| `/music/backup_conv40/` — копии **ДО** записи тегов | ✅ **40/40 целых** (2.08 МБ – 10.0 МБ, проверено `damage_check.py`) | +| `/music/backup_raw/` — оригиналы ogg/flac | ✅ **41/41 целых**, 0 отсутствующих | + +Единственный выживший MP3 — Infected Mushroom *Bust A Move* (18.8 МБ, кодек mp3 549 с): он не попал +в запись (тег был изначально), потому и цел. + +**План отката (3 шага, ~2 мин):** +1. `restore40.py` — копирование 40 файлов из `backup_conv40/` на место (`.tmp` → проверка размера ≥1 МБ → `os.replace`). +2. Проверка `ffprobe` по 40 → все `codec_name=mp3`, 40/40. +3. Запись тегов **правильным способом** (см. варианты выше) + readback через `MP3(p).info`. + +⚠️ `backup_conv40/` — **копии после конвертации, до записи тегов**: аудио там уже MP3 V0 (валидное), +теги пустые/частичные. Этого достаточно: теги берём из плана (`fix40_source.json`, матч 40/40). + +### 📋 МАТЧ 40 ПУСТЫХ MP3 ↔ ПЛАН (2026-09-26) + +**Матч по basename без расширения — 40/40 найдено.** ⚠️ Первая попытка дала **0 совпадений**: +сравнивал полный путь с расширением (`.mp3` на диске vs `.ogg`/`.flac` в плане). +**Правило: ключ матча = `NFC(путь) без расширения` + маппинг `/storage/Music/` → `/music/`.** + +| Группа | Файлов | `new_title` в плане | `new_track` | +|---|---:|---|---| +| Бутусов & Ю-Питер — *Имя рек* | 8 | чистый (`Сердце камня`, `Мера`, …) | 1–8 ✅ | +| Muse — *Absolution* | 14 | чистый (`Hysteria`, `Intro`, …) | 1–14 ✅ | +| Кирпичи — *let's rock!* | 13 | ⚠️ **`<N> - <title>`** (`3 - squaw`, `1 - в загон!`) | 3, 1, … | +| Синглы (Жуки, Дискотека Авария, Мумий Тролль, Би-2, Pushnoy) | 5 | чистый | пусто | + +**✅ ОТВЕТ ALEX ПОЛУЧЕН (2026-09-26, «Нормально делать»):** у Кирпичей ведущий `<N> - ` из `new_title` +**СРЕЗАЕТСЯ** (единообразие с Muse/Бутусовым, номер остаётся в `TRCK`). Реализовано в `fix40.py`: +срезается только если префикс совпадает с `new_track` (`<N> - ` или `<N в 2 знака> - `). +Результат: `3 - squaw` → `squaw`, `1 - в загон!` → `в загон!`, `9 - let's rock!` → `let's rock!`. + +**Механизм (`fix40.py`, написан и запущен — `--apply` 40/40):** список `fix40_source.json` +(40 пар `{mp3, plan row}`) → запись ID3v2.3 UTF-16 (`TIT2`/`TPE1`/`TALB`/`TRCK`) по правилу **B** +(пустой `new_track` → `TRCK` не трогать). Бэкап: `backup_conv40/` (40 копий ДО записи). +🔴 **НО: реализация записи дефектная — аудио уничтожено, см. § КРИТИЧЕСКИЙ ДЕФЕКТ `fix40.py` выше.** ### 🔧 `convert41.py` — конвертация 41 файла (создан 2026-09-26) @@ -264,8 +408,10 @@ Alex трижды переспросил «ты снимал бэкап до п | **🔴 ПРИЧИНА `corrupt` (16)** | ID3-заголовок (`49443303`) поверх контейнера → `moov atom not found`. **Битые до сессии**, `backup_raw/` идентичен диску | | **СПИСОК НА ПЕРЕЗАГРУЗКУ** | ✅ [[personal/tech/music-broken-toload]] — 29 файлов (16 битых + 13 DRM) | | **РЕШЕНИЕ Alex: битые/DRM** | ✅ **удалять** | -| **КОНВЕРТАЦИЯ 41 (`convert41.py`)** | ⏳ **скрипт готов (NAS), DRY-RUN ждёт апрува** | -| **УДАЛЕНИЕ 29 битых/DRM** | ⏳ после конвертации 41 | +| **КОНВЕРТАЦИЯ 41 (`convert41.py`)** | ✅ **выполнено: 41/41, 0 ошибок**, оригиналы ogg/flac удалены (`convert-result.json`) | +| **🔴 ТЕГИ НОВЫХ MP3** | 🔴 **40 из 41 пустые** (`check_conv_tags.py`) — `-map_metadata 0` не переносит Vorbis comments → ID3 | +| **ВОССТАНОВЛЕНИЕ ТЕГОВ 40** | ⏳ `fix40_source.json` собран (40/40 матч), `fix40.py` не написан — ждёт решения Alex по заголовкам Кирпичей | +| **УДАЛЕНИЕ 29 битых/DRM** | ⏳ не выполнено — список сохранён, команды не было | ### Результат независимой сверки `verify_tags.py` (2026-09-26, ПОЛНЫЙ лог) @@ -529,7 +675,16 @@ docker compose --profile tools run --rm \ | `del_junk.py` | NAS | удаление `._*` по списку `junk-files.json` (dry-run без `--apply`) | | `convert_nonmp3.py` | NAS | конвертация не-MP3 → MP3 (VBR V0, `-map_metadata 0`, ID3v2.3), dry-run/`--apply` | | `probe_convert.py` | NAS | классификация 70 не-MP3: `ok`/`drm`/`corrupt`/`other` → `convert-probe.json` | -| `convert41.py` | NAS | **конвертация только 41 `ok`** с валидацией + удаление оригиналов после успеха | +| `convert41.py` | NAS | **конвертация только 41 `ok`** с валидацией + удаление оригиналов после успеха ✅ **выполнено 41/41** | +| `check_conv_tags.py` | NAS | проверка тегов + аудио у 41 новых MP3 → `conv-tags-report.json` (**выявил 40 пустых**) | +| `fix40.py` | NAS | 🔴 **запись тегов 40 новых MP3 — ДЕФЕКТНА** (`ID3(p).save(tmp)` уничтожил аудио, 40 файлов → ~1 КБ) | +| `verify40.py` | NAS | независимая проверка 40: `MP3(p).info` — выявил `can't sync to MPEG frame` у всех 40 | +| `probe40.py`, `probe40b.py` | NAS | `ffprobe -v verbose` по 41 файлу → `Failed to find two consecutive MPEG audio frames` | +| `damage_check.py` | NAS | сверка размеров диск ↔ `backup_conv40/` ↔ `backup_raw/` (доказал: ущерб локализован, откат возможен) | +| `inspect_data.py` | NAS | инвентарь `/app/data` + содержимое отчётов (выявил перезапись `conv-tags-report.json` 2-м прогоном) | +| `empty_match_check.py`, `empty_match2.py` | NAS | матч 40 пустых против плана (v1 дал 0 — питфолл расширения; v2 фикс) | +| `why_zero.py` | NAS | диагностика нулевого матча: расширения в плане + сырые байты путей | +| `dump40.py` | NAS | полный дамп плана по 40 файлам → `fix40_source.json` (источник для `fix40.py`) | | `check_backup_healthy.py` | NAS | сверка байтов/размеров диск ↔ `backup_raw/` (доказал: битые до сессии) | | `prove_backup_before.py` | NAS | доказывает, что `backup_fix/` снят **до** правок (mtime + diff тегов) | | `diag_m4a.py` | NAS | дамп первых байтов m4a/wma → выявил `ID3` поверх `ftyp`/ASF | @@ -613,6 +768,9 @@ docker compose --profile tools run --rm \ - 🔴🔴 **WMA бывает с Windows Media DRM — конвертация НЕВОЗМОЖНА.** Признак: stderr ffmpeg содержит `License_URL : ...&sbp=DRM` и `Decode error rate 0.99... exceeds maximum`. Файл НТЛ `wma/Track 1–13.wma` — такие. **Правило: перед конвертацией любого формата — probe каждого файла** (`probe_convert.py`), конвертировать только класс `ok`; `drm` оставить как есть. Не трактовать DRM-ошибку как сбой скрипта и не пытаться «дожать» повторными прогонами. - ⚠️ **Лог ffmpeg забивается баннером** (`libav*/62.12.102`) — при разборе ошибок грепать маркеры (`License_URL`, `Decode error rate`, `Conversion failed`), а не листать хвост. - 🔴 **Пустой `new_track` в плане = «не задано», а НЕ «удалить тег».** Генерировать `action` на удаление тега нельзя: Alex подтвердил («это косяк плана, не надо удалять номер трека»). Реализация: при пустом `new_track` **исключать** `TRCK` из сравнения целиком (иначе 75 файлов Наутилуса потеряли бы номера). +- 🔴🔴 **КОНВЕРТАЦИЯ МЕЖДУ РАЗНЫМИ СИСТЕМАМИ ТЕГОВ ТЕРЯЕТ ТЕГИ.** `ffmpeg -map_metadata 0` переносит метаданные только между совместимыми контейнерами: у **ogg** теги в **Vorbis comments** (`ARTIST=`, `TITLE=`), у **MP3** — в **ID3v2** (`TPE1`, `TIT2`). ffmpeg этот маппинг **не делает** → на выходе **пустой ID3**. Проверено: 40/41 новых MP3 без тегов (единственный выживший — `.flac`-источник). **Правило: после любой конвертации формата ОБЯЗАТЕЛЬНО проверять теги у выхода** (`check_conv_tags.py`), и предпочитать **запись тегов из плана**, а не надежду на ffmpeg. Не считать «0 ошибок ffmpeg» доказательством корректного результата — ошибок нет, а данные потеряны. +- 🔴 **Матч CSV ↔ диск при смене расширения: ключ = путь БЕЗ расширения + NFC.** В плане пути записаны с исходным расширением (`.ogg`/`.flac`), на диске после конвертации — `.mp3`. Сравнение полного пути даёт **0 совпадений** (ложное «нет в плане»). Правило: `os.path.splitext(nfc(p))[0]` + маппинг `/storage/Music/` → `/music/`. +- ⚠️ **`convert41.py` удаляет оригинал СРАЗУ после успешной валидации** — это заложено в скрипт, отдельного подтверждения не спрашивает. Учитывать: после прогона оригиналов ogg/flac на диске нет, восстановление тегов возможно только из **плана** или из `backup_raw/`. - ⚠️ macOS-мусор `._*` (AppleDouble) попадает в `find` как «m4a» — **исключать** (`if f.startswith("._"): continue`), иначе счёт файлов завышен вдвое (26 vs 13). - 🔴 **Длительные операции на NAS (бэкап/apply) — только в фоне** (`background=true` + `notify_on_complete`): foreground-таймаут 600 с обрывает ssh, операция продолжается «осиротевшей».<br>Два независимых долгих процесса можно вести **параллельно** (ЧТЕНИЕ тегов + КОПИРОВАНИЕ файлов не конфликтуют) — Alex это ожидает. - 🔴 **Апрув-таймаут ≠ «не смог».** Если команда заблокирована («Approval request timed out — do NOT retry»), это значит **Alex не увидел промпт**, а не запрет. Ассистент обязан: (1) сообщить точную формулировку блокировки, (2) **не повторять вслепую**, (3) попросить подтверждение. Alex реагирует жёстко на «почему не сделал?» — нужно сразу объяснять механику блокировки, а не оправдываться. @@ -620,6 +778,11 @@ docker compose --profile tools run --rm \ - 🔴 **НЕ переизобретать работу, которую Alex уже дал.** Если данные/список уже есть (лог verify, CSV) — **взять их**, а не писать новый массовый пересчёт. Alex: «У тебя же уже есть этот список зачем пересчет». Лишний шаг = потеря времени + раздражение. - 🔴 **Вопрос «Чини» = план + выполнение, не новые вопросы.** Alex сказал «Чини» — значит надо чинить. Но перед **деструктивной частью** (удаление TRCK, удаление файлов) — план и подтверждение обязательны (правило стопа/безопасности выше по приоритету, чем краткость). - 🔴 **Не списывать расхождения «всё ок» без разбора.** Alex ловит такие выводы: «ты только что говоришь что там была неправильная кодировка и неверные теги но тут же говоришь что это ок». Если в данных есть mojibake/неверные значения — это **дефект**, а не норма. Сначала классификация по группам и файлам, потом вывод. +- 🔴🔴🔴 **`ID3(path).save(other_path)` УНИЧТОЖАЕТ АУДИО.** `ID3()` — парсер **тег-блока**, он не знает про MPEG-поток: `save()` сериализует **только фреймы ID3**, а `os.replace` затирает оригинал → файл становится ~1 КБ. Проверено: `fix40.py` убил 40 MP3 (2–10 МБ → 1.1 КБ), `ffprobe` → `Failed to find two consecutive MPEG audio frames`. **Правильно:** `ID3(p).save(p)` в тот же путь, либо `MP3(p).save()`, либо `shutil.copy2(p, tmp)` перед правкой. **Ключевое: `save()` в СУЩЕСТВУЮЩИЙ путь** — тогда mutagen копирует поток. Спасение в этом кейсе: `backup_conv40/` (копии до записи) + `backup_raw/` (оригиналы ogg/flac). +- 🔴 **«40/40 записано, 0 ошибок» — НЕ приёмка.** Отчёт `save()` говорит только о записи тега, **не о целостности аудио**. Единственная приёмка: `MP3(p).info.length > 0` (mutagen) **и** `ffprobe -select_streams a:0 -show_entries stream=codec_name` → `mp3`. Валидировать **выход**, а не факт выполнения. Отсутствие этой проверки = потеря 40 файлов, выявленная только через независимый скрипт. +- 🔴 **`ffprobe -of csv=p=0` + `-show_entries stream=...` возвращает ПУСТО** (баг в `check_conv_tags.py`): проверка `"mp3" not in stdout` даёт ложное «аудио-проблемы» на **всех** файлах. Использовать `-of json` и парсить `streams[0].codec_name` — тогда ошибка видна реально. +- 🔴 **Скрипт-чекер ПЕРЕЗАПИСЫВАЕТ свой отчёт** — `check_conv_tags.py` при втором прогоне заменил `conv-tags-report.json`: ключ `empty` (40) стал пустым, те же 40 ушли в `bad`. Далее `probe40.py` читал уже пустой список → «файлов: 0», ложная пустота. **Правило: при разборе смотреть, когда отчёт создан и не запускать чекер повторно «для верности»; источник истины — `convert-probe.json` (`ok: 41`) и `fix40-result.json`.** +- 🔴 **Проверять факт ДО вывода «данные потеряны»:** ущерб 40 файлов был бы «невосстановим», если бы не `backup_conv40/`. Правило: **перед любой массовой правкой тегов — бэкап копией файлов** (не только ID3-дампа), именно в отдельную папку `backup_<что>`. Это спасло сессию. ## Связано diff --git a/personal/tech/music-broken-toload.md b/personal/tech/music-broken-toload.md index aa448503..3fac8d50 100755 --- a/personal/tech/music-broken-toload.md +++ b/personal/tech/music-broken-toload.md @@ -26,6 +26,15 @@ related: personal/tech/yandex-music-api, personal/tech/music-tags-repair-truenas **Бэкап:** все 29 в `/mnt/RED_2TB/storage/Music/backup_raw/`. ⚠️ **Бэкап не помогает** — файлы там идентичны диску (побиты до сессии). +⚠️ **Статус удаления (2026-09-26): НЕ УДАЛЕНЫ.** Alex сказал удалять, но команда на удаление +ещё не отдавалась — все 29 (16 битых + 13 DRM) **по-прежнему лежат на диске**. +Ждут команды после завершения работ по конвертации. + +📌 **Все 29 — из числа 70 не-MP3.** Остальные 41 (40 ogg + 1 flac) **уже сконвертированы в MP3** +(`convert41.py`, 41/41, 0 ошибок) — они в этот список **не входят** и удалению не подлежат. +⚠️ У 40 из них потерялись теги при конвертации (см. [[personal/plans/tagwriter-container]]) — +это отдельная задача восстановления, **не** повод добавлять их сюда. + --- ## 1. Битые (контейнер разрушен) — 16 файлов @@ -108,6 +117,7 @@ Windows Media DRM. Microsoft закрыл инфраструктуру выда - [[personal/tech/yandex-music-api]] — выгрузка библиотеки Яндекс Музыки - [[personal/tech/music-tags-repair-truenas]] — ремонт тегов на TrueNAS +- [[personal/plans/tagwriter-container]] — контейнер применения тегов + конвертация 41 - [[personal/tech/music-tags-final-review]] — финальное ревью --- diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index 109f443a..d7da49d9 100755 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -41,10 +41,19 @@ related: > | 🔴 **ПРИЧИНА `corrupt`** | **ID3-заголовок (`49443303`) записан поверх структуры контейнера** → `moov atom not found`. Файлы битые **до сессии** (в `backup_raw/` те же байты) — восстановление невозможно | > | ✅ Список на перезагрузку | [[personal/tech/music-broken-toload]] — **29 файлов** (16 битых + 13 DRM) | > | ✅ Решение Alex | битые/DRM **удалять** («какая разница, они все равно не играют») | -> | ⏳ Конвертация 41 | `convert41.py` готов, DRY-RUN ждёт апрува | -> | ⏳ Удаление 29 битых/DRM | после конвертации 41 | +> | **Конвертация 41 (`convert41.py --apply`)** | ✅ **выполнено: 41/41, 0 ошибок** — оригиналы ogg/flac удалены скриптом | +> | 🔴 **Теги новых MP3** | 🔴 **40 из 41 ПУСТЫЕ** (`check_conv_tags.py`) — ffmpeg **не переносит Vorbis comments → ID3** | +> | ⏳ Восстановление тегов 40 | `fix40_source.json` собран (матч 40/40 по basename), `fix40.py` не написан — ждёт решения Alex по заголовкам Кирпичей | +> | ⏳ Удаление 29 битых/DRM | **не выполнено** — команды не было (список уже сохранён) | > | Приёмка в Jellyfin | ⏳ ждёт Alex | > +> **🔴 НОВЫЙ КЛАСС ПРОБЛЕМЫ: конвертация теряет теги между системами метаданных.** +> `ogg` хранит теги в **Vorbis comments** (`ARTIST=`/`TITLE=`), `mp3` — в **ID3v2** (`TPE1`/`TIT2`). +> `-map_metadata 0` этот маппинг **не делает** → 40/41 новых MP3 вышли с пустыми тегами +> (единственный выживший — `.flac`-источник). ⚠️ **«0 ошибок ffmpeg» ≠ корректный результат** — +> ошибок не было, а теги потеряны. **Правило: после конвертации формата ОБЯЗАТЕЛЬНО проверять +> теги у выхода и предпочитать запись тегов из плана.** Подробно — [[personal/plans/tagwriter-container]]. +> > **🔴 НОВЫЙ КЛАСС ПРОБЛЕМЫ: зашифрованный контент.** Часть `.wma` (НТЛ `wma/Track 1–13.wma`) > защищена **Windows Media DRM** — ffmpeg не может их декодировать (99.5 % ошибок). Лицензии > Microsoft больше не выдаются → **снять DRM невозможно**. Не сбой скрипта (оригиналы в `backup_raw/`). @@ -239,9 +248,11 @@ related: > (2) ✅ **ответ Alex по TRCK = B** → **apply дельта-правок (165): 165/165, 0 расхождений**; > (3) ✅ **проба 70 не-MP3**: `ok` 41 · `drm` 13 · `corrupt` 16; битые/DRM → список > [[personal/tech/music-broken-toload]], Alex: **удалять**; -> (4) ⏳ конвертация 41 `ok` (`convert41.py`, DRY-RUN ждёт апрува); -> (5) ⏳ удаление 29 битых/DRM; -> (6) ⏳ приёмка в UI Jellyfin (глазами Alex); (7) после приёмки — удалить бэкапы +> (4) ✅ **конвертация 41 `ok`** — выполнено 41/41, 0 ошибок, оригиналы ogg/flac удалены; +> (5) 🔴 **новый дефект: 40 из 41 новых MP3 без тегов** — восстановление из плана +> (`fix40_source.json` собран), `fix40.py` ждёт решения Alex по заголовкам Кирпичей; +> (6) ⏳ удаление 29 битых/DRM (список сохранён, команды не было); +> (7) ⏳ приёмка в UI Jellyfin (глазами Alex); (8) после приёмки — удалить бэкапы > (`backup/`, `backup_raw/`, `backup_fix/`). > > ### 🔴 ПИТФОЛЛ: БЭКАП СНИМАТЬ ДО ПРАВКИ, ДОКАЗЫВАТЬ ФАКТОМ