Compare commits

..

17 Commits

Author SHA1 Message Date
29e6479741 WIP 2025-02-24 22:47:01 +08:00
faec083397 Удалены дубликаты d5a + dz2 2024-11-27 12:11:39 +08:00
01504b84a3 Merge branch 'master' of github.com:anthonyaxenov/iptv
# Conflicts:
#	src/config/playlists.ini
2024-11-27 12:03:54 +08:00
7cb226e5ed По 20 плейлистов на страницу в списке 2024-11-27 11:54:46 +08:00
73f6c8f525 Фикс скрипта iptv 2024-11-27 11:54:26 +08:00
ea55dcaf47 Добавлены 152 плейлиста dmi3y-tv.ru 2024-11-27 11:53:54 +08:00
0df777ef0a Фиксы статусов проверки плейлистов в списке 2024-11-27 09:57:27 +08:00
2a064a74e1 Update playlists.ini
Removed [1] (broken channels)
2024-11-08 19:49:43 +08:00
e42b8dec7d Корректировка подключения кастомного блока 2024-09-27 21:26:44 +08:00
b70491e6fb Доработка страницы плейлиста
- исправлена ширина левой колонки
- добавлен вывод доп. атрибутов плейлиста из #EXTM3U
- id плейлиста в заголовке страницы
2024-09-26 00:38:08 +08:00
c0b7dd9a40 Доработка FAQ 2024-09-25 23:48:01 +08:00
1c57f58936 Фикс подгрузки логотипа из кэша + косметика в подробностях 2024-09-25 11:56:17 +08:00
70e25ded66 Ленивая подгрузка эскиза для отсутствующего логотипа 2024-09-25 09:06:56 +08:00
4e659c0abf Эскиз для отсутствующего логотипа 2024-09-25 09:01:16 +08:00
688ffc547e Фикс ленивой подгрузки логотипов 2024-09-25 08:12:11 +08:00
ab23f8796e Фикс попытки подгрузки пустых логотипов 2024-09-25 08:04:49 +08:00
2f0186e49f Обход ограничения http/https при загрузке логотипов каналов + их ленивое кэширование 2024-09-25 01:13:39 +08:00
56 changed files with 1637 additions and 346 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
*.md
*.example

11
.gitignore vendored
View File

@@ -1,14 +1,13 @@
/.idea
/.vscode
.idea/
.vscode/
downloaded/
/src/commit
/src/cache/*
/src/vendor
/src/views/custom.twig
commit
*.log
.env
*.m3u
*.m3u.*
*.m3u8
*.m3u8.*
!/**/.gitkeep

View File

@@ -4,8 +4,8 @@ networks:
services:
php:
container_name: iptv-php
svc-main:
container_name: iptv-svc-main
env_file:
- .env
environment:
@@ -20,8 +20,9 @@ services:
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
- ./docker/php/${IPTV_ENV}.php.ini:/usr/local/etc/php/conf.d/php.ini:ro
- ./log/php:/var/log/php:rw
- ./src:/var/www:rw
- ./playlists.ini:/var/www/config/playlists.ini:ro
- ./src/svc-main:/var/www:rw
- ./commit:/var/www/commit:ro
- ./playlists.ini:/var/www/playlists.ini:ro
nginx:
container_name: iptv-nginx
@@ -31,12 +32,12 @@ services:
- iptv
volumes:
- /etc/localtime:/etc/localtime:ro
- ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf
- ./log/nginx:/var/log/nginx:rw
- ./src:/var/www:ro
- ./src/svc-main:/var/www:ro
ports:
- '8080:80'
links:
- php
- svc-main
depends_on:
- php
- svc-main

View File

@@ -20,11 +20,12 @@ server {
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_pass php:9000;
fastcgi_pass svc-main:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_hide_header X-Powered-By;
include fastcgi_params;
}
}

View File

@@ -1,12 +1,15 @@
FROM php:8.2-fpm
FROM php:8.4-fpm
RUN apt update && \
apt upgrade -y && \
apt install -y git unzip 7zip
apt install -y --no-install-recommends git unzip 7zip && \
apt-get clean autoclean && \
apt-get autoremove --yes && \
rm -rf /var/lib/{apt,dpkg,cache,log}/
# https://pecl.php.net/package/xdebug
RUN pecl channel-update pecl.php.net && \
pecl install xdebug-3.3.2 unzip && \
pecl install xdebug-3.4.0 unzip && \
mkdir -p /var/log/php
COPY --from=composer /usr/bin/composer /usr/local/bin/composer

View File

@@ -1,5 +1,6 @@
[PHP]
error_reporting = E_ALL
expose_php = Off
file_uploads = Off
memory_limit=-1
max_execution_time=-1

View File

@@ -1,8 +1,11 @@
FROM php:8.2-fpm
FROM php:8.4-fpm
RUN apt update && \
apt upgrade -y && \
apt install -y git
apt install -y --no-install-recommends git && \
apt-get clean autoclean && \
apt-get autoremove --yes && \
rm -rf /var/lib/{apt,dpkg,cache,log}/
COPY --from=composer /usr/bin/composer /usr/local/bin/composer

