diff --git a/family/how-to/navidrome-truenas-android.md b/family/how-to/navidrome-truenas-android.md index f62d37cc..e8542925 100755 --- a/family/how-to/navidrome-truenas-android.md +++ b/family/how-to/navidrome-truenas-android.md @@ -106,8 +106,16 @@ artist=Maria Makarova (organ) ← исполнитель вмес ## Следующие шаги - [x] ~~Сначала: ремонт тегов 7906 файлов~~ → ✅ выполнен (волна 38) +- [ ] 🔴 **АКТИВНО (2026-09-27): ремонт `artist` + `albumartist` по 7861 файлу** — + правила Alex зафиксированы, идёт ручная разбивка по группам. + План — [[personal/plans/music-artist-albumartist-repair]] + - `albumartist`: сборник → `Various Artists`, детские → `Детские песни`, сказки → `Сказки` + - `artist`: из скобок в имени файла, иначе искать в интернете + - мойибаке **492 файла** — расшифровывать CP1251→UTF-8, не маскировать - [ ] **Аудит семантики `artist`** — убрать «Классика в современной обработке» и подобное -- [ ] **Опционально:** добавить `TPE2` = `TPE1` (нужен НОВЫЙ план правок с колонкой `album_artist`) + (поглощён задачей выше) +- [ ] Собрать `TPE2` по плану выше (нужен **НОВЫЙ** план правок с колонкой `album_artist` — + в `tag-fix-plan.csv` её не было) - [ ] Поднять Navidrome в Docker на TrueNAS - [ ] Пробросить порт / настроить Caddy subdomain (Caddyfile root-owned → через alpine-контейнер) - [ ] Установить Symfonium на Android, подключить diff --git a/family/plans/music-jellyfin-lidarr-taiga.md b/family/plans/music-jellyfin-lidarr-taiga.md index 6e1e0c1c..a05380a7 100755 --- a/family/plans/music-jellyfin-lidarr-taiga.md +++ b/family/plans/music-jellyfin-lidarr-taiga.md @@ -1,4 +1,101 @@ -# Music → Jellyfin + Lidarr на Тайге — план (v7, 2026-09-27) +# Music → Jellyfin + Lidarr на Тайге — план (v8, 2026-09-27) + +> ## 🆕 v8: НОВАЯ ЗАДАЧА — ПОЛНЫЙ РЕМОНТ `artist` + `albumartist` (7861 файл) +> +> ✅ **Полный сбор тегов выполнен:** `raw_tags_full.tsv` — **7861 строка** (все mp3, бэкап-папки исключены). +> ✅ **`tag-inventory3.csv`** — 7861 строка, **+колонка `albumartist`** (13 колонок). +> 🔴 **Мойибаке по всей библиотеке: 492 файла** (было известно только 155/156) — см. §МОЙИБАКЕ. +> 🔴 **Правила Alex для `albumartist` зафиксированы** — см. таблицу ниже. +> ⏳ **Следующий шаг: РУЧНАЯ разбивка по группам (НЕ скриптом), списки → проверка → потом скрипт.** +> +> ### 🔴 ПРАВИЛА ALEX (v8) — `artist` / `albumartist` / `album` (дословно) +> +> | Поле | Правило | +> |---|---| +> | `artist` | **есть в скобках в имени файла → брать из скобок.** Нет в скобках и неизвестен → **искать в интернете** | +> | `artist` | **НИКОГДА не маскировать мойибаке** — расшифровывать (CP1251→UTF-8), не оставлять как есть | +> | `albumartist` | **сборник → `Various Artists`** | +> | `albumartist` | **детские песни → `Детские песни`** | +> | `albumartist` | **сказки → `Сказки`** | +> | `albumartist` | не сборник/не детские/не сказки → **= `artist`** | +> | `album` | **сборники и unsorted → ИМЯ ПАПКИ** (пример Alex: `y OOF`) | +> | `album` | сказки → **оставить как есть** (там альбомы уже стоят) | +> +> ### 🔴 КРИТЕРИЙ СБОРНИКА (уточнён Alex в v8) +> +> Alex: «**Сборник это папка не одного артиста**», затем «**Не более чем одним [артистом]**», +> затем прямо: «**radio и retro — там сборники. Как и просто треки просто в корне артиста +> если там есть альбомы — считать сборником типа НТЛ**». +> +> | Признак | ВЕРДИКТ | +> |---|---| +> | Папка `!Radio/*` | ✅ **СБОРНИК** (даже подпапки вида `x 2008-04`) | +> | Папка `!Retro` | ✅ **СБОРНИК** | +> | Треки в корне папки артиста, при наличии у него подпапок-альбомов | ✅ **СБОРНИК** (кейс НТЛ: 63 файла в `!Рэп/НТЛ` корне + 5 альбомов внутри) | +> | `!A Not sorted/Sbornik_Novij_Rok` (184 арт.) | ✅ **СБОРНИК** | +> | Папка с 1 артистом | ❌ **АЛЬБОМ** (albumartist = artist) | +> | `!Pop, Club, Любэ/Benassi Bros` (3 арт. из-за фитов) | ⚠️ **АЛЬБОМ** — не сборник, это альбом с фитами | +> +> 🔴 **Alex ЗАПРЕТИЛ разметку скриптом:** «**Ручная разбивка по группам. Не скриптом блядь. +> Сначала готовишь списки, проверяешь, а потом только скриптом можешь обновлять csv.**» +> Порядок: **списки вручную → проверка Alex → скрипт обновляет CSV** → правка файлов. +> +> ### 📊 МОЙИБАКЕ ПО ВСЕЙ БИБЛИОТЕКЕ (факт, 2026-09-27) — 492 файла +> +> | Поле | Файлов с мойибаке | +> |---|---:| +> | `album_artist` (TPE2) | **269** | +> | `artist` (TPE1) | **160** | +> | `album` (TALB) | **63** | +> | `title` (TIT2) | **1** | +> | **строк с мойибаке хоть в одном поле** | **492** | +> +> **Топ папок:** `!Детские/Детский хит` **156**, `!Rock&Folk/Муммий Тролль` 54, +> `!Pop, Club, Любэ/!Radio` 44, `!Rock&Folk/Пилот` 31, `!Rock&Folk/ЗАХАР МАЙ` 27, +> `!Rock&Folk/Несчастный Случай` 23, `Король и Шут` 17, `Красная Плесень` 16, `Ария, Кипелов` 16, +> `Ундервуд` 16, `Трофим` 14, `Запрещенные Барабанщики` 13, `Агата Кристи` 11, `Земфира` 9. +> +> **Мойибаке — ИСХОДНОЕ состояние файлов, НЕ следствие волны 38.** Доказано сравнением +> `tag-inventory.csv` (v1) vs `tag-inventory2.csv`: значения тегов идентичны, строки отличаются +> только добавленной колонкой `source`. Файлы прошли волну «не изменёнными». +> +> ### 🔴 ПОЧЕМУ ВОЛНА 38 ИХ НЕ ПОЧИНИЛА — корень в `apply_tags.py` +> +> 1. **Генератор плана сдался** на CP1251-байтах → вместо обратного декодирования поставил +> `artist<-cleared-junk` (пусто). **155 файлов** с этим `source`. +> 2. **`delall` стоял ВНУТРИ `if` с проверкой непустого `new_artist`:** +> ```python +> if (r.get("new_artist") or "").strip(): # new_artist пуст → False +> out.append(TPE1(...)) # не выполнено +> for frame in _frames(r): +> tags.delall(frame.FrameID) # ← ТОЖЕ не выполнено +> tags.add(frame) +> ``` +> Пустой план → цикл не сделал ничего → **старый мойибаке уцелел**. +> 3. **`_frames()` вообще не знает про `TPE2`** (`TIT2, TPE1, TALB, TRCK` — и всё) → +> **269 файлов с мойибаке в albumartist скрипт не мог тронуть в принципе.** +> +> **Правило:** `delall` — **всегда**, `add` — только при непустом значении. Иначе «очистить» +> не очищает. **«N/N применено, 0 ошибок» ≠ приёмка** — критерий «не упало», а не «тег стал верным». +> +> ### ✅ ИТОГ v8 (для следующей сессии) +> +> | Артефакт | Номер/статус | +> |---|---| +> | `raw_tags_full.tsv` | **7861 строк**, md5 `0c3647fadb6556b4100c555ed59bfb44` | +> | `tag-inventory3.csv` | **7861 строк**, 13 колонок, +`albumartist` | +> | Сборников по имени папки | **11 папок / 382 файла** (ранее недооценено — `!Radio`/`!Retro` не по имени) | +> | `album_artist` пуст | **6451 из 7861 (82%)** | +> | Мойибаке всего | **492** | +> | Списки для ручной разметки | `list2-folders.txt` (633 папки), `list3-radio.md` (!Radio, 31 папка) | +> +> ⚠️ **Питфолл сбора тегов:** скрипт писал в `/mnt/RED_2TB/storage/tmp/` (**владелец root**), +> а пользователь `truenas_admin` (uid 950) → `permission denied`. **Решение: `docker cp` +> в `/home/truenas_admin/`.** Данные внутри контейнера при этом целы (`/tmp/raw_out.tsv`). +> Ещё питфолл: в скрипте было **два разных имени выходного файла** (`raw_tags_full.tsv` в `OUT`, +> `raw_out.tsv` в `printf`) — сверять имена при написании скрипта. + + > ✅ **СТАТУС v7: JELLYFIN ОТКАЧЕН И СТАБИЛЕН. КУРС СМЕНЁН НА NAVIDROME.** > БД Jellyfin восстановлена на версию до импорта музыки: 6 библиотек, Audio=0, MusicArtist=0. @@ -75,11 +172,13 @@ Lidarr матчит по **MusicBrainz release groups** и пишет полны | # | Задача | Статус | Детали | |---|---|---|---| +| 0 | **Ремонт `artist`+`albumartist` (7861)** | 🔴 **АКТИВНАЯ (v8)** | правила Alex зафиксированы; **ручная разбивка по группам идёт**; список 1 (9 корневых), список 2 (633 папки), список 3 (`!Radio`, 31 папка) готовы | +| 0a | Моибаке 492 файла | 🔴 **входит в задачу 0** | расшифровка CP1251→UTF-8; `ДЕТСКИЙ ХИТ` — образец работающего декодирования | | 1 | Дополнить `Music/backup` | 🟡 **скан готов, ждёт решения** | не хватало **1436 файлов** (876 mp3, 287 jpg, 6 mpg, 5 avi, 18 `.DS_Store`). Вопрос Alex по видео/мусору — ответа нет | -| 2 | Бэкап CSV на Mac | ✅ **выполнено** | `/Users/admin/milestones/music-csv-2026-09-26/` — 3 CSV + MD5SUMS + README | -| 3 | Аудит семантики `artist` | 🔴 не начат | `artist=Классика в современной обработке`, `Maria Makarova (organ)` и т.п. — формально валидны, семантически мусор | +| 2 | Бэкап CSV на Mac | ✅ **выполнено** | `/Users/admin/milestones/music-csv-2026-09-26/` — 3 CSV + MD5SUMS + README (+v8: `tag-inventory3.csv`, `raw_tags_full.tsv`) | +| 3 | Аудит семантики `artist` | 🔴 не начат | поглощён задачей 0 — `!Классика`: `Maria Makarova (organ)`, `Классика в современной обработке` | | 4 | Деплой Navidrome | 🔴 не начат | план — [[family/how-to/navidrome-truenas-android]] | -| 5 | Проставить `TPE2` | ⏸ отложено | нужно **только** если в Navidrome раздробятся сборники; для Jellyfin бессмысленно (баг) | +| 5 | Проставить `TPE2` | 🔴 **поглощено задачей 0** | уже не «отложено»: `albumartist` — центральная часть задачи v8 | | 6 | Lidarr | 🔴 не начат | задача из исходной команды, отложена до библиотеки | | 7 | Починить авто-бэкап Jellyfin | 🔴 не начат | `SQLiteBackups` пуста с 3 июня | diff --git a/personal/plans/music-artist-albumartist-repair.md b/personal/plans/music-artist-albumartist-repair.md new file mode 100644 index 00000000..bfcbe586 --- /dev/null +++ b/personal/plans/music-artist-albumartist-repair.md @@ -0,0 +1,286 @@ +--- +title: Music Artist + AlbumArtist Repair — правила и ход работ +created: '2026-09-27' +updated: '2026-09-27' +type: plan +namespace: personal +tags: + - music + - metadata + - id3 + - albumartist + - repair + - truenas +related: + - '[[family/plans/music-jellyfin-lidarr-taiga]]' + - '[[personal/tech/music-tags-repair-truenas]]' + - '[[personal/tech/jellyfin-tags-cp1251-unreadable]]' + - '[[family/how-to/navidrome-truenas-android]]' +--- + +# Music `artist` + `albumartist` Repair — правила Alex и ход работ + +**Задача Alex (2026-09-27):** +> «Артиста всем трём где он не указан или указан неправильно, включая детские песни ставишь +> в реального артиста. Что не знаешь — ищешь в интернете. Для _всех_ треков. +> Albumartist — смотришь по папке, для сборников various, для детских — Детские песни. +> Для сказок — сказки. Альбом для сборников и unsorted ставишь по папке. Типа y OOF для сборников. +> Для сказок альбомы и так стоят.» + +**Охват:** 7861 mp3 (основная библиотека `/storage/Music`, бэкап-папки исключены). + +## 🔴 ПРАВИЛА (дословно от Alex) — обязательны к исполнению + +| Поле | Правило | +|---|---| +| `artist` | Есть в скобках в имени файла → **брать из скобок**. Нет → **искать в интернете** | +| `artist` | **Никогда не маскировать мойибаке** — расшифровывать CP1251→UTF-8 | +| `albumartist` | **сборник → `Various Artists`** | +| `albumartist` | **детские → `Детские песни`** | +| `albumartist` | **сказки → `Сказки`** | +| `albumartist` | остальное → **= `artist`** | +| `album` | **сборники и unsorted → ИМЯ ПАПКИ** (пример Alex: `y OOF`) | +| `album` | сказки → **оставить как есть** | + +### Пример «артист в скобках» (детские песни) + +Файлы вида `Y/1. Песня о сказке В Шаинский - Ю.Энтин.mp3` — авторы прямо в имени файла +(`В. Шаинский` = композитор, `Ю. Энтин` = поэт). **Это и есть источник `artist` по правилу Alex**, +интернет нужен только там, где в скобках ничего нет. + +## 🔴 КРИТЕРИЙ СБОРНИКА — уточнён Alex + +Первые формулировки Alex звучали противоречиво («папка не одного артиста» → «не более чем одним»), +поэтому зафиксированы **его прямые примеры** — они и есть определение: + +> «**radio и retro — там сборники. Как и просто треки просто в корне артиста +> если там есть альбомы — считать сборником типа НТЛ**» + +| Признак папки | ВЕРДИКТ | +|---|---| +| `!Pop, Club, Любэ/!Radio/*` (все подпапки) | ✅ **СБОРНИК** → `Various Artists`, `album` = имя папки | +| `!Retro` | ✅ **СБОРНИК** | +| Треки в **корне** папки артиста, при наличии у него подпапок-альбомов | ✅ **СБОРНИК** (кейс НТЛ: 63 файла в `!Рэп/НТЛ` + 5 альбомов внутри) | +| `!A Not sorted/Sbornik_Novij_Rok` (184 арт.) | ✅ **СБОРНИК** | +| Папка с 1 уникальным артистом | ❌ **АЛЬБОМ**, `albumartist` = `artist` | +| `!Pop, Club, Любэ/Benassi Bros` (3 арт. из-за фитов) | ⚠️ **АЛЬБОМ** — не сборник, это альбом с фитами | +| `Benassi Bros/2003 Hypnotica Bonus` (4 арт.) | ⚠️ **АЛЬБОМ** — тот же класс, фиты | + +> 🔴 **Питфолл:** механический критерий «>1 уникального артиста = сборник» **неверен** — +> он записывает в `Various Artists` **альбомы с гостевыми фитами**. Alex: «Ты блядь сборник +> от альбома отличить не сможешь или чё?!» + +## 🔴 ПРОЦЕССНЫЙ ЗАПРЕТ ALEX + +> «**Ручная разбивка по группам. Не скриптом блядь. Сначала готовишь списки, проверяешь, +> а потом только скриптом можешь обновлять csv.**» + +**Порядок исполнения (не нарушать):** +1. **Списки вручную** — разбивка папок по группам, вычитка +2. **Проверка Alex** — он глазами подтверждает разметку +3. **Только потом** — скрипт обновляет CSV +4. Правка файлов — после отдельного «go» + +## 📊 СОСТОЯНИЕ РАБОТ + +### Сделано + +| # | Артефакт | Факт | +|---|---|---| +| 1 | Полный сбор тегов | `raw_tags_full.tsv` — **7861 строка**, md5 `0c3647fadb6556b4100c555ed59bfb44` | +| 2 | `tag-inventory3.csv` | **7861 строка**, **13 колонок** (+`albumartist`) | +| 3 | Бэкапы CSV | `tag-inventory2.csv.bak-20260927`, `tag-fix-plan.csv.bak-20260927` | +| 4 | Список 1 | 9 корневых папок | +| 5 | Список 2 | `list2-folders.txt` — **633 папки** с числом файлов и артистов | +| 6 | Список 3 | `list3-radio.md` — `!Radio` разметка вручную: **31 папка, 19 сборников / 12 альбомов** | +| 7 | `folder-categories.csv` | 633 папки с категориями и числом артистов | + +### Факты по библиотеке + +``` +КАТЕГОРИИ (по имени папки): + ОБЫЧНАЯ: 7009 файлов + СБОРНИК: 382 файла (11 папок — только по имени «сборник/sbornik/сборка») + СКАЗКИ: 310 файлов + ДЕТСКИЕ: 157 файлов + UNSORTED: 3 файла + +album_artist пуст: 6451 из 7861 (82%) +artist пуст: 178 +мойибаке хоть в одном поле: 492 +``` + +### ✅ Классификация папок (работает) + +``` +СБОРНИК ← имя папки содержит «сборник» / «sbornik» / «сборка» +СКАЗКИ ← «сказк» / «skazk» / «skazochn» +ДЕТСКИЕ ← «детск» / «detsk» / «песни для детей» +UNSORTED ← «not sorted» / «unsorted» +``` + +⚠️ **Питфолл классификатора:** фильтр по «сказк» **ложно ловит** +`!Rock&Folk/Пилот/2001 Сказка о прыгуне и скользящем` — это **альбом группы Пилот**, не сказки. +**Ограничивать фильтр сказок префиксом `!Детские/Сказки`.** + +## 🔴 ПОЧЕМУ МОЙИБАКЕ 492, А НЕ 155 — РАЗБОР `apply_tags.py` + +Прошлый скрипт-писатель волны 38 **не испортил** файлы, но и **не исправил** их. +Мойибаке уцелели из-за трёх дефектов: + +### ① Генератор плана сдался на CP1251 + +Увидев нечитаемые байты, вместо обратного декодирования поставил `artist<-cleared-junk` +(пусто) для **155 файлов** — включая все 156 из `!Детские/Детский хит`. + +### ② `delall` внутри `if` с проверкой непустого значения + +```python +if (r.get("new_artist") or "").strip(): # new_artist пуст → False + out.append(TPE1(...)) # ← не выполнено +for frame in _frames(r): + tags.delall(frame.FrameID) # ← ТОЖЕ не выполнено + tags.add(frame) +``` + +Пустой план → цикл не сделал ничего → **старый мойибаке уцелел**. +**Правильно:** `delall` — **всегда**; `add` — только при непустом значении. + +### ③ `_frames()` не знает про `TPE2` + +```python +# TIT2, TPE1, TALB, TRCK — и всё +``` + +`albumartist` физически не мог быть записан → **269 файлов с мойибаке в TPE2 скрипт не трогал +в принципе**, их даже не было в плане. + +### Следствие — правило приёмки + +> «N/N применено, **0 ошибок**» **≠ приёмка**. Критерий был «не упало», а не «тег стал верным». +> ОБЯЗАТЕЛЬНА пост-сверка: прочитать тег ПОСЛЕ записи и сравнить с ожиданием; MISMATCH → отчёт. +> Именно эта сверка поймала 165 ложных `no_change` в волне 38 — и не была сделана для `cleared-junk`. + +## 📊 МОЙИБАКЕ — 492 ФАЙЛА (факт по всей библиотеке) + +| Поле | Файлов | +|---|---:| +| `album_artist` (TPE2) | **269** | +| `artist` (TPE1) | **160** | +| `album` (TALB) | **63** | +| `title` (TIT2) | **1** | +| **строк с мойибаке хоть в одном поле** | **492** | + +**Топ папок:** + +``` +156 !Детские/Детский хит ← главный очаг + 54 !Rock&Folk/Муммий Тролль + 44 !Pop, Club, Любэ/!Radio + 31 !Rock&Folk/Пилот + 27 !Rock&Folk/ЗАХАР МАЙ + 23 !Rock&Folk/Несчастный Случай + 17 !Rock&Folk/Король и Шут + 16 z Другое.../Красная Плесень + 16 !Rock&Folk/Ария, Кипелов + 16 !Rock&Folk/Ундервуд + 14 !Рэп/Трофим + 13 !Rock&Folk/Запрещенные Барабанщики + 11 !Rock&Folk/Агата Кристи + 9 !Rock&Folk/Земфира +``` + +### Мойибаке — исходное состояние, НЕ следствие волны 38 + +Доказано сравнением `tag-inventory.csv` (v1) vs `tag-inventory2.csv`: +**значения тегов идентичны символ в символ**, строки отличаются только добавленной колонкой `source`. +Файлы прошли волну «не изменёнными». + +**Образец:** +``` +path: !Детские/Детский хит/128 НЕ ХОДИТЕ В АФРИКУ.mp3 +cur_artist: ÄÅÒÑÊÈÉ ÕÈÒ ← мойибаке +new_artist: (ПУСТО) ← план предлагал ОЧИСТИТЬ +action: fix +source: artist<-cleared-junk +``` + +## 🔬 ДЕКОДЕР МОИБАКЕ (проверен, работает) + +```python +def decode_mojibake(s): + """CP1251-байты, прочитанные как Latin-1 → обратно в русский.""" + if not s or not s.strip(): + return s, False + try: + raw = s.encode("latin-1") + except UnicodeEncodeError: + return s, False + try: + fixed = raw.decode("cp1251") + except UnicodeDecodeError: + return s, False + if fixed == s: + return s, False + cyr = sum(1 for c in fixed if "\u0400" <= c <= "\u04ff") + junk = sum(1 for c in s if "\u00c0" <= c <= "\u00ff") + if cyr > 0 and junk >= 2: + return fixed, True + return s, False +``` + +**Проверено фактом:** `ÄÅÒÑÊÈÉ ÕÈÒ` → `ДЕТСКИЙ ХИТ` (156 файлов расшифрованы в `tag-inventory3.csv`). + +## ⚠️ ПИТФОЛЛЫ СЕССИИ + +### Сбор тегов: владелец каталога root + +Скрипт писал в `/mnt/RED_2TB/storage/tmp/` — **каталог принадлежит root**, пользователь +`truenas_admin` (uid 950) → `open ... permission denied`, файл не создан. + +**Решение:** данные остаются в контейнере (`/tmp/raw_out.tsv`), забрать через +`docker cp jellyfin:/tmp/raw_out.tsv /home/truenas_admin/` — там права есть. + +### Сбор тегов: два разных имени выходного файла + +В скрипте `OUT=/mnt/RED_2TB/storage/tmp/raw_tags_full.tsv`, а `printf` писал в +`/tmp/raw_out.tsv` — `docker cp` забрал верный путь только потому, что он совпал с писателем. +**Сверять имена выходных файлов при написании скрипта.** + +### `docker exec` внутри контейнера — нет curl/sqlite3/ffprobe на хосте + +Теги снимать **только** `ffprobe` из образа Jellyfin: +`docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffprobe ...` + +### `albumartist` из ffprobe ≠ реальный `TPE2` + +ffprobe может **показать** `album_artist`, которого в байтах `TPE2` **нет** +(читает из ID3v1-хвоста или TXXX). Проверять наличие `TPE2` — только `od` по байтам. + +## ⏭ СЛЕДУЮЩИЕ ШАГИ + +1. **Завершить ручную разбивку по группам** — оставшиеся корневые папки: + `!Rock&Folk`, `!Барды`, `!Рэп`, `!Классика`, `z Другое…`, `!Pop, Club, Любэ` (без `!Radio`), + `!Retro`, `!Детские` +2. **Показать списки Alex на проверку** +3. Скрипт обновляет `tag-inventory3.csv` → финальные колонки `fix_artist`, `fix_albumartist`, `fix_album` +4. Поиск артистов в интернете — только для строк, где нет скобок и не мойибаке +5. **Только после «go»** — правка файлов (с бэкапом, с пост-сверкой) + +## 📁 ФАЙЛЫ РАБОТЫ (Mac) + +``` +/Users/admin/milestones/music-csv-2026-09-26/ +├── raw_tags_full.tsv 7861 стр, сырые теги (md5 0c3647fa...) +├── tag-inventory3.csv 7861 стр, 13 колонок, +albumartist +├── tag-inventory2.csv.bak-20260927 бэкап +├── tag-fix-plan.csv.bak-20260927 бэкап +├── albumartist-plan.csv черновик (НЕ финал — см. §Процессный запрет) +├── mojibake_rows.tsv 492 строки с мойибаке +├── verify-problems.csv 12 строк без albumartist +├── folder-categories.csv 633 папки с категориями +├── list2-folders.txt 633 папки (файлы/артисты) +├── list3-radio.md ручная разметка !Radio (31 папка) +├── analyze.py / build_inventory3.py / verify_manual.py / audit_categories.py +└── README.md, MD5SUMS.txt +``` diff --git a/personal/plans/tagwriter-container.md b/personal/plans/tagwriter-container.md index 55a5b976..b6948b10 100755 --- a/personal/plans/tagwriter-container.md +++ b/personal/plans/tagwriter-container.md @@ -20,6 +20,58 @@ updated: '2026-09-26' _Создан: 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]]._ +## 🔴 v8 (2026-09-27): ТРЕТИЙ КЛАСС ДЕФЕКТА `apply_tags.py` — БЕЗДЕЙСТВИЕ ПОД ВИДОМ РАБОТЫ + +**Найдено при полном сборе тегов по всей библиотеке** (`raw_tags_full.tsv`, 7861 строка). +Разбор — [[personal/plans/music-artist-albumartist-repair]]. + +### Дефект ①: `delall` ВНУТРИ `if` с проверкой непустого значения + +```python +if (r.get("new_artist") or "").strip(): # new_artist пуст → False + out.append(TPE1(...)) # ← не выполнено +for frame in _frames(r): + tags.delall(frame.FrameID) # ← ТОЖЕ не выполнено (внутри того же блока) + tags.add(frame) +``` + +**План хотел «очистить» `artist` (значение `new_artist` пустое) — а код не сделал НИЧЕГО.** +Пустой список фреймов → цикл не выполнился → **старый мойибаке уцелел**. + +> **ПРАВИЛЬНО:** `delall` — **ВСЕГДА** (вне `if`), `add` — только при непустом значении. +> Иначе «очистить поле» физически не очищает. + +### Дефект ②: `_frames()` не знает про `TPE2` (albumartist) + +```python +# TIT2, TPE1, TALB, TRCK — и всё +``` + +**`TPE2` физически не мог быть записан, даже если бы был в плане.** Следствие: **269 файлов +с мойибаке в `albumartist` скрипт не трогал в принципе** — их даже не было в плане правок +(`tag-fix-plan.csv` не содержит колонки `album_artist`). + +### Дефект ③ (генератора плана): сдался на CP1251 вместо декодирования + +Генератор увидел нечитаемые байты и вместо обратного декодирования CP1251→UTF-8 поставил +`artist<-cleared-junk` (пусто) для **155 файлов** — включая все 156 из `!Детские/Детский хит`. +**Правильная реакция — декодировать, а не очищать:** `ÄÅÒÑÊÈÉ ÕÈÒ` → `ДЕТСКИЙ ХИТ`. + +### 🔴 КЛАСС ДЕФЕКТА: «бездействие под видом работы» + +**Скрипт НЕ испортил файлы, но и НЕ исправил.** Отчёт «7041/7041 применено, 0 ошибок» был +**ложной приёмкой**: критерий — «не упало», а не «тег стал правильным». Файл с мойибаке прошёл +насквозь и был засчитан как исправленный. + +> **ПРАВИЛО ПРИЁМКИ:** после записи **прочитать тег и сравнить с ожиданием**; MISMATCH → отчёт. +> Именно эта сверка поймала 165 ложных `no_change` — и **не была сделана** для 155 `cleared-junk`, +> поэтому дефект уцелел и вылез только на полном сборе тегов. + +**Итог по ущербу:** файлы целы (старые теги на месте), данные не потеряны — +**это не разрушение, это бездействие**. Правильная формулировка для Alex: скрипт был плохой, +потому что пропускал пустые значения и не знал про `TPE2`. + + ## 🆕 СОСТОЯНИЕ НА КОНЕЦ 2026-09-26 (последние шаги сессии) | Шаг | Статус | diff --git a/personal/tech/jellyfin-tags-cp1251-unreadable.md b/personal/tech/jellyfin-tags-cp1251-unreadable.md index 3ea035a3..fb9cefca 100755 --- a/personal/tech/jellyfin-tags-cp1251-unreadable.md +++ b/personal/tech/jellyfin-tags-cp1251-unreadable.md @@ -392,6 +392,41 @@ MusicBrainz отдаёт его для каждого релиза. Ручная ## 📦 ИНВЕНТАРИЗАЦИЯ БЭКАПОВ И CSV (факты 2026-09-27) +### 🔴 v8: ПОЛНЫЙ СБОР ТЕГОВ — ЦИФРЫ ПО ВСЕЙ БИБЛИОТЕКЕ + +**Источник:** `raw_tags_full.tsv`, **7861 строка** (полный проход `ffprobe` по всем mp3, +бэкап-папки исключены). Ранее все цифры брались из `tag-inventory*.csv` (7906 записей) — +**устаревшего по составу** файлов. + +| Показатель | Всего | Доля | +|---|---:|---:| +| Строк в сборе | **7861** | 100% | +| `album_artist` **заполнен** | **1410** | 18% | +| `album_artist` **пуст** | **6451** | **82%** | +| `artist` заполнен | 7683 | 98% | +| `artist` **пуст** | **178** | 2% | +| `title` пуст | 7 | — | +| **мойибаке хоть в одном поле** | **492** | 6% | + +**Мойибаке по полям:** `album_artist` **269** · `artist` **160** · `album` **63** · `title` **1**. + +> 🔴 **Ключевой факт:** **269 файлов имеют мойибаке в `album_artist`** — этого класса дефекта +> раньше **не видели вообще**, потому что волна 38 не знала про `TPE2` (см. §КОРЕНЬ ПРОПУСКА выше). +> `albumartist` центральная часть новой задачи — [[personal/plans/music-artist-albumartist-repair]]. + +### Расхождение ffprobe vs CSV по `album_artist` + +``` +ffprobe (raw_tags_full.tsv): album_artist заполнен у 1410 +tag-inventory2.csv: album_artist заполнен у 3507 +``` + +**Разница 2097.** Причина — в ранее зафиксированном питфолле: ffprobe **не показывает** +`album_artist`, если его нет в `TPE2` (в CSV значения приходили из ID3v1-хвоста/TXXX). +Следствие: **первый сырой скан теряет часть `album_artist`**, а CSV его сохраняет. +Для правки `TPE2` опираться на **байтовую проверку `od`**, не на ffprobe. + + Ответ на вопрос Alex: «в backup полный бэкап, а в csv полный список треков, или в нём не все?» ### Бэкап — 91%, НЕ полный