diff --git a/personal/plans/tagwriter-container.md b/personal/plans/tagwriter-container.md index 89aef4fe..0a56634d 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: blocked +status: ready-to-apply tags: - music - truenas @@ -17,7 +17,7 @@ updated: '2026-09-26' --- # tagwriter — контейнер применения тегов MP3 (TrueNAS) -_Создан: 2026-09-26. **Статус: 🔴 БЛОКЕР — нет прав на `/mnt/RED_2TB/docker/`.**_ +_Создан: 2026-09-26. **Статус: ✅ контейнер собран, dry-run прошёл (0 MISSING) → ждёт бэкап + apply.**_ _Основание: [[personal/plans/music-tags-wave38-fixes]]._ ## Задача @@ -26,17 +26,19 @@ _Основание: [[personal/plans/music-tags-wave38-fixes]]._ в **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/` создать нельзя**. +`/mnt/RED_2TB/docker/` принадлежит **root**, `truenas_admin` (uid 950) туда **не пишет**, `sudo` требует пароль. +`docker` доступен (юзер в группе `docker`). -**Решения (ждёт Alex):** -1. Alex создаёт `/mnt/RED_2TB/docker/tagwriter/` + `data/` → заливаем и собираем как задумано. -2. Собираем в `/tmp/tagwriter` (там писать можно), папку в `docker/` переносит Alex. -3. Другой путь, доступный на запись uid 950. +**Решение (выполнено):** каталог создан из одноразового root-контейнера с монтированием родителя: + +```bash +docker run --rm -u 0 -v /mnt/RED_2TB/docker:/docker alpine:latest sh -c \ + 'mkdir -p /docker/tagwriter/data && chown -R 950:950 /docker/tagwriter && chmod 755 /docker/tagwriter' +``` + +**Правило:** любые каталоги в `/mnt/RED_2TB/docker/` создавать ИЗ КОНТЕЙНЕРА (`-u 0`), не через ssh под uid 950. ## Разведка окружения (факты, 2026-09-26) @@ -86,12 +88,55 @@ _Основание: [[personal/plans/music-tags-wave38-fixes]]._ ## Порядок работ -1. ⏳ **Сборка образа** — блокер по папке (см. выше). -2. ⏳ **Бэкап** — `--backup` (7905 файлов → `/music/backup/`; 48 ГБ при 960 ГБ свободных). -3. ⏳ **Dry-run** — `--dry-run`, 0 записей. **Приёмка Alex.** +1. ✅ **Сборка образа** — `docker compose --profile tools build` (выполнено). +2. ⏳ **Бэкап** — `--backup` (7041 файлов → `/music/backup/`; 48 ГБ при 960 ГБ свободных). **Следующий шаг.** +3. ✅ **Dry-run** — `--dry-run`, 0 записей, **MISSING 0** (выполнено, ждёт приёмки Alex). 4. ⏳ **Apply** — `--apply` (только по «go»). 5. ⏳ **Read-back** — `--readback` + глазами в Jellyfin. +## ✅ СОСТОЯНИЕ НА 2026-09-26 (факты) + +| Шаг | Статус | +|---|---| +| Каталог `/mnt/RED_2TB/docker/tagwriter/` + `data/` | ✅ создан (владелец 950), **из контейнера** (`docker run -u 0 -v /mnt/RED_2TB/docker:/docker alpine mkdir`) — `truenas_admin` не может писать в `docker/` напрямую, `sudo` требует пароль | +| Проект залит (`Dockerfile`, `docker-compose.yml`, `apply_tags.py`, `data/tag-fix-plan.csv`) | ✅ | +| Образ `tagwriter:latest` (`docker compose --profile tools build`) | ✅ собран (python:3.12-alpine + mutagen 1.47.0) | +| **DRY-RUN** | ✅ **не найдено: 0**, к обработке **7041** (файлов на диске 8483) | +| Бэкап | ⏳ не делался (48 ГБ, свободно 960 ГБ) | +| Применение (`--apply`) | ⏳ ждёт «go» | + +### 🔴 ПИТФОЛЛ 1: права файлов при COPY в образ + +Файлы заливались `scp` как `-rw-------` (600) → `COPY` сохранил их root:root 600 +→ uid 950 не читал `/app/apply_tags.py` (`Permission denied`). +**Фикс:** `RUN chmod 755 /app/apply_tags.py && chmod 755 /app` в Dockerfile + `chmod 644` на файлах проекта. +Проверка: `docker run --rm --entrypoint sh -u 950:950 tagwriter:latest -c 'ls -la /app'`. + +### 🔴🔴 ПИТФОЛЛ 2 (КЛЮЧЕВОЙ): NFD на диске vs NFC в CSV + +Первый dry-run дал **47 MISSING**. Причина: имена на диске в **NFD** +(разложенная форма: `и` + combining breve `U+0306`), в CSV — **NFC**. + +Пораженные папки: `Али-Баба и сорок разбойников` (+`_2`), `Принц и нищий` +(+`(Диск 2)`), `Пеппи Длинный Чулок`, `Летучий корабль`, `Голубой щенок`, `Юрий Никулин`. + +**Фикс:** индекс диска `{NFC(путь): реальный_путь}` через `os.walk`; +поиск идёт по `NFC(CSV-путь)`. **MISSING 47 → 0**. +**Правило: NFC-нормализовать ОБЕ стороны, не только CSV.** + +## Команды + +```bash +cd /mnt/RED_2TB/docker/tagwriter +docker compose --profile tools build # сборка +docker compose --profile tools run --rm tagwriter --dry-run # план, 0 записей +docker compose --profile tools run --rm tagwriter --backup # бэкап 48 ГБ в /music/backup/ +docker compose --profile tools run --rm tagwriter --apply # запись тегов +docker compose --profile tools run --rm tagwriter --readback # проверка +``` + +--- + ## Питфоллы - 🔴 `/mnt/RED_2TB/docker/` — **root-owned**, uid 950 не пишет (это и есть блокер). diff --git a/personal/plans/tagwriter-container.md.bak2 b/personal/plans/tagwriter-container.md.bak2 new file mode 100644 index 00000000..89aef4fe --- /dev/null +++ b/personal/plans/tagwriter-container.md.bak2 @@ -0,0 +1,109 @@ +--- +created: '2026-09-26' +namespace: personal +related: + - '[[personal/plans/music-tags-wave38-fixes]]' + - '[[personal/tech/music-tags-repair-truenas]]' +status: blocked +tags: + - music + - truenas + - docker + - id3 + - plan +title: tagwriter — контейнер применения тегов MP3 +type: plan +updated: '2026-09-26' +--- +# tagwriter — контейнер применения тегов MP3 (TrueNAS) + +_Создан: 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: «Собирай 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/gid **950** | +| Свободно на `/mnt/RED_2TB` | **960 ГБ** (бэкап 48 ГБ влезет) | +| python3 на хосте NAS | 3.11.9, **нет pip, нет ensurepip, нет venv** | +| mutagen | **нигде** (ни на хосте, ни в контейнерах) | +| 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 | + +**Вывод:** пишем теги **своим контейнером**. `pip` есть внутри образа → mutagen ставится при сборке. + +## Решение: контейнер `tagwriter` + +- **Образ:** `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, не поднимается автоматически + +## Файлы проекта (созданы локально) + +| Файл | Назначение | +|---|---| +| `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. ⏳ **Сборка образа** — блокер по папке (см. выше). +2. ⏳ **Бэкап** — `--backup` (7905 файлов → `/music/backup/`; 48 ГБ при 960 ГБ свободных). +3. ⏳ **Dry-run** — `--dry-run`, 0 записей. **Приёмка Alex.** +4. ⏳ **Apply** — `--apply` (только по «go»). +5. ⏳ **Read-back** — `--readback` + глазами в Jellyfin. + +## Питфоллы + +- 🔴 `/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.