From 2f0186e49f52c6a2329e8d2841949627d4b175fe Mon Sep 17 00:00:00 2001 From: AnthonyAxenov Date: Wed, 25 Sep 2024 01:13:39 +0800 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D1=85=D0=BE=D0=B4=20=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20http/h?= =?UTF-8?q?ttps=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=BE=D1=82=D0=B8=D0=BF?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20+?= =?UTF-8?q?=20=D0=B8=D1=85=20=D0=BB=D0=B5=D0=BD=D0=B8=D0=B2=D0=BE=D0=B5=20?= =?UTF-8?q?=D0=BA=D1=8D=D1=88=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/nginx/vhost.conf | 1 + docker/php/dev.php.ini | 1 + docker/php/prod.php.ini | 1 + src/app/Controllers/PlaylistController.php | 32 +++- src/app/Core/Bootstrapper.php | 2 +- src/app/Core/ChannelLogo.php | 177 +++++++++++++++++++++ src/app/Core/Playlist.php | 10 ++ src/composer.json | 1 + src/config/routes.php | 1 + src/public/index.php | 2 +- src/views/details.twig | 8 +- 11 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 src/app/Core/ChannelLogo.php diff --git a/docker/nginx/vhost.conf b/docker/nginx/vhost.conf index c974310..0ba12be 100644 --- a/docker/nginx/vhost.conf +++ b/docker/nginx/vhost.conf @@ -25,6 +25,7 @@ server { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_hide_header X-Powered-By; include fastcgi_params; } } diff --git a/docker/php/dev.php.ini b/docker/php/dev.php.ini index 5fa8984..e3e4643 100644 --- a/docker/php/dev.php.ini +++ b/docker/php/dev.php.ini @@ -1,5 +1,6 @@ [PHP] error_reporting = E_ALL +expose_php = Off file_uploads = Off memory_limit=-1 max_execution_time=-1 diff --git a/docker/php/prod.php.ini b/docker/php/prod.php.ini index 39cde4f..68d6271 100644 --- a/docker/php/prod.php.ini +++ b/docker/php/prod.php.ini @@ -1,5 +1,6 @@ [PHP] error_reporting = E_ALL +expose_php = Off file_uploads = Off ; upload_max_filesize=10M ; post_max_size=10M diff --git a/src/app/Controllers/PlaylistController.php b/src/app/Controllers/PlaylistController.php index 0699c5a..a9a46f5 100644 --- a/src/app/Controllers/PlaylistController.php +++ b/src/app/Controllers/PlaylistController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controllers; +use App\Core\ChannelLogo; use App\Exceptions\PlaylistNotFoundException; use Exception; use Flight; @@ -41,7 +42,6 @@ class PlaylistController extends Controller public function details(string $id): void { $result = $this->getPlaylistResponse($id); - view('details', $result); } @@ -57,4 +57,34 @@ class PlaylistController extends Controller $result = $this->getPlaylistResponse($id, true); 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) { + Flight::notFound(); + die; + } + + $logo->store(); + $body = $logo->asBase64(); + $size = $logo->size(); + $mime = $logo->mimeType(); + + Flight::response() + ->write($body) + ->header('Content-Type', $mime) + ->header('Content-Length', (string)$size); + } } diff --git a/src/app/Core/Bootstrapper.php b/src/app/Core/Bootstrapper.php index 703f180..4cebf16 100644 --- a/src/app/Core/Bootstrapper.php +++ b/src/app/Core/Bootstrapper.php @@ -29,7 +29,7 @@ final class Bootstrapper Flight::set('config', $config); } - public static function bootIni(): void + public static function bootCore(): void { $loader = new IniFile(); $loader->load(); diff --git a/src/app/Core/ChannelLogo.php b/src/app/Core/ChannelLogo.php new file mode 100644 index 0000000..b74f096 --- /dev/null +++ b/src/app/Core/ChannelLogo.php @@ -0,0 +1,177 @@ +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); + } + + /** + * Возвращает base64-кодированное изображение + * + * @return string|null + */ + public function asBase64(): ?string + { + if (!is_string($this->rawData)) { + return null; + } + + $mime = $this->mimeType(); + return "data:$mime;base64," . base64_encode($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(); + } +} diff --git a/src/app/Core/Playlist.php b/src/app/Core/Playlist.php index c1c53dd..0604c21 100644 --- a/src/app/Core/Playlist.php +++ b/src/app/Core/Playlist.php @@ -204,6 +204,16 @@ class Playlist 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); diff --git a/src/composer.json b/src/composer.json index e177096..1f09724 100644 --- a/src/composer.json +++ b/src/composer.json @@ -3,6 +3,7 @@ "php": "^8.2", "ext-json": "*", "ext-curl": "*", + "ext-fileinfo": "*", "mikecao/flight": "^3.12", "symfony/dotenv": "^7.1", "twig/twig": "^3.14" diff --git a/src/config/routes.php b/src/config/routes.php index 75735ac..5b192f1 100644 --- a/src/config/routes.php +++ b/src/config/routes.php @@ -9,6 +9,7 @@ return [ 'GET /' => [HomeController::class, 'index'], 'GET /page/@page:[0-9]+' => [HomeController::class, 'index'], 'GET /faq' => [HomeController::class, 'faq'], + 'GET /logo' => [PlaylistController::class, 'logo'], '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'], diff --git a/src/public/index.php b/src/public/index.php index be10d71..e9638bf 100644 --- a/src/public/index.php +++ b/src/public/index.php @@ -15,6 +15,6 @@ require '../vendor/autoload.php'; (new Dotenv())->loadEnv(root_path() . '/.env'); Bootstrapper::bootSettings(); Bootstrapper::bootTwig(); -Bootstrapper::bootIni(); +Bootstrapper::bootCore(); Bootstrapper::bootRoutes(); Flight::start(); diff --git a/src/views/details.twig b/src/views/details.twig index 40859fb..04b21f2 100644 --- a/src/views/details.twig +++ b/src/views/details.twig @@ -3,7 +3,7 @@ {% block title %}{{ name }} - {{ config('app.title') }}{% endblock %} {% block head %} - + {% endblock %} {% block header %} @@ -84,8 +84,10 @@ {{ loop.index }} - {% if (channel.attributes['tvg-logo']) %} - + {% if (channel.logo.base64) %} + + {% else %} + {% endif %} {{ channel.name }}