Переработка под iptvc

This commit is contained in:
2025-05-12 00:07:43 +08:00
parent f43843bb07
commit 252af50239
29 changed files with 1662 additions and 1268 deletions

View File

@@ -1,195 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Core;
class ChannelLogo implements \Stringable
{
/**
* @var string Валидированная ссылка на изображение
*/
public readonly string $url;
/**
* @var string|null Хэш от ссылки на изображение
*/
public readonly ?string $hash;
/**
* @var string|null Путь к файлу изображению на диске
*/
protected ?string $path = '';
/**
* @var string|null MIME-тип изображения
*/
protected ?string $mimeType = null;
/**
* @var false|string|null Сырое изображение:
* null -- не загружалось;
* false -- ошибка загрузки;
* string -- бинарные данные.
*/
protected false|string|null $rawData = null;
/**
* Конструктор
*
* @param string $url Внешняя ссылка на изображение
*/
public function __construct(string $url)
{
$url = empty($url) ? base_url('public/no-tvg-logo.png') : $this->prepareUrl($url);
if (is_string($url)) {
$this->url = $url;
$this->hash = md5($url);
$this->path = cache_path("tv-logos/$this->hash");
}
}
/**
* Валидирует и очищает ссылку на изображение
*
* @param string $url
* @return false|string
*/
protected function prepareUrl(string $url): false|string
{
$parts = parse_url(trim($url));
if (!is_array($parts) || count($parts) < 2) {
return false;
}
$result = $parts['scheme'] . '://' . $parts['host'];
$result .= (empty($parts['port']) ? '' : ':' . $parts['port']);
return $result . $parts['path'];
}
/**
* Загружает сырое изображение по ссылке и определяет его MIME-тип
*
* @return bool
*/
public function fetch(): bool
{
$this->rawData = @file_get_contents($this->url);
$isFetched = is_string($this->rawData);
if (!$isFetched) {
return false;
}
$this->mimeType = $this->mimeType();
return true;
}
/**
* Сохраняет сырое изображение в кэш
*
* @return bool
*/
public function store(): bool
{
return is_string($this->rawData)
&& $this->prepareCacheDir()
&& @file_put_contents($this->path, $this->rawData);
}
/**
* Считывает изображение из кэша
*
* @return bool
*/
public function readFile(): bool
{
if (!file_exists($this->path)) {
return false;
}
$this->rawData = @file_get_contents($this->path);
return is_string($this->rawData);
}
/**
* Считывает дефолтный эскиз вместо логотипа
*
* @return bool
*/
public function setDefault(): bool
{
$this->path = root_path('public/no-tvg-logo.png');
return $this->readFile();
}
/**
* Возвращает base64-кодированное изображение
*
* @return string|null
*/
public function asBase64(): ?string
{
if (!is_string($this->rawData)) {
return null;
}
return "data:$this->mimeType;base64," . base64_encode($this->rawData);
}
/**
* Возвращает сырое изображение
*
* @return false|string|null
*/
public function raw(): false|string|null
{
return $this->rawData;
}
/**
* Проверяет готовность директории кэша изображений, создавая её при необходимости
*
* @return bool
*/
public function prepareCacheDir(): bool
{
$cacheFileDir = cache_path('tv-logos');
return is_dir($cacheFileDir)
|| @mkdir($cacheFileDir, 0775, true);
}
/**
* Возвращает MIME-тип сырого изображения
*
* @return string|null
*/
public function mimeType(): ?string
{
if (!is_string($this->rawData)) {
return null;
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
return $finfo->buffer($this->rawData) ?: null;
}
/**
* Возвращает размер сырого изображения в байтах
*
* @return int
*/
public function size(): int
{
return strlen((string)$this->rawData);
}
/**
* @inheritDoc
*/
public function __toString(): string
{
return $this->asBase64();
}
}

View File

@@ -1,4 +1,9 @@
<?php
/*
* Copyright (c) 2025, Антон Аксенов
* This file is part of iptv.axenov.dev web interface
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
*/
declare(strict_types=1);
@@ -8,7 +13,7 @@ use App\Errors\PlaylistNotFoundException;
use Exception;
/**
* Класс для работы с ini-файлом плейлистов
* Класс для работы со списком плейлистов
*/
class IniFile
{
@@ -18,65 +23,76 @@ class IniFile
protected array $ini;
/**
* @var Playlist[] Коллекция подгруженных плейлистов
* @var array[] Коллекция подгруженных плейлистов
*/
protected array $playlists = [];
/**
* @var string[] Карта переадресаций плейлистов
*/
protected array $redirections = [];
protected array $playlists;
/**
* @var string Дата последнего обновления списка
*/
protected string $updated_at;
protected string $updatedAt;
/**
* Считывает ini-файл и инициализирует объекты плейлистов
* Считывает ini-файл и инициализирует плейлисты
*
* @return void
* @return array
* @throws Exception
*/
public function load(): void
public function load(): array
{
$ini = redis()->hGetAll('_playlists_');
if (empty($ini)) {
$filepath = config_path('playlists.ini');
$ini = parse_ini_file($filepath, true);
$this->updated_at = date('d.m.Y h:i', filemtime($filepath));
$order = array_keys($ini);
$filepath = config_path('playlists.ini');
$ini = parse_ini_file($filepath, true);
$this->updatedAt = date('d.m.Y h:i', filemtime($filepath));
// сохраняем порядок
foreach (array_keys($ini) as $code) {
$data = redis()->get($code);
if ($data === false) {
$raw = $ini[$code];
$data = [
'code' => $code,
'name' => $raw['name'],
'description' => $raw['desc'],
'url' => $raw['pls'],
'source' => $raw['src'],
'content' => null,
'isOnline' => null,
'attributes' => [],
'groups' => [],
'channels' => [],
'onlineCount' => 0,
'offlineCount' => 0,
'checkedAt' => null,
];
} else if (!isset($data['attributes'])) {
$data['attributes'] = [];
}
$data['hasTvg'] = !empty($data['asttributes']['url-tvg']);
$data['hasCatchup'] = str_contains($data['content'] ?? '', 'catchup');
$data['tags'] = [];
foreach ($data['channels'] ?? [] as $channel) {
$data['tags'] = array_merge($data['tags'], $channel['tags']);
}
$data['tags'] = array_values(array_unique($data['tags']));
sort($data['tags']);
$this->playlists[$code] = $data;
}
$order ??= redis()->get('_order_');
$this->ini ??= $ini;
$this->updated_at ??= redis()->get('_updated_at_');
$transaction = redis()->multi();
foreach ($order as $id) {
$data = $this->ini[$id];
$this->playlists[(string)$id] = $pls = $this->makePlaylist($id, $data);
$transaction->hSet('_playlists_', $id, $pls);
}
$expireAfter = config('redis.ttl_days');
$transaction
->expire('_playlists_', $expireAfter)
->set('_order_', $order, ['EX' => $expireAfter])
->set('_updated_at_', $this->updated_at, ['EX' => $expireAfter])
->exec();
return $this->playlists;
}
/**
* Возвращает объекты плейлистов
* Возвращает плейлисты
*
* @param bool $all true - получить все, false - получить только НЕпереадресованные
* @return Playlist[]
* @return array[]
* @throws Exception
*/
public function playlists(bool $all = true): array
public function getPlaylists(): array
{
return $all
? $this->playlists
: array_filter($this->playlists, static fn ($playlist) => is_null($playlist->redirectId));
return $this->playlists ??= $this->load();
}
/**
@@ -86,50 +102,23 @@ class IniFile
*/
public function updatedAt(): string
{
return $this->updated_at;
return $this->updatedAt;
}
/**
* Возвращает ID плейлиста, на который нужно переадресовать указанный
* Возвращает плейлист по его коду
*
* @param string $id ID плейлиста
* @return string|null
*/
public function getRedirection(string $id): ?string
{
return $this->redirections[$id] ?? null;
}
/**
* Возвращает объект плейлиста
*
* @param string $id ID плейлиста
* @return Playlist|null
* @param string $code Код плейлиста
* @return array|null
* @throws PlaylistNotFoundException
*/
public function getPlaylist(string $id): ?Playlist
{
return $this->playlists[$id] ?? throw new PlaylistNotFoundException($id);
}
/**
* Создаёт объекты плейлистов, рекурсивно определяя переадресации
*
* @param int|string $id ID плейлиста
* @param array $params Описание плейлиста
* @param string|null $redirectId ID для переадресации
* @return Playlist
* @throws Exception
*/
protected function makePlaylist(int|string $id, array $params, ?string $redirectId = null): Playlist
public function getPlaylist(string $code): ?array
{
$id = (string)$id;
if (isset($params['redirect'])) {
$this->redirections[$id] = $redirectId = (string)$params['redirect'];
$params = $this->ini[$redirectId];
return $this->makePlaylist($id, $params, $redirectId);
if (empty($this->playlists)) {
$this->load();
}
return new Playlist($id, $params, $redirectId);
return $this->playlists[$code] ?? throw new PlaylistNotFoundException($code);
}
}

View File

@@ -1,4 +1,9 @@
<?php
/*
* Copyright (c) 2025, Антон Аксенов
* This file is part of iptv.axenov.dev web interface
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
*/
declare(strict_types=1);
@@ -6,6 +11,7 @@ namespace App\Core;
use App\Core\TwigExtention as IptvTwigExtension;
use Dotenv\Dotenv;
use GuzzleHttp\Client;
use InvalidArgumentException;
use Redis;
use Slim\App;
@@ -13,110 +19,73 @@ use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Twig\Error\LoaderError;
use Twig\Extension\DebugExtension;
/**
* Загрузчик приложения
*/
final class Core
final class Kernel
{
/**
* @var Core
* Версия приложения
*/
private static Core $instance;
public const string VERSION = '1.0.0';
/**
* @var Kernel
*/
private static Kernel $instance;
/**
* @var App
*/
protected App $app;
/**
* @var array Конфигурация приложения
*/
protected array $config = [];
/**
* @var Redis
*/
protected Redis $redis;
/**
* @var IniFile
*/
protected IniFile $iniFile;
/**
* @var array Конфигурация приложения
*/
protected array $config = [];
/**
* @var Redis|null
*/
protected ?Redis $cache = null;
/**
* @var Client|null
*/
protected ?Client $httpClient = null;
/**
* Закрытый конструктор
*
* @throws LoaderError
*/
private function __construct()
{
$this->app = AppFactory::create();
$this->loadSettings();
$this->loadRoutes();
$this->loadTwig();
return $this->app;
}
/**
* Возвращает объект приложения
*
* @return Core
* @return Kernel
*/
public static function get(): Core
public static function instance(): Kernel
{
return self::$instance ??= new self();
}
/**
* Загружает приложение
*
* @return App
* @throws LoaderError
*/
public function boot(): App
{
$this->app = AppFactory::create();
$this->bootSettings();
$this->bootRoutes();
$this->bootTwig();
$this->bootRedis();
$this->bootIni();
return $this->app;
}
/**
* Возвращает значение из конфига
*
* @param string $key Ключ в формате "config.key"
* @param mixed|null $default Значение по умолчанию
* @return mixed
*/
public function config(string $key, mixed $default = null): mixed
{
$parts = explode('.', $key);
return $this->config[$parts[0]][$parts[1]] ?? $default;
}
/**
* @return Redis
*/
public function redis(): Redis
{
return $this->redis;
}
/**
* @return IniFile
*/
public function ini(): IniFile
{
return $this->iniFile;
}
/**
* @return App
*/
public function app(): App
{
return $this->app;
}
/**
* Загружает файл .env или .env.$env
*
@@ -139,10 +108,9 @@ final class Core
*
* @return void
*/
protected function bootSettings(): void
protected function loadSettings(): void
{
$env = $this->loadDotEnvFile();
if (!empty($env['APP_ENV'])) {
$this->loadDotEnvFile($env['APP_ENV']);
}
@@ -151,6 +119,8 @@ final class Core
$key = basename($file, '.php');
$this->config += [$key => require_once $file];
}
date_default_timezone_set($this->config['app']['timezone'] ?? 'GMT');
}
/**
@@ -159,18 +129,19 @@ final class Core
* @return void
* @see https://www.slimframework.com/docs/v4/objects/routing.html
*/
protected function bootRoutes(): void
protected function loadRoutes(): void
{
foreach ($this->config['routes'] as $route) {
if (is_array($route['method'])) {
$definition = $this->app->map($route['method'], $route['path'], $route['handler']);
} else {
$isPossible = in_array($route['method'], ['GET', 'POST', 'OPTIONS', 'PUT', 'PATCH', 'DELETE']);
$method = trim($route['method']);
$isPossible = in_array($method, ['GET', 'POST', 'OPTIONS', 'PUT', 'PATCH', 'DELETE']);
$func = match (true) {
$route['method'] === '*' => 'any',
$isPossible => strtolower($route['method']),
default => throw new InvalidArgumentException(sprintf('Неверный HTTP метод %s', $route['method']))
$method === '*' => 'any',
$isPossible => strtolower($method),
default => throw new InvalidArgumentException(sprintf('Неверный HTTP метод %s', $method))
};
$definition = $this->app->$func($route['path'], $route['handler']);
@@ -189,42 +160,84 @@ final class Core
* @throws LoaderError
* @see https://www.slimframework.com/docs/v4/features/twig-view.html
*/
protected function bootTwig(): void
protected function loadTwig(): void
{
$twig = Twig::create(root_path('views'), $this->config['twig']);
$twig->addExtension(new IptvTwigExtension());
$this->app->add(TwigMiddleware::create($this->app, $twig));
if ($this->config['twig']['debug']) {
$twig->addExtension(new DebugExtension());
}
}
/**
* Инициализирует подключение к Redis
* Возвращает объект подключения к Redis
*
* @return void
* @return Redis
* @see https://github.com/phpredis/phpredis/?tab=readme-ov-file
*/
protected function bootRedis(): void
public function redis(): Redis
{
$options = [
'host' => $this->config['redis']['host'],
'port' => (int)$this->config['redis']['port'],
];
if (!empty($this->config['redis']['password'])) {
$options['auth'] = $this->config['redis']['password'];
if (!empty($this->cache)) {
return $this->cache;
}
$this->redis = new Redis($options);
$this->redis->select((int)$this->config['redis']['db']);
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);
$options = [
'host' => $this->config['cache']['host'],
'port' => (int)$this->config['cache']['port'],
];
if (!empty($this->config['cache']['password'])) {
$options['auth'] = $this->config['cache']['password'];
}
$this->cache = new Redis($options);
$this->cache->select((int)$this->config['cache']['db']);
$this->cache->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);
return $this->cache;
}
/**
* Инициализирует объект ini-файла
* Возвращает объект http-клиента
*
* @return void
* @return Client
*/
protected function bootIni(): void
public function guzzle(): Client
{
$this->iniFile = new IniFile();
return $this->httpClient ??= new Client($this->config['http']);
}
/**
* Возвращает значение из конфига
*
* @param string $key Ключ в формате "config.key"
* @param mixed|null $default Значение по умолчанию
* @return mixed
*/
public function config(string $key, mixed $default = null): mixed
{
$parts = explode('.', $key);
return $this->config[$parts[0]][$parts[1]] ?? $default;
}
/**
* Возвращает объект приложения
*
* @return App
*/
public function app(): App
{
return $this->app;
}
/**
* Возвращает объект ini-файла
*
* @return IniFile
*/
public function ini(): IniFile
{
return $this->iniFile ??= new IniFile();
}
}

View File

@@ -1,368 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Core;
use CurlHandle;
use Exception;
use Random\RandomException;
/**
* Плейлист без редиректа
*/
class Playlist
{
/**
* @var string|null Название плейлиста
*/
public ?string $name;
/**
* @var string|null Описание плейлиста
*/
public ?string $desc;
/**
* @var string Прямой URL до файла плейлиста на третьей стороне
*/
public string $pls;
/**
* @var string|null Источник плейлиста
*/
public ?string $src;
/**
* @var string Ссылка на плейлист в рамках проекта
*/
public string $url;
/**
* @var string|null Сырое содержимое плейлиста
*/
protected ?string $rawContent = null;
/**
* @var array Обработанное содержимое плейлиста
*/
protected array $parsedContent = [];
/**
* @var array Статус скачивания плейлиста
*/
protected array $downloadStatus = [
'httpCode' => 'unknown',
'errCode' => 'unknown',
'errText' => 'unknown',
'possibleStatus' => 'unknown',
];
/**
* Конструктор
*
* @param string $id ID плейлиста
* @param array $params Описание плейлиста
* @param string|null $redirectId ID для переадресации
* @throws Exception
*/
public function __construct(
public readonly string $id,
array $params,
public readonly ?string $redirectId = null
) {
empty($params['pls']) && throw new Exception(
"Плейлист с ID=$id обязан иметь параметр pls или redirect"
);
$this->url = base_url($id);
$this->name = empty($params['name']) ? "Плейлист #$id" : $params['name'];
$this->desc = empty($params['desc']) ? null : $params['desc'];
$this->pls = $params['pls'];
$this->src = empty($params['src']) ? null : $params['src'];
}
/**
* Получает содержимое плейлиста с третьей стороны
*
* @return void
*/
public function fetchContent(): void
{
$cached = redis()->get($this->id);
if (is_array($cached)) {
$this->downloadStatus['httpCode'] = $cached['httpCode'];
$this->downloadStatus['errCode'] = $cached['errCode'];
$this->downloadStatus['errText'] = $cached['errText'];
$this->downloadStatus['possibleStatus'] = $cached['possibleStatus'];
$this->rawContent = $cached['content'];
return;
}
$curl = $this->makeCurl();
$content = curl_exec($curl);
$this->rawContent = $content === false ? null : $content;
$this->downloadStatus['httpCode'] = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$this->downloadStatus['errCode'] = curl_errno($curl);
$this->downloadStatus['errText'] = curl_error($curl);
$this->downloadStatus['possibleStatus'] = $this->guessStatus($this->downloadStatus['errCode']);
curl_close($curl);
if ($cached === false) {
redis()->set($this->id, [
'httpCode' => $this->downloadStatus['httpCode'],
'errCode' => $this->downloadStatus['errCode'],
'errText' => $this->downloadStatus['errText'],
'possibleStatus' => $this->downloadStatus['possibleStatus'],
'content' => $this->rawContent,
], ['EX' => config('redis.ttl_days')]);
}
}
/**
* Возвращает статус проверки плейлиста по коду ошибки curl
*
* @param int $curlErrCode
* @return string
*/
protected function guessStatus(int $curlErrCode): string
{
return match ($curlErrCode) {
0 => 'online',
28 => 'timeout',
5, 6, 7, 22, 35 => 'offline',
default => 'error',
};
}
/**
* Парсит полученный от третьей стороны плейлист
*
* @return array Информация о составе плейлиста
* @throws RandomException
*/
public function parse(): array
{
if (!empty($this->parsed())) {
return $this->parsed();
}
$result = [
'attributes' => [],
'channels' => [],
'groups' => [],
'encoding' => [
'name' => 'unknown',
'alert' => false,
],
];
if (is_null($this->rawContent)) {
return $this->parsedContent = $result;
}
$enc = mb_detect_encoding($this->rawContent, config('app.pls_encodings'));
$result['encoding']['name'] = $enc;
if ($enc !== 'UTF-8') {
$result['encoding']['alert'] = true;
$this->rawContent = mb_convert_encoding($this->rawContent, 'UTF-8', $enc);
}
$lines = explode("\n", $this->rawContent);
$isHeader = $isGroup = $isChannel = false;
foreach ($lines as $line) {
if (empty($line = trim($line))) {
continue;
}
if (str_starts_with($line, '#EXTM3U ')) {
$isHeader = true;
$isGroup = $isChannel = false;
$result['attributes'] = $this->parseAttributes($line);
continue;
}
if (str_starts_with($line, '#EXTINF:')) {
$isChannel = true;
$isHeader = $isGroup = false;
$combined = trim(substr($line, strpos($line, ',') + 1));
$exploded = explode(',', $line);
$attrs = $this->parseAttributes($exploded[0]);
$tvgid = empty($attrs['tvg-id']) ? ' неизвестен' : "='{$attrs['tvg-id']}'";
$name = trim($exploded[1] ?? "(канал без названия, tvg-id$tvgid)");
$channel = [
'_id' => md5($name . random_int(1, 99999)),
'name' => trim($name),
'url' => null,
'group' => $attrs['group-title'] ?? null,
'attributes' => $attrs,
];
unset($name, $attrs, $combined, $exploded);
continue;
}
if (str_starts_with($line, '#EXTGRP:')) {
$isGroup = true;
$isHeader = false;
if ($isChannel) {
$exploded = explode(':', $line);
$channel['group'] = $exploded[1];
}
continue;
}
if ($isChannel) {
$channel['url'] = str_starts_with($line, 'http') ? $line : null;
$logoUrl = $channel['attributes']['tvg-logo'] ?? null;
if (is_string($logoUrl)) {
$logo = new ChannelLogo($logoUrl);
$logo->readFile();
$channel['logo'] = [
'base64' => $logo->asBase64(),
'size' => $logo->size(),
'mime-type' => $logo->mimeType(),
];
}
$result['channels'][] = $channel;
$isChannel = false;
unset($channel);
}
}
$groups = [];
foreach ($result['channels'] as $channel) {
$name = $channel['group'] ?? '(без группы)';
$id = md5($name);
if (empty($groups[$id])) {
$groups[$id] = [
'_id' => $id,
'name' => $name,
'channels' => [],
];
}
$groups[$id]['channels'][] = $channel['_id'];
}
$result['groups'] = array_values($groups);
return $this->parsedContent = $result;
}
public function check(): bool
{
$curl = $this->makeCurl([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_NOBODY => true,
CURLOPT_HEADER => true,
CURLOPT_CUSTOMREQUEST => 'HEAD',
]);
$content = curl_exec($curl);
$this->rawContent = $content === false ? null : $content;
$this->downloadStatus['httpCode'] = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$this->downloadStatus['errCode'] = curl_errno($curl);
$this->downloadStatus['errText'] = curl_error($curl);
$this->downloadStatus['possibleStatus'] = $this->guessStatus($this->downloadStatus['errCode']);
curl_close($curl);
return $this->downloadStatus['httpCode'] < 400;
}
protected function makeCurl(array $customOptions = []): CurlHandle
{
$options = [
CURLOPT_URL => $this->pls,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HEADER => false,
CURLOPT_FAILONERROR => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_USERAGENT => config('app.user_agent'),
];
$curl = curl_init();
foreach ($options as $option => $value) {
curl_setopt($curl, $option, $value);
}
// array_merge($options, $customOptions) loses keys
foreach ($customOptions as $option => $value) {
curl_setopt($curl, $option, $value);
}
return $curl;
}
/**
* Парсит атрибуты строки и возвращает ассоциативный массив
*
* @param string $line
* @return array
*/
protected function parseAttributes(string $line): array
{
if (str_starts_with($line, '#')) {
$line = trim(substr($line, strpos($line, ' ') + 1));
}
preg_match_all('#(?<key>[a-z-]+)="(?<value>.*)"#U', $line, $matches);
return array_combine($matches['key'], $matches['value']);
}
/**
* Возвращает содержимое объекта в виде массива
*
* @return array
*/
public function toArray(): array
{
return [
'id' => $this->id,
'url' => $this->url,
'name' => $this->name,
'desc' => $this->desc,
'pls' => $this->pls,
'src' => $this->src,
'status' => $this->status(),
'content' => [
...$this->parsed(),
'channelCount' => count($this->parsed()['channels'])
],
];
}
/**
* Возвращает ссылку на плейлист в рамках проекта
*
* @return string
*/
public function url(): string
{
return sprintf('%s/%s', base_url(), $this->id);
}
/**
* Возвращает статус скачивания плейлиста
*
* @return array|string[]
*/
public function status(): array
{
return $this->downloadStatus;
}
/**
* Возвращает обработанное содержимое плейлиста
*
* @return array
*/
public function parsed(): array
{
return $this->parsedContent;
}
}

View File

@@ -1,41 +1,91 @@
<?php
/*
* Copyright (c) 2025, Антон Аксенов
* This file is part of iptv.axenov.dev web interface
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
*/
declare(strict_types=1);
namespace App\Core;
use Twig\Error\LoaderError;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Расширение twig
*/
class TwigExtention extends AbstractExtension
{
/**
* @inheritDoc
*/
public function getFunctions(): array
{
return [
new TwigFunction('config', [$this, 'config']),
new TwigFunction('commit', [$this, 'commit']),
new TwigFunction('is_file', [$this, 'is_file']),
new TwigFunction('base_url', [$this, 'base_url']),
new TwigFunction('version', [$this, 'version']),
new TwigFunction('is_file', [$this, 'isFile']),
new TwigFunction('base_url', [$this, 'baseUrl']),
new TwigFunction('to_date', [$this, 'toDate']),
];
}
/**
* Возвращает значение из конфига
*
* @param string $key Ключ в формате "config.key"
* @param mixed|null $default Значение по умолчанию
* @return mixed
* @throws LoaderError
*/
public function config(string $key, mixed $default = null): mixed
{
return config($key, $default);
return kernel()->config($key, $default);
}
public function commit(): string
/**
* Возвращает версию приложения
*
* @return string
*/
public function version(): string
{
return file_get_contents(root_path('commit'));
return Kernel::VERSION;
}
public function base_url(string $path = ''): string
/**
* Возвращает базовый URL приложения
*
* @param string $path
* @return string
*/
public function baseUrl(string $path = ''): string
{
return base_url($path);
}
public function is_file(string $path): bool
/**
* Проверячет существование файла
*
* @param string $path Полный путь к файлу
* @return bool
*/
public function isFile(string $path): bool
{
return is_file($path);
}
/**
* Конвертирует unix timestamp в дату и время
*
* @param float|null $timestamp
* @param string $format
* @return string
*/
public function toDate(?float $timestamp, string $format = 'd.m.Y H:i:s'): string
{
return $timestamp === null ? '(неизвестно)' : date($format, (int)$timestamp);
}
}