191 lines
5.8 KiB
PHP
191 lines
5.8 KiB
PHP
<?php
|
||
|
||
/*
|
||
* Copyright (c) 2025, Антон Аксенов
|
||
* This file is part of m3u.su project
|
||
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Core;
|
||
|
||
use App\Errors\PlaylistNotFoundException;
|
||
use Exception;
|
||
|
||
/**
|
||
* Класс для работы со списком плейлистов
|
||
*/
|
||
class IniFile
|
||
{
|
||
/**
|
||
* @var array[] Коллекция подгруженных плейлистов
|
||
*/
|
||
protected array $playlists;
|
||
|
||
/**
|
||
* @var string Дата последнего обновления списка
|
||
*/
|
||
protected string $updatedAt;
|
||
|
||
/**
|
||
* Считывает ini-файл и инициализирует плейлисты
|
||
*
|
||
* @return array
|
||
*/
|
||
public function load(): array
|
||
{
|
||
$filepath = config_path('playlists.ini');
|
||
$this->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,
|
||
);
|
||
}
|
||
}
|