Metal Archive - Catalogo de Metal Underground

Catalogo web de albums de metal underground vinculado a un canal de YouTube. Sincroniza automaticamente los videos del canal, enriquece los datos con artwork de alta calidad, normaliza generos y paises, y ofrece una interfaz de busqueda y descubrimiento para los usuarios.

Construido con Reflex (Python full-stack) y desplegado en una arquitectura dual Vercel + Reflex Cloud.

Arquitectura General

┌─────────────────────────────────────────────────────────────────┐
│                      METAL ARCHIVE                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  FRONTEND (Reflex Components)                        │       │
│  │                                                      │       │
│  │  9 paginas con rutas dinamicas                       │       │
│  │  Landing ── Browse ── Album Detail                   │       │
│  │  Genre/[genre] ── Country/[country] ── Year/[year]   │       │
│  │  Submit ── Promo ── Newsletter                       │       │
│  │                                                      │       │
│  │  Estado via WebSocket (MetalArchiveState)            │       │
│  └────────────────────┬─────────────────────────────────┘       │
│                       │                                         │
│  ┌────────────────────▼─────────────────────────────────┐       │
│  │  BACKEND (Reflex State + SQLModel)                   │       │
│  │                                                      │       │
│  │  MetalArchiveState ── Busqueda, filtros, paginacion  │       │
│  │  FormState ────────── Submissions, newsletter, promo │       │
│  └────────────────────┬─────────────────────────────────┘       │
│                       │                                         │
│  ┌────────────────────▼─────────────────────────────────┐       │
│  │  DATA LAYER                                          │       │
│  │                                                      │       │
│  │  SQLite (SQLModel + Alembic migrations)              │       │
│  │  ┌────────┐ ┌───────┐ ┌─────────────┐                │       │
│  │  │ Albums │ │ Tracks│ │ SimilarBands│                │       │
│  │  └───┬────┘ └───┬───┘ └──────┬──────┘                │       │
│  │      │          │             │                      │       │
│  │      └──────────┴─────────────┘                      │       │
│  │ FK: album_id (sin relationship(), joins manuales)    │       │
│  │                                                      │       │
│  │  ┌──────────────┐ ┌────────────┐ ┌────────────────┐  │       │
│  │  │ Submissions  │ │ Newsletter │ │ ContactMessages│  │       │
│  │  └──────────────┘ └────────────┘ └────────────────┘  │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                 │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  BACKGROUND SYNC (daemon thread, cada 12h)           │       │
│  │                                                      │       │
│  │  YouTube API ──► Parse ──► Upsert DB                 │       │
│  │       ──► Normalizar generos/paises                  │       │
│  │       ──► Enriquecer artwork (DeathGrind + fallback) │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Pipeline de Sincronizacion

Un daemon thread sincroniza el canal de YouTube con la base de datos cada 12 horas en 3 fases:

