Большое обновление

- проект переписан на flight + twig, laravel-like хелперы
- docker-окружение
- новая страница с подробностями о плейлисте
- улучшен json о плейлисте
- нормальный роутинг
- нормальная статусная система
- попытка перекодировки при не utf-8 + предупреждение об этом
- дополнены FAQ + README
This commit is contained in:
2022-09-01 19:54:43 +08:00
parent 649ab85d79
commit c43439b9cc
37 changed files with 2566 additions and 860 deletions

7
src/.env.example Normal file
View File

@@ -0,0 +1,7 @@
APP_TITLE=
APP_URL=
TWIG_CACHE=1
TWIG_DEBUG=0
FLIGHT_CASE_SENSITIVE=0
FLIGHT_HANDLE_ERRORS=1
FLIGHT_LOG_ERRORS=1

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types = 1);
namespace App\Controllers;
abstract class Controller
{
}

View 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),
]);
}
}

View 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),
]);
}
}

View 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);
}
}

View 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
View 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,
];
}
}

View 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;
}
}

View 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,
];
}
}

View 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);
}
}

20
src/bootstrap.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
use App\Core\Bootstrapper;
// autoload composer packages
require 'vendor/autoload.php';
// load .env parameters
Bootstrapper::bootEnv();
// set up framework according to its config
Bootstrapper::bootSettings();
// set up Twig template engine
Bootstrapper::bootTwig();
// set up routes defined in config file
Bootstrapper::bootRoutes();

0
src/cache/.gitkeep vendored Normal file
View File

20
src/composer.json Normal file
View File

