404 + рефакторинг + докблоки и комменты

master
Anthony Axenov 2022-12-04 23:19:40 +08:00
parent f18fb9dd7a
commit bcadc316bc
Signed by: anthony
GPG Key ID: EA9EC32FF7CCD4EC
14 changed files with 285 additions and 83 deletions

View File

@ -1,9 +1,27 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use Exception;
use Flight;
/**
* Абстрактный контроллер для расширения
*/
abstract class Controller abstract class Controller
{ {
/**
* Перебрасывает на страницу 404 при ненайденном плейлисте
*
* @param string $id
* @return void
* @throws Exception
*/
public function notFound(string $id): void
{
Flight::response()->status(404)->sendHeaders();
view('notfound', ['id' => $id]);
}
} }

View File

@ -1,25 +1,35 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Core\PlaylistProcessor; use App\Core\PlaylistProcessor;
use App\Core\RedirectedPlaylist;
use Exception; use Exception;
use Flight; use Flight;
/**
* Контроллер домашней страницы (списка плейлистов)
*/
class HomeController extends Controller class HomeController extends Controller
{ {
/**
* @var PlaylistProcessor Обработчик ini-списка
*/
protected PlaylistProcessor $ini; protected PlaylistProcessor $ini;
/**
* Конструктор
*/
public function __construct() public function __construct()
{ {
$this->ini = new PlaylistProcessor(); $this->ini = new PlaylistProcessor();
} }
/** /**
* @return int * Возвращает размер одной страницы списка плейлистов
*
* @return int Указанный в конфиге размер либо 10, если он выходит за диапазоны от 5 до 100
*/ */
protected function getPageSize(): int protected function getPageSize(): int
{ {
@ -28,19 +38,25 @@ class HomeController extends Controller
} }
/** /**
* Отображает главную страницу на указанной странице списка плейлистов
*
* @param int $page
* @return void
* @throws Exception * @throws Exception
*/ */
public function index(int $page = 1) public function index(int $page = 1): void
{ {
// если пришёл любой get-параметр, то считаем его как id плейлиста и перебрасываем на страницу о нём
if (Flight::request()->query->count() > 0) { if (Flight::request()->query->count() > 0) {
$id = Flight::request()->query->keys()[0]; $id = Flight::request()->query->keys()[0];
Flight::redirect(base_url($id)); Flight::redirect(base_url($id));
die; die;
} }
// иначе формируем и сортируем список при необходимости, рисуем страницу
$per_page = $this->getPageSize(); $per_page = $this->getPageSize();
$list = $this->ini->playlists->where('redirect_id', null); $list = $this->ini->playlists->where('redirect_id', null);
if (config('app.sort_by')) { if ($sort_by = config('app.sort_by')) {
$list = $list->sortBy(config('app.sort_by')); $list = $list->sortBy($sort_by);
} }
$list = $list->forPage($page, $per_page); $list = $list->forPage($page, $per_page);
view('list', [ view('list', [
@ -55,41 +71,13 @@ class HomeController extends Controller
} }
/** /**
* Отображает страницу FAQ
*
* @return void
* @throws Exception * @throws Exception
*/ */
public function faq() public function faq(): void
{ {
view('faq'); view('faq');
} }
/**
* @throws Exception
*/
public function details(string $id): void
{
$playlist = $this->ini->playlist($id);
if ($playlist instanceof RedirectedPlaylist) {
Flight::redirect(base_url($playlist->redirect_id . '/info'));
}
view('details', [
'id' => $id,
'playlist' => $playlist->toArray(),
'info' => $this->ini->parse($id),
]);
}
/**
* @throws Exception
*/
public function ajax(string $id): void
{
$playlist = $this->ini->playlist($id);
if ($playlist instanceof RedirectedPlaylist) {
Flight::redirect(base_url($playlist->redirect_id . '/getInfo'));
}
Flight::json([
'playlist' => $playlist->toArray(),
'info' => $this->ini->parse($id),
]);
}
} }

View File

@ -1,66 +1,102 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Core\PlaylistProcessor; use App\Core\{
use App\Core\RedirectedPlaylist; PlaylistProcessor,
RedirectedPlaylist};
use App\Exceptions\PlaylistNotFoundException;
use Exception; use Exception;
use Flight; use Flight;
/**
* Контроллер методов получения описания плейлистов
*/
class PlaylistController extends Controller class PlaylistController extends Controller
{ {
/**
* @var PlaylistProcessor Обработчик ini-списка
*/
protected PlaylistProcessor $ini; protected PlaylistProcessor $ini;
/**
* Конструктор
*/
public function __construct() public function __construct()
{ {
$this->ini = new PlaylistProcessor(); $this->ini = new PlaylistProcessor();
} }
/** /**
* Отправляет запрос с клиента по прямой ссылке плейлиста
*
* @param $id
* @return void
* @throws Exception * @throws Exception
*/ */
public function download($id) public function download($id): void
{ {
$playlist = $this->ini->playlist($id); try {
if ($playlist instanceof RedirectedPlaylist) { $playlist = $this->ini->playlist($id);
Flight::redirect(base_url($playlist->redirect_id)); if ($playlist instanceof RedirectedPlaylist) {
die; Flight::redirect(base_url($playlist->redirect_id));
die;
}
Flight::redirect($playlist->pls);
} catch (PlaylistNotFoundException) {
$this->notFound($id);
} }
header('Location: ' . $playlist->pls);
die; die;
} }
/** /**
* Отображает страницу описания плейлиста
*
* @param string $id
* @return void
* @throws Exception * @throws Exception
*/ */
public function details(string $id): void public function details(string $id): void
{ {
$playlist = $this->ini->playlist($id); try {
if ($playlist instanceof RedirectedPlaylist) { $playlist = $this->ini->playlist($id);
Flight::redirect(base_url($playlist->redirect_id . '/details')); if ($playlist instanceof RedirectedPlaylist) {
die; Flight::redirect(base_url($playlist->redirect_id . '/details'));
die;
}
view('details', [
...$playlist->toArray(),
...$this->ini->parse($id),
]);
} catch (PlaylistNotFoundException) {
$this->notFound($id);
} }
view('details', [
...$playlist->toArray(),
...$this->ini->parse($id),
]);
} }
/** /**
* Возвращает JSON с описанием плейлиста
*
* @param string $id
* @return void
* @throws Exception * @throws Exception
*/ */
public function json(string $id): void public function json(string $id): void
{ {
$playlist = $this->ini->playlist($id); try {
if ($playlist instanceof RedirectedPlaylist) { $playlist = $this->ini->playlist($id);
Flight::redirect(base_url($playlist->redirect_id . '/json')); if ($playlist instanceof RedirectedPlaylist) {
die; Flight::redirect(base_url($playlist->redirect_id . '/json'));
die;
}
Flight::json([
...$playlist->toArray(),
...$this->ini->parse($id),
]);
} catch (PlaylistNotFoundException) {
Flight::response()->status(404)->sendHeaders();
Flight::json(['error' => ['message' => 'Playlist not found']]);
} }
Flight::json([
...$playlist->toArray(),
...$this->ini->parse($id),
]);
} }
} }

View File

@ -1,15 +1,26 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Core; namespace App\Core;
use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Arrayable;
/**
* Базовый класс плейлиста
*/
abstract class BasicPlaylist implements Arrayable abstract class BasicPlaylist implements Arrayable
{ {
/**
* @var string ID плейлиста
*/
public string $id; public string $id;
/**
* Возвращает ссылку на плейлист в рамках проекта
*
* @return string
*/
public function url(): string public function url(): string
{ {
return sprintf('%s/%s', base_url(), $this->id); return sprintf('%s/%s', base_url(), $this->id);

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Core; namespace App\Core;
@ -15,13 +15,26 @@ use Twig\Environment;
use Twig\Extension\DebugExtension; use Twig\Extension\DebugExtension;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
/**
* Сборщик приложения
*/
final class Bootstrapper final class Bootstrapper
{ {
/**
* Загружает env-переменные
*
* @return void
*/
public static function bootEnv(): void public static function bootEnv(): void
{ {
(new Dotenv())->loadEnv(root_path() . '/.env'); (new Dotenv())->loadEnv(root_path() . '/.env');
} }
/**
* Загружает конфигурацию приложения в контейнер
*
* @return void
*/
public static function bootSettings(): void public static function bootSettings(): void
{ {
$settings = Arr::dot(require_once config_path('app.php')); $settings = Arr::dot(require_once config_path('app.php'));
@ -31,6 +44,11 @@ final class Bootstrapper
Flight::set('config', $settings); Flight::set('config', $settings);
} }
/**
* Загружает шаблонизатор и его расщирения
*
* @return void
*/
public static function bootTwig(): void public static function bootTwig(): void
{ {
$filesystemLoader = new FilesystemLoader(config('views.path')); $filesystemLoader = new FilesystemLoader(config('views.path'));
@ -47,6 +65,11 @@ final class Bootstrapper
); );
} }
/**
* Загружает маршруты
*
* @return void
*/
public static function bootRoutes(): void public static function bootRoutes(): void
{ {
Flight::route( Flight::route(

View File

@ -1,27 +1,51 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Core; namespace App\Core;
use Exception;
/**
* Плейлист без редиректа
*/
class Playlist extends BasicPlaylist class Playlist extends BasicPlaylist
{ {
/**
* @var string|null Название плейлиста
*/
public ?string $name; public ?string $name;
/**
* @var string|null Описание плейлиста
*/
public ?string $desc; public ?string $desc;
/**
* @var string Прямой URL до файла плейлиста на третьей стороне
*/
public string $pls; public string $pls;
/**
* @var string|null Источник плейлиста
*/
public ?string $src; public ?string $src;
/**
* @var string Ссылка на плейлист в рамках проекта
*/
public string $url; public string $url;
/** /**
* @throws \Exception * Конструктор
*
* @param string $id
* @param array $params
* @throws Exception
*/ */
public function __construct(public string $id, array $params) public function __construct(public string $id, array $params)
{ {
empty($params['pls']) && throw new \Exception( empty($params['pls']) && throw new Exception(
"Плейлист с ID=$id обязан иметь параметр pls или redirect" "Плейлист с ID=$id обязан иметь параметр pls или redirect"
); );
$this->url = base_url($id); $this->url = base_url($id);
@ -31,6 +55,9 @@ class Playlist extends BasicPlaylist
$this->src = empty($params['src']) ? null : $params['src']; $this->src = empty($params['src']) ? null : $params['src'];
} }
/**
* @inheritDoc
*/
public function toArray(): array public function toArray(): array
{ {
return [ return [

View File

@ -1,17 +1,30 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Core; namespace App\Core;
use App\Exceptions\PlaylistNotFoundException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
/**
* Обработчик списка плейлистов
*/
final class PlaylistProcessor final class PlaylistProcessor
{ {
/**
* @var Collection Коллекция подгруженных плейлистов
*/
public Collection $playlists; public Collection $playlists;
/**
* @var string Дата последнего обновления списка
*/
protected string $updated_at; protected string $updated_at;
/**
* Конструктор
*/
public function __construct() public function __construct()
{ {
$filepath = config_path('playlists.ini'); $filepath = config_path('playlists.ini');
@ -24,31 +37,60 @@ final class PlaylistProcessor
}); });
} }
/**
* Проверяет есть ли в списке плейлист по его id
*
* @param string $id
* @return bool
*/
public function hasId(string $id): bool public function hasId(string $id): bool
{ {
return in_array($id, $this->playlists->keys()->toArray()); return $this->playlists->keys()->contains($id);
} }
/**
* Возвращает из коллекции указанный плейлист, если он существует
*
* @param string $id
* @return Playlist|RedirectedPlaylist
* @throws PlaylistNotFoundException
*/
public function playlist(string $id): Playlist|RedirectedPlaylist public function playlist(string $id): Playlist|RedirectedPlaylist
{ {
!$this->hasId($id) && throw new \InvalidArgumentException("Плейлист с ID=$id не найден"); !$this->hasId($id) && throw new PlaylistNotFoundException($id);
return $this->playlists[$id]; return $this->playlists[$id];
} }
/**
* Проверяет доступность плейлиста на третьей стороне
*
* @param string $id
* @return bool
* @throws PlaylistNotFoundException
*/
public function check(string $id): bool public function check(string $id): bool
{ {
$curl = curl_init(); $curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $this->playlist($id)['pls']); curl_setopt_array($curl, [
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); CURLOPT_URL => $this->playlist($id)->pls,
curl_setopt($curl, CURLOPT_TIMEOUT, 5); CURLOPT_RETURNTRANSFER => true,
curl_setopt($curl, CURLOPT_HEADER, 0); CURLOPT_TIMEOUT => 5,
curl_setopt($curl, CURLOPT_NOBODY, 1); CURLOPT_HEADER => false,
CURLOPT_NOBODY => true,
]);
curl_exec($curl); curl_exec($curl);
$code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl); curl_close($curl);
return $code < 400; return $code < 400;
} }
/**
* Получает содержимое плейлиста с третьей стороны
*
* @param string $id
* @return array
* @throws PlaylistNotFoundException
*/
protected function fetch(string $id): array protected function fetch(string $id): array
{ {
$curl = curl_init(); $curl = curl_init();
@ -72,6 +114,12 @@ final class PlaylistProcessor
]; ];
} }
/**
* Возвращает статус проверки плейлиста по коду ошибки curl
*
* @param int $curl_err_code
* @return string
*/
protected function guessStatus(int $curl_err_code): string protected function guessStatus(int $curl_err_code): string
{ {
return match ($curl_err_code) { return match ($curl_err_code) {
@ -82,6 +130,13 @@ final class PlaylistProcessor
}; };
} }
/**
* Парсит полученный от третьей стороны плейлист
*
* @param string $id
* @return array Информация о составе плейлиста
* @throws PlaylistNotFoundException
*/
public function parse(string $id): array public function parse(string $id): array
{ {
$fetched = $this->fetch($id); $fetched = $this->fetch($id);
@ -110,6 +165,8 @@ final class PlaylistProcessor
} }
/** /**
* Возвращает дату последнего обновления списка плейлистов
*
* @return string * @return string
*/ */
public function updatedAt(): string public function updatedAt(): string

View File

@ -1,13 +1,19 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Core; namespace App\Core;
/**
* Плейлист с редиректом
*/
class RedirectedPlaylist extends BasicPlaylist class RedirectedPlaylist extends BasicPlaylist
{ {
/** /**
* @throws \Exception * Конструктор
*
* @param string $id
* @param string $redirect_id
*/ */
public function __construct( public function __construct(
public string $id, public string $id,
@ -15,6 +21,9 @@ class RedirectedPlaylist extends BasicPlaylist
) { ) {
} }
/**
* @inheritDoc
*/
public function toArray(): array public function toArray(): array
{ {
return [ return [

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
use Exception;
class PlaylistNotFoundException extends Exception
{
public function __construct(string $pls_code)
{
parent::__construct("Плейлист $pls_code не найден!");
}
}

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
namespace App\Extensions; namespace App\Extensions;

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
use flight\Engine; use flight\Engine;
use flight\net\Response; use flight\net\Response;

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
return [ return [
'flight' => [ 'flight' => [

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types = 1); declare(strict_types=1);
use App\Core\Bootstrapper; use App\Core\Bootstrapper;

View File

@ -0,0 +1,18 @@
{% extends "layouts/default.twig" %}
{% block header %}
<h2>Плейлист не найден</h2>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<p>
Плейлист {{ id }} не найден
</p>
<a class="navbar-brand" href="{{ base_url() }}" title="На главную">
Перейти к списку
</a>
</div>
</div>
{% endblock %}