┌─────────────────────────────────────────────────────────────────┐
│  DAEMON THREAD (background_sync.py)                             │
│  Inicio: 60s despues del boot (0s si DB vacia)                  │
│  Intervalo: 12 horas                                            │
│                                                                 │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  FASE 1: YouTube Sync (sync_youtube_to_db.py)          │     │
│  │                                                        │     │
│  │  YouTube API v3 (OAuth2, rotacion de 7 credenciales)   │     │
│  │         │                                              │     │
│  │         ▼                                              │     │
│  │  Obtener uploads playlist del canal                    │     │
│  │         │                                              │     │
│  │         ▼                                              │     │
│  │  Paginar videos (maxResults=50)                        │     │
│  │  + enriquecer con snippet/contentDetails/statistics    │     │
│  │         │                                              │     │
│  │         ▼                                              │     │
│  │  Parsear cada video:                                   │     │
│  │                                                        │     │
│  │  Titulo: "[🇭🇳] Band - Album (2024) . [Genre] <FULL>"   │     │
│  │           │                                            │     │
│  │           ▼                                            │     │
│  │  parse_title_metadata() ──► banda, album, año,         │     │
│  │                              genero, pais, tipo        │     │
│  │                                                        │     │
│  │  Descripcion: linea por linea                          │     │
│  │           │                                            │     │
│  │           ▼                                            │     │
│  │  parse_description_metadata() ──►                      │     │
│  │    Tracks:  "[00:08] > Track Name"                     │     │
│  │    Links:   "🟢 Spotify: https://..."                  │     │
│  │    Metadata: "Country: Honduras"                       │     │
│  │                                                        │     │
│  │  Upsert por youtube_video_id (batch commit cada 50)    │     │
│  │  Top 10 por views ──► featured = True                  │     │
│  └────────────────────────┬───────────────────────────────┘     │
│                           │                                     │
│  ┌────────────────────────▼───────────────────────────────┐     │
│  │  FASE 2: Normalizacion (normalize_db.py)               │     │
│  │                                                        │     │
│  │  Generos:                                              │     │
│  │  - Quitar prefijos numericos "(9)Metal" ──► "Metal"    │     │
│  │  - Unificar separadores (,|;) ──► "/"                  │     │
│  │  - Corregir typos: "grincore" ──► "Grindcore"          │     │
│  │  - Merge inversos: "Death/Black" vs "Black/Death"      │     │
│  │                                                        │     │
│  │  Paises:                                               │     │
│  │  - Ingles ──► Español (120+ mappings)                  │     │
│  │  - "United States" ──► "Estados Unidos"                │     │
│  │  - Corregir typos: "mexic" ──► "Mexico"                │     │
│  └────────────────────────┬───────────────────────────────┘     │
│                           │                                     │
│  ┌────────────────────────▼───────────────────────────────┐     │
│  │  FASE 3: Artwork Enrichment (batch de 50)              │     │
│  │                                                        │     │
│  │  Albums con thumbnail de YouTube                       │     │
│  │  (ytimg.com o vacio)                                   │     │
│  │         │                                              │     │
│  │         ▼                                              │     │
│  │  ┌─────────────────────────────────┐                   │     │
│  │  │ 1. DeathGrind.club API          │                   │     │
│  │  │    Login ──► Search banda+album │                   │     │
│  │  │    3 niveles de matching:       │                   │     │
│  │  │    Exacto > Parcial > Solo banda│                   │     │
│  │  │    CDN: cdn.deathgrind.club     │                   │     │
│  │  └──────────┬──────────────────────┘                   │     │
│  │             │ Si no encuentra                          │     │
│  │             ▼                                          │     │
│  │  ┌─────────────────────────────────┐                   │     │
│  │  │ 2. Metal Archives API           │                   │     │
│  │  │    Busqueda avanzada de albums  │                   │     │
│  │  │    Extraer URL de portada       │                   │     │
│  │  └──────────┬──────────────────────┘                   │     │
│  │             │ Si no encuentra                          │     │
│  │             ▼                                          │     │
│  │  ┌─────────────────────────────────┐                   │     │
│  │  │ 3. YouTube HD fallback          │                   │     │
│  │  │    Upgrade a maxresdefault      │                   │     │
│  │  └─────────────────────────────────┘                   │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                 │
│  Logica incremental vs full:                                    │
│  - DB < 100 albums ──► siempre full sync                        │
│  - DB >= 100 ──► incremental (solo nuevos)                      │
│  - Cada 4to ciclo ──► full sync para llenar gaps                │
│                                                                 │
│  Credenciales YouTube: rotacion automatica en quotaExceeded     │
│  (hasta 7 sets de client_id/secret/refresh_token)               │
└─────────────────────────────────────────────────────────────────┘

Sistema de Busqueda y Filtrado

