timezone = $timezone; } public function load(array $data, ?Config $config = null): array { $channelConfig = $config ? $config->getChannels() : []; $channels = []; $programmes = []; foreach ($data['channels'] ?? [] as $id => $channel) { $cfg = $channelConfig[$id] ?? []; if (($cfg['enabled'] ?? true) !== true) { continue; } $channels[$id] = [ 'id' => $id, 'name' => $cfg['name'] ?? $channel['name'] ?? $id, 'icon' => $cfg['icon'] ?? $channel['icon'] ?? '', 'number' => (int)($cfg['number'] ?? 999999), 'favorite' => (bool)($cfg['favorite'] ?? false), ]; foreach ($channel['programmes'] ?? [] as $p) { $programmes[$id][] = [ 'title' => $p['title'] ?? '', 'desc' => $p['description'] ?? '', 'category' => $p['category'] ?? '', 'metadata' => $p['metadata'] ?? [], 'start' => (new DateTime())->setTimestamp((int)$p['start'])->setTimezone(new DateTimeZone($this->timezone)), 'stop' => (new DateTime())->setTimestamp((int)$p['stop'])->setTimezone(new DateTimeZone($this->timezone)), ]; } } uasort($channels, function ($a, $b) { return ($a['number'] ?? 999999) <=> ($b['number'] ?? 999999); }); return [$channels, $programmes]; } public function getCurrentProgrammes(array $data, ?Config $config = null): array { $channelConfig = $config ? $config->getChannels() : []; $now = time(); $items = []; foreach ($data['channels'] ?? [] as $id => $channel) { $cfg = $channelConfig[$id] ?? []; if (($cfg['enabled'] ?? true) !== true) { continue; } $programmes = $channel['programmes'] ?? []; usort($programmes, function ($a, $b) { return ((int)($a['start'] ?? 0)) <=> ((int)($b['start'] ?? 0)); }); foreach ($programmes as $index => $p) { $start = (int)($p['start'] ?? 0); $stop = (int)($p['stop'] ?? 0); if ($now >= $start && $now < $stop) { $duration = max(1, $stop - $start); $elapsed = max(0, $now - $start); $percent = min(100, round(($elapsed / $duration) * 100)); $next = $programmes[$index + 1] ?? null; $items[] = [ 'channel_id' => $id, 'channel_name' => $cfg['name'] ?? $channel['name'] ?? $id, 'channel_icon' => $cfg['icon'] ?? $channel['icon'] ?? '', 'channel_number' => (int)($cfg['number'] ?? 999999), 'favorite' => (bool)($cfg['favorite'] ?? false), 'title' => $p['title'] ?? '', 'subtitle' => $p['subtitle'] ?? '', 'description' => $p['description'] ?? '', 'category' => $p['category'] ?? '', 'genre_icon' => $this->getGenreIcon($p['category'] ?? ''), 'metadata' => $p['metadata'] ?? [], 'year' => $p['year'] ?? '', 'rating' => $p['rating'] ?? '', 'episode' => $p['episode'] ?? '', 'quality' => $this->detectQuality($p), 'start' => $start, 'stop' => $stop, 'percent' => $percent, 'remaining_minutes' => max(0, (int)ceil(($stop - $now) / 60)), 'next_title' => $next['title'] ?? '', 'next_subtitle' => $next['subtitle'] ?? '', 'next_start' => isset($next['start']) ? (int)$next['start'] : null, 'next_stop' => isset($next['stop']) ? (int)$next['stop'] : null, 'next_category' => $next['category'] ?? '', ]; break; } } } usort($items, function ($a, $b) { return $a['channel_number'] <=> $b['channel_number']; }); return $items; } public function getDayProgrammes(Config $config, array $data, string $date, ?string $channelId = null): array { $channelConfig = $config->getChannels(); $tz = new DateTimeZone($this->timezone); $dayStart = new DateTime($date . ' 00:00:00', $tz); $dayEnd = clone $dayStart; $dayEnd->modify('+1 day'); $startTs = $dayStart->getTimestamp(); $endTs = $dayEnd->getTimestamp(); $items = []; foreach ($data['channels'] ?? [] as $id => $channel) { if ($channelId !== null && $id !== $channelId) { continue; } $cfg = $channelConfig[$id] ?? []; if (($cfg['enabled'] ?? true) !== true) { continue; } $programmes = []; foreach ($channel['programmes'] ?? [] as $p) { $start = (int)($p['start'] ?? 0); $stop = (int)($p['stop'] ?? 0); if ($start < $endTs && $stop > $startTs) { $visibleStart = max($start, $startTs); $visibleStop = min($stop, $endTs); $startDt = (new DateTime())->setTimestamp($visibleStart)->setTimezone($tz); $stopDt = (new DateTime())->setTimestamp($visibleStop)->setTimezone($tz); $startMinute = ((int)$startDt->format('H') * 60) + (int)$startDt->format('i'); $stopMinute = ((int)$stopDt->format('H') * 60) + (int)$stopDt->format('i'); if ($visibleStop >= $endTs) { $stopMinute = 1440; } $durationMinutes = max(1, (int)(($visibleStop - $visibleStart) / 60)); $programmes[] = [ 'id' => md5($id . '|' . $start . '|' . $stop . '|' . ($p['title'] ?? '')), 'title' => $p['title'] ?? '', 'subtitle' => $p['subtitle'] ?? '', 'description' => $p['description'] ?? '', 'category' => $p['category'] ?? '', 'metadata' => $p['metadata'] ?? [], 'start' => $start, 'stop' => $stop, 'visible_start' => $visibleStart, 'visible_stop' => $visibleStop, 'start_minute' => $startMinute, 'stop_minute' => $stopMinute, 'duration_minutes' => $durationMinutes, 'is_current' => time() >= $start && time() < $stop, 'is_past' => $stop < time(), ]; } } if (!$programmes) { continue; } usort($programmes, function ($a, $b) { return $a['start'] <=> $b['start']; }); $items[] = [ 'channel_id' => $id, 'channel_name' => $cfg['name'] ?? $channel['name'] ?? $id, 'channel_icon' => $cfg['icon'] ?? $channel['icon'] ?? '', 'channel_number' => (int)($cfg['number'] ?? 999999), 'favorite' => (bool)($cfg['favorite'] ?? false), 'programmes' => $programmes, ]; } usort($items, function ($a, $b) { return $a['channel_number'] <=> $b['channel_number']; }); return $items; } public function search( Config $config, array $data, string $query, int $limit = 20 ): array { $query = trim($query); $limit = max(1, min(50, $limit)); if (mb_strlen($query) < 2) { return []; } $channelConfig = $config->getChannels(); $tz = new DateTimeZone($this->timezone); $dayStart = new DateTime('today', $tz); $dayEnd = clone $dayStart; $dayEnd->modify('+1 day'); $startTs = $dayStart->getTimestamp(); $endTs = $dayEnd->getTimestamp(); $channelResults = []; $programmeResults = []; foreach ($data['channels'] ?? [] as $channelId => $channel) { $cfg = $channelConfig[$channelId] ?? []; if (($cfg['enabled'] ?? true) !== true) { continue; } $channelName = $cfg['name'] ?? $channel['name'] ?? $channelId; $channelNumber = (int)($cfg['number'] ?? 999999); $channelIcon = $cfg['icon'] ?? $channel['icon'] ?? ''; $favorite = (bool)($cfg['favorite'] ?? false); if (mb_stripos($channelName, $query) !== false) { $channelResults[] = [ 'type' => 'channel', 'channel_id' => $channelId, 'channel_name' => $channelName, 'channel_number' => $channelNumber, 'channel_icon' => $channelIcon, 'favorite' => $favorite, ]; } foreach ($channel['programmes'] ?? [] as $programme) { $start = (int)($programme['start'] ?? 0); $stop = (int)($programme['stop'] ?? 0); // Nur Sendungen, die den heutigen Tag berühren if ($start >= $endTs || $stop <= $startTs) { continue; } $title = trim((string)($programme['title'] ?? '')); $subtitle = trim((string)($programme['subtitle'] ?? '')); $matches = mb_stripos($title, $query) !== false || mb_stripos($subtitle, $query) !== false; if (!$matches) { continue; } $startDateTime = (new DateTime()) ->setTimestamp($start) ->setTimezone($tz); $stopDateTime = (new DateTime()) ->setTimestamp($stop) ->setTimezone($tz); $programmeResults[] = [ 'type' => 'programme', 'id' => md5( $channelId . '|' . $start . '|' . $stop . '|' . $title ), 'channel_id' => $channelId, 'channel_name' => $channelName, 'channel_number' => $channelNumber, 'channel_icon' => $channelIcon, 'favorite' => $favorite, 'title' => $title, 'subtitle' => $subtitle, 'description' => $programme['description'] ?? '', 'category' => $programme['category'] ?? '', 'metadata' => $programme['metadata'] ?? [], 'year' => $programme['year'] ?? '', 'rating' => $programme['rating'] ?? '', 'episode' => $programme['episode'] ?? '', 'icon' => $programme['icon'] ?? '', 'start' => $start, 'stop' => $stop, 'start_time' => $startDateTime->format('H:i'), 'stop_time' => $stopDateTime->format('H:i'), 'date' => $startDateTime->format('Y-m-d'), 'is_current' => time() >= $start && time() < $stop, 'is_past' => $stop <= time(), ]; } } usort($channelResults, function (array $a, array $b): int { return [ $a['channel_number'], mb_strtolower($a['channel_name']), ] <=> [ $b['channel_number'], mb_strtolower($b['channel_name']), ]; }); usort($programmeResults, function (array $a, array $b): int { return [ $a['start'], $a['channel_number'], ] <=> [ $b['start'], $b['channel_number'], ]; }); return array_slice( array_merge($channelResults, $programmeResults), 0, $limit ); } private function getGenreIcon(string $category): string { $category = mb_strtolower($category); return match (true) { str_contains($category, 'film'), str_contains($category, 'movie'), str_contains($category, 'kino') => '🎬', str_contains($category, 'serie'), str_contains($category, 'series') => '📺', str_contains($category, 'sport'), str_contains($category, 'football'), str_contains($category, 'fußball'), str_contains($category, 'fussball') => '⚽', str_contains($category, 'news'), str_contains($category, 'nachrichten'), str_contains($category, 'journal') => '📰', str_contains($category, 'doku'), str_contains($category, 'documentary') => '🌍', str_contains($category, 'kinder'), str_contains($category, 'kids'), str_contains($category, 'children') => '🧸', str_contains($category, 'musik'), str_contains($category, 'music') => '🎵', default => '▶', }; } private function detectQuality(array $programme): string { $text = mb_strtolower( ($programme['title'] ?? '') . ' ' . ($programme['subtitle'] ?? '') . ' ' . ($programme['description'] ?? '') ); return match (true) { str_contains($text, 'uhd'), str_contains($text, '4k') => 'UHD', str_contains($text, 'hd') => 'HD', default => '', }; } }