Files
Egutierrez 52999ecb86 feat: media_analytics — ETL PC+VPS → ClickHouse + Grafana
2 ETLs cada 5min suben snapshots (Jellyfin, *arr, Prowlarr, gnula,
popelis users/mylist/events) a ClickHouse en el VPS, visualizado en
Grafana (grafana.datardos.com). Ingesta PC via tunel SSH; popelis
via ETL local en el VPS. Usa clickhouse_insert_rows_py_infra.
2026-05-30 14:55:48 +02:00

174 lines
5.8 KiB
SQL

-- Esquema analitico media stack. Todas las tablas de snapshot llevan snapshot_ts
-- (momento de la captura del ETL, cada 5min) → permite analisis temporal del estado.
-- Las tablas de eventos llevan event_ts (instante real del evento).
-- Engine MergeTree, particion mensual, orden por (snapshot_ts, clave).
CREATE DATABASE IF NOT EXISTS analytics;
-- ============ JELLYFIN ============
-- Catalogo: peliculas/series/episodios visibles en la biblioteca.
CREATE TABLE IF NOT EXISTS analytics.jellyfin_items (
snapshot_ts DateTime,
item_id String,
type LowCardinality(String), -- Movie | Series | Episode
name String,
production_year Int32,
runtime_min Float32,
genres Array(String),
community_rating Float32,
official_rating String,
series_name String,
library String,
path String,
date_created DateTime DEFAULT toDateTime(0)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, type, item_id);
-- Usuarios Jellyfin (espejos popelis).
CREATE TABLE IF NOT EXISTS analytics.jellyfin_users (
snapshot_ts DateTime,
user_id String,
name String,
last_login DateTime DEFAULT toDateTime(0),
last_activity DateTime DEFAULT toDateTime(0),
is_admin UInt8
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, user_id);
-- Estado de reproduccion por usuario+item (playcount, visto, ultima vez).
CREATE TABLE IF NOT EXISTS analytics.jellyfin_user_items (
snapshot_ts DateTime,
user_id String,
user_name String,
item_id String,
item_name String,
type LowCardinality(String),
played UInt8,
play_count Int32,
playback_pct Float32,
last_played DateTime DEFAULT toDateTime(0)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, user_id, item_id);
-- Sesiones activas (lo que se esta viendo en el momento del snapshot).
CREATE TABLE IF NOT EXISTS analytics.jellyfin_sessions (
snapshot_ts DateTime,
user_name String,
item_name String,
item_type LowCardinality(String),
client String,
device String,
play_method String,
is_paused UInt8,
position_pct Float32
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, user_name);
-- ============ SCRAPERS TORRENTS (*arr) ============
-- Historial Radarr/Sonarr: grabs, imports, fallos.
CREATE TABLE IF NOT EXISTS analytics.arr_history (
snapshot_ts DateTime,
app LowCardinality(String), -- radarr | sonarr
history_id Int64,
event_type LowCardinality(String), -- grabbed | downloadFolderImported | ...
title String,
source_title String,
indexer String,
download_client String,
quality String,
languages Array(String),
event_date DateTime DEFAULT toDateTime(0)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, app, history_id);
-- Cola activa de descargas.
CREATE TABLE IF NOT EXISTS analytics.arr_queue (
snapshot_ts DateTime,
app LowCardinality(String),
title String,
status String,
tracked_status String,
size_bytes Int64,
sizeleft_bytes Int64,
timeleft String,
indexer String,
download_client String
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, app, title);
-- Indexers Prowlarr: estado + contadores grab/query.
CREATE TABLE IF NOT EXISTS analytics.prowlarr_indexers (
snapshot_ts DateTime,
indexer_id Int32,
name String,
enable UInt8,
protocol String,
privacy String,
num_grabs Int64,
num_queries Int64,
num_grab_fail Int64,
num_query_fail Int64
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, indexer_id);
-- ============ GNULA SCRAPPER ============
-- Catalogo de pelis en castellano detectadas (gnula_catalog.db).
CREATE TABLE IF NOT EXISTS analytics.gnula_movies (
snapshot_ts DateTime,
href String,
title String,
year Int32,
flags String,
lang_es UInt8,
status LowCardinality(String), -- pending | downloaded | failed | have
in_library UInt8,
detected_at String,
downloaded_at String
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, href);
-- ============ POPELIS ============
-- Usuarios (estado).
CREATE TABLE IF NOT EXISTS analytics.popelis_users (
snapshot_ts DateTime,
user_id Int64,
username String,
jf_user_id String,
created_at DateTime DEFAULT toDateTime(0)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, user_id);
-- Mi lista por usuario (estado).
CREATE TABLE IF NOT EXISTS analytics.popelis_mylist (
snapshot_ts DateTime,
user_id Int64,
item_id String,
added_at DateTime DEFAULT toDateTime(0)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(snapshot_ts)
ORDER BY (snapshot_ts, user_id, item_id);
-- Eventos (logins, plays, mylist add/remove) — instrumentados en popelis-api.
-- Tabla de hechos: dedup por event_id con ReplacingMergeTree.
CREATE TABLE IF NOT EXISTS analytics.popelis_events (
event_id Int64,
event_ts DateTime,
user_id Int64,
username String,
event_type LowCardinality(String), -- login | logout | play | mylist_add | mylist_remove
item_id String,
meta String,
ingested_at DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(ingested_at)
PARTITION BY toYYYYMM(event_ts)
ORDER BY (event_id);