[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:
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
|
||||
```
|
||||
@@ -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 (последние шаги сессии)
|
||||
|
||||
| Шаг | Статус |
|
||||
|
||||
Reference in new issue
Block a user