@@ -0,0 +1,20 @@
{
"require": {
"illuminate/collections": "^9.26",
"mikecao/flight": "^2.0",
"symfony/dotenv": "^6.1",
"twig/twig": "^3.4"
},
"autoload": {
"psr-4": {
"App\\": "app/"
},
"files": [
"helpers.php"
]
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}

680
src/composer.lock generated Normal file
View File

@@ -0,0 +1,680 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d8a1bc42a20f2a843ee133cd33f44fd4",
"packages": [
{
"name": "illuminate/collections",
"version": "v9.26.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
"reference": "3bda212d2c245b3261cd9af690dfd47d9878cebf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/collections/zipball/3bda212d2c245b3261cd9af690dfd47d9878cebf",
"reference": "3bda212d2c245b3261cd9af690dfd47d9878cebf",
"shasum": ""
},
"require": {
"illuminate/conditionable": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0",
"php": "^8.0.2"
},
"suggest": {
"symfony/var-dumper": "Required to use the dump method (^6.0)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"files": [
"helpers.php"
],
"psr-4": {
"Illuminate\\Support\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Collections package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2022-08-22T14:29:59+00:00"
},
{
"name": "illuminate/conditionable",
"version": "v9.26.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
"reference": "5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883",
"reference": "5b40f51ccb07e0e7b1ec5559d8db9e0e2dc51883",
"shasum": ""
},
"require": {
"php": "^8.0.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Support\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Conditionable package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2022-07-29T19:44:19+00:00"
},
{
"name": "illuminate/contracts",
"version": "v9.26.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "0d1dd1a7e947072319f2e641cc50081219606502"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/0d1dd1a7e947072319f2e641cc50081219606502",
"reference": "0d1dd1a7e947072319f2e641cc50081219606502",
"shasum": ""
},
"require": {
"php": "^8.0.2",
"psr/container": "^1.1.1|^2.0.1",
"psr/simple-cache": "^1.0|^2.0|^3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Contracts\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2022-08-18T14:18:13+00:00"
},
{
"name": "illuminate/macroable",
"version": "v9.26.1",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
"reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/macroable/zipball/e3bfaf6401742a9c6abca61b9b10e998e5b6449a",
"reference": "e3bfaf6401742a9c6abca61b9b10e998e5b6449a",
"shasum": ""
},
"require": {
"php": "^8.0.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"Illuminate\\Support\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "The Illuminate Macroable package.",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2022-08-09T13:29:29+00:00"
},
{
"name": "mikecao/flight",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/mikecao/flight.git",
"reference": "a130231646e6c7a9e2504a9025f851e9a3bf1975"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mikecao/flight/zipball/a130231646e6c7a9e2504a9025f851e9a3bf1975",
"reference": "a130231646e6c7a9e2504a9025f851e9a3bf1975",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.4|^8.0|^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"files": [
"flight/autoload.php",
"flight/Flight.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike Cao",
"email": "mike@mikecao.com",
"homepage": "http://www.mikecao.com/",
"role": "Original Developer"
}
],
"description": "Flight is a fast, simple, extensible framework for PHP. Flight enables you to quickly and easily build RESTful web applications.",
"homepage": "http://flightphp.com",
"support": {
"issues": "https://github.com/mikecao/flight/issues",
"source": "https://github.com/mikecao/flight/tree/v2.0.1"
},
"time": "2021-12-19T03:03:01+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/simple-cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
"reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\SimpleCache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for simple caching",
"keywords": [
"cache",
"caching",
"psr",
"psr-16",
"simple-cache"
],
"support": {
"source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
},
"time": "2021-10-29T13:26:27+00:00"
},
{
"name": "symfony/dotenv",
"version": "v6.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/dotenv.git",
"reference": "568c11bcedf419e7e61f663912c3547b54de51df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dotenv/zipball/568c11bcedf419e7e61f663912c3547b54de51df",
"reference": "568c11bcedf419e7e61f663912c3547b54de51df",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"conflict": {
"symfony/console": "<5.4"
},
"require-dev": {
"symfony/console": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Dotenv\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Registers environment variables from a .env file",
"homepage": "https://symfony.com",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"source": "https://github.com/symfony/dotenv/tree/v6.1.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-04-01T07:15:35+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "twig/twig",
"version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077",
"reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"autoload": {
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.4.2"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2022-08-12T06:47:24+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

30
src/config/app.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types = 1);
return [
'flight' => [
// https://flightphp.com/learn#configuration
'base_url' => env('APP_URL', 'http://localhost:8080'),
'case_sensitive' => bool(env('FLIGHT_CASE_SENSITIVE', false)),
'handle_errors' => bool(env('FLIGHT_HANDLE_ERRORS', true)),
'log_errors' => bool(env('FLIGHT_LOG_ERRORS', true)),
'views' => [
'path' => views_path(),
'extension' => '.twig',
],
],
'twig' => [
'cache' => bool(env('TWIG_CACHE', true)) ? cache_path() . '/views' : false,
'debug' => bool(env('TWIG_DEBUG', false)),
],
'app' => [
'title' => env('APP_TITLE', 'IPTV Playlists'),
'pls_encodings' => [
'UTF-8',
'CP1251',
// 'CP866',
// 'ISO-8859-5',
],
],
];

452
src/config/playlists.ini Normal file
View File

@@ -0,0 +1,452 @@
[1]
name = 'Рабочий и актуальный IPTV плейлист M3U (smarttvapp.ru)'
desc = 'В этом IPTV плейлисте формата m3u вы найдете очень много каналов в HD качестве. Познавательные: Discovery HD, Discovery Science, Nat Geo, Nat Geo WILD, TLC HD. Детские: Nickelodeon HD. Спортивные, много каналов с фильмами: Дом Кино Премиум HD, Кинопремьера HD. Плейлист актуален на: 3.02.22'
pls = 'https://smarttvapp.ru/app/iptvfull.m3u'
src = 'https://smarttvapp.ru/aktualnyiy-i-rabochiy-iptv-pleylist-m3u/'
[2]
name = 'Самообновляемый IPTV плейлист — июнь 2022 (prodigtv.ru)'
desc = 'Возможно, дублирует какой-то от smarttvnews'
pls = 'https://prodigtv.ru/play/iptv.m3u'
src = 'https://prodigtv.ru/iptv/playlist/samoobnovlyaemyj'
[3]
name = 'IPTV каналы плейлист m3u без тормозов (poiskpmr)'
desc = 'Самые популярные и актуальные жанры iptv каналов m3u в 2022 году'
pls = 'https://iptvmaster.ru/december.m3u'
src = 'https://poiskpmr.ru/blog/ip-kanaly-plejlist-m3u-bez-tormozov-b256'
[4]
name = 'Самообновляемый IPTV плейлист 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/iptv-playlist.m3u'
src = 'https://iptv-russia.ru/playlists/iptv-playlist/'
[5]
name = 'IPTV плейлист с миксом ТВ каналов 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/mix.m3u'
src = 'https://iptv-russia.ru/playlists/mix/'
[p1]
name = 'Каналы в SD и HD качестве (smarttvnews.ru)'
desc = 'Рабочий и актуальный IPTV плейлист M3U — на июнь 2022 года'
pls = 'https://smarttvnews.ru/apps/iptvchannels.m3u'
src = 'https://smarttvnews.ru/rabochiy-i-aktualnyiy-iptv-pleylist-m3u-kanalyi-v-sd-i-hd-kachestve/'
[p2]
name = 'Самообновляемый iptv плейлист 2022 июнь (smarttvnews.ru)'
desc = 'Лучший самообновляемый IPTV плейлист в 2022 году'
pls = 'https://smarttvnews.ru/apps/freeiptv.m3u'
src = 'https://smarttvnews.ru/samoobnovlyaemyj-iptv-plejlist/'
[p4]
name = 'IPTV плейлист на июль 2020 (iptvm3u.ru)'
desc = 'Плейлист содержит 1200+ ТВ каналов всех категорий (музыка, спорт, детские, образовательные, взрослые). Так же в файле есть каналы Украины, Белоруссии, Молдовы. Для удобства каналы других стран расположены в низу списка.'
pls = 'https://iptvm3u.ru/0720.m3u'
src = 'https://iptvm3u.ru/iptv-plejlist-na-ijul-2/'
[p5]
name = 'Плейлист 2020 от iptv-playlisty.ru'
desc = 'Трансляции для детей и подростков. Сериалы и Премьеры кино. Каналы для женщин и мужских развлечений. Документалистика и исторические лента о событиях прошлого.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/2020.m3u'
src = 'https://iptv-playlisty.ru/collection/samyj-svezheobnovlennyj-plejlist-iptv-na-2020-god/'
[p6]
redirect = 'p1'
[kid1]
name = 'Детский IPTV «Kids»'
desc = ''
pls = 'https://webhalpme.ru/kids.m3u'
src = 'https://webhalpme.ru/samoobnovljaemye-plejlisty-iptv-2019/'
[kid2]
name = 'Плейлист детских каналов iptvmaster.ru'
desc = '02.08.2020 Среди детских каналов есть и отечественные, и зарубежные, большинство из них в HD.'
pls = 'https://iptvmaster.ru/kids-all.m3u'
src = 'https://iptvmaster.ru/detskie-kanaly-playlist/'
[np]
name = 'Плейлист newplay (iptv-playlisty.ru)'
desc = 'Общефедеральные. Каналы фильмов. Все на русском. Имеются с зарубежными лентами. Спортивные. Как трансляции, так и кино данной тематики. Детские. Мультфильмы и передачи.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/newplay.m3u'
src = 'https://iptv-playlisty.ru/collection/besplatnyj-iptv-plejlist-formata-m3u/'
[his]
name = 'IPTV плейлист телеканала History (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/history.m3u'
src = 'https://iptv-playlisty.ru/iptv-kanaly/iptv-plejlist-telekanala-history/'
[dis]
name = 'IPTV плейлист телеканала Discovery (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/discovery.m3u'
src = 'https://iptv-playlisty.ru/iptv-kanaly/iptv-plejlist-telekanala-discovery/'
[ngeo]
name = 'IPTV плейлист канала national geographic (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/ngeografik.m3u'
src = 'https://iptv-playlisty.ru/iptv-kanaly/iptv-plejlist-kanala-national-geographic/'
[news]
name = 'Новости'
desc =
pls = 'https://iptvmaster.ru/news.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[mus]
name = 'Музыкальные 1'
desc =
pls = 'https://iptvmaster.ru/music.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[mus1]
name = 'Музыкальные 2 (smarttvnews.ru)'
desc = 'IPTV плейлист музыкальных каналов 2022'
pls = 'https://smarttvnews.ru/apps/music.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[mus2]
name = 'IPTV плейлист с музыкальными каналами (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/music.m3u'
src = 'https://iptv-russia.ru/playlists/music/'
[ser]
name = 'Сериалы'
desc =
pls = 'http://bluecrabstv.do.am/serial.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[kino1]
name = 'Фильмы 1'
desc =
pls = 'https://smarttvnews.ru/apps/Films.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[kino2]
name = 'Фильмы 2'
desc =
pls = 'http://iptvm3u.ru/500newFilms.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[kino3]
name = 'Фильмы 3'
desc =
pls = 'http://iptvm3u.ru/film1.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[kino4]
name = 'Фильмы 4'
desc =
pls = 'http://iptvm3u.ru/film4.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[kino5]
name = 'Фильмы 5'
desc =
pls = 'https://pastebin.com/raw/jLaRge54'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[kino6]
name = 'IPTV плейлист с кино, сериалами и мультфильмами 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/cinematic.m3u'
src = 'https://iptv-russia.ru/playlists/cinematic/'
[ru1]
name = 'Русские 1'
desc =
pls = 'https://webhalpme.ru/RussiaIPTV.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[ru3]
name = 'Русские 3'
desc =
pls = 'https://getsapp.ru/IPTV/Auto_IPTV.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[ru4]
name = 'Русские 4'
desc =
pls = 'https://iptvm3u.ru/list2511.m3u8'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[ru5]
name = 'Русские 5'
desc =
pls = 'https://avdmono.do.am/film/natgeo.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[ru6]
name = 'Русские 6'
desc =
pls = 'http://iptv.ktkru.ru/playlist.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[ru7]
name = 'IPTV плейлист с ТВ каналами России 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/ru-all.m3u'
src = 'https://iptv-russia.ru/playlists/ru-all/'
[reg]
name = 'IPTV Плейлист — Региональные каналы России (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/ru-regional.m3u'
src = 'https://iptv-russia.ru/playlists/ru-regional/'
[ua1]
name = 'Украинские IPTV каналы (smarttvnews.ru)'
desc = ''
pls = 'https://smarttvnews.ru/apps/ukraine.m3u'
src = 'https://smarttvnews.ru/iptv-plejlist-ukrainskih-kanalov/'
[ua2]
name = 'Украинские 2'
desc = ''
pls = 'https://iptvmaster.ru/ukraine.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[ua3]
name = 'IPTV плейлист с ТВ каналами Украины 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/ua-all.m3u'
src = 'https://iptv-russia.ru/playlists/ua-all/'
[ua4]
name = 'IPTV m3u плейлист Украина самообновляемый 2022 (tva.org.ua)'
desc = 'IPTV плейлист m3u бесплатных украинских каналов на 29 мая 2022 року'
pls = 'https://tva.org.ua/ip/u/iptv_ukr.m3u'
src = 'https://tva.org.ua/iptv-m3u-plejlist-ukraina-samoobnovlyaemyj.html'
[by]
name = 'IPTV плейлист с ТВ каналами Беларуси 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/by-all.m3u'
src = 'https://iptv-russia.ru/playlists/by-all/'
[arm]
name = 'IPTV плейлист с ТВ каналами Армении 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/arm-all.m3u'
src = 'https://iptv-russia.ru/playlists/all-arm/'
[uz]
name = 'IPTV плейлист с ТВ каналами Узбекистана 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/uz-all.m3u'
src = 'https://iptv-russia.ru/playlists/uz-all/'
[uz]
name = 'IPTV плейлист с ТВ каналами Казахстана 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/kz-all.m3u'
src = 'https://iptv-russia.ru/playlists/kz-all/'
[tr]
name = 'IPTV плейлист с ТВ каналами Турции и Азербайджана 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/tr-all.m3u'
src = 'https://iptv-russia.ru/playlists/tr-all/'
[usa]
name = 'IPTV плейлист с ТВ каналами США 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/usa-all.m3u'
src = 'https://iptv-russia.ru/playlists/usa-all/'
[ita]
name = 'IPTV плейлист с ТВ каналами Италии 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/ita-all.m3u'
src = 'https://iptv-russia.ru/playlists/ita-all/'
[m2]
name = 'Мультфильмы 2'
desc =
pls = 'https://iptvmaster.ru/kids.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[m3]
name = 'Мультфильмы 3'
desc =
pls = 'https://iptvmaster.ru/multfilm.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[m4]
name = 'Мультфильмы 4'
desc =
pls = 'https://iptvmaster.ru/kids-all.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[m5]
name = 'Мультфильмы 5'
desc =
pls = 'https://smarttvnews.ru/apps/Films.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[m6]
name = 'Мультфильмы 6'
desc =
pls = 'http://iptvm3u.ru/film4.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[m7]
name = 'Мультфильмы 7'
desc =
pls = 'http://iptvm3u.ru/film2.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[m8]
name = 'Мультфильмы 8'
desc =
pls = 'http://iptvm3u.ru/film1.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[m9]
name = 'Мультфильмы 9'
desc =
pls = 'http://iptvm3u.ru/500newFilms.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[m10]
name = 'Детский Iptv плейлист с каналами и мультфильмами (smarttvnews.ru)'
desc = ''
pls = 'https://smarttvnews.ru/apps/mult.m3u'
src = 'https://smarttvnews.ru/samoobnovlyaemyie-iptv-pleylistyi/'
[sci]
name = 'Познавательные'
desc = ''
pls = 'https://iptvmaster.ru/poznavatelnoe.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sci2]
name = 'IPTV плейлист с познавательными ТВ каналами 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/sci-all.m3u'
src = 'https://iptv-russia.ru/playlists/sci-all/'
[sp]
name = 'IPTV плейлист со спортивными каналами 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/sport-all.m3u'
src = 'https://iptv-russia.ru/playlists/sports-all/'
[cam]
name = 'IPTV плейлист с вебкамерами России и мира 2022 на июнь (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/webcams.m3u'
src = 'https://iptv-russia.ru/playlists/webcams/'
[cam2]
name = 'Веб камеры онлайн всего мира m3u (tva.org.ua)'
desc = 'Веб камеры со всего мира онлайн в формате m3u плейлиста iptv.'
pls = 'https://tva.org.ua/ip/web/web-kam-14.12.2021.m3u'
src = 'https://tva.org.ua/veb-kamery-onlayn-vsego-mira-m3u.html'
[r1]
name = 'Радио каналы 1'
desc =
pls = 'http://lradio.c1.biz/ltradio.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[r3]
name = 'Радио каналы 3'
desc =
pls = 'https://iptvmaster.ru/radio.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng1]
name = 'Каналы СНГ 1'
desc =
pls = 'https://iptvm3u.ru/iptv1218.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng2]
name = 'Каналы СНГ 2'
desc =
pls = 'https://iptvm3u.ru/0119.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng3]
name = 'Каналы СНГ 3'
desc =
pls = 'https://iptvm3u.ru/0219.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng4]
name = 'Каналы СНГ 4'
desc =
pls = 'http://iptvm3u.ru/iptv082018.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng5]
name = 'Каналы СНГ 5'
desc =
pls = 'https://iptvm3u.ru/0919.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng6]
name = 'Каналы СНГ 6'
desc =
pls = 'https://iptvm3u.ru/0819.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng7]
name = 'Каналы СНГ 7'
desc =
pls = 'https://iptvm3u.ru/1019.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng8]
name = 'Каналы СНГ 8'
desc =
pls = 'https://iptvm3u.ru/1119.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng10]
name = 'Каналы СНГ 10'
desc =
pls = 'https://webhalpme.ru/donwhm.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng11]
name = 'Каналы СНГ 11'
desc =
pls = 'https://iptvmaster.ru/hd.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng12]
name = 'Каналы Армении'
desc =
pls = 'https://iptvmaster.ru/armenia.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng13]
name = 'Каналы СНГ 13'
desc =
pls = 'https://dl.dropboxusercontent.com/s/iw9v57cln6dfkpu/Vinnitsa.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[sng14]
name = 'Каналы СНГ 14'
desc =
pls = 'http://gorod.tv/iptv.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[x]
name = 'IPTV плейлист для взрослых 2022 (smarttvnews.ru)'
desc = 'Рабочий IPTV плейлист с каналами и фильмами для взрослых'
pls = 'https://smarttvnews.ru/apps/xxx.m3u'
src = 'https://smarttvnews.ru/iptv-plejlist-dlya-vzroslyh/'
[x2]
name = 'IPTV плейлист для взрослых 2022 (iptv-russia.ru)'
desc = ''
pls = 'https://iptv-russia.ru/list/xxx.m3u'
src = 'https://iptv-russia.ru/playlists/xxx/'

4
src/config/routes.php Normal file
View File

@@ -0,0 +1,4 @@
<?php
declare(strict_types = 1);

180
src/helpers.php Normal file
View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types = 1);
use flight\Engine;
use flight\net\Response;
use Illuminate\Support\Arr;
/**
* Returns path to root application directory
*
* @param string $path
* @return string
*/
function root_path(string $path = ''): string
{
return rtrim(sprintf('%s/%s', dirname($_SERVER['DOCUMENT_ROOT']), $path), '/');
}
/**
* Returns path to app
*
* @param string $path
* @return string
*/
function app_path(string $path = ''): string
{
return root_path("app/$path");
}
/**
* Return path to application configuration directory
*
* @param string $path
* @return string
*/
function config_path(string $path = ''): string
{
return root_path("config/$path");
}
/**
* Returns path to app cache
*
* @param string $path
* @return string
*/
function cache_path(string $path = ''): string
{
return root_path("cache/$path");
}
/**
* Return path to public part of application
*
* @param string $path
* @return string
*/
function public_path(string $path = ''): string
{
return root_path("public/$path");
}
/**
* Returns path to app views
*
* @param string $path
* @return string
*/
function views_path(string $path = ''): string
{
return root_path("views/$path");
}
/**
* Returns base URL
*
* @param string $route
* @return string
*/
function base_url(string $route = ''): string
{
return rtrim(sprintf('%s/%s', config('flight.base_url'), $route), '/');
}
/**
* Returns value of environment var
*
* @param string $key
* @param mixed|null $default
* @return mixed
*/
function env(string $key, mixed $default = null): mixed
{
return $_ENV[$key] ?? $default;
}
/**
* Renders template
*
* @param mixed $template
* @param array $data
* @return void
* @throws Exception
*/
function view(mixed $template, array $data = []): void
{
$template = str_contains($template, '.twig') ? $template : "$template.twig";
echo Flight::view()->render($template, $data);
}
/**
* Returns response object
*
* @return Response
*/
function response(): Response
{
return Flight::response();
}
/**
* Returns app object
*
* @return Engine
*/
function app(): Engine
{
return Flight::app();
}
/**
* Returns any value as boolean
*
* @param mixed $value
* @return bool
*/
function bool(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_object($value)) {
return true;
}
if (is_string($value)) {
return match ($value = trim($value)) {
'1', 'yes', 'true' => true,
'0', 'no', 'false' => false,
default => empty($value),
};
}
if ($is_resource = is_resource($value)) {
return $is_resource; // false if closed
}
return !empty($value);
}
/**
* Get config values
*
* @param string $key
* @param mixed|null $default
* @return mixed
*/
function config(string $key, mixed $default = null): mixed
{
$config = Flight::get('config');
if (isset($config["flight.$key"])) {
return $config["flight.$key"];
}
if (isset($config[$key])) {
return $config[$key];
}
$config = Arr::undot($config);
if (Arr::has($config, $key)) {
return Arr::get($config, $key);
}
return $default;
}

