Большая переработка
- миграция с Flight на Slim v4 - кэширование ini-файла - кэширование скачанных плейлистов - прочее
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,9 @@ downloaded/
|
||||
/src/commit
|
||||
/src/cache/*
|
||||
/src/vendor
|
||||
/src/config/playlists.ini
|
||||
/src/views/custom.twig
|
||||
/tmp
|
||||
*.log
|
||||
.env
|
||||
*.m3u
|
||||
|
||||
@@ -40,6 +40,7 @@ services:
|
||||
- ./log/php:/var/log/php:rw
|
||||
- ./src:/var/www:rw
|
||||
- ./playlists.ini:/var/www/config/playlists.ini:ro
|
||||
- ./.git/refs/heads/master:/var/www/commit:ro
|
||||
depends_on:
|
||||
- keydb
|
||||
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
APP_TITLE='Плейлисты IPTV'
|
||||
# config/app.php
|
||||
APP_DEBUG=false
|
||||
APP_ENV=prod
|
||||
APP_URL=http://localhost:8080
|
||||
TWIG_CACHE=1
|
||||
TWIG_DEBUG=0
|
||||
FLIGHT_CASE_SENSITIVE=0
|
||||
FLIGHT_HANDLE_ERRORS=1
|
||||
FLIGHT_LOG_ERRORS=1
|
||||
APP_TITLE='IPTV Плейлисты'
|
||||
USER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x99) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
|
||||
PAGE_SIZE=10
|
||||
|
||||
# config/redis.php
|
||||
REDIS_HOST='keydb'
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
REDIS_TTL_DAYS=14
|
||||
|
||||
# config/redis.php
|
||||
TWIG_USE_CACHE=true
|
||||
TWIG_DEBUG=false
|
||||
|
||||
36
src/app/Controllers/ApiController.php
Normal file
36
src/app/Controllers/ApiController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Random\RandomException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class ApiController extends BasicController
|
||||
{
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function json(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$code = $request->getAttributes()['code'];
|
||||
$playlist = $this->getPlaylist($code, true);
|
||||
$playlist->fetchContent();
|
||||
$playlist->parse();
|
||||
|
||||
$json = json_encode($playlist->toArray(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$response->getBody()->write($json);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withHeader('Content-Length', strlen($json));
|
||||
}
|
||||
}
|
||||
84
src/app/Controllers/BasicController.php
Normal file
84
src/app/Controllers/BasicController.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Playlist;
|
||||
use App\Errors\PlaylistNotFoundException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Slim\Views\Twig;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class BasicController
|
||||
{
|
||||
/**
|
||||
* Отправляет сообщение о том, что метод не найден с кодом страницы 404
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function notFound(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$response->withStatus(404);
|
||||
$this->view($request, $response, 'notfound.twig');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param string $template
|
||||
* @param array $data
|
||||
* @return ResponseInterface
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function view(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
string $template,
|
||||
array $data = [],
|
||||
): ResponseInterface {
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, $template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает плейлист по его ID для обработки
|
||||
*
|
||||
* @param string $id
|
||||
* @param bool $asJson
|
||||
* @return Playlist
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getPlaylist(string $id, bool $asJson = false): Playlist
|
||||
{
|
||||
ini()->load();
|
||||
|
||||
if (ini()->getRedirection($id)) {
|
||||
$redirectTo = base_url(ini()->getRedirection($id) . ($asJson ? '/json' : '/details'));
|
||||
Flight::redirect($redirectTo);
|
||||
die;
|
||||
}
|
||||
|
||||
try {
|
||||
return ini()->getPlaylist($id);
|
||||
} catch (PlaylistNotFoundException) {
|
||||
$this->notFound($id, $asJson);
|
||||
die;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\IniFile;
|
||||
use App\Core\Playlist;
|
||||
use App\Exceptions\PlaylistNotFoundException;
|
||||
use Exception;
|
||||
use Flight;
|
||||
use Random\RandomException;
|
||||
|
||||
/**
|
||||
* Абстрактный контроллер для расширения
|
||||
*/
|
||||
abstract class Controller
|
||||
{
|
||||
/**
|
||||
* @var IniFile Класс для работы с ini-файлом плейлистов
|
||||
*/
|
||||
protected IniFile $ini;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->ini = Flight::get('ini');
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает плейлист по его ID для обработки
|
||||
*
|
||||
* @param string $id
|
||||
* @param bool $asJson
|
||||
* @return Playlist
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getPlaylist(string $id, bool $asJson = false): Playlist
|
||||
{
|
||||
if ($this->ini->getRedirection($id)) {
|
||||
Flight::redirect(base_url($this->ini->getRedirection($id) . ($asJson ? '/json' : '/details')));
|
||||
die;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->ini->getPlaylist($id);
|
||||
} catch (PlaylistNotFoundException) {
|
||||
$this->notFound($id, $asJson);
|
||||
die;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает обработанный плейлист для ответа
|
||||
*
|
||||
* @param string $id ID плейлиста
|
||||
* @param bool $asJson Обрабатывать как json
|
||||
* @return array
|
||||
* @throws RandomException
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getPlaylistResponse(string $id, bool $asJson = false): array
|
||||
{
|
||||
$playlist = $this->getPlaylist($id, $asJson);
|
||||
$playlist->download();
|
||||
$playlist->parse();
|
||||
return $playlist->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Перебрасывает на страницу 404 при ненайденном плейлисте
|
||||
*
|
||||
* @param string $id ID плейлиста
|
||||
* @param bool $asJson Обрабатывать как json
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function notFound(string $id, bool $asJson = false): void
|
||||
{
|
||||
Flight::response()->status(404)->sendHeaders();
|
||||
$asJson || view('notfound', ['id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Exception;
|
||||
use Flight;
|
||||
|
||||
/**
|
||||
* Контроллер домашней страницы (списка плейлистов)
|
||||
*/
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Отображает главную страницу с учётом пагинации списка плейлистов
|
||||
*
|
||||
* @param int $page Текущая страница списка
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function index(int $page = 1): void
|
||||
{
|
||||
// если пришёл любой get-параметр, то считаем его как id плейлиста и перебрасываем на страницу о нём
|
||||
if (Flight::request()->query->count() > 0) {
|
||||
$id = Flight::request()->query->keys()[0];
|
||||
Flight::redirect(base_url($id));
|
||||
die;
|
||||
}
|
||||
|
||||
// иначе формируем и сортируем список при необходимости, рисуем страницу
|
||||
$perPage = 10;
|
||||
$playlists = $this->ini->playlists(false);
|
||||
$count = count($playlists);
|
||||
$pageCount = ceil($count / $perPage);
|
||||
$offset = max(0, ($page - 1) * $perPage);
|
||||
$list = array_slice($playlists, $offset, $perPage, true);
|
||||
|
||||
view('list', [
|
||||
'updated_at' => $this->ini->updatedAt(),
|
||||
'count' => $count,
|
||||
'pages' => [
|
||||
'count' => $pageCount,
|
||||
'current' => $page,
|
||||
],
|
||||
'playlists' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отображает страницу FAQ
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function faq(): void
|
||||
{
|
||||
view('faq');
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\ChannelLogo;
|
||||
use App\Exceptions\PlaylistNotFoundException;
|
||||
use Exception;
|
||||
use Flight;
|
||||
|
||||
/**
|
||||
* Контроллер методов получения описания плейлистов
|
||||
*/
|
||||
class PlaylistController extends Controller
|
||||
{
|
||||
/**
|
||||
* Отправляет запрос с клиента по прямой ссылке плейлиста
|
||||
*
|
||||
* @param string $id ID плейлиста
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function download(string $id): void
|
||||
{
|
||||
try {
|
||||
$playlist = $this->ini->getPlaylist($id);
|
||||
Flight::redirect($playlist->pls);
|
||||
} catch (PlaylistNotFoundException) {
|
||||
$this->notFound($id);
|
||||
}
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отображает страницу описания плейлиста
|
||||
*
|
||||
* @param string $id ID плейлиста
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function details(string $id): void
|
||||
{
|
||||
$result = $this->getPlaylistResponse($id);
|
||||
view('details', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает JSON с описанием плейлиста
|
||||
*
|
||||
* @param string $id ID плейлиста
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function json(string $id): void
|
||||
{
|
||||
$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) {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
115
src/app/Controllers/WebController.php
Normal file
115
src/app/Controllers/WebController.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\ChannelLogo;
|
||||
use Exception;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class WebController extends BasicController
|
||||
{
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
* @throws Exception
|
||||
*/
|
||||
public function home(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
ini()->load();
|
||||
|
||||
$playlists = ini()->playlists(false);
|
||||
$count = count($playlists);
|
||||
$page = (int)($request->getAttributes()['page'] ?? $request->getQueryParams()['page'] ?? 1);
|
||||
$pageSize = config('app.page_size');
|
||||
$pageCount = ceil($count / $pageSize);
|
||||
$offset = max(0, ($page - 1) * $pageSize);
|
||||
$list = array_slice($playlists, $offset, $pageSize, true);
|
||||
|
||||
return $this->view($request, $response, 'list.twig', [
|
||||
'updated_at' => ini()->updatedAt(),
|
||||
'playlists' => $list,
|
||||
'count' => $count,
|
||||
'pageCount' => $pageCount,
|
||||
'pageCurrent' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function faq(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
return $this->view($request, $response, 'faq.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function redirect(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$code = $request->getAttributes()['code'];
|
||||
$playlist = $this->getPlaylist($code);
|
||||
return $response->withHeader('Location', $playlist->pls);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws \Random\RandomException
|
||||
* @throws LoaderError
|
||||
* @throws RuntimeError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
public function details(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$code = $request->getAttributes()['code'];
|
||||
$playlist = $this->getPlaylist($code);
|
||||
$playlist->fetchContent();
|
||||
$playlist->parse();
|
||||
|
||||
return $this->view($request, $response, 'details.twig', $playlist->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function logo(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$input = $request->getQueryParams()['url'] ?? null;
|
||||
|
||||
$logo = new ChannelLogo($input);
|
||||
$logo->readFile() || $logo->fetch();
|
||||
$logo->size() === 0 && $logo->setDefault();
|
||||
$logo->store();
|
||||
$body = $logo->raw();
|
||||
$size = $logo->size();
|
||||
$mime = $logo->mimeType();
|
||||
|
||||
$response->getBody()->write($body);
|
||||
return $response->withHeader('Content-Type', $mime)
|
||||
->withHeader('Content-Length', $size);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Extensions\TwigFunctions;
|
||||
use Flight;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\DebugExtension;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
/**
|
||||
* Сборщик приложения
|
||||
*/
|
||||
final class Bootstrapper
|
||||
{
|
||||
/**
|
||||
* Загружает конфигурацию приложения в контейнер
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function bootSettings(): void
|
||||
{
|
||||
$config = require_once config_path('app.php');
|
||||
foreach ($config as $key => $value) {
|
||||
Flight::set($key, $value);
|
||||
}
|
||||
Flight::set('config', $config);
|
||||
}
|
||||
|
||||
public static function bootCore(): void
|
||||
{
|
||||
$loader = new IniFile();
|
||||
$loader->load();
|
||||
|
||||
Flight::set('ini', $loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает шаблонизатор и его расширения
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function bootTwig(): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает маршруты
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function bootRoutes(): void
|
||||
{
|
||||
$routes = require_once config_path('routes.php');
|
||||
foreach ($routes as $route => $handler) {
|
||||
Flight::route($route, $handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class ChannelLogo implements \Stringable
|
||||
*/
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$url = $this->prepareUrl($url);
|
||||
$url = empty($url) ? base_url('public/no-tvg-logo.png') : $this->prepareUrl($url);
|
||||
if (is_string($url)) {
|
||||
$this->url = $url;
|
||||
$this->hash = md5($url);
|
||||
@@ -57,17 +57,15 @@ class ChannelLogo implements \Stringable
|
||||
*/
|
||||
protected function prepareUrl(string $url): false|string
|
||||
{
|
||||
$url = filter_var(trim($url), FILTER_VALIDATE_URL);
|
||||
if ($url === false) {
|
||||
$parts = parse_url(trim($url));
|
||||
if (!is_array($parts) || count($parts) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if (!is_array($parts)) {
|
||||
return false;
|
||||
}
|
||||
$result = $parts['scheme'] . '://' . $parts['host'];
|
||||
$result .= (empty($parts['port']) ? '' : ':' . $parts['port']);
|
||||
|
||||
return $parts['scheme'] . '://' . $parts['host'] . $parts['path'];
|
||||
return $result . $parts['path'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +120,7 @@ class ChannelLogo implements \Stringable
|
||||
public function setDefault(): bool
|
||||
{
|
||||
$this->path = root_path('public/no-tvg-logo.png');
|
||||
return$this->readFile();
|
||||
return $this->readFile();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
230
src/app/Core/Core.php
Normal file
230
src/app/Core/Core.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Core\TwigExtention as IptvTwigExtension;
|
||||
use Dotenv\Dotenv;
|
||||
use InvalidArgumentException;
|
||||
use Redis;
|
||||
use Slim\App;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Slim\Views\Twig;
|
||||
use Slim\Views\TwigMiddleware;
|
||||
use Twig\Error\LoaderError;
|
||||
|
||||
/**
|
||||
* Загрузчик приложения
|
||||
*/
|
||||
final class Core
|
||||
{
|
||||
/**
|
||||
* @var Core
|
||||
*/
|
||||
private static Core $instance;
|
||||
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected App $app;
|
||||
|
||||
/**
|
||||
* @var array Конфигурация приложения
|
||||
*/
|
||||
protected array $config = [];
|
||||
|
||||
/**
|
||||
* @var Redis
|
||||
*/
|
||||
protected Redis $redis;
|
||||
|
||||
/**
|
||||
* @var IniFile
|
||||
*/
|
||||
protected IniFile $iniFile;
|
||||
|
||||
/**
|
||||
* Закрытый конструктор
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает объект приложения
|
||||
*
|
||||
* @return Core
|
||||
*/
|
||||
public static function get(): Core
|
||||
{
|
||||
return self::$instance ??= new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает приложение
|
||||
*
|
||||
* @return App
|
||||
* @throws LoaderError
|
||||
*/
|
||||
public function boot(): App
|
||||
{
|
||||
$this->app = AppFactory::create();
|
||||
|
||||
$this->bootSettings();
|
||||
$this->bootRoutes();
|
||||
$this->bootTwig();
|
||||
$this->bootRedis();
|
||||
$this->bootIni();
|
||||
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает значение из конфига
|
||||
*
|
||||
* @param string $key Ключ в формате "config.key"
|
||||
* @param mixed|null $default Значение по умолчанию
|
||||
* @return mixed
|
||||
*/
|
||||
public function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$parts = explode('.', $key);
|
||||
return $this->config[$parts[0]][$parts[1]] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redis
|
||||
*/
|
||||
public function redis(): Redis
|
||||
{
|
||||
return $this->redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IniFile
|
||||
*/
|
||||
public function ini(): IniFile
|
||||
{
|
||||
return $this->iniFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return App
|
||||
*/
|
||||
public function app(): App
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает файл .env или .env.$env
|
||||
*
|
||||
* @param string $env
|
||||
* @return array
|
||||
*/
|
||||
protected function loadDotEnvFile(string $env = ''): array
|
||||
{
|
||||
$filename = empty($env) ? '.env' : ".env.$env";
|
||||
if (!file_exists(root_path($filename))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$dotenv = Dotenv::createMutable(root_path(), $filename);
|
||||
return $dotenv->safeLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает конфигурационные файлы
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootSettings(): void
|
||||
{
|
||||
$env = $this->loadDotEnvFile();
|
||||
|
||||
if (!empty($env['APP_ENV'])) {
|
||||
$this->loadDotEnvFile($env['APP_ENV']);
|
||||
}
|
||||
|
||||
foreach (glob(config_path() . '/*.php') as $file) {
|
||||
$key = basename($file, '.php');
|
||||
$this->config += [$key => require_once $file];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает маршруты
|
||||
*
|
||||
* @return void
|
||||
* @see https://www.slimframework.com/docs/v4/objects/routing.html
|
||||
*/
|
||||
protected function bootRoutes(): void
|
||||
{
|
||||
foreach ($this->config['routes'] as $route) {
|
||||
if (is_array($route['method'])) {
|
||||
$definition = $this->app->map($route['method'], $route['path'], $route['handler']);
|
||||
} else {
|
||||
$isPossible = in_array($route['method'], ['GET', 'POST', 'OPTIONS', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
$func = match (true) {
|
||||
$route['method'] === '*' => 'any',
|
||||
$isPossible => strtolower($route['method']),
|
||||
default => throw new InvalidArgumentException(sprintf('Неверный HTTP метод %s', $route['method']))
|
||||
};
|
||||
|
||||
$definition = $this->app->$func($route['path'], $route['handler']);
|
||||
}
|
||||
|
||||
if (!empty($route['name'])) {
|
||||
$definition->setName($route['name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает шаблонизатор и его расширения
|
||||
*
|
||||
* @return void
|
||||
* @throws LoaderError
|
||||
* @see https://www.slimframework.com/docs/v4/features/twig-view.html
|
||||
*/
|
||||
protected function bootTwig(): void
|
||||
{
|
||||
$twig = Twig::create(root_path('views'), $this->config['twig']);
|
||||
$twig->addExtension(new IptvTwigExtension());
|
||||
$this->app->add(TwigMiddleware::create($this->app, $twig));
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует подключение к Redis
|
||||
*
|
||||
* @return void
|
||||
* @see https://github.com/phpredis/phpredis/?tab=readme-ov-file
|
||||
*/
|
||||
protected function bootRedis(): void
|
||||
{
|
||||
$options = [
|
||||
'host' => $this->config['redis']['host'],
|
||||
'port' => (int)$this->config['redis']['port'],
|
||||
];
|
||||
|
||||
if (!empty($this->config['redis']['password'])) {
|
||||
$options['auth'] = $this->config['redis']['password'];
|
||||
}
|
||||
|
||||
$this->redis = new Redis($options);
|
||||
$this->redis->select((int)$this->config['redis']['db']);
|
||||
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует объект ini-файла
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootIni(): void
|
||||
{
|
||||
$this->iniFile = new IniFile();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Exceptions\PlaylistNotFoundException;
|
||||
use App\Errors\PlaylistNotFoundException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ class IniFile
|
||||
/**
|
||||
* @var array Считанное из файла содержимое ini-файла
|
||||
*/
|
||||
protected array $rawIni;
|
||||
protected array $ini;
|
||||
|
||||
/**
|
||||
* @var Playlist[] Коллекция подгруженных плейлистов
|
||||
@@ -40,13 +40,30 @@ class IniFile
|
||||
*/
|
||||
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);
|
||||
$ini = redis()->hGetAll('_playlists_');
|
||||
if (empty($ini)) {
|
||||
$filepath = config_path('playlists.ini');
|
||||
$ini = parse_ini_file($filepath, true);
|
||||
$this->updated_at = date('d.m.Y h:i', filemtime($filepath));
|
||||
$order = array_keys($ini);
|
||||
}
|
||||
|
||||
$order ??= redis()->get('_order_');
|
||||
$this->ini ??= $ini;
|
||||
$this->updated_at ??= redis()->get('_updated_at_');
|
||||
$transaction = redis()->multi();
|
||||
foreach ($order as $id) {
|
||||
$data = $this->ini[$id];
|
||||
$this->playlists[(string)$id] = $pls = $this->makePlaylist($id, $data);
|
||||
$transaction->hSet('_playlists_', $id, $pls);
|
||||
}
|
||||
|
||||
$expireAfter = config('redis.ttl_days');
|
||||
$transaction
|
||||
->expire('_playlists_', $expireAfter)
|
||||
->set('_order_', $order, ['EX' => $expireAfter])
|
||||
->set('_updated_at_', $this->updated_at, ['EX' => $expireAfter])
|
||||
->exec();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,11 +74,9 @@ class IniFile
|
||||
*/
|
||||
public function playlists(bool $all = true): array
|
||||
{
|
||||
if ($all) {
|
||||
return $this->playlists;
|
||||
}
|
||||
|
||||
return array_filter($this->playlists, static fn ($playlist) => is_null($playlist->redirectId));
|
||||
return $all
|
||||
? $this->playlists
|
||||
: array_filter($this->playlists, static fn ($playlist) => is_null($playlist->redirectId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +126,7 @@ class IniFile
|
||||
$id = (string)$id;
|
||||
if (isset($params['redirect'])) {
|
||||
$this->redirections[$id] = $redirectId = (string)$params['redirect'];
|
||||
$params = $this->rawIni[$redirectId];
|
||||
$params = $this->ini[$redirectId];
|
||||
return $this->makePlaylist($id, $params, $redirectId);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use CurlHandle;
|
||||
use Exception;
|
||||
use Random\RandomException;
|
||||
|
||||
@@ -86,20 +87,19 @@ class Playlist
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function download(): void
|
||||
public function fetchContent(): void
|
||||
{
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => $this->pls,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
||||
]);
|
||||
$cached = redis()->get($this->id);
|
||||
if (is_array($cached)) {
|
||||
$this->downloadStatus['httpCode'] = $cached['httpCode'];
|
||||
$this->downloadStatus['errCode'] = $cached['errCode'];
|
||||
$this->downloadStatus['errText'] = $cached['errText'];
|
||||
$this->downloadStatus['possibleStatus'] = $cached['possibleStatus'];
|
||||
$this->rawContent = $cached['content'];
|
||||
return;
|
||||
}
|
||||
|
||||
$curl = $this->makeCurl();
|
||||
$content = curl_exec($curl);
|
||||
$this->rawContent = $content === false ? null : $content;
|
||||
$this->downloadStatus['httpCode'] = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
@@ -107,6 +107,16 @@ class Playlist
|
||||
$this->downloadStatus['errText'] = curl_error($curl);
|
||||
$this->downloadStatus['possibleStatus'] = $this->guessStatus($this->downloadStatus['errCode']);
|
||||
curl_close($curl);
|
||||
|
||||
if ($cached === false) {
|
||||
redis()->set($this->id, [
|
||||
'httpCode' => $this->downloadStatus['httpCode'],
|
||||
'errCode' => $this->downloadStatus['errCode'],
|
||||
'errText' => $this->downloadStatus['errText'],
|
||||
'possibleStatus' => $this->downloadStatus['possibleStatus'],
|
||||
'content' => $this->rawContent,
|
||||
], ['EX' => config('redis.ttl_days')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,6 +251,53 @@ class Playlist
|
||||
return $this->parsedContent = $result;
|
||||
}
|
||||
|
||||
public function check(): bool
|
||||
{
|
||||
$curl = $this->makeCurl([
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_CUSTOMREQUEST => 'HEAD',
|
||||
]);
|
||||
|
||||
$content = curl_exec($curl);
|
||||
$this->rawContent = $content === false ? null : $content;
|
||||
$this->downloadStatus['httpCode'] = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$this->downloadStatus['errCode'] = curl_errno($curl);
|
||||
$this->downloadStatus['errText'] = curl_error($curl);
|
||||
$this->downloadStatus['possibleStatus'] = $this->guessStatus($this->downloadStatus['errCode']);
|
||||
curl_close($curl);
|
||||
|
||||
return $this->downloadStatus['httpCode'] < 400;
|
||||
}
|
||||
|
||||
protected function makeCurl(array $customOptions = []): CurlHandle
|
||||
{
|
||||
$options = [
|
||||
CURLOPT_URL => $this->pls,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_USERAGENT => config('app.user_agent'),
|
||||
];
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
foreach ($options as $option => $value) {
|
||||
curl_setopt($curl, $option, $value);
|
||||
}
|
||||
|
||||
// array_merge($options, $customOptions) loses keys
|
||||
foreach ($customOptions as $option => $value) {
|
||||
curl_setopt($curl, $option, $value);
|
||||
}
|
||||
|
||||
return $curl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит атрибуты строки и возвращает ассоциативный массив
|
||||
*
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Extensions;
|
||||
namespace App\Core;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class TwigFunctions extends AbstractExtension
|
||||
class TwigExtention extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
92
src/app/Errors/ErrorHandler.php
Normal file
92
src/app/Errors/ErrorHandler.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Errors;
|
||||
|
||||
use Psr\Http\Message\{
|
||||
ResponseInterface,
|
||||
ServerRequestInterface};
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\Handlers\ErrorHandler as SlimErrorHandler;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Обработчик ошибок
|
||||
*/
|
||||
class ErrorHandler extends SlimErrorHandler
|
||||
{
|
||||
/**
|
||||
* Логирует ошибку и отдаёт JSON-ответ с необходимым содержимым
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param Throwable $exception
|
||||
* @param bool $displayErrorDetails
|
||||
* @param bool $logErrors
|
||||
* @param bool $logErrorDetails
|
||||
* @param LoggerInterface|null $logger
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
Throwable $exception,
|
||||
bool $displayErrorDetails,
|
||||
bool $logErrors,
|
||||
bool $logErrorDetails,
|
||||
?LoggerInterface $logger = null
|
||||
): ResponseInterface {
|
||||
$payload = $this->payload($exception, $displayErrorDetails);
|
||||
|
||||
$response = app()->getResponseFactory()->createResponse();
|
||||
$response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает структуру исключения для контекста
|
||||
*
|
||||
* @param Throwable $e Исключение
|
||||
* @param bool $logErrorDetails Признак дополнения деталями
|
||||
* @return array
|
||||
*/
|
||||
protected function context(Throwable $e, bool $logErrorDetails): array
|
||||
{
|
||||
$result = ['code' => $e->getCode()];
|
||||
|
||||
$logErrorDetails && $result += [
|
||||
'class' => $e::class,
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace()
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает структуру исключения для передачи в ответе
|
||||
*
|
||||
* @param Throwable $e Исключение
|
||||
* @param bool $displayErrorDetails Признак дополнения деталями
|
||||
* @return array
|
||||
*/
|
||||
protected function payload(Throwable $e, bool $displayErrorDetails): array
|
||||
{
|
||||
$result = [
|
||||
'error' => [
|
||||
'code' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
],
|
||||
];
|
||||
|
||||
$displayErrorDetails && $result['error'] += [
|
||||
'class' => $e::class,
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace(),
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions;
|
||||
namespace App\Errors;
|
||||
|
||||
use Exception;
|
||||
|
||||
29
src/app/Middleware/RequestId.php
Normal file
29
src/app/Middleware/RequestId.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Middleware для добавления запросу заголовка X-Request-ID
|
||||
*/
|
||||
class RequestId
|
||||
{
|
||||
/**
|
||||
* Добавляет запросу заголовок X-Request-ID
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param RequestHandlerInterface $handler
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$request = $request->withHeader('X-Request-ID', uniqid());
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use flight\Engine;
|
||||
use flight\net\Response;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Core\Core;
|
||||
use App\Core\IniFile;
|
||||
use Slim\App;
|
||||
|
||||
/**
|
||||
* Returns path to root application directory
|
||||
@@ -58,7 +58,7 @@ function views_path(string $path = ''): string
|
||||
*/
|
||||
function base_url(string $route = ''): string
|
||||
{
|
||||
return rtrim(sprintf('%s/%s', config('flight.base_url'), $route), '/');
|
||||
return rtrim(sprintf('%s/%s', env('APP_URL'), $route), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ function base_url(string $route = ''): string
|
||||
*/
|
||||
function env(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_ENV[$key] ?? $default;
|
||||
return $_ENV[$key] ?? $_SERVER[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,23 +89,23 @@ function view(mixed $template, array $data = []): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns response object
|
||||
* Returns core object
|
||||
*
|
||||
* @return Response
|
||||
* @return Core
|
||||
*/
|
||||
function response(): Response
|
||||
function core(): Core
|
||||
{
|
||||
return Flight::response();
|
||||
return Core::get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns app object
|
||||
*
|
||||
* @return Engine
|
||||
* @return App
|
||||
*/
|
||||
function app(): Engine
|
||||
function app(): App
|
||||
{
|
||||
return Flight::app();
|
||||
return Core::get()->app();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,5 +135,25 @@ function bool(mixed $value): bool
|
||||
*/
|
||||
function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return Flight::get('config')[$key] ?? $default;
|
||||
return Core::get()->config($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redis instance
|
||||
*
|
||||
* @return Redis
|
||||
*/
|
||||
function redis(): Redis
|
||||
{
|
||||
return Core::get()->redis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ini-file instance
|
||||
*
|
||||
* @return IniFile
|
||||
*/
|
||||
function ini(): IniFile
|
||||
{
|
||||
return Core::get()->ini();
|
||||
}
|
||||
|
||||
0
src/cache/.gitkeep
vendored
Normal file → Executable file
0
src/cache/.gitkeep
vendored
Normal file → Executable file
@@ -16,10 +16,11 @@
|
||||
"ext-curl": "*",
|
||||
"ext-redis": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"mikecao/flight": "^3.12",
|
||||
"symfony/dotenv": "^7.1",
|
||||
"twig/twig": "^3.14"
|
||||
"ext-redis": "*",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"nyholm/psr7": "^1.6",
|
||||
"vlucas/phpdotenv": "*",
|
||||
"slim/slim": "^4.11",
|
||||
"slim/twig-view": "^3.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -37,6 +38,9 @@
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
||||
1628
src/composer.lock
generated
1628
src/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,17 +3,13 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// 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' => [
|
||||
'base_url' => env('APP_URL', 'http://localhost:8080'),
|
||||
'debug' => bool(env('APP_DEBUG', false)),
|
||||
'env' => env('APP_ENV', env('IPTV_ENV', 'prod')),
|
||||
'title' => env('APP_TITLE', 'IPTV Плейлисты'),
|
||||
'user_agent' => env('USER_AGENT'),
|
||||
'page_size' => (int)env('PAGE_SIZE', 10),
|
||||
'pls_encodings' => [
|
||||
'UTF-8',
|
||||
'CP1251',
|
||||
// 'CP866',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
11
src/config/redis.php
Normal file
11
src/config/redis.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'host' => env('REDIS_HOST', 'keydb'),
|
||||
'port' => (int)env('REDIS_PORT', 6379),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'db' => (int)env('REDIS_DB', 0),
|
||||
'ttl_days' => (int)env('REDIS_TTL_DAYS', 14) * 60 * 60 * 24, // 2 недели
|
||||
];
|
||||
@@ -1,17 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Controllers\HomeController;
|
||||
use App\Controllers\PlaylistController;
|
||||
use App\Controllers\ApiController;
|
||||
use App\Controllers\BasicController;
|
||||
use App\Controllers\WebController;
|
||||
|
||||
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'],
|
||||
'GET /@id:[a-zA-Z0-9_-]+/json' => [PlaylistController::class, 'json'],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/[page/{page:[0-9]+}]',
|
||||
'handler' => [WebController::class, 'home'],
|
||||
'name' => 'home',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/faq',
|
||||
'handler' => [WebController::class, 'faq'],
|
||||
'name' => 'faq',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/logo',
|
||||
'handler' => [WebController::class, 'logo'],
|
||||
'name' => 'logo',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/{code:[0-9a-zA-Z]+}',
|
||||
'handler' => [WebController::class, 'redirect'],
|
||||
'name' => 'redirect',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/{code:[0-9a-zA-Z]+}/details',
|
||||
'handler' => [WebController::class, 'details'],
|
||||
'name' => 'details',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/{code:[0-9a-zA-Z]+}/json',
|
||||
'handler' => [ApiController::class, 'json'],
|
||||
'name' => 'json',
|
||||
],
|
||||
[
|
||||
'method' => '*',
|
||||
'path' => '/{path:.*}',
|
||||
'handler' => [BasicController::class, 'notFound'],
|
||||
'name' => 'not-found',
|
||||
],
|
||||
// ...
|
||||
];
|
||||
|
||||
|
||||
8
src/config/twig.php
Normal file
8
src/config/twig.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'cache' => bool(env('TWIG_USE_CACHE', true)) ? cache_path() . '/views' : false,
|
||||
'debug' => bool(env('TWIG_DEBUG', false)),
|
||||
];
|
||||
@@ -2,19 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Core\Bootstrapper;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bootstrap all classes, settings, etc.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
require '../vendor/autoload.php';
|
||||
(new Dotenv())->loadEnv(root_path() . '/.env');
|
||||
Bootstrapper::bootSettings();
|
||||
Bootstrapper::bootTwig();
|
||||
Bootstrapper::bootCore();
|
||||
Bootstrapper::bootRoutes();
|
||||
Flight::start();
|
||||
|
||||
core()->boot()->run();
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if pages.count > 0 %}
|
||||
{% if pageCount > 0 %}
|
||||
<div aria-label="pages">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for page in range(1, pages.count) %}
|
||||
{% if page == pages.current %}
|
||||
{% for page in range(1, pageCount) %}
|
||||
{% if page == pageCurrent %}
|
||||
<li class="page-item active" aria-current="page">
|
||||
<span class="page-link">{{ page }}</span>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user