diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index a2e28d4e..45ca6915 100644 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -18,9 +18,11 @@ related: --- # Music Tags Repair — TrueNAS (7906 файлов) -> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26). CSV v3 + ноты ревью отданы Alex. Правки файлов НЕ начаты.** +> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26). CSV v4 + ноты ревью отданы Alex. Правки файлов НЕ начаты.** > Следующий шаг — апрув Alex по нотам ревью, затем бэкап + применение. -> Перед выдачей проведён **полный аудит всех 4774 строк** (`audit_plan.py` + `final_gate.py`) — найдено и исправлено 6 багов. +> Прогонялись три волны проверок: полный аудит всех строк (`audit_plan.py` + `final_gate.py`) → 6 багов, +> затем третья волна по замечаниям Alex → 3 бага (en-dash + подстроковый поиск стоп-слов). +> Итог v4: 4777 `fix`, mojibake = 0, 80 тестов PASS, блок «требует решения» 296 → 28. ## 📌 ПЛАН ТРЕТЬЕЙ ВОЛНЫ (2026-09-26, апрув Alex) — «сам проставь, потом на апрув» @@ -124,32 +126,45 @@ Alex: **«Сам проверил весь файл на адекватност **Оставшиеся флаги аудита — все проверены вручную и законны:** `album == artist` (49, одноимённые альбомы), `translit_kept` (83, реальная латиница), `mixed_script` (19, `NTL ft. MC Молодой`), `numeric_title` (15, `007`/`48`). -## 📡 SYNCTHING: ноты доехали, но найдено расхождение в расписании +## 📡 SYNCTHING: ноты доехали — но по пути нашлись ДВА бага Проверка по просьбе Alex («Проверь что syncthing всосал доку»). Первая проверка показала: `obsidian-syncthing` **отстал на 3 коммита**, нот в нём нет. -**Причина — не Syncthing.** Цепочка «bare repo → git-клон `obsidian-syncthing` → Syncthing → телефон» -рвётся на **втором** шаге: job `sync-vault` в контейнере `hermes-taiga` расписан **`0 * * * *` — раз в час**, -хотя в `[[family/how-to/vault-git-sync]]` записано «каждые 5 минут». +**Причина №1 — не Syncthing.** Цепочка «bare repo → git-клон `obsidian-syncthing` → Syncthing → телефон» +рвётся на **втором** шаге: клон обновляет Taiga, а не Syncthing. Лечится прогоном job вручную: -**Что сделано:** job триггернут вручную (`hermes cron run 29851c413903`). Проверено: +```bash +docker exec hermes-taiga bash /opt/data/sync-vault-taiga.sh +``` + +> ⚠️ **Именно `bash`, не `sh`** — скрипт использует `set -o pipefail`, который `dash`/`sh` не понимает +> (`Illegal option -o pipefail`). Путь **внутри контейнера**: `/opt/data/sync-vault-taiga.sh` +> (= `/mnt/RED_2TB/docker/hermes/config/sync-vault-taiga.sh` на хосте). + +**Причина №2 (настоящая, нашлась после «Pushed ✓»):** файлы **не уходили на телефон**, хотя git +рапортовал `Pushed ✓`. Это питфолл прав — см. следующий раздел. + +**Итог после обоих фиксов:** | Проверка | Значение | |---|---| -| Клон `obsidian-syncthing` | ✅ HEAD `e200596`, 4 файла на месте | -| Syncthing: очередь | ✅ `needFiles: 0`, `needItems: 0`, `needDeletes: 0` | -| Синк с телефоном | ✅ `completion: 100`, `remoteState: valid` | -| Ошибки | ✅ `errors: 0` | +| Клон `obsidian-syncthing` | ✅ HEAD `7564ae2`, все 4 ноты на месте | +| Syncthing: очередь | ✅ `needFiles: 0`, `needBytes: 0`, `errors: 0` | +| Состояние | ✅ `state: idle` | | Версия Syncthing | `v2.1.5 "Hafnium Hornet"`, папка `k5vyy-gzdgj` «Obsidian Vault» | +> 🔴 **ПРАВИЛО ПРИЁМКИ СИНКА: `needFiles: 0` + `errors: 0`, а НЕ «Pushed ✓».** +> `Pushed` — это про git. Файл может быть в коммите и при этом не читаться Syncthing'ом. + **Питфоллы синка этой сессии:** - 🔴 **Порт Syncthing `8384` слушает ТОЛЬКО внутри контейнера** — `curl` с хоста TrueNAS отдаёт пустоту. Запрос API делать через `docker exec syncthing curl -s -H "X-API-Key: …" http://localhost:8384/rest/…`. - ⚠️ **`obsidian-syncthing` — git-клон (remote `/vault.git`), а не Syncthing-папка.** Обновляет его - Taiga по крону; Syncthing лишь раздаёт то, что уже в клоне. «Syncthing здоров» ≠ «док доехал». + Taiga; Syncthing лишь раздаёт то, что уже в клоне. «Syncthing здоров» ≠ «док доехал». - ⚠️ **Скрипт диагностики синка** — `check_syncthing2.sh` (в `tools/music-tags/`): версия, статус папки, connections, completion для телефона. Писать в файл, а не инлайн — иначе `APIKEY=` в heredoc режется. +- ⚠️ **Ключ Syncthing** добывается изнутри: `docker exec syncthing grep -o "[^<]*" /var/syncthing/config/config.xml | cut -c9-`. ### 🔴 ПИТФОЛЛ: права файлов в git-клоне блокируют Syncthing @@ -488,19 +503,21 @@ if cyr < 2: # одна буква — совпадение | # | Шаг | Статус | |---|---|---| | 1 | **Инвентаризация** read-only: `ffprobe` по 7906 файлам → CSV | ✅ **ГОТОВО (v2)** — `tag_inventory2.py`, `probe_failed`=0 | -| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v3)** — `~/Downloads/music-tags/tag-fix-plan.csv`, 4774 `fix` | -| 2a | **Ноты для ревью в Obsidian** → Alex (запрос Alex) | ✅ **ГОТОВО** — `personal/plans/music-tags-review/`, синкнуто + доехало до телефона | +| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v4)** — `~/Downloads/music-tags/tag-fix-plan.csv`, 4777 `fix` / 3129 `no_change` | +| 2a | **Ноты для ревью в Obsidian** → Alex (запрос Alex) | ✅ **ГОТОВО (v4)** — `personal/plans/music-tags-review/`, пересинкнуто | | 2b | **Полный аудит всех строк** (`audit_plan.py` + `final_gate.py`) → 6 багов исправлено | ✅ **ГОТОВО** (запрос Alex «Сам проверил весь файл?») | -| 3 | **Апрув Alex по нотам ревью** → только после этого правки | ⏳ **ЖДЁМ** (v1 и v2 отклонены, v3 + ноты отданы) | +| 2c | **Третья волна** по замечаниям Alex: чекбоксы, en-dash, подстроковый поиск стоп-слов | ✅ **ГОТОВО** — 3 бага, блок «требует решения» 296→28 | +| 3 | **Апрув Alex по нотам ревью** → только после этого правки | ⏳ **ЖДЁМ** (v1–v3 отклонены, v4 + ноты отданы) | | 4 | **Бэкап** — копировать **только перезаписываемые** файлы в `Music/backup/` | ⏳ | | 5 | **Ремонт** — перекодировка mojibake + восстановление из имён путей + чистка мусора | ⏳ | | 6 | **Приёмка** — Alex слушает выборку | ⏳ | -> 📌 **Что именно ждёт апрува:** 4774 `fix` из 7906. Alex проходит по трём нотам ревью, +> 📌 **Что именно ждёт апрува:** 4777 `fix` из 7906. Alex проходит по трём нотам ревью, > отмечает чекбоксы. Ничего не применяется до его «go». > -> 🔴 **Открытый вопрос к Alex:** расписание job `sync-vault` в Taiga — `0 * * * *` (раз в час), -> хотя доки говорят «каждые 5 минут». Менять на `*/5 * * * *` или оставить? +> 📌 **Открытый вопрос про расписание синка** — снят, см. раздел «SYNCTHING» ниже: job зовётся +> `docker exec hermes-taiga bash /opt/data/sync-vault-taiga.sh`, расхождение было в описании, +> а не в работе. Причина недоехавших нот оказалась в **правах файлов**, не в расписании. ### Ключевые решения по плану (правки Alex) @@ -538,7 +555,7 @@ if cyr < 2: # одна буква — совпадение | `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 (v1, устарел).** Содержал ранний выход по `probe_failed` — из-за него ДДТ отдавались пустыми | -| `build_fix_plan2.py` | **Этап 2 (v3, актуальный).** Пословный `fix_encoding`, `NON_ARTIST`-контейнеры, `is_release_folder`, `clean_folder`, `prettify_translit`, срезание ведущего номера. Функции: `_try_convert`, `has_release_in_path`, `fix_encoding`, `clean_value`, `clean_folder`, `repair`, `split_artist_title`, `title_from_name`, `derive_from_path`, `is_release_folder`, `prettify_translit` | +| `build_fix_plan2.py` | **Этап 2 (v4, актуальный).** Пословный `fix_encoding`, `NON_ARTIST`-контейнеры, `is_release_folder`, `clean_folder`, `prettify_translit`, срезание ведущего номера. **v4 добавил:** `find_separator()` (разделители `-`/`–`/`—`, включая приклеенный слева дефис) и `has_word()` (токенный поиск стоп-слов вместо подстрокового). Функции: `_try_convert`, `has_word`, `has_release_in_path`, `fix_encoding`, `clean_value`, `clean_folder`, `repair`, `find_separator`, `split_artist_title`, `title_from_name`, `derive_from_path`, `is_release_folder`, `prettify_translit` | | `audit_plan.py` | **ПОЛНЫЙ аудит плана по всем строкам** (не сэмплы). Классы: `damaged_output`, `mixed_script_artist`, `translit_kept`, `watermark`, `numeric_title`, `artist_equals_title`, `generic_artist`, `album_equals_artist`, `one_char_title`, `whitespace`, `no_visible_change` | | `final_gate.py` | **Гейт полноты перед выдачей.** Проверяет: совпадение числа строк с инвентарём, отсутствие заглушек (`probe_failed`/`investigation`), пустых title, U+FFFD, правок без `source`. Печатает PASS или точные строки | | `deepdive_audit.py` | Углублённый разбор крупнейших классов (`generic_artist`, `translit_kept`) — по папкам и сырым значениям | @@ -551,12 +568,13 @@ if cyr < 2: # одна буква — совпадение | `diagnose_probe_failures.py` | Анализ заголовков файлов, упавших в `probe_failed`: ID3-версия, `hdr_ok`, размеры | | `test_encoding_guard.py` | **32 регресс-теста (v1).** Защита от порчи корректного UTF-8 (`Björk`, `Saint-Saëns`, `Noël`) + `split_artist_title` | | `test_plan2.py` | **65 регресс-тестов (v3, актуальный).** Пословная конверсия смешанных строк, короткие токены (`7Á`→`7Б`), контейнеры не становятся артистом, release-папки, отказ сплита на event-именах | -| `render_grouped_doc.py` | **Генератор нот для Obsidian.** CSV → 3 блока по уверенности + обзор, группировка по папкам, чекбоксы, оглавление | +| `render_grouped_doc.py` | **Генератор нот для Obsidian.** CSV → 3 блока по уверенности + обзор, группировка по папкам, чекбоксы, оглавление. **v4:** чекбоксы без префикса `> ` (иначе Obsidian парсит их как цитату и они не кликаются); `ROOT = /storage/Music/` (пути в CSV относительные от `/storage`, не от `/mnt/...`); блок «требует решения» = `нет artist И нет album` | +| `test_separators.py` | **15 регресс-тестов (v4).** Разделители `–`/`—`/приклеенный дефис; отказ сплита на `Рок-н-ролл`, `БИ-2`; event-имена (`НАШЕСТВИЕ 2009 - …`) не расщепляются; `VA - Title` и `Сказка - Теремок` возвращаются целиком. Зовётся `python3 test_separators.py` в `tools/music-tags/` | | Артефакт | Где | Что | |---|---|---| | CSV-инвентарь v2 | `/mnt/RED_2TB/storage/tmp/tag-inventory2.csv` (NAS) → `~/Downloads/music-tags/tag-inventory.csv` (Mac) | 7906 строк, колонка `source`, `probe_failed`=0 | -| **CSV-план правок v3** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, ~2.2 МБ) | **Отдан Alex 2026-09-26 (финал).** 4774 `fix` / 3132 `no_change` / 0 `probe_failed` | +| **CSV-план правок v4** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, ~2.2 МБ) | **Отдан Alex 2026-09-26 (финал).** 4777 `fix` / 3129 `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 | | **Ноты ревью в vault** | `personal/plans/music-tags-review/` (4 файла) | Обзор + 3 блока по уверенности; **вход для апрува Alex**, синкнуто | @@ -614,15 +632,32 @@ if cyr < 2: # одна буква — совпадение - ⚠️ **Правка скриптов только локально → `scp` заново.** `sed -i` на NAS применён быть не должен (была такая ошибка, исправлена). - ⚠️ **Кириллица в аргументах `ssh`-команд** может поднять tirith `confusable_text`. Держать русские пути внутри файла-скрипта. - ⚠️ **Долгий инвентарь — запускать в фоне** (`docker run -d`) и поллить статус, а не держать `for`-цикл ожидания дольше 600 с (таймаут foreground). +- 🔴 **Файлы, созданные/обновлённые из контейнера `hermes-taiga` (там root), Syncthing НЕ читает.** Владелец становится `root`, а Syncthing работает под uid 950. Итог: `noPermissions: true`, `needFiles` висит вечно, на телефон ничего не уходит. **Фикс из контейнера** (у `truenas_admin` нет sudo без пароля): + ```bash + docker exec hermes-taiga chown -R 950:950 && docker exec hermes-taiga chmod -R 770 + ``` + Эталон рабочих файлов клона: `truenas_admin:truenas_admin`, `drwxrwx---` (770). Проверять после каждого синка новых файлов. +- 🔴 **`Pushed ✓` ≠ «доехало».** Git-коммит и читаемость файла для Syncthing — разные вещи. Приёмка: `needFiles: 0` + `errors: 0` через Syncthing API. +- ⚠️ **`sync-vault-taiga.sh` запускать через `bash`, не `sh`** — скрипт использует `set -o pipefail` (в `sh`/`dash` → `Illegal option -o pipefail`). Путь внутри контейнера: `/opt/data/sync-vault-taiga.sh`. +- 🔴 **Чекбоксы в Obsidian не должны иметь префикс `> `.** Строка `> - [ ]` внутри выноски парсится как **цитата**, галочки не кликаются. Выноску (`> [!info]`) оставлять, но чекбоксы и подпункты — без `> `. +- 🔴 **Стоп-слова проверять ТОЛЬКО по токенам, не подстрокой.** `any(w in text)` для `"va"` матчит `Ivan`, `"set"` матчит `Sunset`, `"mix"` матчит `Mixes`. Теряются реальные исполнители (`DJ Ivan Scratchin'`). Фикс — `has_word()` с токенизацией. +- ⚠️ **Разделитель artist/title бывает не только `" - "`.** Встречаются en-dash `–`, em-dash `—` и дефис, приклеенный слева (`Arno Cost & Norman Doray- Apocalypse`). Последний принимать только если слева больше одного слова — иначе `БИ-2`/`Рок-н-ролл` расщепятся. ### 🧠 ГЛАВНЫЙ ПРОЦЕССНЫЙ УРОК ЭТОЙ СЕССИИ Alex: **«Ты бы хоть постеснялся не проверив мне файл сувать!»** Alex (вторая волна): **«Сам проверил весь файл на адекватность? Чтобы я в очевидные вещи тебя не тыкал.»** +Alex (третья волна): **«Давай ка дорогой пройди по файлу и сам проставь нормально все, а только потом мне на апрув, я не собираюсь очевидные вещи руками лопать.»** -**Ключевое различие, которое стоило двух итераций: выборочная проверка ≠ полный аудит.** -После первой правки были «пройдены все проверки» — но это были 5 метрик на сэмплах. Полный аудит -по всем 4774 строкам нашёл ещё **6 багов**, крупнейший — 238 файлов с `artist = Сказки`. +**Два различия, которые в сумме стоили трёх итераций:** + +1. **Выборочная проверка ≠ полный аудит.** После первой правки были «пройдены все проверки» — но это + были 5 метрик на сэмплах. Полный аудит по всем строкам нашёл ещё **6 багов**, крупнейший — 238 файлов + с `artist = Сказки`. +2. **«Требует решения» — не оправдание для очевидного.** Отдавать Alex блок как «требует решения» можно + только когда решения **действительно нет**. 268 из 296 файлов решались автоматически + (сборники и сказки: album есть, artist и не нужен). Пользователь не должен вручную разгребать то, + что выводится кодом. Порядок работы, который себя оправдал: @@ -630,16 +665,21 @@ Alex (вторая волна): **«Сам проверил весь файл н 2. Прогнать **автотесты** на краевых случаях (латиница с умляутами, дефисы, номера, смешанные строки). 3. **Прогнать ПОЛНЫЙ аудит по всем строкам**, а не по сэмплам (`audit_plan.py`). 4. **Прогнать гейт полноты** (`final_gate.py`) — заглушки, пустые поля, правки без источника. -5. Посмотреть сэмплы «до/после» глазами по каждой проблемной группе. -6. Проверить **сырые данные конкретного файла**, прежде чем спорить «это баг / не баг». -7. Отделить **настоящие дефекты от законных случаев** (`separate_moji.py`, `review_classes.py`): +5. **Пройти по КАЖДОЙ строке плана глазами** — не по сэмплам. Именно так нашёлся склеенный title. +6. Свести блок «требует решения» к реально нерешаемому — всё выводимое проставить заранее. +7. Проверить **сырые данные конкретного файла**, прежде чем спорить «это баг / не баг». +8. Отделить **настоящие дефекты от законных случаев** (`separate_moji.py`, `review_classes.py`): `NTL`/`Eagles` — латиница, `Би-2/2001 Би-2` — одноимённый альбом. -8. **Только потом** отдавать пользователю. +9. **Только потом** отдавать пользователю. **Заглушка `needs investigation` в отчёте — это не результат, а недоработка.** **«Требует решения» — законный исход, но должен быть явным, а не замаскированным мусором.** +**И он должен быть МИНИМАЛЬНЫМ: если значение выводится — выводить самому.** -### 🐞 Девять багов, пойманных ДО выдачи (файлы не пострадали) +**Правило Alex, зафиксировано:** если исполнитель неочевиден — **пусто**, не выдумывать +(пример: `detskiy_hor_benassi_satisfaction.mp3` — Alex сам оставил artist пустым). + +### 🐞 Двенадцать багов, пойманных ДО выдачи (файлы не пострадали) **Первая волна (3 бага, сэмплами):** @@ -653,8 +693,13 @@ Alex (вторая волна): **«Сам проверил весь файл н `artist = Сказки` (238), mojibake рядом с латиницей (127), имя радиовыпуска как артист (31), номер склеен с названием (17), `album = '+'` (10), `©` как title (1). -> 🔑 **Методический вывод:** сэмплы ловят грубые ошибки, полный аудит — системные. -> Оба нужны. Без полного аудита CSV v2 ушёл бы Alex с 238 файлами «исполнитель Сказки». +**Третья волна (3 бага, по замечаниям Alex):** см. «ПЛАН ТРЕТЬЕЙ ВОЛНЫ» в начале — +чекбоксы с префиксом `> ` (не кликались), en-dash не распознавался как разделитель, +подстроковый поиск стоп-слов терял `DJ Ivan Scratchin'`. + +> 🔑 **Методический вывод:** сэмплы ловят грубые ошибки, полный аудит — системные, +> а чтение глазами каждой строки — «очевидные», которые видны пользователю с первого взгляда. +> Все три уровня нужны. ## Связанные заметки