7
src/public/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

17
src/public/index.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
/*
|--------------------------------------------------------------------------
| Bootstrap all classes, settings, etc.
|--------------------------------------------------------------------------
*/
require '../bootstrap.php';
/*
|--------------------------------------------------------------------------
| Start application
|--------------------------------------------------------------------------
*/
Flight::start();

7
src/public/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

46
src/public/js/checker.js Normal file
View File

@@ -0,0 +1,46 @@
document.querySelectorAll('tr.pls').forEach((tr) => {
const id = tr.attributes['data-playlist-id'].value
const xhr = new XMLHttpRequest()
xhr.responseType = 'json'
xhr.timeout = 60000 // ms = 1 min
let el_status = tr.querySelector('span.status')
let el_count = tr.querySelector('td.count')
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
console.log('[' + id + '] DONE', xhr.response)
el_status.classList.remove('bg-secondary')
el_status.innerText = xhr.response.status
el_count.innerText = xhr.response.count
switch (xhr.response.status) {
case 'online':
el_status.classList.add('bg-success')
break
case 'timeout':
el_status.classList.add('bg-warning')
break
default:
el_status.classList.add('bg-danger')
break
}
}
}
xhr.onerror = () => {
console.log('[' + id + '] ERROR', xhr.response)
el_status.classList.add('bg-danger')
el_status.innerText = 'error'
el_count.innerText = '-'
}
xhr.onabort = () => {
console.log('[' + id + '] ABORTED', xhr.response)
el_status.classList.add('bg-secondary')
el_count.innerText = '-'
}
xhr.ontimeout = () => {
console.log('[' + id + '] TIMEOUT', xhr.response)
el_status.classList.add('bg-secondary')
el_status.innerText = 'timeout'
el_count.innerText = '-'
}
xhr.open('GET', '/' + id + '/json')
xhr.send()
})

