39417-vm/app.php
2026-03-31 15:24:56 +00:00

364 lines
12 KiB
PHP

<?php
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/db/config.php';
date_default_timezone_set('UTC');
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function project_name(): string
{
$name = trim((string) ($_SERVER['PROJECT_NAME'] ?? ''));
return $name !== '' ? $name : 'Lili Records Radio';
}
function project_description(): string
{
$description = trim((string) ($_SERVER['PROJECT_DESCRIPTION'] ?? ''));
return $description !== ''
? $description
: 'Radio online para escuchar Lili Records Radio, revisar la programación y enviar mensajes o pedidos musicales.';
}
function set_flash(string $type, string $message): void
{
$_SESSION['flash'] = [
'type' => $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();