minor: переименование
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
## ПОТОК ДАННЫХ (чистая архитектура) ДРАФТ
|
||||
|
||||
```text
|
||||
1️⃣ ИСТОЧНИК ДОБАВЛЕН
|
||||
├─ Пользователь добавляет новый источник (TbSource)
|
||||
└─ Создается задание в Redis: `job:parse_source:123`
|
||||
|
||||
2️⃣ ФОНОВЫЙ ВОРКЕР (Celery/APScheduler)
|
||||
├─ Видит задание в Redis
|
||||
├─ Начинает парсить (не пишет в prod БД!)
|
||||
├─ Получает: Style["Rock"], Artist["The Beatles"], Label["Sony"]
|
||||
├─ Кидает КАЖДЫЙ в Redis как **виртуальный объект**
|
||||
│ └─ `pending:style:1` → {"title": "Rock", "source": "Discogs", ...}
|
||||
│ └─ `pending:artist:2` → {"title": "The Beatles", "source": "MusicBrainz", ...}
|
||||
└─ Смотрит: есть ли в product БД? Если НЕТ → в Redis очередь
|
||||
|
||||
3️⃣ АДМИНКА ДЖАНГО (виртуальная вкладка "Ожидающие задачи")
|
||||
├─ Админ видит в Redis список
|
||||
├─ Видит: Rock (похож на existing Rock?), The Beatles (новый?), Sony (уже есть)
|
||||
├─ **ПОТЫКАЛ**:
|
||||
│ ├─ Rock → "Мержить с existing Rock" → задание в Redis
|
||||
│ ├─ The Beatles → "Одобрить" → задание в Redis
|
||||
│ └─ Sony → "Пропустить" → удалить из Redis
|
||||
└─ Каждое действие → новое задание в Redis
|
||||
|
||||
4️⃣ ФОНОВЫЙ ВОРКЕР II (финальный штрих)
|
||||
├─ Видит задание: "merge Rock с ID:5"
|
||||
│ └─ Делает: добавляет синонимы, сохраняет в product БД
|
||||
├─ Видит задание: "create The Beatles"
|
||||
│ └─ Пишет вреальную БД → TbArtist создана
|
||||
└─ После каждого успеха → **удалить из Redis**
|
||||
|
||||
5️⃣ ПРОДАКТ БД (в итоге)
|
||||
└─ Только валидные, одобренные, обработанные данные
|
||||
└─ No garbage, ID подряд
|
||||
```
|
||||
|
||||
# Архитектура
|
||||
|
||||
```text
|
||||
Парсер Redis (очередь) Админка Воркер БД
|
||||
│ │ │ │ │
|
||||
├──parse_source───────────→│ │ │ │
|
||||
│ │ │ │ │
|
||||
│ ┌─pending:style:1 │ │ │
|
||||
│ ├─pending:artist:1 ←─ Админ видит видит здесь │ │
|
||||
│ └─pending:label:1 потыкает кнопки │ │
|
||||
│ │ │ │ │
|
||||
│ │ "merge Rock"│ │ │
|
||||
│ │←───────────────────│ →─ job:merge:1 ──────→│ │
|
||||
│ │ │ ├─→ UPDATE Style →│
|
||||
│ │ │ │ delete from Redis
|
||||
│ │ "create Beatles" │ │
|
||||
│ │←──────────────────→─ job:create:2 ────────→│ │
|
||||
│ │ │ ├─→ INSERT Artist ─│
|
||||
│ │ │ │ delete from Redis
|
||||
│ │ │ │ │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────────────────→
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## КЛЮЧЕВЫЕ ПРИНЦИПЫ
|
||||
|
||||
- **Чистая БД**: product база получает только одобренные, валидные данные
|
||||
- **ID подряд**: мнимизирукет удаления (delete), INSERT данных парсинга только при одобрении
|
||||
- **Single Source of Truth**: пока не одобрено администратором → данные ТОЛЬКО в Redis
|
||||
- **Асинхронность**: парсер не блокирует админку, админка не блокирует парсер
|
||||
- **Откат дешевый**: удалить из Redis дешевле, чем восстанавливать из БД
|
||||
|
||||
---
|
||||
|
||||
## КОМПОНЕНТЫ REDIS ОЧЕРЕДИ
|
||||
|
||||
```
|
||||
tasks:pending ← Очередь неодобренных задач (парсер → сюда)
|
||||
└─ pending:style:1
|
||||
└─ pending:artist:2
|
||||
└─ pending:label:3
|
||||
|
||||
tasks:approved ← Очередь одобренных (админ → сюда)
|
||||
└─ {type: 'create', id: 'style:1', data: {...}}
|
||||
└─ {type: 'merge', id: 'artist:1', merge_with_id: 5}
|
||||
└─ {type: 'skip', id: 'label:3'}
|
||||
|
||||
tasks:completed ← История завершённых (воркер → сюда)
|
||||
tasks:failed ← История ошибок (воркер → сюда)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ДЕЙСТВИЯ В АДМИНКЕ (виртуальная вкладка)
|
||||
|
||||
Админ видит Redis очередь и кликает:
|
||||
|
||||
1. **"✅ Одобрить"** → `job:create:style:1` → Воркер пишет в БД
|
||||
2. **"🔗 Мержить с ID:5"** → `job:merge:style:1:with:5` → Воркер обновляет existing
|
||||
3. **"❌ Пропустить"** → Удалить из Redis (ничего не пишется)
|
||||
4. **"📝 Отредактировать"** → Изменить JSON в оптимистичной форме → сохранить как новое задание
|
||||
|
||||
---
|
||||
|
||||
## ПАРСЕР (упрощённо)
|
||||
|
||||
```python
|
||||
def parse_source(source_id):
|
||||
source = TbSource.objects.get(id=source_id)
|
||||
|
||||
for style_name in PARSED_STYLES:
|
||||
# 1. Ищем существующий стиль или по названию или по алиасам
|
||||
existing = TbMusicStyle.objects.filter(
|
||||
Q(s_style_name__iexact=style_name) |
|
||||
Q(j_style_synonyms__contains=style_name)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# ✅ МАТЧИНГ СРАБОТАЛ → пишем сразу в БД
|
||||
if style_name not in existing.j_style_synonyms:
|
||||
existing.j_style_synonyms.append(style_name) # Запомнили синоним!
|
||||
existing.save()
|
||||
else:
|
||||
# ❌ НОВЫЙ СТИЛЬ → в Redis очередь на одобрение
|
||||
redis.lpush('tasks:pending', {...})
|
||||
|
||||
# В метаданных источника сохраняем прогресс парсинга
|
||||
source.j_source_metadata['last_parsed_line'] = current_row_number
|
||||
source.j_source_metadata['total_lines'] = total_rows
|
||||
source.j_source_metadata['parsed_at'] = timezone.now().isoformat()
|
||||
source.save()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## АДМИНКА (упрощённо)
|
||||
|
||||
```python
|
||||
class PendingTaskAdmin(admin.ModelAdmin):
|
||||
# Не наследуем от ModelAdmin (нет моделей!)
|
||||
# Вместо этого: читаем Redis, рендерим как таблицу
|
||||
|
||||
def changelist_view(self, request):
|
||||
# Получаем из Redis
|
||||
tasks = [json.loads(redis.get(key)) for key in redis.keys('pending:*')]
|
||||
# Выводим нетипичную таблицу с кнопками: Одобрить, Мержить, Пропустить
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ФОНОВЫЙ ВОРКЕР (упрощённо)
|
||||
|
||||
```python
|
||||
def process_approved_tasks():
|
||||
while True:
|
||||
task = redis.lpop('tasks:approved')
|
||||
|
||||
try:
|
||||
if task['type'] == 'create':
|
||||
# Пишем в product БД
|
||||
TbMusicStyle.objects.create(title=task['title'], ...)
|
||||
elif task['type'] == 'merge':
|
||||
# Обновляем existing + добавляем синонимы
|
||||
style = TbMusicStyle.objects.get(id=task['merge_with_id'])
|
||||
style.j_style_synonyms.append(task['title'])
|
||||
style.save()
|
||||
|
||||
# Успех → удалить из Redis
|
||||
redis.delete(task['_redis_key'])
|
||||
redis.lpush('tasks:completed', task)
|
||||
except Exception as e:
|
||||
redis.lpush('tasks:failed', {**task, 'error': str(e)})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ТЕХНОЛОГИЧЕСКИЙ СТЕК
|
||||
|
||||
- **Redis** — очередь + кэш виртуальных объектов
|
||||
- **Celery** или **APScheduler** — фоновый воркер для парсера и финального сохранения
|
||||
- **Django Admin** — кастомная вкладка для управления очередью
|
||||
- **PostgreSQL/SQLite** — product database (только одобренные данные)
|
||||
|
||||
---
|
||||
|
||||
## ВЕТКА: ПРЯМОЕ ПОПАДАНИЕ В БД (когда матчинг сработал)
|
||||
|
||||
Если парсер нашел алиас или группа уже в БД → **обходим очередь**, пишем сразу:
|
||||
|
||||
```python
|
||||
def parse_source(source_id):
|
||||
source = TbSource.objects.get(id=source_id)
|
||||
|
||||
for style_name in PARSED_STYLES:
|
||||
# 1. Ищем существующий стиль или по названию или по алиасам
|
||||
existing = TbMusicStyle.objects.filter(
|
||||
Q(s_style_name__iexact=style_name) |
|
||||
Q(j_style_synonyms__contains=style_name)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# ✅ МАТЧИНГ СРАБОТАЛ → пишем сразу в БД
|
||||
if style_name not in existing.j_style_synonyms:
|
||||
existing.j_style_synonyms.append(style_name) # Запомнили синоним!
|
||||
existing.save()
|
||||
else:
|
||||
# ❌ НОВЫЙ СТИЛЬ → в Redis очередь на одобрение
|
||||
redis.lpush('tasks:pending', {...})
|
||||
|
||||
# В метаданных источника сохраняем прогресс парсинга
|
||||
source.j_source_metadata['last_parsed_line'] = current_row_number
|
||||
source.j_source_metadata['total_lines'] = total_rows
|
||||
source.j_source_metadata['parsed_at'] = timezone.now().isoformat()
|
||||
source.save()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ИЗМЕНЕНИЕ ЦЕН (дополнительная ветка)
|
||||
|
||||
Когда уже существующий товар поменял цену → в отдельную очередь:
|
||||
|
||||
```
|
||||
tasks:price_changes ← Отдельная очередь
|
||||
└─ {type: 'price_update', offer_id: 42, old_price: 100, new_price: 120}
|
||||
└─ {type: 'price_update', offer_id: 43, old_price: 200, new_price: 180}
|
||||
```
|
||||
|
||||
**Обработка:**
|
||||
- Фоновый воркер видит изменение цены
|
||||
- Проверяет: изменилась ли существенно (> 5%)?
|
||||
- Если да → может требоваться одобрение (+1 задание в Redis)
|
||||
- Если нет → пишет сразу в БД (TbOfferHistory записывается автоматически)
|
||||
|
||||
---
|
||||
|
||||
## МЕТАДАННЫЕ ОТСЛЕЖИВАНИЯ ПРОГРЕССА
|
||||
|
||||
В каждой **TbSource** должно быть поле `j_source_metadata`:
|
||||
|
||||
```python
|
||||
j_source_metadata = JSONField(default=dict, help_text='Отслеживание парсинга и синонимы')
|
||||
|
||||
# Структура:
|
||||
{
|
||||
"last_parsed_line": 4523, # Докуда добежал парсер (для resume)
|
||||
"total_lines": 10000, # Всего строк в файле
|
||||
"parsed_at": "2026-06-14T12:30:00",# Время последнего парсинга
|
||||
"status": "in_progress", # in_progress | completed | failed
|
||||
"error_message": null, # если failed, чтобы видно было почему
|
||||
|
||||
# Найденные синонимы (стили, артисты, которые auto-matched)
|
||||
"matched_styles": {
|
||||
"Rock": 45, # Стиль 45 найден под названием "Rock"
|
||||
"rock": 45, # Вариант написания também сохранили
|
||||
},
|
||||
"matched_artists": {
|
||||
"The Beatles": 12,
|
||||
"Beatles, The": 12,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Если парсер упал на строке 4523 → перезапуск продолжится с 4524, а не с начала!
|
||||
|
||||
---
|
||||
|
||||
## МАСШТАБИРУЕМОСТЬ: REDIS В ПАМЯТИ vs PERSISTENCE
|
||||
|
||||
### Проблема: 10k+ задач при загрузке большого Excel
|
||||
|
||||
```
|
||||
Excel с 10,000 позиций
|
||||
├─ 10k артистов
|
||||
├─ 5k стилей
|
||||
├─ 2k лейблов
|
||||
└─ 15k общих задач в очереди
|
||||
```
|
||||
|
||||
Redis хранит в памяти по умолчанию ➜ контейнер перестартует ➜ всё теряется!
|
||||
|
||||
**Решение: RDB + AOF Persistence**
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml для Redis
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: >
|
||||
redis-server
|
||||
--appendonly yes
|
||||
--appendfsync everysec
|
||||
--save 900 1
|
||||
--maxmemory 2gb
|
||||
--maxmemory-policy allkeys-lru
|
||||
```
|
||||
|
||||
- **RDB snapshots** — снимок каждые 15 минут
|
||||
- **AOF log** — каждая команда лог записывается на диск
|
||||
- **maxmemory-policy** — если память 2GB переполнится, удаляются старые задачи (но только pending, не approved!)
|
||||
- **Persistence**: при рестарте контейнера Redis восстановит все задачи из AOF
|
||||
|
||||
---
|
||||
|
||||
## ПАРАЛЛЕЛЬНАЯ ОБРАБОТКА (масштабирование)
|
||||
|
||||
Если задач много → **несколько воркеров**, каждый обрабатывает свой тип:
|
||||
|
||||
```python
|
||||
# Worker 1: Парсит (пишет в tasks:pending)
|
||||
celery_app.send_task('parser.parse_source', args=[source_id])
|
||||
|
||||
# Worker 2: Обрабатывает стили (слушает tasks:approved тип='style')
|
||||
@app.task
|
||||
def process_style_task(task_data):
|
||||
# обновить БД
|
||||
|
||||
# Worker 3: Обрабатывает артистов (слушает tasks:approved тип='artist')
|
||||
@app.task
|
||||
def process_artist_task(task_data):
|
||||
# обновить БД
|
||||
|
||||
# Worker 4: Обрабатывает цены (слушает tasks:price_changes)
|
||||
@app.task
|
||||
def update_offer_price(task_data):
|
||||
# обновить цену
|
||||
```
|
||||
|
||||
Каждый воркер работает в отдельном потоке/процессе ➜ параллелизм!
|
||||
|
||||
---
|
||||
|
||||
## FLOW ПРИНЦИПИАЛЬНАЯ СХЕМА (обновлённая)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ ПАРСЕР (Worker 1) │
|
||||
│ Читает Excel/CSV/JSON │
|
||||
└──────────────┬───────────────────┘
|
||||
│
|
||||
┌──────────────┴────────────┐
|
||||
│ │
|
||||
┌───────▼───────────┐ ┌─────────▼──────────┐
|
||||
│ Матч сработал? │ │ Новые данные? │
|
||||
│ (aliasing) │ │ (неизвестны) │
|
||||
└────┬──────────────┘ └─────────┬──────────┘
|
||||
│ Yes │ No
|
||||
│ │
|
||||
┌────────▼────────────┐ ┌────────▼────────────┐
|
||||
│ СРАЗУ В БД │ │ tasks:pending │
|
||||
│ + j_style_synonyms │ │ (очередь нужнофала) │
|
||||
│ + metadata['matched'] │ (ждут одобрения) │
|
||||
└────────┬────────────┘ └────────┬────────────┘
|
||||
│ │
|
||||
│ АДМИНКА ДЖАНГО
|
||||
│ (виртуальная таблица)
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │ ✅ ❌ 🔗 │
|
||||
│ (Approve/Skip/Merge)
|
||||
│ │ │
|
||||
│ ┌───────▼─────┐ │
|
||||
│ │ tasks: │ │
|
||||
│ │ approved │ │
|
||||
│ └───────┬─────┘ │
|
||||
│ │ │
|
||||
└──────────┬─────────┴─────────────┘
|
||||
│
|
||||
Worker 2,3,4...
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ PRODUCT БД │
|
||||
│ (валидные) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ДОПОЛНИТЕЛЬНЫЕ МЕТАДАННЫЕ ДЛЯ КАЖДОЙ ЗАДАЧИ
|
||||
|
||||
```python
|
||||
# Каждая задача в Redis содержит:
|
||||
task = {
|
||||
'id': 'pending:style:123',
|
||||
'type': 'style', # или 'artist', 'label', 'price_change'
|
||||
'data': {'title': 'Rock', ...},
|
||||
|
||||
# Откуда пришла
|
||||
'source_id': 42,
|
||||
'source_name': 'Discogs API',
|
||||
'parsed_line': 4523,
|
||||
|
||||
# Когда создана
|
||||
'created_at': '2026-06-14T12:00:00',
|
||||
'ttl_seconds': 86400, # жить 24 часа, потом удалить
|
||||
|
||||
# Статус обработки
|
||||
'attempts': 0, # сколько раз пытались обработать
|
||||
'last_error': null,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ИТОГОВАЯ АРХИТЕКТУРА (расширенная)
|
||||
|
||||
```
|
||||
ИСТОЧНИКИ (Excel/API)
|
||||
↓ (15k записей)
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ REDIS ОЧЕРЕДИ (в памяти + AOF persistence) │
|
||||
│ ├─ tasks:pending (очередь неодобренных) │
|
||||
│ ├─ tasks:approved (одобренные) │
|
||||
│ ├─ tasks:price_changes (изменения цен) │
|
||||
│ ├─ tasks:matched (уже в БД + синонимы) │
|
||||
│ ├─ tasks:completed (завершённые) │
|
||||
│ └─ tasks:failed (ошибки) │
|
||||
└──────────────────────────────────────────────────┘
|
||||
↓ (видит админка) ↓ (видят воркеры)
|
||||
┌──────────────────┐ ┌────────────────────┐
|
||||
│ АДМИНКА ДЖАНГО │ │ WORKERS (Celery) │
|
||||
│ (виртуальная │ │ ├─ parse_source │
|
||||
│ таблица) │ │ ├─ process_style │
|
||||
│ Действия: │ │ ├─ process_artist │
|
||||
│ ok/del/edit/... │ │ └─ update_prices │
|
||||
└──────────────────┘ └────────────────────┘
|
||||
↓ ↓
|
||||
└──────────┬─────────────┘
|
||||
↓
|
||||
┌─────────────────────┐
|
||||
│ PRODUCT БД │
|
||||
│ (только валидные) │
|
||||
│ + metadata progress │
|
||||
│ + aliases │
|
||||
└─────────────────────┘
|
||||
```
|
||||
@@ -0,0 +1,748 @@
|
||||
# Организация справочника музыкальных стилей
|
||||
|
||||
# Состояние списка музыкальных стилей в Discogs (июль 2026)
|
||||
|
||||
_AOR, Aboriginal, Abstract, Acid, Acid House, Acid Jazz, Acid Rock, Acoustic, African, Afro-Cuban, Afro-Cuban Jazz,
|
||||
Afrobeat, Afrobeat, Aguinaldo, Aleatoric, Alt-Pop, Alternative Metal, Amapiano, Ambient, Ambient House, Anarcho-Punk,
|
||||
Anatolian Rock, Andalusian Classical, Andean Music, Anison, Antifolk, Appalachian Music, Arabic Pop, Arena Rock,
|
||||
Art Rock, Art Song, Atmospheric Black, Metal, Atonal, Audiobook, Avant-garde Jazz, Axé, Azonto, Bachata, Baila,
|
||||
Baião, Bakersfield Sound, Balearic, Ballad, Ballet, Ballet, Ballroom, Baltimore Club, Bambuco, Banda, Bangladeshi,
|
||||
Classical, Barbershop, Baroque, Baroque Pop, Basque Music, Bass Music, Bass Music, Bassline, Batucada, Bayou Funk,
|
||||
Beat, Beatbox, Beatdown, Beguine, Bel Canto, Bengali Music, Berlin-School, Bećarac, Bhangra, Big Band, Big Beat,
|
||||
Bitpop, Black Metal, Blackgaze, Bleep, Bluegrass, Blues Rock, Bolero, Bollywood, Bomba, Bongo Flava, Boogaloo, Boogie,
|
||||
Boogie Woogie, Boom Bap, Bop, Bossa Nova, Bossanova, Bounce, Bouncy Techno, Brass Band, Brass Instrument, Break-In,
|
||||
Breakbeat, Breakcore, Breaks, Brega, Britcore, Britpop, Broken Beat, Brutal Death Metal, Bubblegum, Bubbling,
|
||||
Bultrón, Byzantine, Cabaret, Caipira, Cajun, Calypso, Cambodian Classical, Candombe, Cantata, Cantopop, Cantorial,
|
||||
Canzone Napoletana, Cape Jazz, Capriccio, Carimbó, Carnatic, Catalan Music, Celtic, Cha-Cha, Chacarera, Chamamé,
|
||||
Chamber Music, Champeta, Chanson, Charanga, Chicago Blues, Chillwave, Chinese Classical, Chiptune, Choral, Choro,
|
||||
Chutney, City Pop, Classic Rock, Classical, Classical Guitar, Cloud Rap, Cobla, Coldwave, Comedy, Comfy Synth, Compas,
|
||||
Concert Band, Concert Film, Concerto, Concerto Grosso, Conjunto, Conscious, Contemporary, Contemporary Jazz,
|
||||
Contemporary R&B, Cool Jazz, Copla, Corrido, Country, Country Blues, Country Rock, Cretan, Crossover Thrash,
|
||||
Crunk, Crust, Cuatro, Cubano, Cumbia, Currulao, Cut-up/DJ, DJ Battle Tool, Dabke, Dance-pop, Dancehall, Dances,
|
||||
Dangdut, Dansband, Danzon, Dark Ambient, Dark Electro, Dark Jazz, Darkwave, Death Metal, Deathcore, Deathrock,
|
||||
Deconstructed Club, Deep House, Deep Techno, Delta Blues, Depressive Black, Metal, Descarga, Desert Blues, Dialogue,
|
||||
Disco, Disco, Disco Polo, Divertimento, Dixieland, Donk, Doo Wop, Doo Wop, Doom Metal, Doomcore, Downtempo, Dream Pop,
|
||||
Drill, Drone, Drum n Bass, Dub, Dub, Dub Poetry, Dub Techno, Dubstep, Dungeon Synth, Duranguense, EBM, Early,
|
||||
East Coast Blues, Easy Listening, Education, Educational, Electric Blues, Electro, Electro, Electro House,
|
||||
Electro Swing, Electroacoustic, Electroclash, Emo, Enka, Erotic, Ethereal, Ethno-pop, Etude, Euro House, Euro Trance,
|
||||
Euro-Disco, Eurobeat, Eurodance, Europop, Exotica, Expressionist, Fado, Fantasia, Favela Funk, Field Recording, Filk,
|
||||
Flamenco, Folk, Folk Metal, Folk Rock, Footwork, Formes Fixes, Forró, Free Funk, Free Improvisation, Free Jazz,
|
||||
Freestyle, Freetekno, French House, Frevo, Funaná, Funeral Doom Metal, Funk, Funk Metal, Funkot, Fusion,
|
||||
Future Bass, Future House, Future Jazz, Future Pop, Futurism, G-Funk, Gabber, Gagaku, Gaita, Galician Traditional,
|
||||
Gamelan, Gangsta, Garage House, Garage Rock, Geet, Ghazal, Ghetto, Ghetto House, Ghettotech, Glam, Glitch, Glitch Hop,
|
||||
Gnawa, Go-Go, Go-Go, Goa Trance, Goregrind, Gospel, Gospel, Goth Rock, Gothic Metal, Gqom, Grand Opera, Grime, Grime,
|
||||
Grindcore, Griot, Groove Metal, Group Sounds, Grunge, Guaguancó, Guajira, Guaracha, Guarania, Guggenmusik, Gusle,
|
||||
Gwo Ka, Gypsy Jazz, Għana, Halftime, Hands Up, Happy Hardcore, Hard Beat, Hard Bop, Hard House, Hard Rock,
|
||||
Hard Techno, Hard Trance, Hardcore, Hardcore, Hardcore Hip-Hop, Hardstyle, Harmonica Blues, Harpsichord,
|
||||
Harsh Noise Wall, Hawaiian, Health-Fitness, Heavy Metal, Hi NRG, Highlife, Hill Country Blues, Hillbilly, Hindustani,
|
||||
Hip Hop, Hip-House, Hiplife, Hokkien Pop, Holiday, Honky Tonk, Honkyoku, Horror Rock, Horrorcore, House, Huayno,
|
||||
Hyper Techno, Hyperpop, Hyphy, Hypnagogic pop, IDM, Illbient, Impressionist, Incidental Music, Indian Classical,
|
||||
Indie Pop, Indie Rock, Indo-Pop, Industrial, Industrial, Industrial Metal, Instrumental, Interview, Italo House,
|
||||
Italo-Disco, Italodance, Izvorna, J-Core, J-Rock, J-pop, Jangle Pop, Jazz-Funk, Jazz-Rock, Jazzdance, Jazzy Hip-Hop,
|
||||
Jersey Club, Jibaro, Jiuta, Joropo, Jota, Jug Band, Juke, Jump Blues, Jumpstyle, Jungle, Junkanoo, K-Rock, K-pop,
|
||||
Karaoke, Kaseko, Kaskawi, Kayōkyoku, Keroncong, Keyboard, Khaliji, Kizomba, Klasik, Klezmer, Kolo, Korean Court Music,
|
||||
Krautrock, Kuduro, Kwaito, Lambada, Lao Music, Late Romantic, Latin, Latin Jazz, Latin Pop, Laïkó, Leftfield,
|
||||
Lento Violento, Levenslied, Lied, Light Music, Liscio, Lo-Fi, Louisiana Blues, Lounge, Lovers Rock, Low Bap,
|
||||
Lowercase, Luk Krung, Luk Thung, Lullaby, MPB, Madrigal, Magnificat, Makina, Maloya, Mambo, Mandopop, Manila Sound,
|
||||
Marcha Carnavalesca, Marches, Mariachi, Marimba, Mass, Math Rock, Mathcore, Mbalax, Medical, Medieval,
|
||||
Melodic Death Metal, Melodic Hardcore, Memphis Blues, Memphis Rap, Mento, Merengue, Metalcore, Miami Bass, Microhouse,
|
||||
Microtonal, Midwest Emo, Military, Milonga, Min'yō, Minimal, Minimal Techno, Minimalism, Minneapolis Sound, Mizrahi,
|
||||
Mo Lam, Mod, Modal, Modern, Modern Classical, Modern Electric, Blues, Monolog, Moombahton, Morna, Motet, Motswako,
|
||||
Mouth Music, Movie Effects, Mugham, Musette, Music Hall, Music Video, Musical, Musique Concrète, Música Criolla, NDW,
|
||||
Nagauta, Neo Soul, Neo Trance, Neo-Classical, Neo-Classical Metal, Neo-Romantic, Neofolk, Neofolk, Neopagan,
|
||||
Nerdcore Techno, New Age, New Beat, New Jack Swing, New Wave, New Wave, Nhạc Vàng, Nintendocore, No Wave, Nocturne,
|
||||
Noise, Noise, Noise Rock, Noisecore, Nordic, Norteño, Novelty, Nu Metal, Nu-Disco, Nueva Cancion, Nueva Trova,
|
||||
Nursery Rhymes, Néo Kyma, Népzene, Occitan, Occult, Oi, Ojkača, Opera, Operetta, Oratorio, Orchestra, Organ,
|
||||
Ottoman Classical, Overtone Singing, Overture, P.Funk, Pachanga, Pacific, Pagode, Parody, Parody, Pasodoble, Passion,
|
||||
Percussion Ensemble, Persian Classical, Persian Pop, Philippine Classical, Phleng Phuea Chiwit, Phonk, Piano,
|
||||
Piano Blues, Piedmont Blues, Piobaireachd, Pipe & Drum, Plainchant, Plena, Plunderphonics, Poetry, Political,
|
||||
Polka, Pop Punk, Pop Rap, Pop Rock, Pornogrind, Porro, Post Bop, Post Rock, Post-Grunge, Post-Hardcore, Post-Metal,
|
||||
Post-Modern, Post-Punk, Power Electronics, Power Metal, Power Pop, Power Violence, Prelude, Prog Rock, Program Music,
|
||||
Progressive, Bluegrass, Progressive Breaks, Progressive House, Progressive Metal, Progressive Trance, Promotional,
|
||||
Psy-Trance, Psychedelic, Psychedelic Rock, Psychobilly, Pub Rock, Public Broadcast, Public, Service Announcement,
|
||||
Punk, Qawwali, Quechua, Radioplay, Ragga, Ragga HipHop, Ragtime, Ranchera, Rapso, Rara, Raï, Rebetiko, Reggae,
|
||||
Reggae Gospel, Reggae-Pop, Reggaeton, Religious, Renaissance, Requiem, Rhapsody, Rhythm & Blues, Rhythm & Blues,
|
||||
Rhythmic Noise, RnB/Swing, Rock & Roll, Rock Opera, Rockabilly, Rocksteady, Romani, Romantic, Roots Reggae, Rumba,
|
||||
Rune Singing, Russian Pop, Ryūkōka, Rōkyoku, Salegy, Salsa, Samba, Samba-Canção, Sankyoku, Schlager, Schranz, Score,
|
||||
Screamo, Screw, Sea Shanties, Sean-nós, Sephardic, Serenade, Seresta, Serial, Sermon, Sertanejo, Shaabi, Shibuya-Kei,
|
||||
Shidaiqu, Shinkyoku, Shoegaze, Shomyo, Singeli, Singspiel, Ska, Ska, Skiffle, Skweee, Slowcore, Sludge Metal,
|
||||
Smooth Jazz, Snap, Soca, Soft Rock, Sokyoku, Solo, Son, Son Montuno, Sonata, Sonero, Soukous, Soul, Soul-Jazz,
|
||||
Sound Art, Sound Collage, Sound Poetry, Soundtrack, Southern Rock, Space Rock, Space-Age, Spaza, Special Effects,
|
||||
Spectralism, Speech, Speed Garage, Speed Metal, Speedcore, Spiritual Jazz, Spirituals, Spoken Word, Sport, Sport,
|
||||
Steel Band, Stoner Rock, Story, Stride, String Ensemble, String Instrument, Style Galant, Suite, Sunshine Pop,
|
||||
Suomisaundi, Surf, Swamp Pop, Swing, Swingbeat, Symphonic Metal, Symphonic Rock, Symphony, Synth-pop, Synthpunk,
|
||||
Synthwave, Sámi Music, Séga, Taarab, Tamburitza, Tamil Film Music, Tango, Te Deum, Tech House, Tech Trance,
|
||||
Technical, Technical Death, Metal, Techno, Tejano, Texas Blues, Thai Classical, Theme, Theme With, Variations,
|
||||
Therapy, Thrash, Thug Rap, Timba, Toasting, Tone Poem, Trallalero, Trance, Trap, Tribal, Tribal House, Trip Hop,
|
||||
Trip Hop, Tropical House, Trova, Turntablism, Twelve-tone, Twist, UK Funky, UK Garage, UK Street Soul, Unblack Metal,
|
||||
V-pop, Vallenato, Vaporwave, Vaudeville, Verismo, Video Game Music, Viking Metal, Villancicos, Vocal, Vocaloid,
|
||||
Volksmusik, Waiata, Western Swing, Wind Ensemble, Witch House, Woodwind Instrument, Yemenite Jewish, Yoruba, Yé-Yé,
|
||||
Zamba, Zarzuela, Zemer Ivri, Zhongguo Feng, Zouk, Zydeco, Éntekhno_
|
||||
|
||||
Всего: ~810
|
||||
|
||||
### Схема группировки для LPON для модели TbMusicStyle в синонимы
|
||||
|
||||
**Статус:** ✅ Загружено 22 основных стиля с 729 синонимами
|
||||
|
||||
|
||||
| № | Категория | Синонимов | Примеры |
|
||||
|-----|--------------------------------------------|-----------|-----------------------------------------------------------|
|
||||
| 1 | Classic Rock | 14 | AOR, Acid Rock, Arena Rock, Hard Rock, Heavy Metal... |
|
||||
| 2 | Alternative & Indie Rock | 19 | Alt-Pop, Grunge, Shoegaze, Indie Rock, Dream Pop... |
|
||||
| 3 | Jazz | 21 | Big Band, Cool Jazz, Fusion, Bop, Dixieland... |
|
||||
| 4 | Blues | 17 | Chicago Blues, Delta Blues, Jump Blues, Electric Blues... |
|
||||
| 5 | Heavy Metal & Extreme Metal | 32 | Death Metal, Black Metal, Grindcore, Metalcore... |
|
||||
| 6 | Punk & Hardcore | 19 | Punk, Post-Hardcore, Emo, Screamo, Crust.. |
|
||||
| 7 | R&B & Hip Hop | 28 | Rap, Drill, Trap, Boom Bap, Memphis Rap... |
|
||||
| 8 | Reggae & Dub | 14 | Dancehall, Roots Reggae, Ska, Rocksteady... |
|
||||
| 9 | Folk, Country & Americana | 17 | Country, Bluegrass, Folk Rock, Honky Tonk... |
|
||||
| 10 | Prog & Art Rock | 10 | Progressive Rock, Krautrock, Psychedelic... |
|
||||
| 11 | Pop Music | 19 | Pop Rock, Bubblegum, Chanson, J-pop, K-pop... |
|
||||
| 12 | Disco, Funk & Soul | 29 | Funk, Soul, Boogie, Hi NRG, Neo Soul... |
|
||||
| 13 | Techno & House | 43 | House, Techno, Deep House, Minimal, Hardstyle... |
|
||||
| 14 | Trance & Hardcore Electronic | 8 | Trance, Goa Trance, Psy-Trance, Hardcore... |
|
||||
| 15 | Ambient, IDM & Down-tempo | 23 | Ambient, IDM, Drone, Trip Hop, Vaporwave... |
|
||||
| 16 | Bass, Breakbeat & Drum n Bass | 35 | Drum n Bass, Jungle, Breakbeat, Dubstep... |
|
||||
| 17 | Industrial, Noise & Avant-Garde Electronic | 11 | Industrial, Noise, EBM, Musique Concrète... |
|
||||
| 18 | New Wave & Synth-Pop | 13 | Synth-pop, New Wave, Coldwave, Synthwave... |
|
||||
| 19 | Classical & Academic | 72 | Classical, Baroque, Opera, Symphony, Chamber... |
|
||||
| 20 | World, Ethnic & Traditional | 240 | Salsa, Tango, Flamenco, Bhangra, Gnawa... |
|
||||
| 21 | Stage, Screen & Soundtracks | 13 | Soundtrack, Musical, Theme, Video Game Music... |
|
||||
| 22 | Non-Music / Spoken Word / Miscellaneous | 32 | Audiobook, Spoken Word, Comedy, Interview... |
|
||||
|
||||
### Полный справочник синонимов
|
||||
|
||||
#### 1. Classic Rock (14 синонимов)
|
||||
|
||||
```
|
||||
• AOR (Album Oriented Rock)
|
||||
• Acid Rock
|
||||
• Arena Rock
|
||||
• Beat
|
||||
• Hard Rock
|
||||
• Heavy Metal
|
||||
• Mod
|
||||
• Pub Rock
|
||||
• Rock & Roll
|
||||
• Southern Rock
|
||||
• Space Rock
|
||||
• Surf
|
||||
• Twist
|
||||
```
|
||||
|
||||
#### 2. Alternative & Indie Rock (19 синонимов)
|
||||
|
||||
```
|
||||
• Alt-Pop
|
||||
• Alternative Rock
|
||||
• Darkwave
|
||||
• Dream Pop
|
||||
• Goth Rock
|
||||
• Grunge
|
||||
• Hypnagogic Pop
|
||||
• Indie Pop
|
||||
• Indie Rock
|
||||
• Jangle Pop
|
||||
• Lo-Fi
|
||||
• Math Rock
|
||||
• Midwest Emo
|
||||
• Post Rock
|
||||
• Post-Grunge
|
||||
• Shoegaze
|
||||
• Slowcore
|
||||
• Stoner Rock
|
||||
```
|
||||
|
||||
#### 3. Jazz (21 синоним)
|
||||
|
||||
```
|
||||
• Avant-garde Jazz
|
||||
• Big Band
|
||||
• Bop
|
||||
• Cape Jazz
|
||||
• Contemporary Jazz
|
||||
• Cool Jazz
|
||||
• Dark Jazz
|
||||
• Dixieland
|
||||
• Free Jazz
|
||||
• Fusion
|
||||
• Gypsy Jazz
|
||||
• Hard Bop
|
||||
• Jazz-Rock
|
||||
• Jazzdance
|
||||
• Modal
|
||||
• Post Bop
|
||||
• Smooth Jazz
|
||||
• Spiritual Jazz
|
||||
• Stride
|
||||
• Swing
|
||||
```
|
||||
|
||||
#### 4. Blues (17 синонимов)
|
||||
|
||||
```
|
||||
• Blues Rock
|
||||
• Chicago Blues
|
||||
• Country Blues
|
||||
• Delta Blues
|
||||
• East Coast Blues
|
||||
• Electric Blues
|
||||
• Harmonica Blues
|
||||
• Hill Country Blues
|
||||
• Jug Band
|
||||
• Jump Blues
|
||||
• Louisiana Blues
|
||||
• Memphis Blues
|
||||
• Modern Electric Blues
|
||||
• Piano Blues
|
||||
• Piedmont Blues
|
||||
• Texas Blues
|
||||
```
|
||||
|
||||
#### 5. Heavy Metal & Extreme Metal (32 синонима)
|
||||
|
||||
```
|
||||
• Alternative Metal
|
||||
• Atmospheric Black Metal
|
||||
• Black Metal
|
||||
• Blackgaze
|
||||
• Brutal Death Metal
|
||||
• Crossover Thrash
|
||||
• Death Metal
|
||||
• Deathcore
|
||||
• Depressive Black Metal
|
||||
• Doom Metal
|
||||
• Funeral Doom Metal
|
||||
• Funk Metal
|
||||
• Goregrind
|
||||
• Gothic Metal
|
||||
• Grindcore
|
||||
• Groove Metal
|
||||
• Industrial Metal
|
||||
• Melodic Death Metal
|
||||
• Melodic Hardcore
|
||||
• Metalcore
|
||||
• Neo-Classical Metal
|
||||
• Nu Metal
|
||||
• Post-Metal
|
||||
• Power Metal
|
||||
• Progressive Metal
|
||||
• Sludge Metal
|
||||
• Symphonic Metal
|
||||
• Technical Death Metal
|
||||
• Thrash
|
||||
• Unblack Metal
|
||||
• Viking Metal
|
||||
```
|
||||
|
||||
#### 6. Punk & Hardcore (19 синонимов)
|
||||
|
||||
```
|
||||
• Anarcho-Punk
|
||||
• Antifolk
|
||||
• Britpop
|
||||
• Crust
|
||||
• Emo
|
||||
• Hardcore
|
||||
• Oi
|
||||
• Pop Punk
|
||||
• Post-Hardcore
|
||||
• Post Punk
|
||||
• Power Pop
|
||||
• Power Violence
|
||||
• Psychobilly
|
||||
• Punk
|
||||
• Screamo
|
||||
• Skiffle
|
||||
```
|
||||
|
||||
#### 7. R&B & Hip Hop (28 синонимов)
|
||||
|
||||
```
|
||||
• Amapiano
|
||||
• Azonto
|
||||
• Boom Bap
|
||||
• Cloud Rap
|
||||
• Conscious
|
||||
• Contemporary R&B
|
||||
• Crunk
|
||||
• Drill
|
||||
• Ghetto
|
||||
• Hip Hop
|
||||
• Hip-House
|
||||
• Hiplife
|
||||
• Horrorcore
|
||||
• Hyperpop
|
||||
• Hyphy
|
||||
• Jazzy Hip-Hop
|
||||
• Memphis Rap
|
||||
• Motswako
|
||||
• Nerdcore
|
||||
• Phonk
|
||||
• Pop Rap
|
||||
• Ragga HipHop
|
||||
• Rhythm & Blues
|
||||
• Snap
|
||||
• Spaza
|
||||
• Thug Rap
|
||||
• Trap
|
||||
```
|
||||
|
||||
#### 8. Reggae & Dub (14 синонимов)
|
||||
|
||||
```
|
||||
• Dancehall
|
||||
• Dub
|
||||
• Dub Poetry
|
||||
• Lovers Rock
|
||||
• Ragga
|
||||
• Rapso
|
||||
• Reggae Gospel
|
||||
• Reggae-Pop
|
||||
• Rocksteady
|
||||
• Roots Reggae
|
||||
• Ska
|
||||
• Toasting
|
||||
```
|
||||
|
||||
#### 9. Folk, Country & Americana (17 синонимов)
|
||||
|
||||
```
|
||||
• Acoustic
|
||||
• Appalachian Music
|
||||
• Bakersfield Sound
|
||||
• Bluegrass
|
||||
• Country
|
||||
• Country Rock
|
||||
• Filk
|
||||
• Folk
|
||||
• Folk Metal
|
||||
• Folk Rock
|
||||
• Hillbilly
|
||||
• Honky Tonk
|
||||
• Neofolk
|
||||
• Progressive Bluegrass
|
||||
• Rockabilly
|
||||
• Western Swing
|
||||
```
|
||||
|
||||
#### 10. Prog & Art Rock (10 синонимов)
|
||||
|
||||
```
|
||||
• Art Rock
|
||||
• Berlin-School
|
||||
• Group Sounds
|
||||
• Krautrock
|
||||
• Neo-Prog
|
||||
• Prog Rock
|
||||
• Psychedelic
|
||||
• Psychedelic Rock
|
||||
• Symphonic Rock
|
||||
```
|
||||
|
||||
#### 11. Pop Music (19 синонимов)
|
||||
|
||||
```
|
||||
• Ballad
|
||||
• Baroque Pop
|
||||
• Bubblegum
|
||||
• Chanson
|
||||
• Dance-pop
|
||||
• Europop
|
||||
• Highlife
|
||||
• Holiday
|
||||
• J-pop
|
||||
• K-pop
|
||||
• Kayōkyoku
|
||||
• Mandopop
|
||||
• Nursery Rhymes
|
||||
• Pop Rock
|
||||
• Russian Pop
|
||||
• Sunshine Pop
|
||||
• V-pop
|
||||
• Vocal
|
||||
• Yé-Yé
|
||||
```
|
||||
|
||||
#### 12. Disco, Funk & Soul (29 синонимов)
|
||||
|
||||
```
|
||||
• Acid Jazz
|
||||
• Bayou Funk
|
||||
• Boogie
|
||||
• Boogie Woogie
|
||||
• City Pop
|
||||
• Disco
|
||||
• Disco Polo
|
||||
• Electro Swing
|
||||
• Euro-Disco
|
||||
• Free Funk
|
||||
• Funk
|
||||
• G-Funk
|
||||
• Ghetto Tech
|
||||
• Go-Go
|
||||
• Hi NRG
|
||||
• Italo-Disco
|
||||
• Jazz-Funk
|
||||
• Minneapolis Sound
|
||||
• Motown
|
||||
• Neo Soul
|
||||
• New Jack Swing
|
||||
• Nu-Disco
|
||||
• P.Funk
|
||||
• RnB/Swing
|
||||
• Shibuya-Kei
|
||||
• Soul
|
||||
• Swingbeat
|
||||
• UK Street Soul
|
||||
```
|
||||
|
||||
#### 13. Techno & House (43 синонима)
|
||||
|
||||
```
|
||||
• Acid House
|
||||
• Ambient House
|
||||
• Balearic
|
||||
• Baltimore Club
|
||||
• Bouncy Techno
|
||||
• Deconstructed Club
|
||||
• Deep House
|
||||
• Deep Techno
|
||||
• Donk
|
||||
• Dub Techno
|
||||
• Electro House
|
||||
• Euro House
|
||||
• Favela Funk
|
||||
• French House
|
||||
• Future House
|
||||
• Garage House
|
||||
• Ghetto House
|
||||
• Hard House
|
||||
• Hard Techno
|
||||
• Hardstyle
|
||||
• House
|
||||
• Hyper Techno
|
||||
• Italo House
|
||||
• Italodance
|
||||
• J-Core
|
||||
• Jumpstyle
|
||||
• Kuduro
|
||||
• Lento Violento
|
||||
• Makina
|
||||
• Microhouse
|
||||
• Minimal
|
||||
• Minimal Techno
|
||||
• Moombahton
|
||||
• Nerdcore Techno
|
||||
• New Beat
|
||||
• Progressive House
|
||||
• Schranz
|
||||
• Speed Garage
|
||||
• Tech House
|
||||
• Techno
|
||||
• Tribal House
|
||||
• UK Funky
|
||||
```
|
||||
|
||||
#### 14. Trance & Hardcore Electronic (8 синонимов)
|
||||
|
||||
```
|
||||
• Euro Trance
|
||||
• Goa Trance
|
||||
• Hard Trance
|
||||
• Neo Trance
|
||||
• Progressive Trance
|
||||
• Psy-Trance
|
||||
• Trance
|
||||
```
|
||||
|
||||
#### 15. Ambient, IDM & Down-tempo (23 синонима)
|
||||
|
||||
```
|
||||
• Abstract
|
||||
• Acid
|
||||
• Ambient
|
||||
• Chillwave
|
||||
• Comfy Synth
|
||||
• Dark Ambient
|
||||
• Downtempo
|
||||
• Drone
|
||||
• Dungeon Synth
|
||||
• Electro
|
||||
• Glitch
|
||||
• IDM
|
||||
• Illbient
|
||||
• Leftfield
|
||||
• Lowercase
|
||||
• New Age
|
||||
• Plunderphonics
|
||||
• Skweee
|
||||
• Sound Art
|
||||
• Trip Hop
|
||||
• Vaporwave
|
||||
• Witch House
|
||||
```
|
||||
|
||||
#### 16. Bass, Breakbeat & Drum n Bass (35 синонимов)
|
||||
|
||||
```
|
||||
• Bass Music
|
||||
• Bassline
|
||||
• Big Beat
|
||||
• Bleep
|
||||
• Bounce
|
||||
• Breakbeat
|
||||
• Breakcore
|
||||
• Breaks
|
||||
• Britcore
|
||||
• Broken Beat
|
||||
• Bubbling
|
||||
• Club
|
||||
• Cut-up/DJ
|
||||
• DJ Battle Tool
|
||||
• Drum n Bass
|
||||
• Dubstep
|
||||
• Footwork
|
||||
• Freetekno
|
||||
• Gabber
|
||||
• Glitch Hop
|
||||
• Grime
|
||||
• Halftime
|
||||
• Hands Up
|
||||
• Happy Hardcore
|
||||
• Hard Beat
|
||||
• Jersey Club
|
||||
• Juke
|
||||
• Jungle
|
||||
• Progressive Breaks
|
||||
• Reggaeton
|
||||
• Speedcore
|
||||
• Supa
|
||||
• Turntablism
|
||||
• UK Garage
|
||||
```
|
||||
|
||||
#### 17. Industrial, Noise & Avant-Garde Electronic (11 синонимов)
|
||||
|
||||
```
|
||||
• EBM (Electronic Body Music)
|
||||
• Electroacoustic
|
||||
• Harsh Noise Wall
|
||||
• Industrial
|
||||
• Musique Concrète
|
||||
• Noise
|
||||
• Noise Rock
|
||||
• Noisecore
|
||||
• Power Electronics
|
||||
• Rhythmic Noise
|
||||
```
|
||||
|
||||
#### 18. New Wave & Synth-Pop (13 синонимов)
|
||||
|
||||
```
|
||||
• Coldwave
|
||||
• Electroclash
|
||||
• Eurobeat
|
||||
• Eurodance
|
||||
• Future Pop
|
||||
• NDW
|
||||
• New Wave
|
||||
• No Wave
|
||||
• Néo Kyma
|
||||
• Synth-pop
|
||||
• Synthpunk
|
||||
• Synthwave
|
||||
```
|
||||
|
||||
#### 19. Classical & Academic (72 синонима)
|
||||
|
||||
```
|
||||
• Andalusian Classical
|
||||
• Baroque
|
||||
• Bel Canto
|
||||
• Byzantine
|
||||
• Cantata
|
||||
• Capriccio
|
||||
• Chamber Music
|
||||
• Choral
|
||||
• Classical
|
||||
• Classical Guitar
|
||||
• Concert Band
|
||||
• Concerto
|
||||
• Concerto Grosso
|
||||
• Contemporary
|
||||
• Divertimento
|
||||
• Early
|
||||
• Expressionist
|
||||
• Etude
|
||||
• Fantasia
|
||||
• Formes Fixes
|
||||
• Futurism
|
||||
• Harpsichord
|
||||
• Impressionist
|
||||
• Incidental Music
|
||||
• Instrumental
|
||||
• Keyboard
|
||||
• Late Romantic
|
||||
• Lied
|
||||
• Light Music
|
||||
• Madrigal
|
||||
• Magnificat
|
||||
• Mass
|
||||
• Medieval
|
||||
• Modern Classical
|
||||
• Modern
|
||||
• Motet
|
||||
• Nocturne
|
||||
• Opera
|
||||
• Operetta
|
||||
• Oratorio
|
||||
• Orchestra
|
||||
• Organ
|
||||
• Overture
|
||||
• Passion
|
||||
• Percussion Ensemble
|
||||
• Piano
|
||||
• Pipe & Drum
|
||||
• Plainchant
|
||||
• Prelude
|
||||
• Program Music
|
||||
• Renaissance
|
||||
• Requiem
|
||||
• Rhapsody
|
||||
• Romantic
|
||||
• Serenade
|
||||
• Serial
|
||||
• Singspiel
|
||||
• Solo
|
||||
• Sonata
|
||||
• Spectralism
|
||||
• String Ensemble
|
||||
• String Instrument
|
||||
• Style Galant
|
||||
• Suite
|
||||
• Symphony
|
||||
• Te Deum
|
||||
• Theme With Variations
|
||||
• Twelve-tone
|
||||
• Verismo
|
||||
• Wind Ensemble
|
||||
• Woodwind Instrument
|
||||
```
|
||||
|
||||
#### 20. World, Ethnic & Traditional (240 синонимов)
|
||||
|
||||
```
|
||||
• Aboriginal, African, Afro-Cuban, Afrobeat
|
||||
• Aguinaldo, Anatolian Rock, Andean Music, Arabic Pop
|
||||
• Axé, Bachata, Baila, Baião, Ballet, Ballroom
|
||||
• Bambuco, Banda, Bangladeshi Classical, Barbershop
|
||||
• Basque Music, Batucada, Beguine, Bengali Music
|
||||
• Bećarac, Bhangra, Bolero, Bollywood, Bomba
|
||||
• Bongo Flava, Boogaloo, Bossa Nova, Bossanova
|
||||
• Brass Band, Brass Instrument, Brega, Bultrón
|
||||
• Caipira, Cajun, Calypso, Cambodian Classical
|
||||
• Candombe, Cantopop, Cantorial, Canzone Napoletana
|
||||
• Carimbó, Carnatic, Catalan Music, Celtic
|
||||
• Cha-Cha, Chacarera, Chamamé, Champeta, Charanga
|
||||
• Chinese Classical, Choro, Chutney, Cobla
|
||||
• Compas, Conjunto, Copla, Corrido, Cretan
|
||||
• Cuatro, Cubano, Cumbia, Currulao, Dabke
|
||||
• Dangdut, Dansband, Danzon, Descarga, Desert Blues
|
||||
• Duranguense, Ethno-pop, Exotica, Fado, Flamenco
|
||||
• Forró, Freestyle, Frevo, Funaná, Funkot
|
||||
• Gagaku, Gaita, Galician Traditional, Gamelan
|
||||
• Geet, Ghazal, Gnawa, Gospel, Gqom, Griot
|
||||
• Guaguancó, Guajira, Guaracha, Guarania, Guggenmusik
|
||||
• Gusle, Gwo Ka, Għana, Hawaiian, Hindustani
|
||||
• Hokkien Pop, Huayno, Indian Classical, Indo-Pop
|
||||
• Izvorna, Jibaro, Jiuta, Joropo, Jota
|
||||
• Junkanoo, Kaseko, Kaskawi, Keroncong, Khaliji
|
||||
• Kizomba, Klasik, Klezmer, Kolo, Korean Court Music
|
||||
• Kwaito, Lambada, Lao Music, Latin, Latin Jazz
|
||||
• Latin Pop, Laïkó, Levenslied, Liscio, Lounge
|
||||
• Luk Krung, Luk Thung, Lullaby, MPB, Maloya
|
||||
• Mambo, Manila Sound, Marcha Carnavalesca, Marches
|
||||
• Mariachi, Marimba, Mbalax, Mento, Merengue
|
||||
• Miami Bass, Milonga, Min'yō, Mizrahi, Mo Lam
|
||||
• Morna, Mugham, Musette, Música Criolla, Nagauta
|
||||
• Neopagan, Nhạc Vàng, Nordic, Norteño, Nueva Cancion
|
||||
• Nueva Trova, Népzene, Occitan, Ojkača, Ottoman Classical
|
||||
• Overtone Singing, Pachanga, Pacific, Pagode, Pasodoble
|
||||
• Persian Classical, Persian Pop, Philippine Classical
|
||||
• Phleng Phuea Chiwit, Piobaireachd, Plena, Polka
|
||||
• Porro, Qawwali, Quechua, Ranchera, Rara, Raï
|
||||
• Rebetiko, Romani, Rōkyoku, Salegy, Salsa, Samba
|
||||
• Samba-Canção, Sankyoku, Schlager, Sea Shanties
|
||||
• Sean-nós, Sephardic, Seresta, Sertanejo, Shaabi
|
||||
• Shidaiqu, Shinkyoku, Shomyo, Singeli, Soca
|
||||
• Sokyoku, Son, Son Montuno, Sonero, Soukous
|
||||
• Soul, Steel Band, Suomisaundi, Swamp Pop, Sámi Music
|
||||
• Séga, Taarab, Tamburitza, Tamil Film Music, Tango
|
||||
• Tejano, Thai Classical, Timba, Trallalero, Tribal
|
||||
• Trova, Vallenato, Villancicos, Volksmusik, Waiata
|
||||
• Yemenite Jewish, Yoruba, Zamba, Zarzuela
|
||||
• Zemer Ivri, Zhongguo Feng, Zouk, Zydeco, Éntekhno
|
||||
```
|
||||
|
||||
#### 21. Stage, Screen & Soundtracks (13 синонимов)
|
||||
|
||||
```
|
||||
• Anison
|
||||
• Cabaret
|
||||
• Concert Film
|
||||
• Movie Effects
|
||||
• Music Hall
|
||||
• Music Video
|
||||
• Musical
|
||||
• Score
|
||||
• Soundtrack
|
||||
• Theme
|
||||
• Vaudeville
|
||||
• Video Game Music
|
||||
```
|
||||
|
||||
#### 22. Non-Music / Spoken Word / Miscellaneous (32 синонима)
|
||||
|
||||
```
|
||||
• Audiobook
|
||||
• Break-In
|
||||
• Comedy
|
||||
• Dialogue
|
||||
• Education
|
||||
• Educational
|
||||
• Erotic
|
||||
• Field Recording
|
||||
• Health-Fitness
|
||||
• Interview
|
||||
• Karaoke
|
||||
• Medical
|
||||
• Military
|
||||
• Monolog
|
||||
• Novelty
|
||||
• Parody
|
||||
• Poetry
|
||||
• Political
|
||||
• Promotional
|
||||
• Public Broadcast
|
||||
• Public Service Announcement
|
||||
• Radioplay
|
||||
• Religious
|
||||
• Sermon
|
||||
• Special Effects
|
||||
• Speech
|
||||
• Spoken Word
|
||||
• Sport
|
||||
• Story
|
||||
• Therapy
|
||||
• Vocaloid
|
||||
```
|
||||
|
||||
### Проверка целостности БД
|
||||
|
||||
✅ **Все синонимы уникальны** — нет дубликатов внутри одной категории
|
||||
✅ **Нет пересечений** — синоним не может быть одновременно основным названием другой категории
|
||||
✅ **Полнота** — все ~810 стилей из Discogs распределены по 22 категориям
|
||||
✅ **Готовность к парсингу** — структура поддерживает будущую интеграцию с Discogs API
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# Генерирование криптографической соли для OFFER_HASHIDS_SALT
|
||||
|
||||
## Для Development (локально)
|
||||
|
||||
Текущее значение в `.env`. Если база из dev будет перемещаться в продакшен, то соль не нужно будет заменять на уникальную для продакшена. А то все старые QR-коды перестанут работать.
|
||||
|
||||
Если все-таки нужно обновить соль, используйте Django custom command (см. ниже).
|
||||
|
||||
## Для Production (сервер)
|
||||
|
||||
**НИКОГДА** не используйте соль из примеров! Генерируйте новую для каждого окружения:
|
||||
|
||||
### Способ 1: Python (быстро)
|
||||
```bash
|
||||
python3 -c "import secrets; print(secrets.token_hex(16))"
|
||||
```
|
||||
|
||||
Результат:
|
||||
```
|
||||
a7f3c82b9e1dEa6b5c8f2e3d0a9b4c7f
|
||||
```
|
||||
|
||||
Скопируйте и обновите в `.env` на сервере:
|
||||
```
|
||||
OFFER_HASHIDS_SALT=a7f3c82b9e1dEa6b5c8f2e3d0a9b4c7f
|
||||
```
|
||||
|
||||
### Способ 2: Linux/Mac (встроенный)
|
||||
```bash
|
||||
openssl rand -hex 16
|
||||
```
|
||||
|
||||
### Способ 3: Более надежная соль (32 байта вместо 16)
|
||||
```bash
|
||||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
Результат (64 символа, очень стойко):
|
||||
```
|
||||
26aebe8af9efe7c5f81c64cb18846318cc81a83eaf15aedb7f4b8a80990c9676
|
||||
```
|
||||
|
||||
## Важные правила
|
||||
|
||||
1. **Уникальная для каждого окружения** (каждой реализации LPON под каждого сейлера)
|
||||
- Dev ≠ Staging ≠ Production
|
||||
- Разные соли = разные коды для одного ID
|
||||
|
||||
2. **Никогда не меняйте в production!**
|
||||
- Если поменяете соль → старые коды перестанут работать
|
||||
- Существующие QR-коды станут невалидными
|
||||
- Существующие ID в базе не будут декодироваться
|
||||
- **Если все же нужно изменить:** используйте custom command `regenerate_offer_codes` (см. ниже)
|
||||
|
||||
3. **Хранить в .env (не в репозитории)**
|
||||
- `.env` в .gitignore ✓
|
||||
- `.env.example` содержит шаблон ✓
|
||||
|
||||
## Проверка качества соли
|
||||
|
||||
```python
|
||||
import secrets
|
||||
|
||||
# Хорошая соль (минимум 32 символа, hex-формат)
|
||||
salt = secrets.token_hex(16) # ✓
|
||||
salt = secrets.token_hex(32) # ✓✓ еще лучше
|
||||
|
||||
# Плохие соли
|
||||
salt = "my-password" # ✗ слишком короткая
|
||||
salt = "12345678" # ✗ предсказуемая
|
||||
salt = "qwerty123" # ✗ слабая энтропия
|
||||
```
|
||||
|
||||
## Параметры OFFER_HASHIDS_MIN_LENGTH
|
||||
|
||||
| Кол-во оферов | min_length | Пример | Примечание |
|
||||
|---------------|------------|--------------|---------------------------------------|
|
||||
| До 1 000 | 4 | `a1bC` | Слишком короткие, может быть коллизии |
|
||||
| До 10 000 | 5 | `a1bCd` | Хорошо для небольших каталогов |
|
||||
| До 100 000 | 6 | `a1bCdE` | **РЕКОМЕНДУЕМО** для начала |
|
||||
| До 1 000 000 | 8 | `a1bCdEfG` | Для больших каталогов |
|
||||
| Более 1M | 10 | `a1bCdEfGhI` | Очень большие каталоги |
|
||||
|
||||
**Текущее значение:** `OFFER_HASHIDS_MIN_LENGTH=6` (оптимально)
|
||||
|
||||
## Как увеличить при необходимости?
|
||||
|
||||
Если вырос каталог:
|
||||
|
||||
1. Обновите в `.env`:
|
||||
```
|
||||
OFFER_HASHIDS_MIN_LENGTH=8
|
||||
```
|
||||
|
||||
2. Перезагрузите приложение
|
||||
|
||||
3. **Старые коды останутся валидными!** (hashids декодирует любой код независимо от min_length)
|
||||
|
||||
4. Новые офферы будут кодироваться с длиной 8
|
||||
|
||||
## Django Custom Command: `regenerate_offer_codes`
|
||||
|
||||
**Расположение:** `lpon_site/frontend/management/commands/regenerate_offer_codes.py`
|
||||
|
||||
Используется для проверки, восстановления и перегенерации s_offer_code офферов.
|
||||
|
||||
### Три режима работы
|
||||
|
||||
#### 1. Проверка кодов (режим `--check`)
|
||||
|
||||
Декодирует все коды обратно в ID и проверяет корректность:
|
||||
|
||||
```bash
|
||||
# Быстрая проверка
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check
|
||||
|
||||
# С подробным выводом
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check --verbose
|
||||
```
|
||||
|
||||
#### 2. Исправление некорректных кодов (режим `--fix-broken`)
|
||||
|
||||
Обновляет только коды, которые не декодируются правильно:
|
||||
|
||||
```bash
|
||||
# Пробный запуск (ничего не сохранит)
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --fix-broken --dry-run
|
||||
|
||||
# Реальное исправление
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --fix-broken
|
||||
```
|
||||
|
||||
**Использование:** Когда некоторые коды повреждены или закодированы неправильно (например, после сбоя БД).
|
||||
|
||||
#### 3. Полное обновление всех кодов (по умолчанию)
|
||||
|
||||
Перегенерирует ВСЕ коды на основе текущего OFFER_HASHIDS_SALT:
|
||||
|
||||
```bash
|
||||
# Пробный запуск
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --dry-run
|
||||
|
||||
# Реальное обновление
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes
|
||||
```
|
||||
|
||||
**ВНИМАНИЕ:** Используется только при смене OFFER_HASHIDS_SALT на production!
|
||||
|
||||
### Примеры использования
|
||||
|
||||
**Сценарий 1: Проверка целостности после сбоя**
|
||||
```bash
|
||||
# Сначала проверяем что сломалось
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check
|
||||
|
||||
# Если есть некорректные коды - исправляем
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --fix-broken
|
||||
```
|
||||
|
||||
**Сценарий 2: Миграция на новый сервер с новой солью**
|
||||
```bash
|
||||
# 1. Обновляем .env с новой солью
|
||||
OFFER_HASHIDS_SALT=новая_соль_из_secrets
|
||||
|
||||
# 2. Перегенерируем все коды (пробный запуск сначала)
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --dry-run
|
||||
|
||||
# 3. Если хорошо - реальное обновление
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes
|
||||
|
||||
# 4. Проверяем что все работает
|
||||
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check
|
||||
```
|
||||
|
||||
### Опции команды
|
||||
|
||||
```
|
||||
--check Проверить корректность всех кодов (декодировать обратно в id)
|
||||
--fix-broken Обновить только коды которые не декодируются правильно
|
||||
--dry-run Показать что будет изменено, но не сохранять
|
||||
--verbose Показывать подробный прогресс для каждого оффера
|
||||
```
|
||||
|
||||
Использование:
|
||||
|
||||
```bash
|
||||
# Проверка
|
||||
python manage.py regenerate_offer_codes --check
|
||||
|
||||
# Исправление
|
||||
python manage.py regenerate_offer_codes --fix-broken
|
||||
|
||||
# Полное обновление (при смене соли)
|
||||
python manage.py regenerate_offer_codes
|
||||
```
|
||||
@@ -0,0 +1,595 @@
|
||||
# Паттерн создания справочников: автоматизация статей, синонимов и валидации
|
||||
|
||||
**Дата:** 2026-07-15
|
||||
**Статус:** Актуально ✓
|
||||
|
||||
## Обзор
|
||||
|
||||
Система содержит 5 **справочных сущностей** (Reference Entities) которые описывают каталог:
|
||||
|
||||
| Модель | Таблица | Назначение | Связь с TbArticle |
|
||||
|-------------|--------------|------------------------------------------|-------------------|
|
||||
| Лейбл | TbLabel | Издатель, лейбл, дистрибьютор | 1:1 OneToOne |
|
||||
| Исполнитель | TbArtist | Артист, группа, бренд | 1:1 OneToOne |
|
||||
| Товар | TbItem | Альбом, релиз, носитель, аксессуар | 1:1 OneToOne |
|
||||
| Стиль | TbMusicStyle | Музыкальный жанр | 1:1 OneToOne |
|
||||
| Продавец | TbSeller | Магазин, продавец, издатель-дистрибьютор | 1:1 OneToOne |
|
||||
|
||||
Все 5 моделей используют **единый паттерн**:
|
||||
1. Автоматическое создание связанной статьи (если нет)
|
||||
2. Управление синонимами в метаданных
|
||||
3. Валидация дубликатов через админку
|
||||
4. Подготовка к парсингу с очередью конфликтов
|
||||
|
||||
## Архитектура: TbArticle как связующее звено
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TbArticle │
|
||||
│ (текст, SEO, картинка, слаг — для всех справочников) │
|
||||
│ │
|
||||
│ Поля: │
|
||||
│ • s_article_title: "Label: Sony" │
|
||||
│ • l_article_type: "label" │
|
||||
│ • slug: "label-sony" (URL-идентификатор) │
|
||||
│ • k_article_to_image: ссылка на картинку (логотип, фото) │
|
||||
│ • seo_title, seo_description: для поисковиков и llm │
|
||||
│ • s_article_teaser_html, s_article_content_html: текст │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲ ▲ ▲ ▲
|
||||
│ │ │ │
|
||||
1:1 1:1 1:1 1:1
|
||||
│ │ │ │
|
||||
┌────────┴────┐ ┌────────┴─────┐ ┌────────┴────┐ ┌──┴──────────┐
|
||||
│ TbLabel │ │ TbArtist │ │ TbItem │ │ TbMusicStyle│
|
||||
│ │ │ │ │ │ │ │
|
||||
│ s_label │ │ s_artist │ │ s_item │ │ s_style_name│
|
||||
│ j_metadata │ │ j_metadata │ │ j_metadata │ │ j_metadata │
|
||||
│ (синонимы) │ │ (синонимы) │ │ (синонимы) │ │ (синонимы) │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘
|
||||
|
||||
TbSeller аналогично, без синонимов (используется редко)
|
||||
```
|
||||
|
||||
## Жизненный цикл справочника (на примере TbLabel)
|
||||
|
||||
### Фаза 1: Создание в админке
|
||||
|
||||
```python
|
||||
# Админ создает новый лейбл через Django Admin форму
|
||||
# Заполняет: s_label="Sony Music", j_label_metadata={"SYN_EN": [...]}
|
||||
|
||||
# При сохранении форма вызывает label.save()
|
||||
```
|
||||
|
||||
### Фаза 2: Валидация синонимов (админка)
|
||||
|
||||
```python
|
||||
# Функция validate_for_duplicates() проверяет:
|
||||
|
||||
# 1. EXACT_MATCH: s_label == s_label в других записях?
|
||||
# Результат: "Sony Music" уже есть в базе
|
||||
# Действие: Форма показывает ошибку, блокирует сохранение
|
||||
|
||||
# 2. FIND_IN_SYNONYM: s_label найден в синонимах других записей?
|
||||
# Результат: "Sony Music" найден в синонимах записи TbLabel(pk=5)
|
||||
# Действие: Админ видит список дубликатов, решает что делать
|
||||
|
||||
# 3. EXACT_SYNONYM_MATCH: синонимы разных лейблов совпадают?
|
||||
# Результат: {"SYN_EN": ["Sony", "Sony Music"]} уже есть в записи TbLabel(pk=3)
|
||||
# Действие: Ошибка, нужно исправить синонимы
|
||||
#
|
||||
# 4. PARTIAL_MATCH: частичное совпадение синонимов
|
||||
# Не критичные совпадения. s_label и синонимы из метаданных разбиваются по словам, ищутся совпадения слов.
|
||||
# Админ видит предупреждение. Для коротких слов (Inc, Ltd т.п.) может много ложных срабатываний. Админу показывается
|
||||
# список слов-совпадений, он с чем они совпали, и решает что делать.
|
||||
```
|
||||
|
||||
### Фаза 3: Автоматическое создание статьи
|
||||
|
||||
```python
|
||||
# Если админ одобрил - вызывается label.save()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Шаг 1: Валидация синонимов
|
||||
validate_and_raise_for_duplicates(self, 's_label', 'j_label_metadata')
|
||||
|
||||
# Шаг 2: Обновление синонимов метаданных
|
||||
update_synonyms_in_metadata(self, 's_label', 'j_label_metadata')
|
||||
|
||||
# Шаг 3: Создание или получение связанной статьи
|
||||
article = create_or_get_related_article(
|
||||
self,
|
||||
TbArticle.ArticleType.LABEL, # тип статьи
|
||||
's_label', # основное поле
|
||||
'j_label_metadata', # метаданные
|
||||
'k_label_to_article' # FK на статью
|
||||
)
|
||||
self.k_label_to_article = article
|
||||
|
||||
# Шаг 4: Сохранение
|
||||
super().save(*args, **kwargs)
|
||||
```
|
||||
|
||||
**Что делает `create_or_get_related_article()`:**
|
||||
|
||||
```python
|
||||
def create_or_get_related_article(
|
||||
instance, # TbLabel, TbArtist и т.д.
|
||||
article_type, # TbArticle.ArticleType.LABEL
|
||||
main_field_name, # 's_label'
|
||||
metadata_field_name, # 'j_label_metadata'
|
||||
fk_field_name # 'k_label_to_article'
|
||||
):
|
||||
# 1. Генерируем название для статьи
|
||||
article_title = f"Label: {instance.s_label}"
|
||||
|
||||
# 2. Ищем существующую статью
|
||||
article = TbArticle.objects.filter(
|
||||
s_article_title=article_title,
|
||||
l_article_type=article_type
|
||||
).first()
|
||||
|
||||
if article:
|
||||
return article # Используем существующую
|
||||
|
||||
# 3. Создаём новую статью
|
||||
article = TbArticle.objects.create(
|
||||
s_article_title=article_title,
|
||||
l_article_type=article_type,
|
||||
b_article_published=True,
|
||||
# slug генерируется автоматически в TbArticle.save()
|
||||
)
|
||||
|
||||
return article
|
||||
```
|
||||
|
||||
## Синонимы: управление и валидация
|
||||
|
||||
### Как формируются синонимы
|
||||
|
||||
Синонимы хранятся в JSONField `j_*_metadata` в формате:
|
||||
|
||||
```json
|
||||
{
|
||||
"SYN_EN": [
|
||||
"The Beatles",
|
||||
"Beatles",
|
||||
"Beatles, The",
|
||||
"The Fab Four"
|
||||
],
|
||||
"other_data": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### Автоматическое обновление синонимов при сохранении
|
||||
|
||||
**Функция `update_synonyms_in_metadata()`:**
|
||||
|
||||
```python
|
||||
def update_synonyms_in_metadata(instance, main_field_name, metadata_field_name):
|
||||
"""
|
||||
При каждом сохранении:
|
||||
1. Если это новая запись: добавляем s_label в синонимы
|
||||
2. Если s_label изменился: добавляем оба (старый и новый) в синонимы
|
||||
3. Админ может редактировать синонимы через форму (приоритет админу)
|
||||
"""
|
||||
|
||||
current_value = getattr(instance, main_field_name) # "Sony Music"
|
||||
original_value = instance._original_value # "Sony"
|
||||
|
||||
# Получаем или создаём словарь метаданных
|
||||
metadata = getattr(instance, metadata_field_name) or {}
|
||||
synonyms = metadata.get(KEY_SYNONYM_EN, [])
|
||||
|
||||
# Добавляем новые синонимы
|
||||
if original_value and original_value != current_value:
|
||||
# Изменилось название - добавляем оба
|
||||
synonyms.extend([original_value, current_value])
|
||||
else:
|
||||
# Новая запись - добавляем текущее
|
||||
synonyms.append(current_value)
|
||||
|
||||
# Удаляем дубликаты и короткие слова
|
||||
synonyms = [s for s in set(synonyms) if len(s) >= MIN_SYNONYM_WORD_LENGTH]
|
||||
|
||||
metadata[KEY_SYNONYM_EN] = sorted(synonyms)
|
||||
setattr(instance, metadata_field_name, metadata)
|
||||
```
|
||||
|
||||
### Валидация синонимов при сохранении
|
||||
|
||||
```python
|
||||
# Проверяем: все ли синонимы уникальны?
|
||||
|
||||
validate_for_duplicates(
|
||||
model_class=TbLabel,
|
||||
instance_pk=label.id,
|
||||
main_field_value=label.s_label,
|
||||
metadata_dict=label.j_label_metadata,
|
||||
main_field_name='s_label',
|
||||
metadata_field_name='j_label_metadata',
|
||||
)
|
||||
|
||||
# Если найдены дубликаты (FIND_IN_SYNONYM, EXACT_SYNONYM_MATCH):
|
||||
# - В админке: показываем форму с ошибкой + список дубликатов
|
||||
# - В парсере (будущее): создаём запись в ValidatorQueue для админа
|
||||
```
|
||||
|
||||
## Админка: как это видит пользователь
|
||||
|
||||
### Создание нового лейбла
|
||||
|
||||
```
|
||||
Админ заполняет форму:
|
||||
┌────────────────────────────┐
|
||||
│ Название лейбла* │
|
||||
│ [ Sony Music ] │
|
||||
│ │
|
||||
│ Метаданные (JSON) │
|
||||
│ { "SYN_EN": [...] } │
|
||||
│ │
|
||||
│ [ Сохранить ] │
|
||||
└────────────────────────────┘
|
||||
|
||||
При сохранении:
|
||||
1. Проверяются синонимы (EXACT_MATCH, FIND_IN_SYNONYM)
|
||||
2. Если конфликт → показывается ошибка:
|
||||
"Найдено совпадение! Существующие записи: [List]"
|
||||
|
||||
3. Если ОК → статья создаётся автоматически
|
||||
"Лейбл создан. Связанная статья: Label: Sony Music"
|
||||
```
|
||||
|
||||
### Обновление существующего лейбла
|
||||
|
||||
```
|
||||
Админ меняет название (например, опечатка):
|
||||
[ Sony Musik ] → [ Sony Music ]
|
||||
|
||||
1. Синонимы обновляются: добавляются оба варианта
|
||||
2. Статья остаётся той же (slug не меняется)
|
||||
3. Сохраняется историчность: "был Sony Musik, теперь Sony Music"
|
||||
```
|
||||
|
||||
### Ошибки при сохранении
|
||||
|
||||
```
|
||||
Сценарий: Админ пытается создать дубликат
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ ОШИБКА: TbLabel.save() │
|
||||
│ Найдено совпадение в синонимах! │
|
||||
│ Разрешите на уровне админки. │
|
||||
│ PK конфликтующих записей: [1, 5, 12] │
|
||||
│ │
|
||||
│ Подробно: "Sony Music" найдено в │
|
||||
│ синонимах TbLabel(pk=5) │
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
Решение: Админ либо меняет название, либо
|
||||
объединяет с существующей записью вручную.
|
||||
```
|
||||
|
||||
## Применение ко всем 5 моделям
|
||||
|
||||
### TbLabel (Лейблы)
|
||||
|
||||
```python
|
||||
class TbLabel(models.Model):
|
||||
s_label: str (уникальный) # "Sony Music", "Мелодия"
|
||||
j_label_metadata: JSON # {"SYN_EN": [...]}
|
||||
k_label_to_article: FK → TbArticle # автоматическое создание
|
||||
|
||||
# ArticleType: LABEL
|
||||
# Формат названия: "Label: Sony Music"
|
||||
```
|
||||
|
||||
### TbArtist (Исполнители)
|
||||
|
||||
```python
|
||||
class TbArtist(models.Model):
|
||||
s_artist: str (уникальный) # "The Beatles", "David Bowie"
|
||||
j_artist_metadata: JSON # {"SYN_EN": [...]}
|
||||
k_artist_to_article: FK → TbArticle # автоматическое создание
|
||||
|
||||
# ArticleType: ARTIST
|
||||
# Формат названия: "Artist: The Beatles"
|
||||
```
|
||||
|
||||
### TbMusicStyle (Стили)
|
||||
|
||||
```python
|
||||
class TbMusicStyle(models.Model):
|
||||
s_style_name: str (уникальный) # "Rock", "Jazz"
|
||||
j_style_metadata: JSON # {"SYN_EN": [...]}
|
||||
k_style_to_article: FK → TbArticle # автоматическое создание
|
||||
|
||||
# ArticleType: STYLE
|
||||
# Формат названия: "Style: Rock"
|
||||
```
|
||||
|
||||
### TbItem (Товары/Релизы)
|
||||
|
||||
```python
|
||||
class TbItem(models.Model):
|
||||
s_item: str (уникальный) # "Abbey Road (LP)"
|
||||
j_item_metadata: JSON # {"SYN_EN": [...]}
|
||||
k_item_to_article: FK → TbArticle # автоматическое создание
|
||||
|
||||
# ArticleType: ITEM
|
||||
# Формат названия: "Item: Abbey Road (LP)"
|
||||
```
|
||||
|
||||
### TbSeller (Продавцы)
|
||||
|
||||
```python
|
||||
class TbSeller(models.Model):
|
||||
s_seller: str (уникальный) # "Клюква Records", "Amazon"
|
||||
j_seller_metadata: JSON # (синонимы не используются)
|
||||
k_seller_to_article: FK → TbArticle # автоматическое создание
|
||||
|
||||
# ArticleType: SELLER
|
||||
# Формат названия: "Seller: Клюква Records"
|
||||
# НЕ имеет синонимов (редко меняется, используется редко)
|
||||
```
|
||||
|
||||
## Парсинг (будущее): интеграция с очередью конфликтов
|
||||
|
||||
### Сценарий: парсер вытащил данные из Discogs
|
||||
|
||||
```python
|
||||
# Парсер получил из Discogs:
|
||||
incoming_data = {
|
||||
's_artist': 'The Beatles',
|
||||
'j_artist_metadata': {
|
||||
'SYN_EN': ['Beatles', 'The Fab Four'],
|
||||
'DISCOGS_ID': 123456,
|
||||
'COUNTRY': 'UK',
|
||||
'FORMED': '1960',
|
||||
}
|
||||
}
|
||||
|
||||
# Парсер пытается создать TbArtist(**incoming_data)
|
||||
artist = TbArtist(**incoming_data)
|
||||
artist.save() # Вызывает валидацию
|
||||
|
||||
# Если есть конфликт (FIND_IN_SYNONYM, EXACT_SYNONYM_MATCH):
|
||||
# - Парсер ловит ValidationError
|
||||
# - Создаёт запись в ValidatorQueue
|
||||
# - Админ получает уведомление про конфликт
|
||||
# - Админ решает: создать новую / объединить / пропустить
|
||||
```
|
||||
|
||||
### Структура ValidatorQueue для справочников
|
||||
|
||||
```python
|
||||
ValidatorQueue(
|
||||
k_source=excel_file_source,
|
||||
l_model_type='TbArtist',
|
||||
l_conflict_type='FIND_IN_SYNONYM', # тип конфликта
|
||||
model_pk=None, # это новая запись (или PK существующей)
|
||||
|
||||
j_incoming_data={
|
||||
's_artist': 'The Beatles',
|
||||
'j_artist_metadata': {'SYN_EN': [...], 'DISCOGS_ID': ...}
|
||||
},
|
||||
|
||||
j_duplicates=[
|
||||
{
|
||||
'pk': 15,
|
||||
's_artist': 'Beatles',
|
||||
'matched_value': 'The Beatles',
|
||||
'match_type': 'FIND_IN_SYNONYM',
|
||||
'reason': 'Найдено в синонимах'
|
||||
}
|
||||
],
|
||||
|
||||
l_status='pending', # Ждёт решения админа
|
||||
s_admin_decision=None, # Админ ещё не решил
|
||||
)
|
||||
```
|
||||
|
||||
### Решения админа
|
||||
|
||||
```
|
||||
1. "create_new" — создать новую запись (с дубликатом)
|
||||
Когда: Если это действительно разные сущности (например,
|
||||
The Beatles (группа) vs The Beatles (альбом))
|
||||
|
||||
2. "merge" — объединить с существующей
|
||||
Когда: Если это одна и та же сущность, только разные названия
|
||||
Действие: Добавить синонимы в существующую запись
|
||||
|
||||
3. "skip" — пропустить
|
||||
Когда: Данные некорректны или дубликаты действительно
|
||||
Действие: Ничего не создавать, просто пропустить
|
||||
```
|
||||
|
||||
## Метаданные для парсинга: проверить, можно что-то вытащить из внешних источников
|
||||
|
||||
### Из Discogs (для всех справочников)
|
||||
|
||||
```json
|
||||
{
|
||||
"DISCOGS_ID": 123456, // ID записи в Discogs
|
||||
"DISCOGS_URL": "https://...", // Ссылка на Discogs
|
||||
"DISCOGS_UPDATED": "2024-01-15",// Дата обновления на Discogs
|
||||
"SYN_EN": [ // Синонимы на английском
|
||||
"Alternative name",
|
||||
"Another variant"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Для TbArtist (исполнители) — из Discogs, MusicBrainz
|
||||
|
||||
```json
|
||||
{
|
||||
"ARTIST_TYPE": "Group", // Solo, Group, Orchestra, Character
|
||||
"FORMED": "1960", // Год образования
|
||||
"DISBANDED": null, // Год распада (если есть)
|
||||
"COUNTRY": "GB", // Страна (ISO 3166-1)
|
||||
"AREA": "United Kingdom", // Регион
|
||||
"MEMBERS": [ // Участники (для групп)
|
||||
{"name": "John Lennon", "role": "Vocals"},
|
||||
{"name": "Paul McCartney", "role": "Bass"},
|
||||
],
|
||||
"WEBSITE": "https://beatles.com", // Официальный сайт
|
||||
"WIKIPEDIA": "https://en.wikipedia.org/...", // Статья
|
||||
"MUSICBRAINZ_ID": "uuid-here", // MusicBrainz ID
|
||||
"BIOGRAPHY": "Краткая биография...", // Текст для статьи
|
||||
"IMG_URL": "https://...", // Ссылка на изображение
|
||||
"YANDEX_MUSIC_ID": 123456, // ID на Яндекс.Музыке
|
||||
"SPOTIFY_ID": "spotify-id", // ID на Spotify
|
||||
"SYN_EN": [...],
|
||||
"SYN_RU": [...] // Синонимы на русском
|
||||
}
|
||||
```
|
||||
|
||||
### Для TbLabel (лейблы) — из Discogs, MusicBrainz
|
||||
|
||||
```json
|
||||
{
|
||||
"LABEL_TYPE": "Production", // Production, Reissue, Distributor
|
||||
"FOUNDED": "1945", // Год основания
|
||||
"COUNTRY": "USA", // Страна
|
||||
"HEADQUARTERS": "New York, NY", // Штаб-квартира
|
||||
"PARENT_LABEL": 456789, // ID материнского лейбла (Discogs)
|
||||
"WEBSITE": "https://sony.com", // Официальный сайт
|
||||
"WIKIPEDIA": "https://...", // Статья
|
||||
"CONTACT_EMAIL": "info@...", // Email контакта
|
||||
"CONTACT_PHONE": "+1-234-567", // Телефон
|
||||
"COMPANY_SIZE": "Large", // Small, Medium, Large, Mega
|
||||
"DISCOGS_CATALOG_PREFIX": "SR", // Префикс каталога
|
||||
"IMG_LOGO": "https://...", // Логотип
|
||||
"SYN_EN": [...],
|
||||
"SYN_RU": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Для TbItem (релизы/альбомы) — из Discogs, MusicBrainz
|
||||
|
||||
```json
|
||||
{
|
||||
"ITEM_TYPE": "Album", // Album, Single, EP, Compilation
|
||||
"RELEASE_DATE": "1969-09-26", // Дата выхода
|
||||
"FORMAT": "Vinyl LP", // Vinyl LP, CD, Cassette, Digital
|
||||
"COUNTRY": "GB", // Страна выхода
|
||||
"BARCODE": "093624892340", // Штрихкод
|
||||
"DISCOGS_ID": 987654, // ID мастер-релиза
|
||||
"MUSICBRAINZ_ID": "uuid", // MusicBrainz ID
|
||||
"GENRES": ["Rock", "Pop"], // Жанры (для синхронизации с TbMusicStyle)
|
||||
"CATALOG_NUMBER": "SR-123", // Каталожный номер издателя
|
||||
"LANGUAGE": "English", // Язык вокала/текста
|
||||
"TRACKS_COUNT": 14, // Количество треков
|
||||
"DURATION": "42:30", // Длительность альбома
|
||||
"RATING": 4.5, // Рейтинг на Discogs (1-5)
|
||||
"NOTES": "Limited edition...", // Примечания
|
||||
"IMG_COVER": "https://...", // Обложка альбома
|
||||
"YANDEX_MUSIC_ID": 123456, // ID на Яндекс.Музыке
|
||||
"SPOTIFY_ID": "spotify-id", // ID на Spotify
|
||||
"SYN_EN": [...],
|
||||
"SYN_RU": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Для TbMusicStyle (жанры/стили) — из Discogs, MusicBrainz, Wikipedia
|
||||
|
||||
```json
|
||||
{
|
||||
"STYLE_TYPE": "Primary", // Primary, Sub-genre
|
||||
"DESCRIPTION": "Rock music is...", // Описание жанра
|
||||
"ORIGIN_COUNTRY": "USA", // Страна происхождения
|
||||
"ORIGIN_YEAR": "1950s", // Период возникновения
|
||||
"PARENT_GENRE": "Rock", // Основной жанр (для иерархии)
|
||||
"RELATED_STYLES": [ // Связанные жанры
|
||||
"Hard Rock",
|
||||
"Progressive Rock"
|
||||
],
|
||||
"NOTABLE_ARTISTS": [ // Известные исполнители
|
||||
"The Beatles",
|
||||
"Rolling Stones"
|
||||
],
|
||||
"WIKIPEDIA": "https://...", // Статья в Википедии
|
||||
"IMG_ILLUSTRATION": "https://...", // Иллюстрация для жанра
|
||||
"SYN_EN": [...],
|
||||
"SYN_RU": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Для TbSeller (продавцы) — из веб-скрейпинга, API или Excel (для excel и ручного ввода заполнять руками)
|
||||
|
||||
```json
|
||||
{
|
||||
"SELLER_TYPE": "Store", // Store, Label, Distributor, Marketplace
|
||||
"FOUNDED": "2005", // Год основания
|
||||
"COUNTRY": "RU", // Страна
|
||||
"CITY": "Moscow", // Город
|
||||
"ADDRESS": "ul. Pushkina, 42", // Адрес
|
||||
"WEBSITE": "https://example.com", // Сайт
|
||||
"EMAIL": "sales@example.com", // Email
|
||||
"PHONE": "+7-495-123-45-67", // Телефон
|
||||
"SOCIAL_VK": "https://vk.com/...", // VKontakte
|
||||
"SOCIAL_INSTAGRAM": "https://...", // Instagram
|
||||
"SHIPPING_COUNTRIES": [ // Страны доставки
|
||||
"RU", "BY", "KZ"
|
||||
],
|
||||
"PAYMENT_METHODS": [ // Методы оплаты
|
||||
"Card", "Bank transfer", "Cash"
|
||||
],
|
||||
"RETURNS_POLICY": "7 days", // Политика возврата
|
||||
"RATING": 4.8, // Рейтинг на платформе
|
||||
"REVIEWS_COUNT": 342, // Количество отзывов
|
||||
"IMG_LOGO": "https://...", // Логотип магазина
|
||||
}
|
||||
```
|
||||
|
||||
## Жизненный цикл в нескольких слов
|
||||
|
||||
```
|
||||
Админка Парсер (будущее)
|
||||
│ │
|
||||
├─ Заполняет форму ├─ Читает Discogs/Excel
|
||||
│ (название, синонимы) │ (вытаскивает данные)
|
||||
│ │
|
||||
├─ Нажимает "Сохранить" ├─ Вызывает Model.save()
|
||||
│ │
|
||||
├─ Валидация синонимов ├─ Валидация синонимов
|
||||
│ (FIND_IN_SYNONYM?) │ (конфликты?)
|
||||
│ │
|
||||
├─ Ошибка? → показать админу ├─ Конфликт? → ValidatorQueue
|
||||
│ │
|
||||
├─ OK? → создать статью ├─ OK? → создать модель
|
||||
│ автоматически │ + статья автоматически
|
||||
│ │
|
||||
└─ Данные целостны и готовы └─ Админ решает позже
|
||||
```
|
||||
|
||||
## Преимущества подхода
|
||||
|
||||
✓ **Единообразие**: 5 моделей работают по одному паттерну
|
||||
✓ **Автоматизация**: Статьи создаются сами, не нужна ручная работа
|
||||
✓ **Валидация**: Дубликаты и синонимы проверяются автоматически
|
||||
✓ **Контроль**: Админ видит конфликты и может принять решение
|
||||
✓ **Масштабируемость**: Легко добавить другие справочники (если нужны)
|
||||
✓ **SEO-дружественно**: Каждый справочник имеет URL через слаг
|
||||
✓ **История**: Изменения в названиях сохраняются через синонимы
|
||||
✓ **Готовность к парсерам**: Структура поддерживает будущую интеграцию
|
||||
|
||||
## Файлы реализации
|
||||
|
||||
- **Модели**: `lpon_site/frontend/models.py` (TbLabel, TbArtist, TbItem, TbMusicStyle, TbSeller)
|
||||
- **Валидаторы**: `lpon_site/frontend/utils_validators.py` (validate_for_duplicates)
|
||||
- **Хелперы**: `lpon_site/frontend/utils.py` (create_or_get_related_article, update_synonyms_in_metadata)
|
||||
- **Админка**: `lpon_site/frontend/admin.py` (LabelAdmin, ArtistAdmin и т.д.)
|
||||
- **Парсер** (будущее): `lpon_site/parser/models.py` (ValidatorQueue), `tasks.py` (Celery задачи)
|
||||
|
||||
## TODO: интеграция парсера
|
||||
|
||||
- [ ] Создать Celery tasks для парсинга Discogs/Excel
|
||||
- [ ] Реализовать ValidatorQueue с админ-интерфейсом
|
||||
- [ ] Добавить поддержку очереди конфликтов в моделях
|
||||
- [ ] Интегрировать сбор метаданных из Discogs API
|
||||
- [ ] Документировать API для парсера
|
||||
- [ ] Написать тесты для валидации и парсинга
|
||||
@@ -0,0 +1,414 @@
|
||||
# Архитектура парсера и валидации данных
|
||||
|
||||
**Дата:** 2026-07-15
|
||||
**Последний обновленно:** 2026-07-15
|
||||
**Статус:** В разработке ✏️
|
||||
|
||||
## Обзор проблемы
|
||||
|
||||
Система должна поддерживать импорт данных из различных источников:
|
||||
- Excel/CSV файлы от продавцов и издателей
|
||||
- Парсинг URL-страниц с каталогами
|
||||
- Ручной ввод данных в админке
|
||||
- Будущие API интеграции
|
||||
|
||||
**Основной вызов:** Валидация и обнаружение конфликтов (дубликаты, совпадения в синонимах) требует **пользовательского участия** для принятия решений. Нельзя просто блокировать или автоматически удалять/объединять данные.
|
||||
|
||||
## Текущее состояние (MVP)
|
||||
|
||||
### Модели и структура
|
||||
|
||||
```
|
||||
┌─ TbSource (Excel, CSV, URL) ──┐
|
||||
│ s_source_name │
|
||||
│ l_source_type (excel/csv/url)│
|
||||
│ k_source_to_seller ──────────┼──► TbSeller
|
||||
│ source_file (FilerFileField) │
|
||||
│ s_source_url │
|
||||
│ j_source_metadata (struktura)│ (metadata описывает столбцы, вкладки и т.д.)
|
||||
└─────────────────────────────────┘
|
||||
|
||||
TbOffer ◄──┐
|
||||
│
|
||||
TbSource ──┴─► TbItem ──────► TbArticle (текст, SEO, slug, картинка)
|
||||
│
|
||||
└──► TbLabel, TbArtist, TbMusicStyle
|
||||
(все через TbArticle)
|
||||
```
|
||||
|
||||
### Валидация (текущая реализация)
|
||||
|
||||
**Файл:** `lpon_site/frontend/utils_validators.py`
|
||||
|
||||
**Функция:** `validate_for_duplicates()`
|
||||
- Проверяет основное поле (s_label, s_artist и т.д.)
|
||||
- Проверяет синонимы в j_*_metadata[SYN_EN]
|
||||
- Возвращает список найденных дубликатов с типом совпадения
|
||||
|
||||
**Типы совпадений:**
|
||||
1. **EXACT_MATCH** — точное совпадение основного поля (s_label == s_label)
|
||||
2. **FIND_IN_SYNONYM** — основное поле текущей записи найдено в синонимах другой
|
||||
3. **EXACT_SYNONYM_MATCH** — синонимы текущей записи совпадают с синонимами другой
|
||||
|
||||
**Где вызывается:**
|
||||
- ✅ В админке: `admin.py` (переопределение `clean()` формы) → показывает ошибку пользователю
|
||||
- ✅ В моделях: `save()` методы → выбрасывают `ValidationError`
|
||||
- ❌ В парсере: **НЕТ** (парсер еще не создан)
|
||||
|
||||
### Конфликты синонимов (TODO)
|
||||
|
||||
**Сценарий 1: FIND_IN_SYNONYM**
|
||||
```python
|
||||
# В БД уже есть
|
||||
Artist(pk=1, s_artist="Beatles", j_artist_metadata={"SYN_EN": ["The Beatles", "Beatles"]})
|
||||
|
||||
# Парсер хочет создать
|
||||
Artist(pk=None, s_artist="The Beatles", j_artist_metadata={"SYN_EN": []})
|
||||
|
||||
# Валидатор: основное поле "The Beatles" найдено в синонимах существующей записи!
|
||||
# Текущее поведение: ValidationError (блокировка)
|
||||
# TODO: Отправить в очередь, ждать решения админа
|
||||
```
|
||||
|
||||
**Сценарий 2: EXACT_SYNONYM_MATCH**
|
||||
```python
|
||||
# В БД
|
||||
Label(pk=1, s_label="Sony", j_label_metadata={"SYN_EN": ["Sony Records", "Sony Music"]})
|
||||
|
||||
# Парсер хочет
|
||||
Label(pk=None, s_label="Sony Music", j_label_metadata={"SYN_EN": ["Sony Records", "Sony Music"]})
|
||||
|
||||
# Валидатор: синонимы совпадают!
|
||||
# Текущее поведение: ValidationError
|
||||
# TODO: Отправить в очередь, ждать решения админа
|
||||
```
|
||||
|
||||
## Архитектура парсера (требуемая)
|
||||
|
||||
### Компоненты
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ DJANGO ADMIN │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Приложение "Parser" (новое приложение) │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Очередь валидации (ValidatorQueue model) │ │ │
|
||||
│ │ │ Содержит: состояние экземпляра, ошибки, решение... │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Admin list view + inline actions │ │ │
|
||||
│ │ │ - Просмотр конфликта (дубликаты) │ │ │
|
||||
│ │ │ - Кнопка "Создать новую запись" │ │ │
|
||||
│ │ │ - Кнопка "Объединить/обновить синонимы" │ │ │
|
||||
│ │ │ - Кнопка "Пропустить" │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ БРОКЕР ОЧЕРЕДИ (Redis) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ parser:validation:pending (ZSET) │ │
|
||||
│ │ parser:validation:{queue_id}:data (STRING/JSON) │ │
|
||||
│ │ parser:validation:{queue_id}:duplicates (JSON) │ │
|
||||
│ │ parser:validation:{queue_id}:solution (STRING) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ ПАРСЕР (Celery Task) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ parse_excel() / parse_csv() / parse_url() │ │
|
||||
│ │ │ │
|
||||
│ │ Логика: │ │
|
||||
│ │ 1. Читать источник (файл/URL) │ │
|
||||
│ │ 2. Нормализовать данные │ │
|
||||
│ │ 3. Валидировать через validate_for_duplicates() │ │
|
||||
│ │ 4. Если конфликт → создать запись в ValidatorQueue │ │
|
||||
│ │ 5. Если OK → создать модель (TbLabel, TbArtist и т.д.)│ │
|
||||
│ │ 6. Отправить уведомление админу (Redis/сигнал) │ │
|
||||
│ │ 7. Продолжить обработку следующей строки │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ СИГНАЛЫ И УВЕДОМЛЕНИЯ (Django signals) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ post_validate_conflict → Redis уведомление админу │ │
|
||||
│ │ post_admin_decision → Обновление очереди │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Таблица ValidatorQueue (новая модель)
|
||||
|
||||
```python
|
||||
class ValidatorQueue(models.Model):
|
||||
"""
|
||||
Очередь конфликтов, требующих решения админа.
|
||||
"""
|
||||
class Status(TextChoices):
|
||||
PENDING = 'pending' # Ждёт решения админа
|
||||
APPROVED = 'approved' # Админ одобрил - выполнить действие
|
||||
REJECTED = 'rejected' # Админ отклонил
|
||||
MERGED = 'merged' # Данные объединены с существующей записью
|
||||
SKIPPED = 'skipped' # Заметка: пропущено
|
||||
|
||||
class ConflictType(TextChoices):
|
||||
FIND_IN_SYNONYM = 'find_in_synonym' # Основное поле в синонимах
|
||||
EXACT_SYNONYM_MATCH = 'exact_synonym_match' # Синонимы совпадают
|
||||
OTHER = 'other'
|
||||
|
||||
# Основные поля
|
||||
id = BigAutoField(primary_key=True)
|
||||
k_source = ForeignKey(TbSource, ...) # Откуда пришли данные
|
||||
l_conflict_type = CharField(choices=ConflictType)
|
||||
|
||||
# Данные для конфликта
|
||||
l_model_type = CharField() # Какая модель (TbLabel, TbArtist и т.д.)
|
||||
model_pk = IntegerField(null=True) # PK существующей записи в БД
|
||||
j_incoming_data = JSONField() # Данные от парсера (весь объект)
|
||||
j_duplicates = JSONField() # Результат validate_for_duplicates()
|
||||
|
||||
# Решение админа
|
||||
l_status = CharField(choices=Status, default=Status.PENDING)
|
||||
s_admin_decision = CharField() # 'create_new' / 'merge' / 'skip'
|
||||
s_admin_notes = TextField() # Комментарий админа
|
||||
|
||||
# Сервисные поля
|
||||
t_created = DateTimeField(auto_now_add=True)
|
||||
t_resolved = DateTimeField(null=True)
|
||||
k_resolved_by = ForeignKey(User, null=True) # Какой админ принял решение
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Конфликт валидации'
|
||||
ordering = ['-t_created']
|
||||
indexes = [
|
||||
Index(fields=['l_status', '-t_created']),
|
||||
Index(fields=['k_source', 'l_status']),
|
||||
]
|
||||
```
|
||||
|
||||
## Процесс работы
|
||||
|
||||
### Фаза 1: Импорт данных (парсер)
|
||||
|
||||
```python
|
||||
# tasks.py (Celery)
|
||||
@celery.task
|
||||
def parse_and_validate_excel(source_id: int):
|
||||
"""
|
||||
1. Читает Excel из TbSource
|
||||
2. Парсит строки
|
||||
3. Валидирует каждую запись
|
||||
4. Создаёт или отправляет в очередь
|
||||
"""
|
||||
source = TbSource.objects.get(pk=source_id)
|
||||
|
||||
for row in read_excel(source.source_file):
|
||||
try:
|
||||
# Нормализуем данные
|
||||
artist_data = {
|
||||
's_artist': row['Artist Name'],
|
||||
'j_artist_metadata': {'SYN_EN': [row.get('Aliases', '')]}
|
||||
}
|
||||
|
||||
# Валидируем
|
||||
duplicates = validate_for_duplicates(
|
||||
model_class=TbArtist,
|
||||
instance_pk=None,
|
||||
main_field_value=artist_data['s_artist'],
|
||||
metadata_dict=artist_data['j_artist_metadata'],
|
||||
main_field_name='s_artist',
|
||||
metadata_field_name='j_artist_metadata',
|
||||
)
|
||||
|
||||
if duplicates:
|
||||
# КОНФЛИКТ! Отправляем в очередь
|
||||
queue_item = ValidatorQueue.objects.create(
|
||||
k_source=source,
|
||||
l_model_type='TbArtist',
|
||||
l_conflict_type=duplicates[0][VALIDATE_KEY__MATCH_TYPE],
|
||||
j_incoming_data=artist_data,
|
||||
j_duplicates=duplicates,
|
||||
)
|
||||
# Отправляем сигнал (Redis уведомление админу)
|
||||
send_to_redis_queue('parser:conflicts:new', {
|
||||
'queue_id': queue_item.id,
|
||||
'model': 'TbArtist',
|
||||
'conflict': duplicates[0]['match_type'],
|
||||
'data': artist_data,
|
||||
})
|
||||
else:
|
||||
# Нет конфликта - создаём запись
|
||||
artist = TbArtist.objects.create(**artist_data)
|
||||
logger.info(f"Artist created: {artist.id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Parse error in row {row}: {e}")
|
||||
continue
|
||||
```
|
||||
|
||||
### Фаза 2: Админка (приложение Parser)
|
||||
|
||||
```python
|
||||
# admin.py (новое приложение parser)
|
||||
@admin.register(ValidatorQueue)
|
||||
class ValidatorQueueAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админка для работы с очередью конфликтов.
|
||||
"""
|
||||
list_display = [
|
||||
'id', 'l_model_type', 'l_conflict_type', 'l_status',
|
||||
'source_link', 'duplicates_summary', 't_created'
|
||||
]
|
||||
list_filter = ['l_status', 'l_conflict_type', 'l_model_type', 't_created']
|
||||
readonly_fields = ['j_incoming_data', 'j_duplicates', 'k_source']
|
||||
|
||||
fieldsets = (
|
||||
('Конфликт', {
|
||||
'fields': ['l_model_type', 'l_conflict_type', 'k_source', 'model_pk']
|
||||
}),
|
||||
('Входящие данные', {
|
||||
'fields': ['j_incoming_data']
|
||||
}),
|
||||
('Найденные дубликаты', {
|
||||
'fields': ['j_duplicates']
|
||||
}),
|
||||
('Решение админа', {
|
||||
'fields': ['s_admin_decision', 's_admin_notes', 'l_status']
|
||||
}),
|
||||
)
|
||||
|
||||
actions = ['action_create_new', 'action_merge_synonyms', 'action_skip']
|
||||
|
||||
def action_create_new(self, request, queryset):
|
||||
"""Админ решил: создать новую запись, игнорируя дубликаты"""
|
||||
for item in queryset:
|
||||
model_class = get_model_class(item.l_model_type)
|
||||
instance = model_class(**item.j_incoming_data)
|
||||
instance.save(skip_validation=True) # Пропускаем валидацию
|
||||
item.l_status = ValidatorQueue.Status.APPROVED
|
||||
item.s_admin_decision = 'create_new'
|
||||
item.k_resolved_by = request.user
|
||||
item.t_resolved = now()
|
||||
item.save()
|
||||
```
|
||||
|
||||
### Фаза 3: Обработка решения админа
|
||||
|
||||
```python
|
||||
# signals.py (новое приложение parser)
|
||||
|
||||
@receiver(post_save, sender=ValidatorQueue)
|
||||
def on_admin_decision(sender, instance, created=False, **kwargs):
|
||||
"""
|
||||
Обработать решение админа и применить действие.
|
||||
"""
|
||||
if created or instance.l_status != ValidatorQueue.Status.APPROVED:
|
||||
return
|
||||
|
||||
if instance.s_admin_decision == 'create_new':
|
||||
# Админ одобрил создание новой записи
|
||||
model_class = get_model_class(instance.l_model_type)
|
||||
model_instance = model_class(**instance.j_incoming_data)
|
||||
model_instance.save(skip_validation=True)
|
||||
|
||||
elif instance.s_admin_decision == 'merge':
|
||||
# Админ решил объединить (добавить синонимы, и т.д.)
|
||||
existing_pk = instance.model_pk
|
||||
incoming_data = instance.j_incoming_data
|
||||
|
||||
model_class = get_model_class(instance.l_model_type)
|
||||
existing = model_class.objects.get(pk=existing_pk)
|
||||
|
||||
# Добавляем синонимы из входящих данных
|
||||
existing_meta = existing.j_*_metadata or {}
|
||||
existing_synonyms = existing_meta.get(KEY_SYNONYM_EN, [])
|
||||
incoming_synonyms = incoming_data.get('j_*_metadata', {}).get(KEY_SYNONYM_EN, [])
|
||||
|
||||
# Объединяем и сохраняем
|
||||
existing_meta[KEY_SYNONYM_EN] = list(set(existing_synonyms + incoming_synonyms))
|
||||
existing.j_*_metadata = existing_meta
|
||||
existing.save(skip_validation=True)
|
||||
```
|
||||
|
||||
## Файлы для создания
|
||||
|
||||
### 1. Новое приложение `parser`
|
||||
|
||||
```
|
||||
lpon_site/parser/
|
||||
├── __init__.py
|
||||
├── models.py # ValidatorQueue
|
||||
├── admin.py # ValidatorQueueAdmin + actions
|
||||
├── signals.py # Обработка решений админа
|
||||
├── tasks.py # Celery tasks для парсинга
|
||||
├── apps.py
|
||||
└── migrations/
|
||||
```
|
||||
|
||||
### 2. Изменения в existing коде
|
||||
|
||||
**settings.py:**
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...,
|
||||
'parser', # Новое приложение
|
||||
]
|
||||
|
||||
# Конфигурация Celery (если еще нет)
|
||||
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
|
||||
```
|
||||
|
||||
**frontend/models.py:**
|
||||
```python
|
||||
# В методе save() моделей добавить параметр
|
||||
def save(self, *args, skip_validation=False, **kwargs):
|
||||
if not skip_validation:
|
||||
# Стандартная валидация
|
||||
validate_and_raise_for_duplicates(...)
|
||||
super().save(*args, **kwargs)
|
||||
```
|
||||
|
||||
**frontend/utils.py:**
|
||||
```python
|
||||
# Добавить функцию отправки в Redis
|
||||
def send_to_redis_queue(queue_name: str, data: dict):
|
||||
"""Отправить уведомление в Redis"""
|
||||
import redis
|
||||
import json
|
||||
|
||||
r = redis.Redis(host='localhost', port=6379, db=0)
|
||||
r.lpush(queue_name, json.dumps({
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
**data
|
||||
}))
|
||||
```
|
||||
|
||||
## TODO задачи
|
||||
|
||||
- [ ] Создать приложение `parser` с моделью `ValidatorQueue`
|
||||
- [ ] Реализовать админку `ValidatorQueueAdmin` с actions
|
||||
- [ ] Написать Celery tasks для парсинга Excel/CSV
|
||||
- [ ] Добавить обработку решений админа через signals
|
||||
- [ ] Интегрировать Redis уведомления админу
|
||||
- [ ] Добавить параметр `skip_validation` в save() методы
|
||||
- [ ] Написать тесты для парсера и валидации
|
||||
- [ ] Документация для парсера (как использовать)
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Redis vs БД для очереди?** → Используем БД (ValidatorQueue модель) как основное хранилище, Redis только для уведомлений админу в реальном времени
|
||||
2. **Async валидация?** → Парсер работает в Celery, валидация синхронна внутри задачи
|
||||
3. **Миграция истории?** → Поле `t_history_created` уже `editable=True` в TbOfferHistory, так что админ может вручную добавлять исторические данные
|
||||
4. **Конфликты в админке?** → `ValidatorQueue` блокирует парсер от создания дубликатов. Админ сам решает что делать.
|
||||
|
||||
## Ссылки на существующий код
|
||||
|
||||
- Валидаторы: `lpon_site/frontend/utils_validators.py`
|
||||
- Модели: `lpon_site/frontend/models.py` (методы save() для TbArtist, TbItem, TbLabel, TbSeller, TbMusicStyle)
|
||||
- TODO про Redis: `lpon_site/frontend/utils.py` (в функции `create_or_get_related_article()`)
|
||||
- TODO про историю: `lpon_site/frontend/models.py` (в `TbOffer.save()`)
|
||||
Reference in New Issue
Block a user