$type, 'message' => $message, ]; } function pull_flash(): ?array { if (!isset($_SESSION['flash'])) { return null; } $flash = $_SESSION['flash']; unset($_SESSION['flash']); return $flash; } function ensure_radio_schema(): void { db()->exec( "CREATE TABLE IF NOT EXISTS station_entries ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, entry_type VARCHAR(20) NOT NULL, title VARCHAR(160) NOT NULL, subtitle VARCHAR(160) DEFAULT NULL, body TEXT DEFAULT NULL, meta_url VARCHAR(255) DEFAULT NULL, meta_value VARCHAR(255) DEFAULT NULL, weekday TINYINT UNSIGNED DEFAULT NULL, start_time TIME DEFAULT NULL, end_time TIME DEFAULT NULL, status VARCHAR(20) NOT NULL DEFAULT 'published', sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_type_status (entry_type, status), INDEX idx_schedule (entry_type, weekday, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); } function seed_radio_content(): void { $seedMap = [ 'program' => [ [ 'title' => 'Mañanas de Vinilo', 'subtitle' => 'DJ Lili', 'body' => 'Selección fresca de soul, funk y novedades independientes para arrancar el día.', 'weekday' => 1, 'start_time' => '09:00:00', 'end_time' => '11:00:00', 'sort_order' => 10, ], [ 'title' => 'Cabina Abierta', 'subtitle' => 'Mika Set', 'body' => 'Sesión con pedidos, mensajes de la audiencia y mezclas en vivo.', 'weekday' => 3, 'start_time' => '18:00:00', 'end_time' => '20:00:00', 'sort_order' => 20, ], [ 'title' => 'Noches Lili Records', 'subtitle' => 'Nora Waves', 'body' => 'Curaduría de house, afrobeat y lanzamientos del sello.', 'weekday' => 5, 'start_time' => '21:00:00', 'end_time' => '23:00:00', 'sort_order' => 30, ], [ 'title' => 'Domingo Replay', 'subtitle' => 'Equipo Lili', 'body' => 'Repetición de los mejores momentos y entrevistas de la semana.', 'weekday' => 7, 'start_time' => '16:00:00', 'end_time' => '18:00:00', 'sort_order' => 40, ], ], 'dj' => [ [ 'title' => 'DJ Lili', 'subtitle' => 'Dirección artística', 'body' => 'Host principal de la estación. Mezcla clásicos del catálogo con estrenos y entrevistas cortas.', 'meta_url' => 'https://instagram.com/', 'sort_order' => 10, ], [ 'title' => 'Mika Set', 'subtitle' => 'Live selector', 'body' => 'Especialista en sesiones híbridas y curaduría para pedidos en vivo.', 'meta_url' => 'https://facebook.com/', 'sort_order' => 20, ], [ 'title' => 'Nora Waves', 'subtitle' => 'Guest & curator', 'body' => 'Explora sonidos nocturnos con foco en electrónica latina e invitados del sello.', 'meta_url' => 'https://youtube.com/', 'sort_order' => 30, ], ], 'social' => [ [ 'title' => 'Instagram', 'subtitle' => '@lilirecordsradio', 'body' => 'Clips, detrás de cabina y anuncios de programación.', 'meta_url' => 'https://instagram.com/', 'sort_order' => 10, ], [ 'title' => 'Facebook', 'subtitle' => 'Lili Records Radio', 'body' => 'Comunidad, eventos y transmisiones compartidas.', 'meta_url' => 'https://facebook.com/', 'sort_order' => 20, ], [ 'title' => 'YouTube', 'subtitle' => 'Sesiones grabadas', 'body' => 'Entrevistas, especiales y sets destacados.', 'meta_url' => 'https://youtube.com/', 'sort_order' => 30, ], ], ]; $countStmt = db()->prepare('SELECT COUNT(*) FROM station_entries WHERE entry_type = :entry_type'); foreach ($seedMap as $type => $items) { $countStmt->execute(['entry_type' => $type]); if ((int) $countStmt->fetchColumn() > 0) { continue; } foreach ($items as $item) { save_entry(array_merge($item, [ 'entry_type' => $type, 'status' => 'published', 'meta_value' => $item['meta_value'] ?? null, ])); } } } function normalize_entry_type(string $type): string { $allowed = ['program', 'dj', 'social', 'message']; return in_array($type, $allowed, true) ? $type : 'program'; } function allowed_statuses(): array { return ['published', 'draft', 'new', 'reviewed']; } function save_entry(array $data, ?int $id = null): int { $payload = [ 'entry_type' => normalize_entry_type((string) ($data['entry_type'] ?? 'program')), 'title' => trim((string) ($data['title'] ?? '')), 'subtitle' => trim((string) ($data['subtitle'] ?? '')), 'body' => trim((string) ($data['body'] ?? '')), 'meta_url' => trim((string) ($data['meta_url'] ?? '')), 'meta_value' => trim((string) ($data['meta_value'] ?? '')), 'weekday' => array_key_exists('weekday', $data) && $data['weekday'] !== null && $data['weekday'] !== '' ? (int) $data['weekday'] : null, 'start_time' => array_key_exists('start_time', $data) && $data['start_time'] !== null && $data['start_time'] !== '' ? (string) $data['start_time'] : null, 'end_time' => array_key_exists('end_time', $data) && $data['end_time'] !== null && $data['end_time'] !== '' ? (string) $data['end_time'] : null, 'status' => in_array((string) ($data['status'] ?? 'published'), allowed_statuses(), true) ? (string) $data['status'] : 'published', 'sort_order' => (int) ($data['sort_order'] ?? 0), ]; if ($payload['title'] === '') { throw new InvalidArgumentException('El título es obligatorio.'); } if ($payload['meta_url'] !== '' && filter_var($payload['meta_url'], FILTER_VALIDATE_URL) === false) { throw new InvalidArgumentException('El enlace debe ser una URL válida.'); } if ($payload['entry_type'] === 'program') { if ($payload['weekday'] === null || $payload['weekday'] < 1 || $payload['weekday'] > 7) { throw new InvalidArgumentException('Selecciona un día válido para el programa.'); } if ($payload['start_time'] === null || $payload['end_time'] === null) { throw new InvalidArgumentException('Define hora de inicio y fin del programa.'); } } if ($payload['entry_type'] === 'message') { $payload['status'] = in_array($payload['status'], ['new', 'reviewed'], true) ? $payload['status'] : 'new'; } if ($id !== null) { $sql = 'UPDATE station_entries SET entry_type = :entry_type, title = :title, subtitle = :subtitle, body = :body, meta_url = :meta_url, meta_value = :meta_value, weekday = :weekday, start_time = :start_time, end_time = :end_time, status = :status, sort_order = :sort_order WHERE id = :id'; $stmt = db()->prepare($sql); $stmt->execute($payload + ['id' => $id]); return $id; } $sql = 'INSERT INTO station_entries (entry_type, title, subtitle, body, meta_url, meta_value, weekday, start_time, end_time, status, sort_order) VALUES (:entry_type, :title, :subtitle, :body, :meta_url, :meta_value, :weekday, :start_time, :end_time, :status, :sort_order)'; $stmt = db()->prepare($sql); $stmt->execute($payload); return (int) db()->lastInsertId(); } function get_entry(int $id): ?array { $stmt = db()->prepare('SELECT * FROM station_entries WHERE id = :id LIMIT 1'); $stmt->execute(['id' => $id]); $entry = $stmt->fetch(); return $entry ?: null; } function get_entries(string $type, array $statuses = []): array { $type = normalize_entry_type($type); $sql = 'SELECT * FROM station_entries WHERE entry_type = :entry_type'; $params = ['entry_type' => $type]; if ($statuses !== []) { $placeholders = []; foreach (array_values($statuses) as $index => $status) { $key = 'status_' . $index; $placeholders[] = ':' . $key; $params[$key] = $status; } $sql .= ' AND status IN (' . implode(', ', $placeholders) . ')'; } $sql .= ' ORDER BY sort_order ASC, weekday ASC, start_time ASC, created_at DESC'; $stmt = db()->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); } function get_entry_counts(): array { $rows = db()->query('SELECT entry_type, COUNT(*) AS total FROM station_entries GROUP BY entry_type')->fetchAll(); $counts = ['program' => 0, 'dj' => 0, 'social' => 0, 'message' => 0]; foreach ($rows as $row) { $counts[$row['entry_type']] = (int) $row['total']; } return $counts; } function weekday_name(?int $weekday): string { $days = [1 => 'Lunes', 2 => 'Martes', 3 => 'Miércoles', 4 => 'Jueves', 5 => 'Viernes', 6 => 'Sábado', 7 => 'Domingo']; return $days[$weekday ?? 0] ?? 'Sin día'; } function time_label(?string $time): string { if (!$time) { return '—'; } return substr($time, 0, 5); } function current_program(array $programs): ?array { $weekday = (int) date('N'); $time = date('H:i:s'); foreach ($programs as $program) { if ((int) $program['weekday'] !== $weekday) { continue; } if ($program['start_time'] <= $time && $program['end_time'] >= $time) { return $program; } } return null; } function next_program(array $programs): ?array { $nowWeekday = (int) date('N'); $nowTime = date('H:i:s'); $best = null; $bestWeight = null; foreach ($programs as $program) { $weekday = (int) $program['weekday']; $weight = (($weekday - $nowWeekday + 7) % 7) * 1440; $minutes = ((int) substr((string) $program['start_time'], 0, 2)) * 60 + (int) substr((string) $program['start_time'], 3, 2); $nowMinutes = ((int) substr($nowTime, 0, 2)) * 60 + (int) substr($nowTime, 3, 2); if ($weight === 0 && $minutes <= $nowMinutes) { $weight = 7 * 1440 + $minutes; } else { $weight += $minutes; } if ($bestWeight === null || $weight < $bestWeight) { $best = $program; $bestWeight = $weight; } } return $best; } function group_programs_by_day(array $programs): array { $grouped = []; foreach ($programs as $program) { $grouped[(int) $program['weekday']][] = $program; } ksort($grouped); return $grouped; } ensure_radio_schema(); seed_radio_content();