Compare commits
3 Commits
07692b08ce
...
b5d3b60356
| Author | SHA1 | Date | |
|---|---|---|---|
|
b5d3b60356
|
|||
|
c75da39b87
|
|||
|
67349bb909
|
@@ -3,14 +3,15 @@
|
||||
######################################
|
||||
|
||||
# config/app.php
|
||||
APP_TITLE='Агрегатор плейлистов'
|
||||
APP_TITLE='Проверка плейлистов'
|
||||
APP_URL=http://localhost:8080
|
||||
APP_DEBUG=false
|
||||
APP_ENV=prod
|
||||
APP_TIMEZONE=Europe/Moscow
|
||||
PAGE_SIZE=0
|
||||
REPO_URL='https://git.axenov.dev/IPTV'
|
||||
|
||||
# config/bot.php
|
||||
# config/api.php
|
||||
TG_BOT_TOKEN=
|
||||
TG_BOT_SECRET=
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
Использует [playlists.ini](https://git.axenov.dev/IPTV/playlists) с описанием плейлистов для своей работы.
|
||||
|
||||
> **Веб-сайт:** [m3u.su](https://m3u.su)
|
||||
> **Зеркало:** [m3u.su](https://m3u.su)
|
||||
> **Документация:** [m3u.su/docs](https://m3u.su/docs)
|
||||
> Исходный код: [git.axenov.dev/IPTV/web](https://git.axenov.dev/IPTV/web)
|
||||
> Telegram-канал: [@iptv_aggregator](https://t.me/iptv_aggregator)
|
||||
> Обсуждение: [@iptv_aggregator_chat](https://t.me/iptv_aggregator_chat)
|
||||
> Бот: [@iptv_aggregator_bot](https://t.me/iptv_aggregator_bot)
|
||||
> Дополнительные сведения: [git.axenov.dev/IPTV/.profile](https://git.axenov.dev/IPTV/.profile)
|
||||
|
||||
## О веб-сервисе
|
||||
|
||||
@@ -9,6 +9,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Bot;
|
||||
use App\Core\Kernel;
|
||||
use App\Core\StatisticsService;
|
||||
use App\Errors\PlaylistNotFoundException;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
@@ -81,4 +84,106 @@ class ApiController extends BasicController
|
||||
return $response->withStatus(200)
|
||||
->withHeader('Content-Type', $mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает информацию о плейлисте
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
public function version(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
return $this->responseJson($response, 200, [
|
||||
'web' => Kernel::VERSION,
|
||||
'php' => PHP_VERSION,
|
||||
'keydb' => redis()->info('server')['redis_version'],
|
||||
'checker' => 'todo',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает информацию о плейлисте
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
public function health(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
function getSize(string $directory): int
|
||||
{
|
||||
$size = 0;
|
||||
foreach (glob($directory . '/*') as $path){
|
||||
is_file($path) && $size += filesize($path);
|
||||
is_dir($path) && $size += getSize($path);
|
||||
}
|
||||
return $size;
|
||||
}
|
||||
|
||||
$tgBotInfo = config('bot.token') ? new Bot()->api->getMe() : null;
|
||||
$redisInfoServer = redis()->info('server'); // General information about the Redis server
|
||||
$redisInfoClients = redis()->info('clients'); // Client connections section
|
||||
$redisInfoMemory = redis()->info('memory'); // Memory consumption related information
|
||||
$redisInfoPers = redis()->info('persistence'); // RDB and AOF related information
|
||||
$redisInfoStats = redis()->info('stats'); // General statistics
|
||||
$redisInfoCpu = redis()->info('cpu'); // CPU consumption statistics
|
||||
$redisInfoCmd = redis()->info('commandstats'); // Redis command statistics
|
||||
$redisInfoKeysp = redis()->info('keyspace'); // Database related statistics
|
||||
$redisInfoErr = redis()->info('errorstats'); // Redis error statistics
|
||||
|
||||
$health = [
|
||||
'fileCache' => [
|
||||
'tv-logos' => [
|
||||
'sizeB' => $size = getSize(cache_path('tv-logos')),
|
||||
'sizeMiB' => round($size / 1024 / 1024, 3),
|
||||
'count' => count(glob(cache_path('tv-logos') . '/*')),
|
||||
],
|
||||
'qr-codes' => [
|
||||
'sizeB' => $size = getSize(cache_path('qr-codes')),
|
||||
'sizeMiB' => round($size / 1024 / 1024, 3),
|
||||
'count' => count(glob(cache_path('qr-codes') . '/*')),
|
||||
],
|
||||
],
|
||||
'telegram' => [
|
||||
'id' => $tgBotInfo->getId(),
|
||||
'first_name' => $tgBotInfo->getFirstName(),
|
||||
'username' => $tgBotInfo->getUsername(),
|
||||
],
|
||||
'redis' => [
|
||||
'isConnected' => redis()->isConnected(),
|
||||
'info' => [
|
||||
'server' => [
|
||||
'uptime_in_seconds' => $redisInfoServer['uptime_in_seconds'],
|
||||
'uptime_in_days' => $redisInfoServer['uptime_in_days'],
|
||||
],
|
||||
'clients' => ['connected_clients' => $redisInfoClients['connected_clients']],
|
||||
'memory' => $redisInfoMemory,
|
||||
'persistence' => $redisInfoPers,
|
||||
'stats' => $redisInfoStats,
|
||||
'cpu' => $redisInfoCpu,
|
||||
'commandstats' => $redisInfoCmd,
|
||||
'keyspace' => $redisInfoKeysp,
|
||||
'errorstats' => $redisInfoErr,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->responseJson($response, 200, $health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает информацию о плейлисте
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface
|
||||
* @throws Exception
|
||||
*/
|
||||
public function stats(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
return $this->responseJson($response, 200, new StatisticsService()->get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,9 @@ class BasicController
|
||||
* @param array $data
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function responseJson(ResponseInterface $response, int $status, array $data): ResponseInterface
|
||||
protected function responseJson(ResponseInterface $response, int $status, mixed $data): ResponseInterface
|
||||
{
|
||||
is_scalar($data) && $data = [$data];
|
||||
$data = array_merge(['timestamp' => time()], $data);
|
||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
||||
154
app/Core/Bot.php
154
app/Core/Bot.php
@@ -33,37 +33,35 @@ class Bot
|
||||
/**
|
||||
* @var BotApi Объект Telegram Bot API
|
||||
*/
|
||||
protected BotApi $bot;
|
||||
public readonly BotApi $api;
|
||||
|
||||
/**
|
||||
* @var Update Объект обновления бота
|
||||
*/
|
||||
protected Update $update;
|
||||
|
||||
/**
|
||||
* @var ServerRequestInterface Пришедший от Telegram запрос
|
||||
*/
|
||||
protected ServerRequestInterface $request;
|
||||
|
||||
/**
|
||||
* Конструктор
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ServerRequestInterface|null $request
|
||||
* @throws InvalidTelegramSecretException
|
||||
* @throws JsonException
|
||||
* @throws InvalidArgumentException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(ServerRequestInterface $request)
|
||||
public function __construct(?ServerRequestInterface $request = null)
|
||||
{
|
||||
$this->checkSecret($request);
|
||||
|
||||
$body = json_decode((string)$request->getBody(), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new JsonException(json_last_error_msg());
|
||||
$this->api = new BotApi(config('bot.token'));
|
||||
if ($request) {
|
||||
$this->checkSecret($request);
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
$this->bot = new BotApi(config('bot.token'));
|
||||
$this->update = Update::fromResponse($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Запсукает обработку команды
|
||||
* Запускает обработку команды
|
||||
*
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
@@ -72,7 +70,9 @@ class Bot
|
||||
*/
|
||||
public function process(): bool
|
||||
{
|
||||
$this->parseRequestBody();
|
||||
$commandText = $this->getBotCommandText();
|
||||
|
||||
return match (true) {
|
||||
str_starts_with($commandText, '/start') => $this->processHelpCommand(),
|
||||
str_starts_with($commandText, '/list') => $this->processListCommand(),
|
||||
@@ -84,6 +84,22 @@ class Bot
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает объект события бота
|
||||
*
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
* @throws JsonException
|
||||
*/
|
||||
protected function parseRequestBody(): void
|
||||
{
|
||||
$body = json_decode((string)$this->request->getBody(), true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new JsonException(json_last_error_msg());
|
||||
}
|
||||
$this->update = Update::fromResponse($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает команду /list
|
||||
*
|
||||
@@ -94,7 +110,7 @@ class Bot
|
||||
*/
|
||||
protected function processListCommand(): bool
|
||||
{
|
||||
$this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
$this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
|
||||
$playlists = ini()->getPlaylists();
|
||||
if (empty($playlists)) {
|
||||
@@ -139,7 +155,7 @@ class Bot
|
||||
*/
|
||||
protected function processInfoCommand(): bool
|
||||
{
|
||||
$this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
$this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
|
||||
$message = $this->update->getMessage();
|
||||
$text = $message->getText();
|
||||
@@ -235,7 +251,7 @@ class Bot
|
||||
*/
|
||||
protected function processHelpCommand(): bool
|
||||
{
|
||||
$this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
$this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
|
||||
$replyText[] = 'Бот предоставляет короткую сводку о плейлистах, которые видны на сайте ' .
|
||||
$this->escape(base_url()) . '\.';
|
||||
@@ -265,17 +281,16 @@ class Bot
|
||||
*/
|
||||
protected function processLinksCommand(): bool
|
||||
{
|
||||
$this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
$this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
|
||||
$replyText[] = '*Ресурсы и страницы*';
|
||||
$replyText[] = '';
|
||||
$replyText[] = '🌏 Сайт: ' . $this->escape(base_url());
|
||||
config('app.mirror_url') && $replyText[] = '🪞 Зеркало: ' . $this->escape(mirror_url());
|
||||
$replyText[] = '👩💻 Исходный код: ' . $this->escape('https://git.axenov.dev/IPTV');
|
||||
$replyText[] = '👩💻 Исходный код: ' . $this->escape(config('app.repo_url'));
|
||||
$replyText[] = '✈️ Telegram\-канал: @iptv\_aggregator';
|
||||
$replyText[] = '✈️ Обсуждение: @iptv\_aggregator\_chat';
|
||||
$replyText[] = '📚 Доп\. сведения:';
|
||||
$replyText[] = '\- ' . $this->escape('https://git.axenov.dev/IPTV/.profile');
|
||||
$replyText[] = '\- ' . $this->escape(config('app.repo_url') . '/.profile');
|
||||
$replyText[] = '\- ' . $this->escape(base_url('faq'));
|
||||
|
||||
return $this->reply(implode("\n", $replyText));
|
||||
@@ -289,87 +304,30 @@ class Bot
|
||||
*/
|
||||
protected function processStatsCommand(): bool
|
||||
{
|
||||
$this->bot->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
|
||||
$allChannels = [];
|
||||
foreach (ini()->getPlaylists() as $pls) {
|
||||
$allChannels = array_merge($allChannels, $pls['channels'] ?? []);
|
||||
}
|
||||
|
||||
$onlinePls = array_filter(
|
||||
ini()->getPlaylists(),
|
||||
static fn (array $pls) => $pls['isOnline'] === true,
|
||||
);
|
||||
|
||||
$offlinePls = array_filter(
|
||||
ini()->getPlaylists(),
|
||||
static fn (array $pls) => $pls['isOnline'] === false,
|
||||
);
|
||||
|
||||
$unknownPls = array_filter(
|
||||
ini()->getPlaylists(),
|
||||
static fn (array $pls) => $pls['isOnline'] === null,
|
||||
);
|
||||
|
||||
$adultPls = array_filter(
|
||||
$onlinePls,
|
||||
static fn (array $pls) => in_array('adult', $pls['tags']),
|
||||
);
|
||||
|
||||
$catchupPls = array_filter(
|
||||
$onlinePls,
|
||||
static fn (array $pls) => $pls['hasCatchup'] === true,
|
||||
);
|
||||
|
||||
$tvgPls = array_filter(
|
||||
$onlinePls,
|
||||
static fn (array $pls) => $pls['hasTvg'] === true,
|
||||
);
|
||||
|
||||
$grouppedPls = array_filter(
|
||||
$onlinePls,
|
||||
static fn (array $pls) => count($pls['groups'] ?? []) > 0
|
||||
);
|
||||
|
||||
$onlineCh = $offlineCh = $adultCh = [];
|
||||
foreach ($onlinePls as $pls) {
|
||||
$tmpOnline = array_filter(
|
||||
$pls['channels'] ?? [],
|
||||
static fn (array $ch) => $ch['isOnline'] === true,
|
||||
);
|
||||
|
||||
$tmpOffline = array_filter(
|
||||
$pls['channels'] ?? [],
|
||||
static fn (array $ch) => $ch['isOnline'] === false,
|
||||
);
|
||||
|
||||
$tmpAdult = array_filter(
|
||||
$pls['channels'] ?? [],
|
||||
static fn (array $ch) => in_array('adult', $ch['tags']),
|
||||
);
|
||||
|
||||
$onlineCh = array_merge($onlineCh, $tmpOnline);
|
||||
$offlineCh = array_merge($offlineCh, $tmpOffline);
|
||||
$adultCh = array_merge($adultCh, $tmpAdult);
|
||||
}
|
||||
$this->api->sendChatAction($this->update->getMessage()->getChat()->getId(), 'typing');
|
||||
$stats = new StatisticsService()->get();
|
||||
|
||||
$replyText[] = '📊 *Статистика*';
|
||||
$replyText[] = '';
|
||||
$replyText[] = '*Список изменён:* ' . $this->escape(ini()->updatedAt());
|
||||
$replyText[] = '';
|
||||
$replyText[] = '*Плейлистов:* ' . count(ini()->getPlaylists());
|
||||
$replyText[] = '🟢 Онлайн \- ' . count($onlinePls);
|
||||
$replyText[] = '🔴 Оффлайн \- ' . count($offlinePls);
|
||||
$replyText[] = '⚪ В очереди \- ' . count($unknownPls);
|
||||
$replyText[] = '🔞 Для взрослых \- ' . count($adultPls);
|
||||
$replyText[] = '⏪ С перемоткой \- ' . count($catchupPls);
|
||||
$replyText[] = '🗞️ С телепрограммой \- ' . count($tvgPls);
|
||||
$replyText[] = '🗂️ С группировкой каналов \- ' . count($grouppedPls);
|
||||
$replyText[] = '*Плейлистов:* ' . $stats['playlists']['all'];
|
||||
$replyText[] = '🟢 Онлайн \- ' . $stats['playlists']['online'];
|
||||
$replyText[] = '🔴 Оффлайн \- ' . $stats['playlists']['offline'];
|
||||
$replyText[] = '⚪ В очереди \- ' . $stats['playlists']['unknown'];
|
||||
$replyText[] = '🔞 Для взрослых \- ' . $stats['playlists']['adult'];
|
||||
$replyText[] = '⏪ С перемоткой \- ' . $stats['playlists']['hasCatchup'];
|
||||
$replyText[] = '🗞️ С телепрограммой \- ' . $stats['playlists']['hasTvg'];
|
||||
$replyText[] = '🗂️ С группировкой каналов \- ' . $stats['playlists']['groupped'];
|
||||
$replyText[] = '';
|
||||
$replyText[] = '*Каналов:* ' . count($allChannels);
|
||||
$replyText[] = '🟢 Онлайн \- ' . count($onlineCh);
|
||||
$replyText[] = '🔴 Оффлайн \- ' . count($offlineCh);
|
||||
$replyText[] = '🔞 Для взрослых \- ' . count($adultCh);
|
||||
$replyText[] = '*Каналов:* ' . $stats['channels']['all'];
|
||||
$replyText[] = '🟢 Онлайн \- ' . $stats['channels']['online'];
|
||||
$replyText[] = '🔴 Оффлайн \- ' . $stats['channels']['offline'];
|
||||
$replyText[] = '🔞 Для взрослых \- ' . $stats['channels']['adult'];
|
||||
$replyText[] = '';
|
||||
$replyText[] = '';
|
||||
$replyText[] = '*Самая свежая проверка*';
|
||||
$replyText[] = '🕔 `' . $stats['channels']['latest']['code'] . '` (' . $stats['channels']['latest']['timeFmt'] . ')';
|
||||
$replyText[] = '';
|
||||
$replyText[] = '';
|
||||
|
||||
@@ -433,7 +391,7 @@ class Bot
|
||||
InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $keyboard = null,
|
||||
): bool {
|
||||
try {
|
||||
$this->bot->sendMessage(
|
||||
$this->api->sendMessage(
|
||||
chatId: $this->update->getMessage()->getChat()->getId(),
|
||||
text: $text,
|
||||
parseMode: 'MarkdownV2',
|
||||
|
||||
132
app/Core/StatisticsService.php
Normal file
132
app/Core/StatisticsService.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2025, Антон Аксенов
|
||||
* This file is part of m3u.su project
|
||||
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Обработчик команд бота
|
||||
*/
|
||||
class StatisticsService
|
||||
{
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
protected array $playlists = [];
|
||||
|
||||
protected array $channels = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->playlists = ini()->getPlaylists();
|
||||
$this->channels = $this->getAllChannels();
|
||||
}
|
||||
|
||||
protected function getPlaylistsByField(string $field, int|string|bool|null $value): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->playlists,
|
||||
static fn (array $pls) => $pls[$field] === $value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function getPlaylistsByTag(string $tag): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->playlists,
|
||||
static fn (array $pls) => in_array($tag, $pls['tags']),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getPlaylistsWithGroups(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->playlists,
|
||||
static fn (array $pls) => !empty($pls['groups']),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getLatestPlaylist(): array
|
||||
{
|
||||
$e = array_combine(
|
||||
array_column($this->playlists, 'code'),
|
||||
array_column($this->playlists, 'checkedAt'),
|
||||
);
|
||||
$e = array_filter($e);
|
||||
asort($e);
|
||||
$latest = array_slice($e, 0, 1);
|
||||
|
||||
return [
|
||||
'code' => array_first(array_keys($latest)),
|
||||
'time' => $time = array_first($latest),
|
||||
'timeFmt' => date('H:i:s d.m.Y', $time),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getAllChannels(): array
|
||||
{
|
||||
$channels = [];
|
||||
foreach ($this->playlists as $pls) {
|
||||
$channels = array_merge($channels, $pls['channels']);
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
protected function getAllChannelsCount(): int
|
||||
{
|
||||
return count($this->channels);
|
||||
}
|
||||
|
||||
protected function getChannelsByField(string $field, int|string|bool|null $value): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->channels,
|
||||
static fn (array $channel) => $channel[$field] === $value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function getChannelsByTag(string $tag): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->channels,
|
||||
static fn (array $channel) => in_array($tag, $channel['tags']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает команду /stats
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
return [
|
||||
'playlists' => [
|
||||
'all' => count($this->playlists),
|
||||
'online' => count($this->getPlaylistsByField('isOnline', true)),
|
||||
'offline' => count($this->getPlaylistsByField('isOnline', false)),
|
||||
'unknown' => count($this->getPlaylistsByField('isOnline', null)),
|
||||
'adult' => count($this->getPlaylistsByTag('adult')),
|
||||
'hasCatchup' => count($this->getPlaylistsByField('hasCatchup', true)),
|
||||
'hasTvg' => count($this->getPlaylistsByField('hasTvg', true)),
|
||||
'groupped' => count($this->getPlaylistsWithGroups()),
|
||||
'latest' => $this->getLatestPlaylist(),
|
||||
],
|
||||
'channels' => [
|
||||
'all' => $this->getAllChannelsCount(),
|
||||
'online' => count($this->getChannelsByField('isOnline', true)),
|
||||
'offline' => count($this->getChannelsByField('isOnline', false)),
|
||||
'adult' => count($this->getChannelsByTag('adult')),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ return [
|
||||
'env' => env('APP_ENV', env('IPTV_ENV', 'prod')),
|
||||
'timezone' => env('APP_TIMEZONE', 'GMT'),
|
||||
'page_size' => int(env('PAGE_SIZE', 10)),
|
||||
'repo_url' => env('REPO_URL', 'https://git.axenov.dev/IPTV'),
|
||||
'pls_encodings' => [
|
||||
'UTF-8',
|
||||
'CP1251',
|
||||
|
||||
@@ -67,6 +67,24 @@ return [
|
||||
'handler' => [ApiController::class, 'makeQrCode'],
|
||||
'name' => 'api::makeQrCode',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/api/version',
|
||||
'handler' => [ApiController::class, 'version'],
|
||||
'name' => 'api::version',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/api/health',
|
||||
'handler' => [ApiController::class, 'health'],
|
||||
'name' => 'api::health',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/api/stats',
|
||||
'handler' => [ApiController::class, 'stats'],
|
||||
'name' => 'api::stats',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -249,12 +249,12 @@
|
||||
<ul>
|
||||
<li>
|
||||
создать pull-request в открытом репозитории проекта с удалением данных о плейлисте из файла
|
||||
<a href="https://git.axenov.dev/IPTV/playlists/src/branch/master/playlists.ini"
|
||||
<a href="{{ config('app.repo_url') }}/playlists/src/branch/master/playlists.ini"
|
||||
target="_blank"
|
||||
>playlists.ini</a>, указав в комментарии к коммиту юридически значимую информацию;
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://git.axenov.dev/IPTV/playlists/issues/new" target="_blank">создать
|
||||
<a href="{{ config('app.repo_url') }}/playlists/issues/new" target="_blank">создать
|
||||
публичное обращение</a> в открытом репозитории проекта, указав юридически значимую
|
||||
информацию;
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{###########################################################################
|
||||
# Copyright (c) 2025, Антон Аксенов
|
||||
# This file is part of m3u.su project
|
||||
# MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
|
||||
# MIT License: {{config('app.repo_url}}/web/src/branch/master/LICENSE
|
||||
###########################################################################}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -107,7 +107,7 @@
|
||||
<a target="_blank" href="/docs" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="document-text-outline"></ion-icon>Документация
|
||||
</a>
|
||||
<a target="_blank" href="https://git.axenov.dev/IPTV" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<a target="_blank" href="{{ config('app.repo_url') }}" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
<ion-icon name="code-slash-outline"></ion-icon>Исходники
|
||||
</a>
|
||||
<a target="_blank" href="https://axenov.dev" class="text-light text-decoration-none d-flex align-items-center gap-1">
|
||||
@@ -125,7 +125,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<a class="small text-secondary d-inline-flex align-items-center gap-1"
|
||||
href="https://git.axenov.dev/IPTV/web/releases/tag/v{{ version() }}"
|
||||
href="{{ config('app.repo_url') }}/web/releases/tag/v{{ version() }}"
|
||||
target="_blank"
|
||||
>
|
||||
<ion-icon name="pricetag-outline"></ion-icon>v{{ version() }}
|
||||
|
||||
Reference in New Issue
Block a user