[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

@@ -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 (последние шаги сессии)
| Шаг | Статус |