diff --git a/docker/php/dev.php.ini b/docker/php/dev.php.ini index e7e6b6f..5fa8984 100644 --- a/docker/php/dev.php.ini +++ b/docker/php/dev.php.ini @@ -1,6 +1,8 @@ [PHP] error_reporting = E_ALL file_uploads = Off +memory_limit=-1 +max_execution_time=-1 ; upload_max_filesize=10M ; post_max_size=10M diff --git a/src/app/Controllers/Controller.php b/src/app/Controllers/Controller.php index cbb6e03..f348c4a 100644 --- a/src/app/Controllers/Controller.php +++ b/src/app/Controllers/Controller.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace App\Controllers; +use App\Core\IniFile; +use App\Core\Playlist; +use App\Exceptions\PlaylistNotFoundException; use Exception; use Flight; @@ -12,6 +15,41 @@ use Flight; */ abstract class Controller { + /** + * @var IniFile Класс для работы с ini-файлом плейлистов + */ + protected IniFile $ini; + + /** + * Конструктор + */ + public function __construct() + { + $this->ini = Flight::get('ini'); + } + + /** + * Возвращает плейлист по его ID + * + * @param string $id + * @return Playlist + * @throws Exception + */ + protected function getPlaylist(string $id): Playlist + { + if ($this->ini->getRedirection($id)) { + Flight::redirect(base_url($this->ini->getRedirection($id) . '/details')); + die; + } + + try { + return $this->ini->getPlaylist($id); + } catch (PlaylistNotFoundException) { + $this->notFound($id); + die; + } + } + /** * Перебрасывает на страницу 404 при ненайденном плейлисте * diff --git a/src/app/Controllers/HomeController.php b/src/app/Controllers/HomeController.php index bed4388..4a5bc69 100644 --- a/src/app/Controllers/HomeController.php +++ b/src/app/Controllers/HomeController.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace App\Controllers; -use App\Core\PlaylistProcessor; -use App\Core\RedirectedPlaylist; use Exception; use Flight; @@ -14,19 +12,6 @@ use Flight; */ class HomeController extends Controller { - /** - * @var PlaylistProcessor Обработчик ini-списка - */ - protected PlaylistProcessor $ini; - - /** - * Конструктор - */ - public function __construct() - { - $this->ini = new PlaylistProcessor(); - } - /** * Отображает главную страницу на указанной странице списка плейлистов * @@ -44,19 +29,20 @@ class HomeController extends Controller } // иначе формируем и сортируем список при необходимости, рисуем страницу - $per_page = 10; - $list = $this->ini->playlists - ->filter(static fn ($playlist) => !($playlist instanceof RedirectedPlaylist)) - ->forPage($page, $per_page); + $perPage = 10; + $count = count($this->ini->playlists()); + $pageCount = ceil($count / $perPage); + $offset = max(0, ($page - 1) * $perPage); + $list = array_slice($this->ini->playlists(), $offset, $perPage, true); view('list', [ 'updated_at' => $this->ini->updatedAt(), - 'count' => $this->ini->playlists->count(), + 'count' => $count, 'pages' => [ - 'count' => ceil($this->ini->playlists->count() / $per_page), + 'count' => $pageCount, 'current' => $page, ], - 'playlists' => $list->toArray(), + 'playlists' => $list, ]); } diff --git a/src/app/Controllers/PlaylistController.php b/src/app/Controllers/PlaylistController.php index e850647..9c80f31 100644 --- a/src/app/Controllers/PlaylistController.php +++ b/src/app/Controllers/PlaylistController.php @@ -4,9 +4,7 @@ declare(strict_types=1); namespace App\Controllers; -use App\Core\{ - PlaylistProcessor, - RedirectedPlaylist}; +use App\Core\IniFile; use App\Exceptions\PlaylistNotFoundException; use Exception; use Flight; @@ -16,19 +14,6 @@ use Flight; */ class PlaylistController extends Controller { - /** - * @var PlaylistProcessor Обработчик ini-списка - */ - protected PlaylistProcessor $ini; - - /** - * Конструктор - */ - public function __construct() - { - $this->ini = new PlaylistProcessor(); - } - /** * Отправляет запрос с клиента по прямой ссылке плейлиста * @@ -39,8 +24,8 @@ class PlaylistController extends Controller public function download($id): void { try { - $playlist = $this->ini->playlist($id); - if ($playlist instanceof RedirectedPlaylist) { + $playlist = $this->ini->getPlaylist($id); + if ($playlist instanceof IniFile) { Flight::redirect(base_url($playlist->redirect_id)); die; } @@ -60,19 +45,23 @@ class PlaylistController extends Controller */ public function details(string $id): void { - try { - $playlist = $this->ini->playlist($id); - if ($playlist instanceof RedirectedPlaylist) { - Flight::redirect(base_url($playlist->redirect_id . '/details')); - die; - } - view('details', [ - ...$playlist->toArray(), - ...$this->ini->parse($id), - ]); - } catch (PlaylistNotFoundException) { - $this->notFound($id); + $playlist = $this->getPlaylist($id); + $playlist->download(); + if ($playlist->status()['errCode'] > 0) { + $stop = 1; + // return [ + // 'status' => $this->guessStatus($fetched['errCode']), + // 'error' => [ + // 'code' => $fetched['errCode'], + // 'message' => $fetched['errText'], + // ], + // ]; } + + $playlist->parse(true); + $result = $playlist->toArray(); + + view('details', $result); } /** @@ -86,7 +75,7 @@ class PlaylistController extends Controller { try { $playlist = $this->ini->playlist($id); - if ($playlist instanceof RedirectedPlaylist) { + if ($playlist instanceof IniFile) { Flight::redirect(base_url($playlist->redirect_id . '/json')); die; } diff --git a/src/app/Core/BasicPlaylist.php b/src/app/Core/BasicPlaylist.php deleted file mode 100644 index 8a958fe..0000000 --- a/src/app/Core/BasicPlaylist.php +++ /dev/null @@ -1,28 +0,0 @@ -id); - } -} diff --git a/src/app/Core/Bootstrapper.php b/src/app/Core/Bootstrapper.php index c0f2291..703f180 100644 --- a/src/app/Core/Bootstrapper.php +++ b/src/app/Core/Bootstrapper.php @@ -4,10 +4,8 @@ declare(strict_types=1); namespace App\Core; -use App\Controllers\AjaxController; use App\Extensions\TwigFunctions; use Flight; -use Illuminate\Support\Arr; use Twig\Environment; use Twig\Extension\DebugExtension; use Twig\Loader\FilesystemLoader; @@ -24,11 +22,19 @@ final class Bootstrapper */ public static function bootSettings(): void { - $settings = Arr::dot(require_once config_path('app.php')); - Arr::map($settings, function ($value, $key) { - Flight::set("flight.$key", $value); - }); - Flight::set('config', $settings); + $config = require_once config_path('app.php'); + foreach ($config as $key => $value) { + Flight::set($key, $value); + } + Flight::set('config', $config); + } + + public static function bootIni(): void + { + $loader = new IniFile(); + $loader->load(); + + Flight::set('ini', $loader); } /** @@ -38,18 +44,20 @@ final class Bootstrapper */ public static function bootTwig(): void { - $filesystemLoader = new FilesystemLoader(config('views.path')); - Flight::register( - 'view', - Environment::class, - [$filesystemLoader, config('twig')], - function ($twig) { - /** @var Environment $twig */ - Flight::set('twig', $twig); - $twig->addExtension(new TwigFunctions()); - $twig->addExtension(new DebugExtension()); - } - ); + $twigCfg = [ + 'cache' => config('twig.cache'), + 'debug' => config('twig.debug'), + ]; + + $closure = static function ($twig) { + /** @var Environment $twig */ + Flight::set('twig', $twig); + $twig->addExtension(new TwigFunctions()); + $twig->addExtension(new DebugExtension()); + }; + + $loader = new FilesystemLoader(config('flight.views.path')); + Flight::register('view', Environment::class, [$loader, $twigCfg], $closure); } /** diff --git a/src/app/Core/IniFile.php b/src/app/Core/IniFile.php new file mode 100644 index 0000000..066217b --- /dev/null +++ b/src/app/Core/IniFile.php @@ -0,0 +1,92 @@ +updated_at = date('d.m.Y h:i', filemtime($filepath)); + + $this->rawIni = parse_ini_file($filepath, true); + foreach ($this->rawIni as $id => $data) { + $this->playlists[(string)$id] = $this->makePlaylist($id, $data); + } + // ksort($this->playlists); + } + + public function playlists(): array + { + return $this->playlists; + } + + public function updatedAt(): string + { + return $this->updated_at; + } + + public function getRedirection(string $id): ?string + { + return $this->redirections[$id] ?? null; + } + + /** + * @param string $id + * @return Playlist|null + * @throws PlaylistNotFoundException + */ + public function getPlaylist(string $id): ?Playlist + { + return $this->playlists[$id] ?? throw new PlaylistNotFoundException($id); + } + + /** + * @param int|string $id + * @param array $params + * @return Playlist + * @throws \Exception + */ + protected function makePlaylist(int|string $id, array $params): Playlist + { + $id = (string)$id; + if (isset($params['redirect'])) { + $this->redirections[$id] = $params['redirect']; + $params = $this->rawIni[$this->redirections[$id]]; + return $this->makePlaylist($id, $params); + } + + return new Playlist($id, $params); + } +} diff --git a/src/app/Core/Playlist.php b/src/app/Core/Playlist.php index 33534cc..746d001 100644 --- a/src/app/Core/Playlist.php +++ b/src/app/Core/Playlist.php @@ -9,7 +9,7 @@ use Exception; /** * Плейлист без редиректа */ -class Playlist extends BasicPlaylist +class Playlist { /** * @var string|null Название плейлиста @@ -36,6 +36,26 @@ class Playlist extends BasicPlaylist */ public string $url; + /** + * @var string + */ + protected string $rawContent = ''; + + /** + * @var array + */ + protected array $parsedContent = []; + + /** + * @var array|null[] + */ + protected array $downloadStatus = [ + 'httpCode' => 'unknown', + 'errCode' => 'unknown', + 'errText' => 'unknown', + 'possibleStatus' => 'unknown', + ]; + /** * Конструктор * @@ -48,6 +68,7 @@ class Playlist extends BasicPlaylist 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']; @@ -55,6 +76,172 @@ class Playlist extends BasicPlaylist $this->src = empty($params['src']) ? null : $params['src']; } + /** + * Получает содержимое плейлиста с третьей стороны + * + * @return void + */ + public function download(): void + { + $curl = curl_init(); + curl_setopt_array($curl, [ + CURLOPT_URL => $this->pls, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HEADER => false, + CURLOPT_FAILONERROR => true, + ]); + + $content = curl_exec($curl); + if (!is_string($content)) { + $stop = 1; + } + + $this->rawContent = $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); + } + + /** + * Возвращает статус проверки плейлиста по коду ошибки 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 Информация о составе плейлиста + */ + public function parse(bool $groupped = false): array + { + if (!empty($this->parsed())) { + return $this->parsed(); + } + + $result = [ + 'attributes' => [], + 'channels' => [], + 'groups' => [], + 'encoding' => [ + 'name' => 'unknown', + 'alert' => false, + ], + ]; + + $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(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(',', $combined); + $exploded = explode(',', $line); + $attrs = $this->parseAttributes($exploded[0]); + if (count($exploded) > 2) { + $name = implode(',', array_slice($exploded, 1)); + } else { + $name = $exploded[1] ?? '(канал без названия)'; + } + $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; + $result['channels'][] = $channel; + $isChannel = false; + unset($channel); + } + } + + // if ($groupped) { + $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; + } + + /** + * Парсит атрибуты строки и возвращает ассоциативный массив + * + * @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('#(?[a-z-]+)="(?.*)"#U', $line, $matches); + return array_combine($matches['key'], $matches['value']); + } + /** * @inheritDoc */ @@ -67,6 +254,31 @@ class Playlist extends BasicPlaylist '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); + } + + public function status(): array + { + return $this->downloadStatus; + } + + public function parsed(): array + { + return $this->parsedContent; + } } diff --git a/src/app/Core/PlaylistProcessor.php b/src/app/Core/PlaylistProcessor.php deleted file mode 100644 index c4ae2d1..0000000 --- a/src/app/Core/PlaylistProcessor.php +++ /dev/null @@ -1,175 +0,0 @@ -updated_at = date('d.m.Y h:i', filemtime($filepath)); - $this->playlists = collect(parse_ini_file($filepath, true)) - ->transform(static fn ($playlist, $id) => empty($playlist['redirect']) - ? new Playlist((string)$id, $playlist) - : new RedirectedPlaylist((string)$id, $playlist['redirect']) - ); - } - - /** - * Проверяет есть ли в списке плейлист по его id - * - * @param string $id - * @return bool - */ - public function hasId(string $id): bool - { - return $this->playlists->keys()->contains($id); - } - - /** - * Возвращает из коллекции указанный плейлист, если он существует - * - * @param string $id - * @return Playlist|RedirectedPlaylist - * @throws PlaylistNotFoundException - */ - public function playlist(string $id): Playlist|RedirectedPlaylist - { - !$this->hasId($id) && throw new PlaylistNotFoundException($id); - return $this->playlists[$id]; - } - - /** - * Проверяет доступность плейлиста на третьей стороне - * - * @param string $id - * @return bool - * @throws PlaylistNotFoundException - */ - public function check(string $id): bool - { - $curl = curl_init(); - curl_setopt_array($curl, [ - CURLOPT_URL => $this->playlist($id)->pls, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 5, - CURLOPT_HEADER => false, - CURLOPT_NOBODY => true, - ]); - curl_exec($curl); - $code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); - curl_close($curl); - return $code < 400; - } - - /** - * Получает содержимое плейлиста с третьей стороны - * - * @param string $id - * @return array - * @throws PlaylistNotFoundException - */ - protected function fetch(string $id): array - { - $curl = curl_init(); - curl_setopt_array($curl, [ - CURLOPT_URL => $this->playlist($id)->pls, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 30, - CURLOPT_HEADER => false, - CURLOPT_FAILONERROR => true, - ]); - $content = curl_exec($curl); - $http_code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); - $err_code = curl_errno($curl); - $err_text = curl_error($curl); - curl_close($curl); - return [ - 'content' => $content, - 'http_code' => $http_code, - 'err_code' => $err_code, - 'err_text' => $err_text, - ]; - } - - /** - * Возвращает статус проверки плейлиста по коду ошибки curl - * - * @param int $curl_err_code - * @return string - */ - protected function guessStatus(int $curl_err_code): string - { - return match ($curl_err_code) { - 0 => 'online', - 28 => 'timeout', - 5, 6, 7, 22, 35 => 'offline', - default => 'error', - }; - } - - /** - * Парсит полученный от третьей стороны плейлист - * - * @param string $id - * @return array Информация о составе плейлиста - * @throws PlaylistNotFoundException - */ - public function parse(string $id): array - { - $fetched = $this->fetch($id); - if ($fetched['err_code'] > 0) { - return [ - 'status' => $this->guessStatus($fetched['err_code']), - 'error' => [ - 'code' => $fetched['err_code'], - 'message' => $fetched['err_text'], - ], - ]; - } - $result['status'] = $this->guessStatus($fetched['err_code']); - $result['encoding']['name'] = 'UTF-8'; - $result['encoding']['alert'] = false; - if (($enc = mb_detect_encoding($fetched['content'], config('app.pls_encodings'))) !== 'UTF-8') { - $fetched['content'] = mb_convert_encoding($fetched['content'], 'UTF-8', $enc); - $result['encoding']['name'] = $enc; - $result['encoding']['alert'] = true; - } - $matches = []; - preg_match_all("/^#EXTINF:-?\d.*,\s*(.*)/m", $fetched['content'], $matches); - $result['channels'] = array_map('trim', $matches[1]); - $result['count'] = $fetched['http_code'] < 400 ? count($result['channels']) : 0; - return $result; - } - - /** - * Возвращает дату последнего обновления списка плейлистов - * - * @return string - */ - public function updatedAt(): string - { - return $this->updated_at; - } -} diff --git a/src/app/Core/RedirectedPlaylist.php b/src/app/Core/RedirectedPlaylist.php deleted file mode 100644 index 6c55947..0000000 --- a/src/app/Core/RedirectedPlaylist.php +++ /dev/null @@ -1,34 +0,0 @@ - $this->id, - 'redirect_id' => $this->redirect_id, - ]; - } -} diff --git a/src/app/Exceptions/PlaylistNotFoundException.php b/src/app/Exceptions/PlaylistNotFoundException.php index f8864b1..bae0833 100644 --- a/src/app/Exceptions/PlaylistNotFoundException.php +++ b/src/app/Exceptions/PlaylistNotFoundException.php @@ -8,8 +8,8 @@ use Exception; class PlaylistNotFoundException extends Exception { - public function __construct(string $pls_code) + public function __construct(string $id) { - parent::__construct("Плейлист $pls_code не найден!"); + parent::__construct("Плейлист $id не найден!"); } } diff --git a/src/app/helpers.php b/src/app/helpers.php index e3b1f1f..f6a38f1 100644 --- a/src/app/helpers.php +++ b/src/app/helpers.php @@ -134,16 +134,5 @@ function bool(mixed $value): bool */ function config(string $key, mixed $default = null): mixed { - $config = Flight::get('config'); - if (isset($config["flight.$key"])) { - return $config["flight.$key"]; - } - if (isset($config[$key])) { - return $config[$key]; - } - $config = Arr::undot($config); - if (Arr::has($config, $key)) { - return Arr::get($config, $key); - } - return $default; + return Flight::get('config')[$key] ?? $default; } diff --git a/src/composer.json b/src/composer.json index 7d4b1b3..e177096 100644 --- a/src/composer.json +++ b/src/composer.json @@ -2,7 +2,7 @@ "require": { "php": "^8.2", "ext-json": "*", - "illuminate/collections": "^11.23", + "ext-curl": "*", "mikecao/flight": "^3.12", "symfony/dotenv": "^7.1", "twig/twig": "^3.14" diff --git a/src/composer.lock b/src/composer.lock index ef0cac3..8257e3f 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -4,203 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8e2b70162003ee57e4e033d98f833457", + "content-hash": "3cbd8253b2f0790d682e38f308df6e7f", "packages": [ - { - "name": "illuminate/collections", - "version": "v11.23.5", - "source": { - "type": "git", - "url": "https://github.com/illuminate/collections.git", - "reference": "cbea9d7a82984bbc1a9376498533cc77513f9a09" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/cbea9d7a82984bbc1a9376498533cc77513f9a09", - "reference": "cbea9d7a82984bbc1a9376498533cc77513f9a09", - "shasum": "" - }, - "require": { - "illuminate/conditionable": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "php": "^8.2" - }, - "suggest": { - "symfony/var-dumper": "Required to use the dump method (^7.0)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - } - }, - "autoload": { - "files": [ - "helpers.php" - ], - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Collections package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2024-09-12T14:50:04+00:00" - }, - { - "name": "illuminate/conditionable", - "version": "v11.23.5", - "source": { - "type": "git", - "url": "https://github.com/illuminate/conditionable.git", - "reference": "362dd761b9920367bca1427a902158225e9e3a23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/362dd761b9920367bca1427a902158225e9e3a23", - "reference": "362dd761b9920367bca1427a902158225e9e3a23", - "shasum": "" - }, - "require": { - "php": "^8.0.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Conditionable package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2024-06-28T20:10:30+00:00" - }, - { - "name": "illuminate/contracts", - "version": "v11.23.5", - "source": { - "type": "git", - "url": "https://github.com/illuminate/contracts.git", - "reference": "5a4c6dcf633c1f69e1b70bbea1ef1b7d2186d3da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/5a4c6dcf633c1f69e1b70bbea1ef1b7d2186d3da", - "reference": "5a4c6dcf633c1f69e1b70bbea1ef1b7d2186d3da", - "shasum": "" - }, - "require": { - "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1", - "psr/simple-cache": "^1.0|^2.0|^3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Contracts\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Contracts package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2024-09-12T15:25:08+00:00" - }, - { - "name": "illuminate/macroable", - "version": "v11.23.5", - "source": { - "type": "git", - "url": "https://github.com/illuminate/macroable.git", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", - "shasum": "" - }, - "require": { - "php": "^8.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Macroable package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2024-06-28T20:10:30+00:00" - }, { "name": "mikecao/flight", "version": "v3.12.0", @@ -272,110 +77,6 @@ }, "time": "2024-08-22T17:05:34+00:00" }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "psr/simple-cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ], - "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" - }, - "time": "2021-10-29T13:26:27+00:00" - }, { "name": "symfony/deprecation-contracts", "version": "v3.5.0", @@ -445,16 +146,16 @@ }, { "name": "symfony/dotenv", - "version": "v7.1.3", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "a26be30fd61678dab694a18a85084cea7673bbf3" + "reference": "6d966200b399fa59759286f3fc7c919f0677c449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/a26be30fd61678dab694a18a85084cea7673bbf3", - "reference": "a26be30fd61678dab694a18a85084cea7673bbf3", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/6d966200b399fa59759286f3fc7c919f0677c449", + "reference": "6d966200b399fa59759286f3fc7c919f0677c449", "shasum": "" }, "require": { @@ -499,7 +200,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v7.1.3" + "source": "https://github.com/symfony/dotenv/tree/v7.1.5" }, "funding": [ { @@ -515,7 +216,7 @@ "type": "tidelift" } ], - "time": "2024-07-09T19:36:07+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/polyfill-ctype", @@ -840,7 +541,8 @@ "prefer-lowest": false, "platform": { "php": "^8.2", - "ext-json": "*" + "ext-json": "*", + "ext-curl": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/src/config/app.php b/src/config/app.php index 7188cd0..64b81ef 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -3,28 +3,20 @@ declare(strict_types=1); return [ - 'flight' => [ - // https://flightphp.com/learn#configuration - 'base_url' => env('APP_URL', 'http://localhost:8080'), - 'case_sensitive' => bool(env('FLIGHT_CASE_SENSITIVE', false)), - 'handle_errors' => bool(env('FLIGHT_HANDLE_ERRORS', true)), - 'log_errors' => bool(env('FLIGHT_LOG_ERRORS', true)), - 'views' => [ - 'path' => views_path(), - 'extension' => '.twig', - ], - ], - 'twig' => [ - 'cache' => bool(env('TWIG_CACHE', true)) ? cache_path() . '/views' : false, - 'debug' => bool(env('TWIG_DEBUG', false)), - ], - 'app' => [ - 'title' => env('APP_TITLE', 'IPTV Playlists'), - 'pls_encodings' => [ - 'UTF-8', - 'CP1251', - // 'CP866', - // 'ISO-8859-5', - ], + // https://flightphp.com/learn#configuration + 'flight.base_url' => env('APP_URL', 'http://localhost:8080'), + 'flight.case_sensitive' => bool(env('FLIGHT_CASE_SENSITIVE', false)), + 'flight.handle_errors' => bool(env('FLIGHT_HANDLE_ERRORS', true)), + 'flight.log_errors' => bool(env('FLIGHT_LOG_ERRORS', true)), + 'flight.views.path' => views_path(), + 'flight.views.extension' => '.twig', + 'twig.cache' => bool(env('TWIG_CACHE', true)) ? cache_path() . '/views' : false, + 'twig.debug' => bool(env('TWIG_DEBUG', false)), + 'app.title' => env('APP_TITLE', 'IPTV Playlists'), + 'app.pls_encodings' => [ + 'UTF-8', + 'CP1251', + // 'CP866', + // 'ISO-8859-5', ], ]; diff --git a/src/config/playlists.ini b/src/config/playlists.ini index 11181a3..f330140 100644 --- a/src/config/playlists.ini +++ b/src/config/playlists.ini @@ -1,6 +1,6 @@ [1] name = 'free-tv.me' -desc = 'Каналы РФ и Беларуси; мультики, новости, кино, музыка, спорт, 18+ и мн. др.' +desc = 'Каналы СНГ. Обновления бывают очень большими. Политика, мультики, новости, кино, музыка, спорт, 18+ и мн. др.' pls = 'https://free-tv.me/iptv/tv' src = diff --git a/src/config/routes.php b/src/config/routes.php index 0ae2acb..75735ac 100644 --- a/src/config/routes.php +++ b/src/config/routes.php @@ -6,11 +6,11 @@ use App\Controllers\HomeController; use App\Controllers\PlaylistController; return [ - 'GET /' => (new HomeController())->index(...), - 'GET /page/@page:[0-9]+' => (new HomeController())->index(...), - 'GET /faq' => (new HomeController())->faq(...), - 'GET /@id:[a-zA-Z0-9_-]+' => (new PlaylistController())->download(...), - 'GET /?[a-zA-Z0-9_-]+' => (new PlaylistController())->download(...), - 'GET /@id:[a-zA-Z0-9_-]+/details' => (new PlaylistController())->details(...), - 'GET /@id:[a-zA-Z0-9_-]+/json' => (new PlaylistController())->json(...), + 'GET /' => [HomeController::class, 'index'], + 'GET /page/@page:[0-9]+' => [HomeController::class, 'index'], + 'GET /faq' => [HomeController::class, 'faq'], + 'GET /@id:[a-zA-Z0-9_-]+' => [PlaylistController::class, 'download'], + 'GET /?[a-zA-Z0-9_-]+' => [PlaylistController::class, 'download'], + 'GET /@id:[a-zA-Z0-9_-]+/details' => [PlaylistController::class, 'details'], + 'GET /@id:[a-zA-Z0-9_-]+/json' => [PlaylistController::class, 'json'], ]; diff --git a/src/public/index.php b/src/public/index.php index b9db04e..be10d71 100644 --- a/src/public/index.php +++ b/src/public/index.php @@ -11,20 +11,10 @@ use Symfony\Component\Dotenv\Dotenv; |-------------------------------------------------------------------------- */ -// autoload composer packages require '../vendor/autoload.php'; - -// load .env parameters (new Dotenv())->loadEnv(root_path() . '/.env'); - -// set up framework according to its config Bootstrapper::bootSettings(); - -// set up Twig template engine Bootstrapper::bootTwig(); - -// set up routes defined in config file +Bootstrapper::bootIni(); Bootstrapper::bootRoutes(); - -// start application Flight::start(); diff --git a/src/views/details.twig b/src/views/details.twig index ea5d26f..4f759de 100644 --- a/src/views/details.twig +++ b/src/views/details.twig @@ -3,25 +3,24 @@ {% block title %}{{ title }}{% endblock %} {% block header %} -

{{ name }}

- {% if (encoding.alert) %} +

О плейлисте {{ name }}

+ {% if (content.encoding.alert) %} {% endif %} {% if (error) %} {% endif %} {% endblock %} {% block content %}
-
-

О плейлисте

+
@@ -58,18 +57,54 @@ + + + +
Источник {{ src }}
Количество каналов{{ content.channelCount ?? 0 }}
-
-

Список каналов ({{ count ?? 0 }})

-
+ +
+

Список каналов

+{#
#} +{# {% for group in content.groups %}#} +{#
#} +{#

#} +{# #} +{#

#} +{#
#} +{#
#} +{#
    #} +{# {% for group in content.channels %}#} +{#
  • An item
  • #} +{# {% endfor %}#} +{#
#} +{#
#} +{#
#} +{#
#} +{# {% endfor %}#} +{#
#} + + + + +
- {% for channel in channels %} + {% for channel in content.channels %} - + + {% endfor %} diff --git a/src/views/template.twig b/src/views/template.twig index 0206068..9b60b6f 100644 --- a/src/views/template.twig +++ b/src/views/template.twig @@ -6,7 +6,11 @@ - + @@ -55,10 +59,10 @@ -
+
{% block header %}{% endblock %} {% block content %}{% endblock %} -
+
{{ loop.index }}{{ channel }} + {% if (channel.attributes['tvg-logo']) %} +
+ +
+ {% endif %} +
{{ channel.name }}