playlists = parse_ini_file($filepath, true); $this->updatedAt = date('d.m.Y h:i', filemtime($filepath)); return $this->playlists; } /** * Возвращает плейлисты * * @return array[] * @throws Exception */ public function getPlaylists(array $plsCodes = []): array { $playlists = []; empty($this->playlists) && $this->load(); empty($plsCodes) && $plsCodes = array_keys($this->playlists); $cached = array_combine($plsCodes, redis()->mget($plsCodes)); foreach ($cached as $code => $data) { $playlists[$code] = $this->initPlaylist($code, $data); } return $playlists; } /** * Возвращает плейлист по его коду * * @param string $code Код плейлиста * @return array|null * @throws PlaylistNotFoundException * @throws Exception */ public function getPlaylist(string $code): ?array { empty($this->playlists) && $this->load(); $data = redis()->get($code); return $this->initPlaylist($code, $data); } /** * Возвращает дату обновления ini-файла * * @return string */ public function updatedAt(): string { return $this->updatedAt; } /** * Подготавливает данные о плейлисте в расширенном формате * * @param string $code * @param array|false $data * @return array * @throws PlaylistNotFoundException */ protected function initPlaylist(string $code, array|false $data): array { if ($data === false) { $raw = $this->playlists[$code] ?? throw new PlaylistNotFoundException($code); $data = [ 'code' => $code, 'name' => $raw['name'] ?? "Плейлист #{$code}", 'description' => $raw['desc'] ?? null, 'url' => $raw['pls'], 'source' => $raw['src'] ?? null, 'content' => null, 'isOnline' => null, 'attributes' => [], 'groups' => [], 'channels' => [], 'checkedAt' => null, ]; } // приколы golang $data['attributes'] === null && $data['attributes'] = []; $data['groups'] === null && $data['groups'] = []; $data['channels'] === null && $data['channels'] = []; $data['onlinePercent'] = 0; $data['offlinePercent'] = 0; if ($data['isOnline'] === true && count($data['channels']) > 0) { $data['onlinePercent'] = round($data['onlineCount'] / count($data['channels']) * 100); $data['offlinePercent'] = round($data['offlineCount'] / count($data['channels']) * 100); } $data['hasCatchup'] = str_contains($data['content'] ?? '', 'catchup'); $data['hasTvg'] = !empty($data['attributes']['url-tvg']) || !empty($data['attributes']['x-tvg-url']); $data['hasTokens'] = $this->hasTokens($data); $data['tags'] = []; foreach ($data['channels'] as &$channel) { $data['tags'] = array_merge($data['tags'], $channel['tags']); $channel['hasToken'] = $this->hasTokens($channel); } $data['tags'] = array_values(array_unique($data['tags'])); sort($data['tags']); return $data; } /** * Проверяет наличие токенов в плейлисте * * Сделано именно так, а не через тег unstable, чтобы разделить логику: есть заведомо нестабильные каналы, * которые могут не транслироваться круглосуточно, а есть платные круглосуточные, которые могут оборваться * в любой момент. * * @param array $data * @return bool */ protected function hasTokens(array $data): bool { $string = ($data['url'] ?? '') . ($data['content'] ?? ''); if (empty($string)) { return false; } $badAttributes = [ // токены и ключи '[?&]token=', '[?&]drmreq=', // логины '[?&]u=', '[?&]user=', '[?&]username=', // пароли '[?&]p=', '[?&]pwd=', '[?&]password=', // неизвестные // 'free=true', // 'uid=', // 'c_uniq_tag=', // 'rlkey=', // '?s=', // '&s=', // '?q=', // '&q=', ]; return array_any( $badAttributes, static fn (string $badAttribute) => preg_match_all("/{$badAttribute}/", $string) >= 1, ); } }