mirror of
https://github.com/anthonyaxenov/iptv.git
synced 2024-11-21 21:14:46 +00:00
Compare commits
6 Commits
d097366605
...
1c57f58936
Author | SHA1 | Date | |
---|---|---|---|
1c57f58936 | |||
70e25ded66 | |||
4e659c0abf | |||
688ffc547e | |||
ab23f8796e | |||
2f0186e49f |
@ -25,6 +25,7 @@ server {
|
|||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
fastcgi_hide_header X-Powered-By;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
[PHP]
|
[PHP]
|
||||||
error_reporting = E_ALL
|
error_reporting = E_ALL
|
||||||
|
expose_php = Off
|
||||||
file_uploads = Off
|
file_uploads = Off
|
||||||
memory_limit=-1
|
memory_limit=-1
|
||||||
max_execution_time=-1
|
max_execution_time=-1
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
[PHP]
|
[PHP]
|
||||||
error_reporting = E_ALL
|
error_reporting = E_ALL
|
||||||
|
expose_php = Off
|
||||||
file_uploads = Off
|
file_uploads = Off
|
||||||
; upload_max_filesize=10M
|
; upload_max_filesize=10M
|
||||||
; post_max_size=10M
|
; post_max_size=10M
|
||||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Core\ChannelLogo;
|
||||||
use App\Exceptions\PlaylistNotFoundException;
|
use App\Exceptions\PlaylistNotFoundException;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Flight;
|
use Flight;
|
||||||
@ -41,7 +42,6 @@ class PlaylistController extends Controller
|
|||||||
public function details(string $id): void
|
public function details(string $id): void
|
||||||
{
|
{
|
||||||
$result = $this->getPlaylistResponse($id);
|
$result = $this->getPlaylistResponse($id);
|
||||||
|
|
||||||
view('details', $result);
|
view('details', $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,4 +57,33 @@ class PlaylistController extends Controller
|
|||||||
$result = $this->getPlaylistResponse($id, true);
|
$result = $this->getPlaylistResponse($id, true);
|
||||||
Flight::json($result);
|
Flight::json($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает логотип канала, кэшируя при необходимости
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function logo(): void
|
||||||
|
{
|
||||||
|
$input = Flight::request()->query['url'] ?? null;
|
||||||
|
|
||||||
|
$logo = new ChannelLogo($input);
|
||||||
|
if (!$logo->readFile()) {
|
||||||
|
$logo->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($logo->size() === 0) {
|
||||||
|
$logo->setDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
$logo->store();
|
||||||
|
$body = $logo->raw();
|
||||||
|
$size = $logo->size();
|
||||||
|
$mime = $logo->mimeType();
|
||||||
|
|
||||||
|
Flight::response()
|
||||||
|
->write($body)
|
||||||
|
->header('Content-Type', $mime)
|
||||||
|
->header('Content-Length', (string)$size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ final class Bootstrapper
|
|||||||
Flight::set('config', $config);
|
Flight::set('config', $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function bootIni(): void
|
public static function bootCore(): void
|
||||||
{
|
{
|
||||||
$loader = new IniFile();
|
$loader = new IniFile();
|
||||||
$loader->load();
|
$loader->load();
|
||||||
|
197
src/app/Core/ChannelLogo.php
Normal file
197
src/app/Core/ChannelLogo.php
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<?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 = $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
|
||||||
|
{
|
||||||
|
$url = filter_var(trim($url), FILTER_VALIDATE_URL);
|
||||||
|
if ($url === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = parse_url($url);
|
||||||
|
if (!is_array($parts)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parts['scheme'] . '://' . $parts['host'] . $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();
|
||||||
|
}
|
||||||
|
}
|
@ -204,6 +204,16 @@ class Playlist
|
|||||||
|
|
||||||
if ($isChannel) {
|
if ($isChannel) {
|
||||||
$channel['url'] = str_starts_with($line, 'http') ? $line : null;
|
$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;
|
$result['channels'][] = $channel;
|
||||||
$isChannel = false;
|
$isChannel = false;
|
||||||
unset($channel);
|
unset($channel);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
|
"ext-fileinfo": "*",
|
||||||
"mikecao/flight": "^3.12",
|
"mikecao/flight": "^3.12",
|
||||||
"symfony/dotenv": "^7.1",
|
"symfony/dotenv": "^7.1",
|
||||||
"twig/twig": "^3.14"
|
"twig/twig": "^3.14"
|
||||||
|
@ -9,6 +9,7 @@ return [
|
|||||||
'GET /' => [HomeController::class, 'index'],
|
'GET /' => [HomeController::class, 'index'],
|
||||||
'GET /page/@page:[0-9]+' => [HomeController::class, 'index'],
|
'GET /page/@page:[0-9]+' => [HomeController::class, 'index'],
|
||||||
'GET /faq' => [HomeController::class, 'faq'],
|
'GET /faq' => [HomeController::class, 'faq'],
|
||||||
|
'GET /logo' => [PlaylistController::class, 'logo'],
|
||||||
'GET /@id:[a-zA-Z0-9_-]+' => [PlaylistController::class, 'download'],
|
'GET /@id:[a-zA-Z0-9_-]+' => [PlaylistController::class, 'download'],
|
||||||
'GET /?[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_-]+/details' => [PlaylistController::class, 'details'],
|
||||||
|
@ -15,6 +15,6 @@ require '../vendor/autoload.php';
|
|||||||
(new Dotenv())->loadEnv(root_path() . '/.env');
|
(new Dotenv())->loadEnv(root_path() . '/.env');
|
||||||
Bootstrapper::bootSettings();
|
Bootstrapper::bootSettings();
|
||||||
Bootstrapper::bootTwig();
|
Bootstrapper::bootTwig();
|
||||||
Bootstrapper::bootIni();
|
Bootstrapper::bootCore();
|
||||||
Bootstrapper::bootRoutes();
|
Bootstrapper::bootRoutes();
|
||||||
Flight::start();
|
Flight::start();
|
||||||
|
BIN
src/public/no-tvg-logo.png
Normal file
BIN
src/public/no-tvg-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -3,7 +3,13 @@
|
|||||||
{% block title %}{{ name }} - {{ config('app.title') }}{% endblock %}
|
{% block title %}{{ name }} - {{ config('app.title') }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<style>.tvg-logo-background{max-width:100px;max-height:100px;background:white;padding:2px;border-radius:5px}</style>
|
<style>
|
||||||
|
img.tvg-logo{max-width:80px;max-height:80px;padding:2px;border-radius:5px}
|
||||||
|
tr.chrow td{padding:3px}
|
||||||
|
td.chindex{width:1%}
|
||||||
|
td.chlogo{width:100px}
|
||||||
|
div.chlist-table{max-height:550px}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@ -76,19 +82,31 @@
|
|||||||
<h4>Список каналов ({{ content.channelCount ?? 0 }})</h4>
|
<h4>Список каналов ({{ content.channelCount ?? 0 }})</h4>
|
||||||
{% if (content.channelCount > 0) %}
|
{% if (content.channelCount > 0) %}
|
||||||
<div id="chlist">
|
<div id="chlist">
|
||||||
<input type="text" class="form-control form-control-sm bg-dark text-light mb-2 fuzzy-search" placeholder="Поиск...">
|
<input type="text"
|
||||||
<div class="overflow-auto" style="max-height:550px">
|
class="form-control form-control-sm bg-dark text-light mb-2 fuzzy-search"
|
||||||
|
placeholder="Поиск..."
|
||||||
|
/>
|
||||||
|
<div class="chlist-table overflow-auto">
|
||||||
<table class="table table-dark table-hover small">
|
<table class="table table-dark table-hover small">
|
||||||
<tbody class="list">
|
<tbody class="list">
|
||||||
{% for channel in content.channels %}
|
{% for channel in content.channels %}
|
||||||
<tr class="chrow">
|
<tr class="chrow">
|
||||||
<td class="p-1" class="chindex">{{ loop.index }}</td>
|
<td class="chindex">{{ loop.index }}</td>
|
||||||
<td class="p-1">
|
<td class="chlogo text-center">
|
||||||
{% if (channel.attributes['tvg-logo']) %}
|
<img class="tvg-logo"
|
||||||
<img class="tvg-logo-background" src="{{ channel.attributes['tvg-logo'] }}" />
|
{% if (channel.logo.base64) %}
|
||||||
|
src="{{ channel.logo.base64 }}"
|
||||||
|
{% elseif (channel.attributes['tvg-logo']) %}
|
||||||
|
src="{{ base_url('logo?url=' ~ channel.attributes['tvg-logo']) }}"
|
||||||
|
loading="lazy"
|
||||||
|
{% else %}
|
||||||
|
src="{{ base_url('no-tvg-logo.png') }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
alt="Логотип канала '{{ channel.name }}'"
|
||||||
|
title="Логотип канала '{{ channel.name }}'"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-1 chname">{{ channel.name }}</td>
|
<td class="chname">{{ channel.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
Loading…
Reference in New Issue
Block a user