mirror of
https://github.com/anthonyaxenov/iptv.git
synced 2024-11-25 06:44:25 +00:00
wip
This commit is contained in:
parent
8f8b6501cf
commit
b02e68f635
@ -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
|
||||
|
||||
|
@ -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 при ненайденном плейлисте
|
||||
*
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* Базовый класс плейлиста
|
||||
*/
|
||||
abstract class BasicPlaylist implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @var string ID плейлиста
|
||||
*/
|
||||
public string $id;
|
||||
|
||||
/**
|
||||
* Возвращает ссылку на плейлист в рамках проекта
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return sprintf('%s/%s', base_url(), $this->id);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
93
src/app/Core/IniFile.php
Normal file
93
src/app/Core/IniFile.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Exceptions\PlaylistNotFoundException;
|
||||
|
||||
/**
|
||||
* Класс для работы с ini-файлом плейлистов
|
||||
*/
|
||||
class IniFile
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $rawIni;
|
||||
|
||||
/**
|
||||
* @var Playlist[] Коллекция подгруженных плейлистов
|
||||
*/
|
||||
protected array $playlists = [];
|
||||
|
||||
/**
|
||||
* @var array Карта переадресаций плейлистов
|
||||
*/
|
||||
protected array $redirections = [];
|
||||
|
||||
/**
|
||||
* @var string Дата последнего обновления списка
|
||||
*/
|
||||
protected string $updated_at;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function load(): void
|
||||
{
|
||||
$filepath = config_path('playlists.ini');
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -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,158 @@ 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' => [],
|
||||
'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 = [
|
||||
'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) {
|
||||
|
||||
$stop = 1;
|
||||
}
|
||||
|
||||
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('#(?<key>[a-z-]+)="(?<value>.*)"#U', $line, $matches);
|
||||
return array_combine($matches['key'], $matches['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@ -67,6 +240,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;
|
||||
}
|
||||
}
|
||||
|
@ -1,175 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Exceptions\PlaylistNotFoundException;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Обработчик списка плейлистов
|
||||
*/
|
||||
final class PlaylistProcessor
|
||||
{
|
||||
/**
|
||||
* @var Collection Коллекция подгруженных плейлистов
|
||||
*/
|
||||
public Collection $playlists;
|
||||
|
||||
/**
|
||||
* @var string Дата последнего обновления списка
|
||||
*/
|
||||
protected string $updated_at;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$filepath = config_path('playlists.ini');
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* Плейлист с редиректом
|
||||
*/
|
||||
class RedirectedPlaylist extends BasicPlaylist
|
||||
{
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $redirect_id
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $redirect_id,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'redirect_id' => $this->redirect_id,
|
||||
];
|
||||
}
|
||||
}
|
@ -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 не найден!");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
316
src/composer.lock
generated
316
src/composer.lock
generated
@ -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"
|
||||
|
@ -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' => [
|
||||
'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',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -1,6 +1,6 @@
|
||||
[1]
|
||||
name = 'free-tv.me'
|
||||
desc = 'Каналы РФ и Беларуси; мультики, новости, кино, музыка, спорт, 18+ и мн. др.'
|
||||
desc = 'Каналы СНГ. Обновления бывают очень большими. Политика, мультики, новости, кино, музыка, спорт, 18+ и мн. др.'
|
||||
pls = 'https://free-tv.me/iptv/tv'
|
||||
src =
|
||||
|
||||
|
@ -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'],
|
||||
];
|
||||
|
@ -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();
|
||||
|
@ -4,23 +4,23 @@
|
||||
|
||||
{% block header %}
|
||||
<h2>{{ name }}</h2>
|
||||
{% if (encoding.alert) %}
|
||||
{% if (content.encoding.alert) %}
|
||||
<div class="alert alert-warning small" role="alert">
|
||||
Кодировка исходного плейлиста отличается от UTF-8.
|
||||
Он был автоматически с конвертирован из {{ encoding.name }}, чтобы отобразить здесь список каналов.
|
||||
Он был автоматически с конвертирован из {{ content.encoding.name }}, чтобы отобразить здесь список каналов.
|
||||
Однако названия каналов могут отображаться некорректно, причём не только здесь, но и в плеере.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if (error) %}
|
||||
<div class="alert alert-danger small" role="alert">
|
||||
Ошибка плейлиста: [{{ error.code }}] {{ error.message }}
|
||||
Ошибка плейлиста: [{{ status.errCode }}] {{ status.errText }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-7">
|
||||
<h4>О плейлисте</h4>
|
||||
<table class="table table-dark table-hover small">
|
||||
<tbody>
|
||||
@ -61,15 +61,23 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>Список каналов ({{ count ?? 0 }})</h4>
|
||||
<div class="overflow-auto" style="max-height: 350px;">
|
||||
|
||||
<div class="col-md-5">
|
||||
<h4>Список каналов ({{ content.channelCount ?? 0 }})</h4>
|
||||
<div class="overflow-auto" style="max-height:500px">
|
||||
<table class="table table-dark table-hover small">
|
||||
<tbody>
|
||||
{% for channel in channels %}
|
||||
{% for channel in content.channels %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ channel }}</td>
|
||||
<td>
|
||||
{% if (channel.attributes['tvg-logo']) %}
|
||||
<div class="tvg-logo-background">
|
||||
<img src="{{ channel.attributes['tvg-logo'] }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ channel.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -6,7 +6,11 @@
|
||||
<meta name="description" content="Самообновляемые бесплатные IPTV-плейлисты для домашнего просмотра по коротким ссылкам, списки каналов, проверка доступности">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<style>.cursor-pointer{cursor:pointer}</style>
|
||||
<style>
|
||||
.cursor-pointer{cursor:pointer}
|
||||
.tvg-logo-background{max-width:100px;max-height:100px;background:white}
|
||||
.tvg-logo-background img{max-width:inherit;max-height:inherit;}
|
||||
</style>
|
||||
<link href="{{ base_url('css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ base_url('/favicon/apple-touch-icon.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ base_url('/favicon/favicon-32x32.png') }}">
|
||||
@ -55,10 +59,10 @@
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="pt-lg-3 px-0 pb-0">
|
||||
<section class="container-fluid h-100 pt-lg-3 px-0 pb-0">
|
||||
{% block header %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="py-4 text-center">
|
||||
<script src="{{ base_url('js/bootstrap.bundle.min.js') }}"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user