76
src/views/details.twig Normal file
View File

@@ -0,0 +1,76 @@
{% extends "layouts/default.twig" %}
{% block title %}{{ title }}{% endblock %}
{% block header %}
<a href="{{ base_url() }}" class="btn btn-outline-light mb-3"><< Назад</a>
<h2>{{ name }}</h2>
{% if (encoding.alert) %}
<div class="alert alert-warning small" role="alert">
Кодировка исходного плейлиста отличается от UTF-8.
Он был автоматически с конвертирован из {{ encoding.name }}, чтобы отобразить здесь список каналов.
Однако названия каналов могут отображаться некорректно, причём не только здесь, но и в плеере.
</div>
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<h4>О плейлисте</h4>
<table class="table table-dark table-hover small">
<tbody>
<tr>
<td class="w-25">ID</td>
<td>
{{ id }}&nbsp;{% if status == 'online' %}
<span class="badge small text-dark bg-success">online</span>
{% elseif status == 'offline' %}
<span class="badge small text-dark bg-danger">offline</span>
{% elseif status == 'timeout' %}
<span class="badge small text-dark bg-warning">timeout</span>
{% elseif status == 'error' %}
<span class="badge small text-dark bg-danger">error</span>
{% endif %}
</td>
</tr>
<tr>
<td>Описание</td>
<td><p>{{ desc }}</p></td>
</tr>
<tr>
<td title="Нажми на ссылку, чтобы скопировать её в буфер обмена"><b>Ccылка для ТВ</b></td>
<td><b onclick="prompt('Скопируй адрес плейлиста', '{{ url }}')"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Нажми на ссылку, чтобы скопировать её в буфер обмена"
class="font-monospace cursor-pointer">{{ url }}</b></td>
</tr>
<tr>
<td>M3U</td>
<td>{{ pls }}</td>
</tr>
<tr>
<td>Источник</td>
<td>{{ src }}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-4">
<h4>Список каналов ({{ count }})</h4>
<div class="overflow-auto" style="max-height: 350px;">
<table class="table table-dark table-hover small">
<tbody>
{% for channel in channels %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ channel }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

