[2026-09-27] eagle: family/how-to/navidrome-truenas-android.md family/plans/music-jellyfin-lidarr-taiga.md personal/plans/music-artist-albumartist-repair.md personal/plans/tagwriter-container.md personal/tech/jellyfin-tags-cp1251-unreadable.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 11:32:14 +06:00
1 parent 7a6ec37e43
commit d2a8ed8a06
5 files changed
+485 -5

No files matched your search

+9 -1
View File
@@ -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, подключить
+103 -4
View File
@@ -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 июня |
@@ -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
```
+52
View File
@@ -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 (последние шаги сессии)
| Шаг | Статус |
@@ -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%, НЕ полный