View File

@@ -1,5 +1,6 @@
[PHP]
error_reporting = E_ALL
expose_php = Off
file_uploads = Off
; upload_max_filesize=10M
; post_max_size=10M

View File

@@ -1,4 +1,4 @@
#!/bin/bash
# хук пробрасывает хэш свежего коммита в контейнер
# для его отображения в подвале страницы
git rev-parse HEAD > src/commit
git rev-parse HEAD > commit

View File

@@ -3,5 +3,5 @@
# для его отображения в подвале страницы и очищает
# кеш шаблонов twig после слияния веток
# главным образом необходимо при git pull
git rev-parse HEAD > src/commit
docker exec -ti iptv-php rm -rf cache/views
git rev-parse HEAD > commit
docker exec -ti svc-main rm -rf cache/views

6
iptv
View File

@@ -1,9 +1,9 @@
#!/bin/bash
# https://gist.github.com/anthonyaxenov/89c99e09ddb195985707e2b24a57257d
CONTAINER="iptv-php" # the name of the container in which to 'exec' something
CONTAINER="iptv-main" # the name of the container in which to 'exec' something
CONFIG="$(dirname $([ -L $0 ] && readlink -f $0 || echo $0))/docker-compose.yml" # path to compose yml file
CMD="docker-compose -f $CONFIG" # docker-compose command
CMD="docker compose -f $CONFIG" # docker-compose command
APP_URL='http://localhost:8080/'
open_browser() {
@@ -29,6 +29,6 @@ case "$1" in
'restart' ) $CMD stop && $CMD start ;; # restart containers
'rebuild' ) $CMD down --remove-orphans && $CMD up -d --build ;; # rebuild containers
'open' ) open_browser $APP_URL && echo -e "\nYou're welcome!\n\t$APP_URL" ;;
'hooks' ) ./hooks/post-commit && cp hooks/* .git/hooks ;;
'hooks' ) сp -f hooks/* .git/hooks ;;
* ) docker exec -ti $CONTAINER $* ;; # exec anything else in container
esac

View File

@@ -1 +0,0 @@
src/config/playlists.ini

1153
playlists.ini Normal file

File diff suppressed because it is too large Load Diff

0
src/cache/.gitkeep vendored
View File

View File

@@ -1,198 +0,0 @@
[1]
name = 'free-tv.me'
desc = 'Каналы СНГ. Обновления бывают очень большими. Политика, мультики, новости, кино, музыка, спорт, 18+ и мн. др.'
pls = 'https://free-tv.me/iptv/tv'
src =
[2]
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/'
[3]
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/'
[ru]
name = 'Русские'
desc =
pls = 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/ru.m3u'
src = 'https://github.com/iptv-org/iptv'
[ru2]
redirect = ru
[ru3]
redirect = ru
[p5]
redirect = 2
[np]
redirect = 3
[tp]
name = 'TaurerPlus'
desc =
pls = 'https://raw.githubusercontent.com/TaurerMedia/TaurerPlus/main/index.m3u8'
src = 'https://github.com/anthonyaxenov/iptv/issues/2'
[mus]
name = 'Музыкальные IPTV каналы (iptv-playlisty.ru)'
desc = 'Музыкальный плейлист наиболее популярных каналов на нашем телевидении.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/music.m3u'
src = 'https://iptv-playlisty.ru/janriptv/muzykalnye-iptv-kanaly-v-formate-m3u/'
[det]
name = 'Плейлист детских IPTV каналов (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/deti.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-detskix-iptv-kanalov-v-formate-m3u/'
[det2]
name = 'Плейлист IPTV Мультфильмов для детей и подростков (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/multy.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-iptv-multfilmov-dlya-detej-i-podrostkov/'
[sng]
name = 'Каналы СНГ'
desc =
pls = 'https://dl.dropboxusercontent.com/s/iw9v57cln6dfkpu/Vinnitsa.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[sng2]
redirect = cam
[cam]
name = 'Веб-камеры'
desc =
pls = 'http://gorod.tv/iptv.m3u'
src = 'https://iptvsensei.ru/samoobnovlyayemyye-pleylisty-iptv'
[moto]
name = 'Плейлист Авто и Мото каналов IPTV (iptv-playlisty.ru)'
desc = 'В список были включены популярные не только у нас каналы авто тематики.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/automoto.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-avto-i-moto-kanalov-iptv/'
[poz]
name = 'Плейлист IPTV образовательных каналов (iptv-playlisty.ru)'
desc = 'В список вошло более десятка трансляций. Зритель сможет найти здесь передачи для разностороннего обучения и в целом просвещения.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/obrazovanie.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-iptv-obrazovatelnyx-kanalov-m3u/'
[poz2]
name = 'Познавательные каналы IPTV плейлист (iptv-playlisty.ru)'
desc = 'Наиболее подходящие трансляции, с выпусками интересных познавательных передач на любой вкус'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/poznav.m3u'
src = 'https://iptv-playlisty.ru/janriptv/poznavatelnye-kanaly-iptv-plejlist-v-formate-m3u/'
[moda]
name = 'Плейлист IPTV модных телеканалов (iptv-playlisty.ru)'
desc = 'Сюда вошли только топовые компании, на которых ежедневно рассказывают о последних тенденциях в одежде и дизайне.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/moda.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-iptv-modnyx-telekanalov-m3u/'
[fun]
name = 'Плейлист IPTV каналов развлечений (iptv-playlisty.ru)'
desc = 'Огромное количество развлекательных каналов'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/razvlechenie.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-iptv-kanalov-razvlechenij/'
[hd]
name = 'Плейлист IPTV каналов в HD формате (iptv-playlisty.ru)'
desc = 'Плейлист доступных по настоящий момент HD каналов'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/hd.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-iptv-kanalov-v-hd-formate/'
[nauka]
name = 'Научные IPTV каналы в формате (iptv-playlisty.ru)'
desc = 'Современные научные каналы: история, документальное кино, психология, культура...'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/nauka.m3u'
src = 'https://iptv-playlisty.ru/janriptv/nauchnye-iptv-kanaly-v-formate-m3u/'
[eda]
name = 'IPTV плейлист кулинарных каналов (iptv-playlisty.ru)'
desc = 'В список вошли наиболее интересные кулинарные каналы в формате M3U. Большинство каналов на русском языке'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/eda.m3u'
src = 'https://iptv-playlisty.ru/janriptv/iptv-plejlist-kulinarnyx-kanalov/'
[heal]
name = 'IPTV плейлист каналов здоровья (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/zdorov.m3u'
src = 'https://iptv-playlisty.ru/janriptv/iptv-plejlist-kanalov-zdorovya/'
[sport]
name = 'Плейлист IPTV спортивных каналов (iptv-playlisty.ru)'
desc = 'В коллекции можно встретить: футбольные матчи, баскетбольные встречи, теннис, хоккей, автоспорт и даже гольф.'
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/sport.m3u'
src = 'https://iptv-playlisty.ru/janriptv/plejlist-iptv-sportivnyx-kanalov/'
[strah]
name = 'Бесплатный IPTV плейлист каналов ужасов (iptv-playlisty.ru)'
desc =
pls = 'https://iptv-playlisty.ru/wp-content/uploads/m3u/Strah.m3u'
src = 'https://iptv-playlisty.ru/janriptv/besplatnyj-iptv-plejlist-kanalov-uzhasov/'
[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/'
[kino5]
name = 'Фильмы 5 (iptvsensei.ru)'
desc =
pls = 'https://pastebin.com/raw/jLaRge54'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[kz2]
name = 'Казахстан'
desc =
pls = 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/kz.m3u'
src = 'https://github.com/iptv-org/iptv'
[r1]
name = 'Радио каналы 1'
desc =
pls = 'http://lradio.c1.biz/ltradio.m3u'
src = 'https://iptvsensei.ru/novye-samoobnovlyaemye-plejlisty'
[sng13]
redirect = sng
[sng14]
redirect = sng2
[b1]
name = 'IPTV плейлисты для GX3235T2C и AV2568T2C'
desc =
pls = 'https://www.digitaltv.ru/upload/iblock/034/tvlist.m3u'
src = 'https://www.digitaltv.ru/news/iptv_pleylisty.html'
[az]
name='Каналы Азербайджана'
desc =
pls = 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/az.m3u'
src = 'https://github.com/iptv-org/iptv'
[az2]
redirect = az

View File

@@ -18,7 +18,7 @@
TOOLS_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
DL_DIR="$TOOLS_DIR/downloaded"
INI_FILE="$(dirname "$TOOLS_DIR")/playlists.ini"
INI_FILE="$(dirname "$TOOLS_DIR")/../../playlists.ini"
rm -rf "$DL_DIR" && \
mkdir -p "$DL_DIR" && \

10
src/svc-main/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
vendor/
cache/
views/custom.twig
playlists.ini
commit
*.log
.env
!/**/.gitkeep