149
src/views/faq.twig Normal file
View File

@@ -0,0 +1,149 @@
{% extends "layouts/default.twig" %}
{% block header %}
<a href="{{ base_url() }}" class="btn btn-outline-light mb-3"><< Назад</a>
<h2>FAQ</h2>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<p>
На этой странице собраны ссылки на IPTV-плейлисты, которые находятся в открытом доступе.
Они отбираются вручную и постоянно проверяются здесь автоматически.
</p>
<p>
Сервис "{{ config('app.title') }}" ({{ base_url() }}) не предназначен для хранения или трансляции
видео/аудио потоков, программ телепередач, плейлистов и их поддержки. Этим занимаются администраторы
ресурсов, указанные как источник, и те, с чьих ресурсов ведётся трансляция.
</p>
<p class="mb-5">
Сервис "{{ config('app.title') }}" ({{ base_url() }}) предоставляет только информацию об активности
плейлистов, найденных в открытом доступе, и короткие ссылки на них для удобства использования в ПО.
Вопросы по содержанию и работоспособности плейлистов, а также вопросы юридического характера, адресуйте
тем, кто несёт за них ответственность (см. источники плейлистов).
</p>
<h3>Как пользоваться сервисом?</h3>
<p class="mb-5">
На главной странице отображается список доступных в плейлистов, их идентификаторы, статусы,
количество каналов и короткие ссылки.
Для просмотра списка каналов следует нажать на ссылку <b>"Подробнее..."</b> под интересующим плейлистом.
Для добавления плейлиста в свой медиаплеер удобно использовать <b>"Ссылку для ТВ"</b>.
Это делается для удобства ввода, например, на телевизоре с пульта.
На странице детальной информации также есть прямая ссылка на сам плейлист от источника.
Можно использовать и её.
</p>
<h3>Эти плейлисты и каналы в них -- бесплатны?</h3>
<p class="mb-5">Возможно. По крайней мере, так утверждают источники. Но гарантий никаких никто не даёт.</p>
<h3>Как подключить плейлист?</h3>
<p class="mb-5">
<a href="https://www.google.com/search?q=%D0%BA%D0%B0%D0%BA%20%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B8%D1%82%D1%8C%20iptv%20%D0%BF%D0%BB%D0%B5%D0%B9%D0%BB%D0%B8%D1%81%D1%82%20%D0%BF%D0%BE%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B5">
Добавь в свой медиаплеер</a> "Ссылку для ТВ".
</p>
<h3>Какие плейлисты попадают сюда?</h3>
<p>Есть некоторые критерии, по которым плейлисты отбираются в этот список:</p>
<ul>
<li>Прежде всего -- каналы РФ и бывшего СНГ, но не только</li>
<li>Открытый источник</li>
<li>Прямая ссылка на плейлист</li>
<li>Автообновление плейлиста</li>
</ul>
<p>
В основном, в плейлистах именно трансляции телеканалов, но могут быть просто список каких-то
(мульт)фильмов и передач, находящихся на чужих дисках (как если бы вы сами составили плейлист с музыкой,
например).
</p>
<h3>Что означают статусы плейлистов?</h3>
<ul>
<li>
<span class="badge small text-dark bg-secondary">?</span>
Загрузка данных, нужно немного подождать.
</li>
<li>
<span class="badge small text-dark bg-success">online</span>
Плейлист, возможно, активен.
</li>
<li>
<span class="badge small text-dark bg-warning">timeout</span>
Не удалось вовремя проверить плейлист.
</li>
<li>
<span class="badge small text-dark bg-danger">offline</span>
Плейлист недоступен.
</li>
<li>
<span class="badge small text-dark bg-danger">error</span>
Ошибка при проверке плейлиста.
</li>
</ul>
<p class="mb-5">
На странице детального описания статус может отображаться только online/offline.
Это временно. В некоем скором времени это будет доработано.
</p>
<h3>Почему нельзя доверять результатам проверки?</h3>
<p>
Я не гарантирую корректность и актуальность информации, которую ты увидишь здесь.
Хотя я и стараюсь улучшать качество проверок, но всё же рекомендую проверять желаемые
плейлисты самостоятельно вручную, ибо нет никаких гарантий:
</p>
<ul class="mb-5">
<li>
что это вообще плейлисты, а не чьи-то архивы с мокрыми кисками;
</li>
<li>
что плейлисты по разным ссылкам не дублируют друг друга и отличаются каналами хотя бы на четверть;
</li>
<li>
что плейлист работоспособен (каналы работают, корректно названы, имеют аудио, etc.);
</li>
<li>
что подгрузится корректное количество каналов и их список (хотя на это я ещё могу влиять и
стараюсь как-то улучшить).
</li>
</ul>
<h3>Какова гарантия, что я добавлю себе плейлист отсюда и он работать хоть сколько-нибудь долго?</h3>
<p class="mb-5">
Никакова.
Мёртвые плейлисты я периодически вычищаю, реже -- добавляю новые.
ID плейлистов могут меняться, поэтому вполне может произойти внезапная подмена одного другим, однако
намеренно я так не делаю.
Если один плейлист переезжает на новый адрес, то я ставлю временное перенаправление со старого ID на
новый.
Плюс читай выше про доверие результатам проверки (проблема может быть не стороне сервиса).
</p>
<h3>Где взять программу передач (EPG)?</h3>
<ul class="mb-5">
<li><b>https://iptvx.one/viewtopic.php?f=12&t=4</b></li>
<li>https://iptvmaster.ru/epg-for-iptv</li>
<li>https://google.com</li>
</ul>
<h3>Как часто обновляется этот список?</h3>
<p class="mb-5">
Время от времени.
Иногда я захожу сюда и проверяю всё ли на месте, иногда занимаюсь какими-то доработками.
Если есть кандидаты на добавление, то читай ниже.
</p>
<h3>Как часто обновляется содержимое плейлистов?</h3>
<p class="mb-5">Зависит от источника. Я этим не занимаюсь.</p>
<h3>Есть ли API? Как им пользоваться?</h3>
<p class="mb-5">Есть, подробности <a href="https://github.com/anthonyaxenov/iptv2#api">здесь</a>.</p>
<h3>Как пополнить этот список?</h3>
<p class="mb-5">
Сделать pull-request в <a href="https://github.com/anthonyaxenov/iptv">репозиторий</a>.
Я проверю плейлист и добавлю его в общий список, если всё ок.
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title>{{ config('app.title') }}</title>
<meta charset="utf-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link href="{{ base_url('css/bootstrap.min.css') }}" rel="stylesheet">
<style>.cursor-pointer {
cursor: pointer
}</style>
{% block head %}{% endblock %}
</head>
<body class="bg-dark text-light">
<div class="col-lg-8 mx-auto p-3 pt-md-5 pb-0">
<header class="pb-3 mb-3">
<a href="/" class="text-light text-decoration-none">
<h1>{{ config('app.title') }}</h1>
</a>
<p class="small text-muted">
<a class="small" href="{{ base_url('faq') }}">FAQ</a>&nbsp;|&nbsp;<a
class="small" href="https://github.com/anthonyaxenov/iptv">GitHub</a>&nbsp;|&nbsp;<a
class="small" href="https://axenov.dev">axenov.dev</a>
</p>
{% block header %}{% endblock %}
</header>
<div class="container">
{% block content %}{% endblock %}
</div>
</div>
<footer class="py-4 text-center">
<a href="https://github.com/anthonyaxenov/iptv">GitHub</a> | <a href="https://axenov.dev">axenov.dev</a>
<script src="{{ base_url('js/bootstrap.bundle.min.js') }}"></script>
{% block footer %}{% endblock %}
</footer>
</body>
</html>