┌──────────────────────────────────────────────────────────────────┐
│  MetalArchiveState (rx.State)                                    │
│                                                                  │
│  LANDING PAGE                                                    │
│  ┌──────────────────────────────────────────────────────┐        │
│  │  featured_albums ──► Top 10 por views (featured=True)│        │
│  │  latest_albums ────► 12 mas recientes por upload_date│        │
│  │  genre_counts ─────► GROUP BY genre + COUNT          │        │
│  │  country_counts ──► GROUP BY country + COUNT         │        │
│  │  year_counts ──────► GROUP BY year + COUNT (5+ albums)│       │
│  └──────────────────────────────────────────────────────┘        │
│                                                                  │
│  LIVE SEARCH (cada keystroke)                                    │
│  ┌──────────────────────────────────────────────────────┐        │
│  │  Input >= 2 caracteres                               │        │
│  │         │                                            │        │
│  │         ▼                                            │        │
│  │  ILIKE %query% en:                                   │        │
│  │  band_name, album_title, genre                       │        │
│  │         │                                            │        │
│  │         ▼                                            │        │
│  │  Top 8 resultados por views DESC                     │        │
│  │  Dropdown con live_search_open = True                │        │
│  └──────────────────────────────────────────────────────┘        │
│                                                                  │
│  BROWSE (filtros combinables)                                    │
│  ┌──────────────────────────────────────────────────────┐        │
│  │  Filtros:                                            │        │
│  │  search_query + filter_genre + filter_country        │        │
│  │  + filter_year + filter_release_type                 │        │
│  │                                                      │        │
│  │  Orden:                                              │        │
│  │  newest | oldest | az | za | views                   │        │
│  │                                                      │        │
│  │  Paginacion:                                         │        │
│  │  offset + limit (12) ──► fetch limit+1 para has_more │        │
│  │  "Cargar mas" appends a lista existente              │        │
│  │                                                      │        │
│  │  URL-driven: /browse?genre=Death+Metal               │        │
│  │  Random filters: genero o pais aleatorio             │        │
│  └──────────────────────────────────────────────────────┘        │
│                                                                  │
│  ALBUM DETAIL (/album/[id])                                      │
│  ┌──────────────────────────────────────────────────────┐        │
│  │  Album + Tracks (ordenados por track_number)         │        │
│  │  + SimilarBands                                      │        │
│  │  + Albums similares (mismo genero, top 4 por views)  │        │
│  │  + YouTube embed + links de streaming                │        │
│  └──────────────────────────────────────────────────────┘        │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Modelo de Datos

┌─────────────────────────────────────────────────────────────────┐
│  SQLite (SQLModel + Alembic)                                    │
│                                                                 │
│  albums ────────────────────────────────────────────────────────│
│  │ band_name (idx)        │ album_artwork_url                │  │
│  │ album_title (idx)      │ youtube_url                      │  │
│  │ year (idx)             │ spotify_url, bandcamp_url        │  │
│  │ country (idx)          │ apple_music_url                  │  │
│  │ genre (idx)            │ metal_archives_url               │  │
│  │ release_type (idx)     │ facebook_url, instagram_url      │  │
│  │ youtube_video_id (uniq)│ description                      │  │
│  │ views (idx)            │ duration_minutes                 │  │
│  │ featured (idx)         │ upload_date (idx)                │  │
│  │                                                           │  │
│  │  FK album_id                                              │  │
│  │  ├──► tracks (track_number, track_name, timestamp)        │  │
│  │  └──► similar_bands (similar_band_name)                   │  │
│  │                                                           │  │
│  submissions ── band_name, email, genre, country, status     │  │
│  newsletter_subscribers ── email (uniq), active              │  │
│  contact_messages ── name, email, company, message           │  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Despliegue

Arquitectura dual para separar el sitio estatico del backend con estado:

┌──────────────────┐         ┌──────────────────────────┐
│     Vercel       │         │     Reflex Cloud         │
│                  │         │                          │
│  Portfolio       │  proxy  │  Metal Archive           │
│  estatico        │────────►│  (Python + WebSocket)    │
│                  │         │                          │
│  /metal-archive/*│ ──────► │  /metal-archive/*        │
│  (vercel.json    │         │                          │
│   redirects)     │         │  SQLite + Daemon thread  │
│                  │         │  (sync cada 12h)         │
└──────────────────┘         └──────────────────────────┘

Alternativa local: systemd timer (sync_web.timer) ejecuta la sincronizacion 2 veces al dia (06:00 y 18:00).


Formularios

Submission de bandas (/metal-archive/submit): Las bandas envian su musica para ser considerada en el canal. Se guarda en DB con status “pendiente”.

Promo (/metal-archive/promo): Formulario extendido con hasta 5 links adicionales y selector de genero custom. Envia notificacion por email via Gmail SMTP.

Newsletter (/metal-archive/newsletter): Registro de email para actualizaciones. Lista de suscriptores almacenada en DB.


Stack Tecnologico

CategoriaTecnologias
FrameworkReflex 0.8.26 (Python full-stack, WebSocket state)
Base de datosSQLite via SQLModel + Alembic migrations
APIs externasYouTube Data API v3 (OAuth2, 7 credenciales)
ArtworkDeathGrind.club API, Metal Archives API, YouTube HD
EmailGmail SMTP (notificaciones de promo)
DeployVercel (static proxy) + Reflex Cloud (backend)
SyncDaemon thread (12h) o systemd timer (2x/dia)