diff --git a/personal/plans/tagwriter-container.md b/personal/plans/tagwriter-container.md index 7faab143..89aef4fe 100644 --- 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: plan +status: blocked tags: - music - truenas @@ -17,62 +17,93 @@ updated: '2026-09-26' --- # tagwriter — контейнер применения тегов MP3 (TrueNAS) -_Создан: 2026-09-26. **Статус: ПЛАН → СБОРКА.** Основание: [[personal/plans/music-tags-wave38-fixes]]._ +_Создан: 2026-09-26. **Статус: 🔴 БЛОКЕР — нет прав на `/mnt/RED_2TB/docker/`.**_ +_Основание: [[personal/plans/music-tags-wave38-fixes]]._ ## Задача Применить `tag-fix-plan.csv` (7905 строк) к MP3 на `/storage/Music`, **без пересжатия аудио**, -в **UTF-8/UTF-16**. Alex: «в контейнер». Чужие контейнеры (transmission/immich/jellyfin) **НЕ трогаем**. +в **UTF-8/UTF-16**. Alex: «Собирай tagwriter. С docker compose, in docker folder.» +Чужие контейнеры (`transmission`/`immich`/`jellyfin`) — **НЕ трогать** (прямой запрет Alex). + +## 🔴 БЛОКЕР + +`/mnt/RED_2TB/docker/` принадлежит **root** (`drwxr-xr-x root root`). +`truenas_admin` (uid 950) **не может писать** туда, `sudo` требует пароль (`sudo -n` → password required). +При этом `docker` доступен (юзер в группе `docker`, uid 999) → образ собрать можно, +но **проектную папку в `docker/` создать нельзя**. + +**Решения (ждёт Alex):** +1. Alex создаёт `/mnt/RED_2TB/docker/tagwriter/` + `data/` → заливаем и собираем как задумано. +2. Собираем в `/tmp/tagwriter` (там писать можно), папку в `docker/` переносит Alex. +3. Другой путь, доступный на запись uid 950. ## Разведка окружения (факты, 2026-09-26) | Что | Факт | |---|---| -| `/storage/Music` | 48 ГБ, владелец `truenas_admin` (uid 950) | -| Свободно на `/mnt/RED_2TB` | 960 ГБ (бэкап влезет) | +| `/storage/Music` | 48 ГБ, владелец `truenas_admin` uid/gid **950** | +| Свободно на `/mnt/RED_2TB` | **960 ГБ** (бэкап 48 ГБ влезет) | | python3 на хосте NAS | 3.11.9, **нет pip, нет ensurepip, нет venv** | | mutagen | **нигде** (ни на хосте, ни в контейнерах) | -| ffmpeg | только `immich-server` (музыки не видит) | +| ffmpeg | только `immich-server` (**музыки не видит**) | | `/storage/Music` видят | `jellyfin`, `radarr`, `sonarr`, `transmission` | +| python3 в чужих контейнерах | только `transmission` (3.14.7) — **чужой, не трогаем** | | Docker Hub | доступен (`python:3.12-alpine` скачался) | | docker compose | v2.29.1 | | `media_net` | есть | +| ACL Music | `owner@ grup@` ALLOW, NOINHERIT | -**Вывод:** пишем теги своим контейнером. mutagen ставится в образ при сборке (pip внутри образа есть). +**Вывод:** пишем теги **своим контейнером**. `pip` есть внутри образа → mutagen ставится при сборке. ## Решение: контейнер `tagwriter` -- **Путь проекта:** `/mnt/RED_2TB/docker/tagwriter/` -- **Образ:** `python:3.12-alpine` + `pip install mutagen` -- **Пользователь:** uid/gid `950:950` (`user: "950:950"`) — владелец Music -- **Монтирование:** `/mnt/RED_2TB/storage/Music` → `/music` (rw) -- **Теги:** ID3v2.3, кодировка `encoding=1` (UTF-16), аудио НЕ пересжимается (mutagen правит только тег-блок) +- **Образ:** `python:3.12-alpine` + `pip install mutagen==1.47.0` +- **Пользователь:** `user: "950:950"` (владелец Music; иначе запись невозможна) +- **Монтирование:** `/mnt/RED_2TB/storage/Music` → `/music` (rw), `/mnt/RED_2TB/docker/tagwriter/data` → `/app/data` +- **Теги:** ID3v2.3, `encoding=1` (**UTF-16**), `v2_version=3` — только тег-блок, аудио не пересжимается +- **Профиль** `tools` — контейнер one-shot, не поднимается автоматически -## Почему не чужой контейнер - -`python3` есть только в `transmission`, но он чужой сервис — Alex запретил. Свой контейнер -изолирован: не влияет на торренты/медиасервисы, снимается без следа. - -## Скрипты +## Файлы проекта (созданы локально) | Файл | Назначение | |---|---| -| `apply_tags_truenas.py` | `--backup` → копия всех файлов к изменению в `/music/backup/`; `--dry-run` → печать плана; `--apply` → запись тегов | -| `docker-compose.yml` | проект контейнера | -| `Dockerfile` | `python:3.12-alpine` + mutagen | -| `build_and_run.sh` | сборка + запуск | +| `metrics/apply_tags.py` (в проекте — `apply_tags.py`) | `--backup` / `--dry-run` / `--apply` / `--readback` / `--limit N` | +| `docker-compose.yml` | сервис `tagwriter`, профиль `tools`, монтирование Music | +| `Dockerfile` | `python:3.12-alpine` + mutagen, `USER 950:950` | + +Локальные исходники: `~/Downloads/music-tags/tagwriter/`. + +## Скрипт `apply_tags.py` — логика + +- Читает `/app/data/tag-fix-plan.csv` (кладут в `data/`). +- **Пропускает `skip` и `no_change`** — правит только `fix`. +- **NFC-нормализация** путей (`unicodedata.normalize("NFC")`) — имена на диске могут быть в NFD. +- **Маппинг путей**: `/storage/Music/` → `/music/`. +- `--backup` идемпотентен (существующий файл бэкапа не перезаписывает). +- `--apply` **отказывается работать без бэкапа** (`ABORT: нет бэкапа`). +- Фреймы: `TIT2`/`TPE1`/`TALB`/`TRCK`, перед записью `delall` по каждому FrameID. ## Порядок работ -1. ✅ **Бэкап** — `--backup` копирует 7905 файлов (48 ГБ → 960 ГБ свободно) в `/music/backup/`. -2. ✅ **Dry-run** — печать плана, 0 записей. Приёмка Alex. -3. ✅ **Apply** — запись тегов (по «go»). -4. ✅ **Приёмка** — выборочная проверка тегов (`mutagen` read-back) + глазами в Jellyfin/webdav. +1. ⏳ **Сборка образа** — блокер по папке (см. выше). +2. ⏳ **Бэкап** — `--backup` (7905 файлов → `/music/backup/`; 48 ГБ при 960 ГБ свободных). +3. ⏳ **Dry-run** — `--dry-run`, 0 записей. **Приёмка Alex.** +4. ⏳ **Apply** — `--apply` (только по «go»). +5. ⏳ **Read-back** — `--readback` + глазами в Jellyfin. -## Питфоллы (предвиденные) +## Питфоллы -- 🔴 Имена файлов на диске **НЕ в UTF-8** (mojibake в `ls`) → работать только python-скриптом (`os.listdir`), NFC-нормализация. -- 🔴 Пути в CSV контейнерные (`/storage/Music/...`) → мапить в `/music/...`. -- 🔴 mutagen сохраняет тег-блок; **аудио-поток не трогается** — пересжатия нет by design. -- ⚠️ uid: контейнер под 950, иначе не сможет писать в Music. -- ⚠️ Бэкап 48 ГБ — проверить `df` до и после. +- 🔴 `/mnt/RED_2TB/docker/` — **root-owned**, uid 950 не пишет (это и есть блокер). +- 🔴 `sudo` без пароля **недоступен**. +- 🔴 Имена файлов на диске **не в UTF-8** (mojibake в `ls`) → только python-скрипт, NFC. +- 🔴 Пути в CSV контейнерные (`/storage/Music/…`) → мапить в `/music/…`. +- ⚠️ `ffmpeg` для записи тегов **не нужен** — mutagen работает с тег-блоком напрямую. +- ⚠️ uid контейнера обязан быть `950`, иначе запись в Music упадёт. +- ⚠️ Команды на NAS требуют апрува — таймаут апрува = команда не выполнена (не повторять вслепую). + +## Связано + +- [[personal/plans/music-tags-wave38-fixes]] — 4 правки CSV, выполнены. +- [[personal/tech/music-tags-repair-truenas]] — техдок ремонта 7905 тегов. +- [[personal/tech/music-tags-rename-audit]] — финальный аудит CSV. diff --git a/personal/tech/music-tags-final-review.md b/personal/tech/music-tags-final-review.md index b7f79f7f..91ff39d1 100755 --- a/personal/tech/music-tags-final-review.md +++ b/personal/tech/music-tags-final-review.md @@ -47,6 +47,16 @@ _Обновлено: 2026-09-26 (волна 38). Источник: `~/Downloads/ > Скрипты `fix_wave38_p2p3.py`, `fix_wave38_p4.py` идемпотентны (повтор = 0); > `rebuild_summaries.py` идемпотентен (shasum совпал). > ⚠️ Открыто (решение Alex, не трогаем): 136 пар дублей `(album, track)` — папки не удалять/не переименовывать. +> +> ⏭ **СЛЕДУЮЩИЙ ШАГ — применение тегов:** контейнер `tagwriter` +> ([[personal/plans/tagwriter-container]]): `python:3.12-alpine` + mutagen, uid `950:950`, +> монтирует `/mnt/RED_2TB/storage/Music` → `/music`. Скрипт `apply_tags.py` +> (`--backup` → `--dry-run` → `--apply` → `--readback`), ID3v2.3 UTF-16, аудио не пересжимается. +> 🔴 **БЛОКЕР:** `/mnt/RED_2TB/docker/` принадлежит **root**, uid 950 туда не пишет, +> `sudo` требует пароль. Ждёт решения Alex (создать папку / собирать в `/tmp`). +> Факты окружения: python3 на хосте NAS есть (3.11.9), но **нет pip/ensurepip/venv**; +> mutagen нет нигде; ffmpeg только в `immich-server` (музыки не видит); `/storage/Music` видят +> `jellyfin`/`radarr`/`sonarr`/`transmission` — чужое не трогаем. Music = 48 ГБ, свободно 960 ГБ. > ## 🔍 38-я волна — ФИНАЛЬНЫЙ АУДИТ CSV («список на переименование») > diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index b40974ec..3f5fadf7 100755 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -1,5 +1,5 @@ --- -title: Music Tags Repair — TrueNAS 7906 файлов +title: Music Tags Repair — TrueNAS 7906 → 7905 файлов created: '2026-09-26' updated: '2026-09-26' type: tech @@ -17,6 +17,8 @@ related: - '[[family/how-to/jellyfin-config]]' - '[[family/projects/media-toolbox-taiga]]' - '[[personal/tech/music-tags-rename-audit]]' + - '[[personal/plans/music-tags-wave38-fixes]]' + - '[[personal/plans/tagwriter-container]]' --- # Music Tags Repair — TrueNAS (7906 → 7905 файлов) @@ -41,6 +43,41 @@ related: > | P3. Техпапка в `new_album` | 1 | `Король и Шут/Танец гения`: `New` → пусто | > | P4. `new_title='Часть N'` | 25 | `Новые Бременские` 19 → `Серия N`; `Малыш и Карлсон` 6 → название сказки | > +> ### ✅ ДОП. ПРАВКИ ТОЙ ЖЕ ВОЛНЫ +> +> | Класс | Объём | Фикс | +> |---|---:|---| +> | Строка `НАШЕСТВИЕ 2009` (нет файла на диске) | 1 | ✅ удалена (`fix_wave38_nashestvie.py`); **база 7906 → 7905** | +> | `часть N` в `new_album` | 77 | ✅ убрано (`fix_wave38_chast.py`): `Избранное I/II`, `Сделано в России` (Ария 1–20), `Триллер`, Окуджава, Магомаев | +> +> ⚠️ **Побочная находка:** `ДДТ/Гори, гори.mp3` имел `cur_album='Сделано в России'` — **чужой тег +> от граббера**, попадал в альбом Арии. Урок: `cur_album` при группировке проверять по папке файла. +> +> ### 🔴 `НАШЕСТВИЕ 2009` — «открытый вопрос», которого не было +> +> Ассистент вынес строку как «ждёт решения Alex». Alex: **«Нашествие блядь удалили!!! Тебе в 10й +> над повторить надо?!»** Файла нет на диске (волна 35, `os.walk`), действие уже стояло +> `dropped (file removed by user)` — Alex сказал удалить **ещё в 10-й волне**. +> **Урок: перед любым «ждёт решения» — `grep` по техдоку. Повторный симптом = открыть ФАЙЛ.** +> +> ### 📊 ИТОГ 38-Й ВОЛНЫ (финальные числа) +> +> | Показатель | Значение | +> |---|---:| +> | `tag-fix-plan.csv` | **7905** строк (было 7906), 11 колонок, **0 битых** | +> | `artists-single-track.csv` | **653** | +> | `artists-multi-track.csv` | **7224** | +> | Сумма со skip | **653 + 7224 + 28 = 7905** ✅ | +> | Строк не в сводках | **0** | +> | `часть` в album / `Часть N` в title / `New` в album | **0 / 0 / 0** | +> +> Скрипты (все идемпотентны): `fix_wave38_p2p3.py`, `fix_wave38_p4.py`, `fix_wave38_nashestvie.py`, +> `fix_wave38_chast.py`, `fix_wave38_aria.py`, `fix_wave38_gorigori.py` + правка `rebuild_summaries.py`. +> Бэкапы: `bak-wave38-p2p3`, `-p4`, `-nashestvie`, `-chast`, `-chast2`, `-gorigori`. +> +> ⏭ **Дальше:** применение тегов через контейнер `tagwriter` — [[personal/plans/tagwriter-container]]. +> Блокер: `/mnt/RED_2TB/docker/` root-owned, uid 950 не пишет, sudo требует пароль. +> > ### 🔴 ПИТФОЛЛ: sentinel-группа собирается по КОРНЮ сборника, НЕ по ближайшей папке > > Первая версия правки группировала пустой `artist` по `parts[-2]` →