View File

@@ -1,18 +1,21 @@
{
"require": {
"php": "^8.2",
"php": "^8.4",
"ext-json": "*",
"ext-curl": "*",
"ext-fileinfo": "*",
"mikecao/flight": "^3.12",
"symfony/dotenv": "^7.1",
"twig/twig": "^3.14"
},
"autoload": {
"psr-4": {
"App\\": "app/"
"Core\\": "core/",
"Controllers\\": "controllers/",
"Exceptions\\": "exceptions/"
},
"files": [
"app/helpers.php"
"helpers.php"
]
},
"scripts": {

View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3cbd8253b2f0790d682e38f308df6e7f",
"content-hash": "1aaea4609092e8a88074f050dab42323",
"packages": [
{
"name": "mikecao/flight",
"version": "v3.12.0",
"version": "v3.13.0",
"source": {
"type": "git",
"url": "https://github.com/flightphp/core.git",
"reference": "63fbf9b0316969e8aa6c318f8479d1ed0578f9b4"
"reference": "1307e8a39d89fadba69d0c2dad53b6e0da83fd96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/flightphp/core/zipball/63fbf9b0316969e8aa6c318f8479d1ed0578f9b4",
"reference": "63fbf9b0316969e8aa6c318f8479d1ed0578f9b4",
"url": "https://api.github.com/repos/flightphp/core/zipball/1307e8a39d89fadba69d0c2dad53b6e0da83fd96",
"reference": "1307e8a39d89fadba69d0c2dad53b6e0da83fd96",
"shasum": ""
},
"require": {
@@ -26,7 +26,7 @@
},
"require-dev": {
"ext-pdo_sqlite": "*",
"flightphp/runway": "^0.2.0",
"flightphp/runway": "^0.2.3 || ^1.0",
"league/container": "^4.2",
"level-2/dice": "^4.0",
"phpstan/extension-installer": "^1.3",
@@ -73,22 +73,22 @@
"homepage": "http://flightphp.com",
"support": {
"issues": "https://github.com/flightphp/core/issues",
"source": "https://github.com/flightphp/core/tree/v3.12.0"
"source": "https://github.com/flightphp/core/tree/v3.13.0"
},
"time": "2024-08-22T17:05:34+00:00"
"time": "2024-10-30T19:52:23+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": ""
},
"require": {
@@ -126,7 +126,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
},
"funding": [
{
@@ -142,20 +142,20 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/dotenv",
"version": "v7.1.5",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/dotenv.git",
"reference": "6d966200b399fa59759286f3fc7c919f0677c449"
"reference": "28347a897771d0c28e99b75166dd2689099f3045"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dotenv/zipball/6d966200b399fa59759286f3fc7c919f0677c449",
"reference": "6d966200b399fa59759286f3fc7c919f0677c449",
"url": "https://api.github.com/repos/symfony/dotenv/zipball/28347a897771d0c28e99b75166dd2689099f3045",
"reference": "28347a897771d0c28e99b75166dd2689099f3045",
"shasum": ""
},
"require": {
@@ -200,7 +200,7 @@
"environment"
],
"support": {
"source": "https://github.com/symfony/dotenv/tree/v7.1.5"
"source": "https://github.com/symfony/dotenv/tree/v7.2.0"
},
"funding": [
{
@@ -216,7 +216,7 @@
"type": "tidelift"
}
],
"time": "2024-09-17T09:16:35+00:00"
"time": "2024-11-27T11:18:42+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -455,16 +455,16 @@
},
{
"name": "twig/twig",
"version": "v3.14.0",
"version": "v3.16.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
"reference": "475ad2dc97d65d8631393e721e7e44fb544f0561"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/475ad2dc97d65d8631393e721e7e44fb544f0561",
"reference": "475ad2dc97d65d8631393e721e7e44fb544f0561",
"shasum": ""
},
"require": {
@@ -475,6 +475,7 @@
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
@@ -518,7 +519,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.14.0"
"source": "https://github.com/twigphp/Twig/tree/v3.16.0"
},
"funding": [
{
@@ -530,20 +531,21 @@
"type": "tidelift"
}
],
"time": "2024-09-09T17:55:12+00:00"
"time": "2024-11-29T08:27:05+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^8.2",
"ext-json": "*",
"ext-curl": "*"
"ext-curl": "*",
"ext-fileinfo": "*"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@@ -2,12 +2,12 @@
declare(strict_types=1);
namespace App\Controllers;
namespace Controllers;
use App\Core\IniFile;
use App\Core\Playlist;
use App\Exceptions\PlaylistNotFoundException;
use Core\IniFile;
use Core\Playlist;
use Exception;
use Exceptions\PlaylistNotFoundException;
use Flight;
use Random\RandomException;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controllers;
namespace Controllers;
use Exception;
use Flight;
@@ -29,7 +29,7 @@ class HomeController extends Controller
}
// иначе формируем и сортируем список при необходимости, рисуем страницу
$perPage = 10;
$perPage = 20;
$playlists = $this->ini->playlists(false);
$count = count($playlists);
$pageCount = ceil($count / $perPage);

View File

@@ -2,10 +2,11 @@
declare(strict_types=1);
namespace App\Controllers;
namespace Controllers;
use App\Exceptions\PlaylistNotFoundException;
use Core\ChannelLogo;
use Exception;
use Exceptions\PlaylistNotFoundException;
use Flight;
/**
@@ -41,7 +42,6 @@ class PlaylistController extends Controller
public function details(string $id): void
{
$result = $this->getPlaylistResponse($id);
view('details', $result);
}
@@ -57,4 +57,33 @@ class PlaylistController extends Controller
$result = $this->getPlaylistResponse($id, true);
Flight::json($result);
}
/**
* Возвращает логотип канала, кэшируя при необходимости
*
* @return void
*/
public function logo(): void
{
$input = Flight::request()->query['url'] ?? null;
$logo = new ChannelLogo($input);
if (!$logo->readFile()) {
$logo->fetch();
}
if ($logo->size() === 0) {
$logo->setDefault();
}
$logo->store();
$body = $logo->raw();
$size = $logo->size();
$mime = $logo->mimeType();
Flight::response()
->write($body)
->header('Content-Type', $mime)
->header('Content-Length', (string)$size);
}
}

View File

@@ -2,9 +2,8 @@
declare(strict_types=1);
namespace App\Core;
namespace Core;
use App\Extensions\TwigFunctions;
use Flight;
use Twig\Environment;
use Twig\Extension\DebugExtension;
@@ -22,14 +21,14 @@ final class Bootstrapper
*/
public static function bootSettings(): void
{
$config = require_once config_path('app.php');
$config = require_once root_path('config.php');
foreach ($config as $key => $value) {
Flight::set($key, $value);
}
Flight::set('config', $config);
}
public static function bootIni(): void
public static function bootCore(): void
{
$loader = new IniFile();
$loader->load();
@@ -67,7 +66,7 @@ final class Bootstrapper
*/
public static function bootRoutes(): void
{
$routes = require_once config_path('routes.php');
$routes = require_once root_path('routes.php');
foreach ($routes as $route => $handler) {
Flight::route($route, $handler);
}

View File

@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace Core;
class ChannelLogo implements \Stringable
{
/**
* @var string Валидированная ссылка на изображение
*/
public readonly string $url;
/**
* @var string|null Хэш от ссылки на изображение
*/
public readonly ?string $hash;
/**
* @var string|null Путь к файлу изображению на диске
*/
protected ?string $path = '';
/**
* @var string|null MIME-тип изображения
*/
protected ?string $mimeType = null;
/**
* @var false|string|null Сырое изображение:
* null -- не загружалось;
* false -- ошибка загрузки;
* string -- бинарные данные.
*/
protected false|string|null $rawData = null;
/**
* Конструктор
*
* @param string $url Внешняя ссылка на изображение
*/
public function __construct(string $url)
{
$url = $this->prepareUrl($url);
if (is_string($url)) {
$this->url = $url;
$this->hash = md5($url);
$this->path = cache_path("tv-logos/$this->hash");
}
}
/**
* Валидирует и очищает ссылку на изображение
*
* @param string $url
* @return false|string
*/
protected function prepareUrl(string $url): false|string
{
$url = filter_var(trim($url), FILTER_VALIDATE_URL);
if ($url === false) {
return false;
}
$parts = parse_url($url);
if (!is_array($parts)) {
return false;
}
return $parts['scheme'] . '://' . $parts['host'] . $parts['path'];
}
/**
* Загружает сырое изображение по ссылке и определяет его MIME-тип
*
* @return bool
*/
public function fetch(): bool
{
$this->rawData = @file_get_contents($this->url);
$isFetched = is_string($this->rawData);
if (!$isFetched) {
return false;
}
$this->mimeType = $this->mimeType();
return true;
}
/**
* Сохраняет сырое изображение в кэш
*
* @return bool
*/
public function store(): bool
{
return is_string($this->rawData)
&& $this->prepareCacheDir()
&& @file_put_contents($this->path, $this->rawData);
}
/**
* Считывает изображение из кэша
*
* @return bool
*/
public function readFile(): bool
{
if (!file_exists($this->path)) {
return false;
}
$this->rawData = @file_get_contents($this->path);
return is_string($this->rawData);
}
/**
* Считывает дефолтный эскиз вместо логотипа
*
* @return bool
*/
public function setDefault(): bool
{
$this->path = root_path('public/no-tvg-logo.png');
return$this->readFile();
}
/**
* Возвращает base64-кодированное изображение
*
* @return string|null
*/
public function asBase64(): ?string
{
if (!is_string($this->rawData)) {
return null;
}
return "data:$this->mimeType;base64," . base64_encode($this->rawData);
}
/**
* Возвращает сырое изображение
*
* @return false|string|null
*/
public function raw(): false|string|null
{
return $this->rawData;
}
/**
* Проверяет готовность директории кэша изображений, создавая её при необходимости
*
* @return bool
*/
public function prepareCacheDir(): bool
{
$cacheFileDir = cache_path('tv-logos');
return is_dir($cacheFileDir)
|| @mkdir($cacheFileDir, 0775, true);
}
/**
* Возвращает MIME-тип сырого изображения
*
* @return string|null
*/
public function mimeType(): ?string
{
if (!is_string($this->rawData)) {
return null;
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
return $finfo->buffer($this->rawData) ?: null;
}
/**
* Возвращает размер сырого изображения в байтах
*
* @return int
*/
public function size(): int
{
return strlen((string)$this->rawData);
}
/**
* @inheritDoc
*/
public function __toString(): string
{
return $this->asBase64();
}
}

View File

@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Core;
namespace Core;
use App\Exceptions\PlaylistNotFoundException;
use Exception;
use Exceptions\PlaylistNotFoundException;
/**
* Класс для работы с ini-файлом плейлистов
@@ -40,7 +40,7 @@ class IniFile
*/
public function load(): void
{
$filepath = config_path('playlists.ini');
$filepath = root_path('playlists.ini');
$this->updated_at = date('d.m.Y h:i', filemtime($filepath));
$this->rawIni = parse_ini_file($filepath, true);

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Core;
namespace Core;
use Exception;
use Random\RandomException;
@@ -204,6 +204,16 @@ class Playlist
if ($isChannel) {
$channel['url'] = str_starts_with($line, 'http') ? $line : null;
$logoUrl = $channel['attributes']['tvg-logo'] ?? null;
if (is_string($logoUrl)) {
$logo = new ChannelLogo($logoUrl);
$logo->readFile();
$channel['logo'] = [
'base64' => $logo->asBase64(),
'size' => $logo->size(),
'mime-type' => $logo->mimeType(),
];
}
$result['channels'][] = $channel;
$isChannel = false;
unset($channel);

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Extensions;
namespace Core;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Exceptions;
namespace Exceptions;
use Exception;

View File

@@ -17,17 +17,6 @@ function root_path(string $path = ''): string
return rtrim(sprintf('%s/%s', dirname($_SERVER['DOCUMENT_ROOT']), $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
*

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
use App\Core\Bootstrapper;
use Core\Bootstrapper;
use Symfony\Component\Dotenv\Dotenv;
/*
@@ -15,6 +15,6 @@ require '../vendor/autoload.php';
(new Dotenv())->loadEnv(root_path() . '/.env');
Bootstrapper::bootSettings();
Bootstrapper::bootTwig();
Bootstrapper::bootIni();
Bootstrapper::bootCore();
Bootstrapper::bootRoutes();
Flight::start();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -2,13 +2,14 @@
declare(strict_types=1);
use App\Controllers\HomeController;
use App\Controllers\PlaylistController;
use Controllers\HomeController;
use Controllers\PlaylistController;
return [
'GET /' => [HomeController::class, 'index'],
'GET /page/@page:[0-9]+' => [HomeController::class, 'index'],
'GET /faq' => [HomeController::class, 'faq'],
'GET /logo' => [PlaylistController::class, 'logo'],
'GET /@id:[a-zA-Z0-9_-]+' => [PlaylistController::class, 'download'],
'GET /?[a-zA-Z0-9_-]+' => [PlaylistController::class, 'download'],
'GET /@id:[a-zA-Z0-9_-]+/details' => [PlaylistController::class, 'details'],

View File

@@ -1,13 +1,19 @@
{% extends "template.twig" %}
{% block title %}{{ name }} - {{ config('app.title') }}{% endblock %}
{% block title %}[{{ id }}] {{ name }} - {{ config('app.title') }}{% endblock %}
{% block head %}
<style>.tvg-logo-background{max-width:100px;max-height:100px;background:white;padding:2px;border-radius:5px}</style>
<style>
img.tvg-logo{max-width:80px;max-height:80px;padding:2px;border-radius:5px}
tr.chrow td{padding:3px}
td.chindex{width:1%}
td.chlogo{width:100px}
div.chlist-table{max-height:550px}
</style>
{% endblock %}
{% block header %}
<h2>О плейлисте {{ name }}</h2>
<h2>О плейлисте: {{ name }}</h2>
{% if (content.encoding.alert) %}
<div class="alert alert-warning small" role="alert">
Кодировка исходного плейлиста отличается от UTF-8.
@@ -31,12 +37,12 @@
{% block content %}
<div class="row">
<div class="col-md-6">
<table class="table table-dark table-hover small">
<div class="col-lg-7">
<table class="table table-dark table-hover small mb-lg-5">
<tbody>
<tr>
<td class="w-25">ID</td>
<td>
<th class="w-25" scope="row">ID</th>
<td class="text-break">
<code>{{ id }}</code>&nbsp;{% if status.possibleStatus == 'online' %}
<span class="badge small text-dark bg-success">online</span>
{% elseif status.possibleStatus == 'offline' %}
@@ -45,50 +51,76 @@
<span class="badge small text-dark bg-warning">timeout</span>
{% elseif status.possibleStatus == 'error' %}
<span class="badge small text-dark bg-danger">error</span>
{% endif %}
{% endif %}
</td>
</tr>
<tr>
<td>Описание</td>
<td><p>{{ desc }}</p></td>
<th scope="row">Описание</th>
<td class="text-break"><p>{{ desc }}</p></td>
</tr>
<tr>
<td><b>Ccылка для ТВ</b></td>
<th scope="row">Ccылка для ТВ</th>
<td><b onclick="prompt('Скопируй адрес плейлиста', '{{ url }}')"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Нажми на ссылку, чтобы скопировать её в буфер обмена"
class="font-monospace cursor-pointer">{{ url }}</b></td>
class="font-monospace cursor-pointer text-break">{{ url }}</b></td>
</tr>
<tr>
<td>M3U</td>
<td>{{ pls }}</td>
<th scope="row">M3U</th>
<td class="text-break">{{ pls }}</td>
</tr>
<tr>
<td>Источник</td>
<td>{{ src }}</td>
<th scope="row">Источник</th>
<td class="text-break">{{ src }}</td>
</tr>
</tbody>
</table>
{% if (content.attributes) %}
<h4>Дополнительные атрибуты</h4>
<table class="table table-dark table-hover small">
<tbody>
{% for attribute,value in content.attributes %}
<tr>
<th class="w-25" scope="row">{{ attribute }}</th>
<td class="text-break">{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
<div class="col-md-6">
<div class="col-lg-5">
<h4>Список каналов ({{ content.channelCount ?? 0 }})</h4>
{% if (content.channelCount > 0) %}
<div id="chlist">
<input type="text" class="form-control form-control-sm bg-dark text-light mb-2 fuzzy-search" placeholder="Поиск...">
<div class="overflow-auto" style="max-height:550px">
<input type="text"
class="form-control form-control-sm bg-dark text-light mb-2 fuzzy-search"
placeholder="Поиск..."
/>
<div class="chlist-table overflow-auto">
<table class="table table-dark table-hover small">
<tbody class="list">
{% for channel in content.channels %}
<tr class="chrow">
<td class="p-1" class="chindex">{{ loop.index }}</td>
<td class="p-1">
{% if (channel.attributes['tvg-logo']) %}
<img class="tvg-logo-background" src="{{ channel.attributes['tvg-logo'] }}" />
<td class="chindex">{{ loop.index }}</td>
<td class="chlogo text-center">
<img class="tvg-logo"
{% if (channel.logo.base64) %}
src="{{ channel.logo.base64 }}"
{% elseif (channel.attributes['tvg-logo']) %}
src="{{ base_url('logo?url=' ~ channel.attributes['tvg-logo']) }}"
loading="lazy"
{% else %}
src="{{ base_url('no-tvg-logo.png') }}"
{% endif %}
alt="Логотип канала '{{ channel.name }}'"
title="Логотип канала '{{ channel.name }}'"
/>
</td>
<td class="p-1 chname">{{ channel.name }}</td>
<td class="chname text-break">{{ channel.name }}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -16,6 +16,10 @@
видео/аудио потоков, программ телепередач, плейлистов и их поддержки. Этим занимаются администраторы
ресурсов, указанные как источник, и те, с чьих ресурсов ведётся трансляция.
</p>
<p>
За содержимое плейлистов и их качество отвечают авторы плейлистов. На стороне сервиса управляются сами
плейлисты.
</p>
<p class="mb-5">
Сервис "{{ config('app.title') }}" ({{ base_url() }}) предоставляет только информацию об активности
плейлистов, найденных в открытом доступе, и короткие ссылки на них для удобства использования в ПО.
@@ -24,8 +28,26 @@
</p>
<div class="accordion" id="faq-accordion">
<div class="accordion-item bg-dark">
<h2 class="accordion-header bg-dark" id="h-howtouse">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header" id="h-purpose">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#purpose" aria-expanded="false" aria-controls="purpose">
Для чего нужен сервис?
</button>
</h2>
<div id="purpose" class="accordion-collapse collapse" aria-labelledby="h-purpose" data-bs-parent="#faq-accordion">
<div class="accordion-body">
<p>Изначально сервис создавался "для себя", чтобы:</p>
<ul>
<li>сократить ссылки на сторонние плейлисты и их было проще вводить с пульта;</li>
<li>собрать в одном месте наиболее годные плейлисты.</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header" id="h-howtouse">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#howtouse" aria-expanded="false" aria-controls="howtouse">
Как пользоваться сервисом?
</button>
@@ -42,7 +64,8 @@
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-howtoconnect">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#howtoconnect" aria-expanded="false" aria-controls="howtoconnect">
Как подключить плейлист?
@@ -55,17 +78,35 @@
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-isitfree">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#isitfree" aria-expanded="false" aria-controls="isitfree">
Эти плейлисты и каналы в них -- бесплатны?
</button>
</h2>
<div id="isitfree" class="accordion-collapse collapse" aria-labelledby="h-isitfree" data-bs-parent="#faq-accordion">
<p class="accordion-body">Возможно. По крайней мере, так утверждают источники. Но гарантий никаких никто не даёт.</p>
<p class="accordion-body">
Возможно. По крайней мере, так утверждают источники. Но гарантий никаких никто не даёт.
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header" id="h-logos">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#logos" aria-expanded="false" aria-controls="logos">
Откуда берутся логотипы каналов и программы передач?
</button>
</h2>
<div id="logos" class="accordion-collapse collapse" aria-labelledby="h-logos" data-bs-parent="#faq-accordion">
<p class="accordion-body">
Всё это (не) указывается внутри плейлиста его авторами.
Но в некоторых плеерах можно вручную указывать программу передач (см. ниже).
</p>
</div>
</div>
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-which">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#which" aria-expanded="false" aria-controls="which">
Какие плейлисты попадают сюда?
@@ -88,7 +129,8 @@
</div>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-statuses">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#statuses" aria-expanded="false" aria-controls="statuses">
Что означают статусы плейлистов?
@@ -123,7 +165,8 @@
</div>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-donttrust">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#donttrust" aria-expanded="false" aria-controls="donttrust">
Почему нельзя доверять результатам проверки?
@@ -145,7 +188,8 @@
</div>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-guarantee">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#guarantee" aria-expanded="false" aria-controls="guarantee">
Какова гарантия, что я добавлю себе плейлист отсюда и он работать хоть сколько-нибудь долго?
@@ -163,7 +207,19 @@
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header" id="h-panic">
<button class="accordion-button text-warning bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#panic" aria-expanded="false" aria-controls="panic">
У меня перестал работать/исчез любимый канал/плейлист! Нет лого канала/программы передач!
</button>
</h2>
<div id="panic" class="accordion-collapse collapse" aria-labelledby="h-panic" data-bs-parent="#faq-accordion">
<p class="accordion-body">Ну штош ¯\_(ツ)_/¯</p>
</div>
</div>
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-epg">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#epg" aria-expanded="false" aria-controls="epg">
Где взять программу передач (EPG)?
@@ -179,10 +235,11 @@
</div>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-howoftenlist">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#howoftenlist" aria-expanded="false" aria-controls="howoftenlist">
Как часто обновляется этот список?
Как часто обновляется список плейлистов?
</button>
</h2>
<div id="howoftenlist" class="accordion-collapse collapse" aria-labelledby="h-howoftenlist" data-bs-parent="#faq-accordion">
@@ -193,7 +250,8 @@
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-howoftench">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#howoftench" aria-expanded="false" aria-controls="howoftench">
Как часто обновляется содержимое плейлистов?
@@ -205,7 +263,8 @@
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-api">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#api" aria-expanded="false" aria-controls="api">
Есть ли API? Как им пользоваться?
@@ -217,7 +276,8 @@
</p>
</div>
</div>
<div class="accordion-item bg-dark">
<div class="accordion-item bg-dark text-light">
<h2 class="accordion-header bg-dark" id="h-howtoadd">
<button class="accordion-button text-light bg-dark" type="button" data-bs-toggle="collapse" data-bs-target="#howtoadd" aria-expanded="false" aria-controls="howtoadd">
Как пополнить этот список?
@@ -230,6 +290,7 @@
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -22,9 +22,7 @@
<tbody>
{% for id, playlist in playlists %}
<tr class="pls" data-playlist-id="{{ id }}">
<td class="text-center id">
<strong>{{ id }}</strong>
</td>
<td class="text-center font-monospace id">{{ id }}</td>
<td class="info">
<span class="badge small bg-secondary text-dark status">loading</span>
<strong>{{ playlist.name }}</strong>
@@ -42,8 +40,6 @@
</td>
<td class="col-3 d-none d-sm-table-cell">
<span onclick="prompt('Скопируй адрес плейлиста', '{{ playlist.url }}')"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Нажми на ссылку, чтобы скопировать её в буфер обмена"
class="font-monospace cursor-pointer">
{{ playlist.url }}
@@ -84,11 +80,10 @@
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.possibleStatus
el_status.innerText = xhr.response?.status.possibleStatus ?? 'error'
el_count.innerText = xhr.response?.content.channelCount ?? 0
switch (xhr.response.status.possibleStatus) {
switch (el_status.innerText) {
case 'online':
el_status.classList.add('bg-success')
break
@@ -99,24 +94,21 @@
el_status.classList.add('bg-danger')
break
}
if (xhr.response.error) {
if (xhr.response?.error) {
el_status.title = '[' + xhr.response.error.code + '] ' + xhr.response.error.message
}
}
}
xhr.onerror = () => {
console.log('[' + id + '] ERROR', xhr.response)
el_status.classList.add('bg-danger')
el_status.innerText = 'error'
el_count.innerText = 0
}
xhr.onabort = () => {
console.log('[' + id + '] ABORTED', xhr.response)
el_status.classList.add('bg-secondary')
el_count.innerText = 0
}
xhr.ontimeout = () => {
console.log('[' + id + '] TIMEOUT', xhr.response)
el_status.classList.add('bg-secondary')
el_status.innerText = 'timeout'
el_count.innerText = 0

View File

@@ -16,7 +16,6 @@
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="msapplication-TileImage" content="{{ base_url('/favicon/mstile-144x144.png') }}">
<meta name="theme-color" content="#212529">
{% include("custom.twig") ignore missing %}
{% block head %}{% endblock %}
</head>
<body class="bg-dark text-light">
@@ -55,7 +54,7 @@
</nav>
</header>
<section class="container-fluid h-100 pt-lg-3 px-0 pb-0">
<section class="container h-100 pt-lg-3 px-0 pb-0">
{% block header %}{% endblock %}
{% block content %}{% endblock %}
</section>
@@ -75,5 +74,6 @@
</span>
</footer>
</div>
{% include("custom.twig") ignore missing %}
</body>
</html>