63
src/views/list.twig Normal file
View File

@@ -0,0 +1,63 @@
{% extends "layouts/default.twig" %}
{% block title %}{{ title }}{% endblock %}
{% block header %}
<p class="text-muted small">
Обновлено:&nbsp;{{ updated_at }}&nbsp;МСК<br/>
Плейлистов в списке:&nbsp;<strong>{{ count }}</strong>
</p>
<hr/>
{% endblock %}
{% block content %}
<table class="table table-dark table-hover small">
<thead>
<tr>
<th>ID</th>
<th>Информация о плейлисте</th>
<th>Каналов</th>
<th title="Нажми на ссылку, чтобы скопировать её в буфер обмена">Ссылка для ТВ</th>
</tr>
</thead>
<tbody>
{% for id, playlist in playlists %}
<tr class="pls" data-playlist-id="{{ id }}">
<td class="text-center id">
<strong>{{ id }}</strong>
</td>
<td class="info">
<strong>{{ playlist.name }}</strong>
<span class="badge small bg-secondary text-dark status">?</span>
<div class="small">
{% if playlist.desc|length > 0 %}
<p class="my-1">{{ playlist.desc }}</p>
{% endif %}
<a href="{{ base_url(id ~ '/details') }}"
target="_blank"
rel="noopener nofollow">Подробнее...</a>
</div>
</td>
<td class="text-center count">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">загрузка...</span>
</div>
</td>
<td class="col-3">
<span onclick="prompt('Скопируй адрес плейлиста', '{{ playlist.url }}')"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Нажми на ссылку, чтобы скопировать её в буфер обмена"
class="font-monospace cursor-pointer">
{{ playlist.url }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block footer %}
<script src="{{ base_url('js/checker.js') }}"></script>
{% endblock %}