Большое обновление
- проект переписан на flight + twig, laravel-like хелперы - docker-окружение - новая страница с подробностями о плейлисте - улучшен json о плейлисте - нормальный роутинг - нормальная статусная система - попытка перекодировки при не utf-8 + предупреждение об этом - дополнены FAQ + README
This commit is contained in:
9
src/app/Controllers/Controller.php
Normal file
9
src/app/Controllers/Controller.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
}
|
||||
76
src/app/Controllers/HomeController.php
Normal file
76
src/app/Controllers/HomeController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\PlaylistProcessor;
|
||||
use App\Core\RedirectedPlaylist;
|
||||
use Exception;
|
||||
use Flight;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
protected PlaylistProcessor $ini;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ini = new PlaylistProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if (Flight::request()->query->count() > 0) {
|
||||
$id = Flight::request()->query->keys()[0];
|
||||
Flight::redirect(base_url("$id"));
|
||||
die;
|
||||
}
|
||||
view('list', [
|
||||
'updated_at' => $this->ini->updatedAt(),
|
||||
'count' => $this->ini->playlists->count(),
|
||||
'playlists' => $this->ini->playlists->where('redirect_id', null)->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
65
src/app/Controllers/PlaylistController.php
Normal file
65
src/app/Controllers/PlaylistController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\PlaylistProcessor;
|
||||
use App\Core\RedirectedPlaylist;
|
||||
use Exception;
|
||||
use Flight;
|
||||
|
||||
class PlaylistController extends Controller
|
||||
{
|
||||
protected PlaylistProcessor $ini;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ini = new PlaylistProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function download($id)
|
||||
{
|
||||
$playlist = $this->ini->playlist($id);
|
||||
if ($playlist instanceof RedirectedPlaylist) {
|
||||
Flight::redirect(base_url($playlist->redirect_id));
|
||||
die;
|
||||
}
|
||||
Flight::redirect($playlist->pls);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function details(string $id): void
|
||||
{
|
||||
$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),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function json(string $id): void
|
||||
{
|
||||
$playlist = $this->ini->playlist($id);
|
||||
if ($playlist instanceof RedirectedPlaylist) {
|
||||
Flight::redirect(base_url($playlist->redirect_id . '/json'));
|
||||
die;
|
||||
}
|
||||
Flight::json([
|
||||
...$playlist->toArray(),
|
||||
...$this->ini->parse($id),
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
src/app/Core/BasicPlaylist.php
Normal file
17
src/app/Core/BasicPlaylist.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
abstract class BasicPlaylist implements Arrayable
|
||||
{
|
||||
public string $id;
|
||||
|
||||
public function url(): string
|
||||
{
|
||||
return sprintf('%s/%s', base_url(), $this->id);
|
||||
}
|
||||
}
|
||||
77
src/app/Core/Bootstrapper.php
Normal file
77
src/app/Core/Bootstrapper.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Controllers\AjaxController;
|
||||
use App\Controllers\HomeController;
|
||||
use App\Controllers\PlaylistController;
|
||||
use App\Extensions\TwigFunctions;
|
||||
use Flight;
|
||||
use Illuminate\Support\Arr;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\DebugExtension;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
final class Bootstrapper
|
||||
{
|
||||
public static function bootEnv(): void
|
||||
{
|
||||
(new Dotenv())->loadEnv(root_path() . '/.env');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static function bootTwig(): void
|
||||
{
|
||||
$filesystemLoader = new FilesystemLoader(config('views.path'));
|
||||
Flight::register(
|
||||
'view',
|
||||
Environment::class,
|
||||
[$filesystemLoader, config('twig')],
|
||||
function ($twig) {
|
||||
/** @var Environment $twig */
|
||||
Flight::set('twig', $twig);
|
||||
$twig->addExtension(new TwigFunctions());
|
||||
$twig->addExtension(new DebugExtension());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function bootRoutes(): void
|
||||
{
|
||||
Flight::route(
|
||||
'GET /',
|
||||
fn() => (new HomeController())->index()
|
||||
);
|
||||
Flight::route(
|
||||
'GET /faq',
|
||||
fn() => (new HomeController())->faq()
|
||||
);
|
||||
Flight::route(
|
||||
'GET /@id:[a-zA-Z0-9_-]+',
|
||||
fn($id) => (new PlaylistController())->download($id)
|
||||
);
|
||||
Flight::route(
|
||||
'GET /?[a-zA-Z0-9_-]+',
|
||||
fn($id) => (new PlaylistController())->download($id)
|
||||
);
|
||||
Flight::route(
|
||||
'GET /@id:[a-zA-Z0-9_-]+/details',
|
||||
fn($id) => (new PlaylistController())->details($id)
|
||||
);
|
||||
Flight::route(
|
||||
'GET /@id:[a-zA-Z0-9_-]+/json',
|
||||
fn($id) => (new PlaylistController())->json($id)
|
||||
);
|
||||
}
|
||||
}
|
||||
45
src/app/Core/Playlist.php
Normal file
45
src/app/Core/Playlist.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Playlist extends BasicPlaylist
|
||||
{
|
||||
public ?string $name;
|
||||
|
||||
public ?string $desc;
|
||||
|
||||
public string $pls;
|
||||
|
||||
public ?string $src;
|
||||
|
||||
public string $url;
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(public string $id, array $params)
|
||||
{
|
||||
empty($params['pls']) && throw new \Exception(
|
||||
"Плейлист с ID=$id обязан иметь параметр pls или redirect"
|
||||
);
|
||||
$this->url = str_replace(['http://', 'https://'], '', base_url($id));
|
||||
$this->name = $params['name'] ?? "Плейлист #$id";
|
||||
$this->desc = $params['desc'] ?? null;
|
||||
$this->pls = $params['pls'];
|
||||
$this->src = $params['src'] ?? null;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'url' => $this->url,
|
||||
'name' => $this->name,
|
||||
'desc' => $this->desc,
|
||||
'pls' => $this->pls,
|
||||
'src' => $this->src,
|
||||
];
|
||||
}
|
||||
}
|
||||
119
src/app/Core/PlaylistProcessor.php
Normal file
119
src/app/Core/PlaylistProcessor.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class PlaylistProcessor
|
||||
{
|
||||
public Collection $playlists;
|
||||
|
||||
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(function ($playlist, $id) {
|
||||
return empty($playlist['redirect'])
|
||||
? new Playlist((string)$id, $playlist)
|
||||
: new RedirectedPlaylist((string)$id, $playlist['redirect']);
|
||||
});
|
||||
}
|
||||
|
||||
public function hasId(string $id): bool
|
||||
{
|
||||
return in_array($id, $this->playlists->keys()->toArray());
|
||||
}
|
||||
|
||||
public function playlist(string $id): Playlist|RedirectedPlaylist
|
||||
{
|
||||
!$this->hasId($id) && throw new \InvalidArgumentException("Плейлист с ID=$id не найден");
|
||||
return $this->playlists[$id];
|
||||
}
|
||||
|
||||
public function check(string $id): bool
|
||||
{
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_URL, $this->playlist($id)['pls']);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($curl, CURLOPT_HEADER, 0);
|
||||
curl_setopt($curl, CURLOPT_NOBODY, 1);
|
||||
curl_exec($curl);
|
||||
$code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($curl);
|
||||
return $code < 400;
|
||||
}
|
||||
|
||||
protected function fetch(string $id)
|
||||
{
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => $this->playlist($id)->pls,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
25
src/app/Core/RedirectedPlaylist.php
Normal file
25
src/app/Core/RedirectedPlaylist.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class RedirectedPlaylist extends BasicPlaylist
|
||||
{
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $redirect_id,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'redirect_id' => $this->redirect_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
29
src/app/Extensions/TwigFunctions.php
Normal file
29
src/app/Extensions/TwigFunctions.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace App\Extensions;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class TwigFunctions extends AbstractExtension
|
||||
{
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('config', [$this, 'config']),
|
||||
new TwigFunction('base_url', [$this, 'base_url']),
|
||||
];
|
||||
}
|
||||
|
||||
public function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return config($key, $default);
|
||||
}
|
||||
|
||||
public function base_url(string $path = ''): string
|
||||
{
|
||||
return base_url($path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user