364 lines
12 KiB
PHP
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();
|