mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-04-13 08:28:32 +00:00
Steps 11.1–11.6: migrated all standalone API entry points to controller classes dispatched via the Front Controller, then deleted the legacy PHP files. Controllers created: - PlayerApiController (player_api.php, 12 actions + numeric aliases) - Enigma2ApiController (enigma2.php + xplugin.php) - PlaylistApiController (playlist.php) - EpgApiController (epg.php) - InternalApiController (api.php, ~40 server-to-server actions) Nginx changes (MAIN + LB configs): - Add location block for streaming API endpoints → FC with XC_SCOPE=api - Add location block for internal API (/api.php) → FC with XC_API=internal - Change 6 rewrite rules from break→last for deleted file URLs - Remove allow/deny from /api.php location — auth handled by PHP (password + IP whitelist + brute-force guard) Front Controller (public/index.php): - Add section 3a: REST API dispatch (XC_SCOPE=includes/api/*) - Add section 3b: Streaming API dispatch (XC_SCOPE=api, XC_API=*) - Move autoloader require to top (section 1b) for all code paths - Controller map: endpoint name → controller class → init → dispatch Bootstrap changes: - www/init.php, www/stream/init.php: replace FC_API_NAME constant with direct $rFilename variable (set by FC before require, checked via isset) Bug fixes: - PlayerApiController: fix 3 PHP 8 warnings ($rBouquets undefined, missing 'rating'/'subtitle' array keys in get_vod_info) - Fix server-to-server API calls blocked by nginx deny all (api_url_ip uses external server IP, not 127.0.0.1) Deleted files (8): - www/player_api.php, www/enigma2.php, www/xplugin.php - www/epg.php, www/playlist.php, www/api.php - includes/api/admin/index.php, includes/api/reseller/index.php New files: - tools/test_player_api.sh — comprehensive Player API test suite (13 sections)
746 lines
28 KiB
Bash
746 lines
28 KiB
Bash
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
# Player API Test Suite
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
#
|
||
# Тестирует все endpoints PlayerApiController через HTTP.
|
||
# Каждый тест проверяет HTTP-статус, Content-Type и структуру JSON.
|
||
#
|
||
# Использование:
|
||
# ./tools/test_player_api.sh <base_url> <username> <password>
|
||
# ./tools/test_player_api.sh https://example.com testuser testpass
|
||
#
|
||
# Опционально (env variables):
|
||
# STREAM_ID=123 — ID live-стрима для EPG-тестов
|
||
# VOD_ID=456 — ID VOD для get_vod_info
|
||
# SERIES_ID=789 — ID сериала для get_series_info
|
||
# CATEGORY_ID=10 — ID категории для фильтрации
|
||
# VERBOSE=1 — показывать тела ответов
|
||
# TIMEOUT=10 — таймаут curl в секундах
|
||
#
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
#
|
||
# API Documentation — Player API (player_api.php)
|
||
#
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
#
|
||
# Аутентификация:
|
||
# GET /player_api.php?username=XXX&password=YYY
|
||
# GET /player_api.php?token=ZZZ
|
||
#
|
||
# Все параметры передаются через query string (GET).
|
||
#
|
||
# ───────────────────────────────────────────────────────────────────
|
||
# Endpoints (action=...)
|
||
# ───────────────────────────────────────────────────────────────────
|
||
#
|
||
# 1. (без action) — Информация о пользователе и сервере
|
||
# Params: —
|
||
# Response: {
|
||
# "user_info": {
|
||
# "username", "password", "message", "auth", "status",
|
||
# "exp_date", "is_trial", "active_cons", "created_at",
|
||
# "max_connections", "allowed_output_formats"
|
||
# },
|
||
# "server_info": {
|
||
# "version", "url", "port", "https_port", "server_protocol",
|
||
# "rtmp_port", "timestamp_now", "time_now", "timezone", "process"
|
||
# }
|
||
# }
|
||
#
|
||
# 2. get_live_categories — Категории Live/Radio потоков
|
||
# Params: —
|
||
# Response: [{ "category_id", "category_name", "parent_id" }]
|
||
#
|
||
# 3. get_vod_categories — Категории VOD (фильмов)
|
||
# Params: —
|
||
# Response: [{ "category_id", "category_name", "parent_id" }]
|
||
#
|
||
# 4. get_series_categories — Категории сериалов
|
||
# Params: —
|
||
# Response: [{ "category_id", "category_name", "parent_id" }]
|
||
#
|
||
# 5. get_live_streams — Список live-потоков
|
||
# Params: category_id (опц.), params[offset], params[items_per_page]
|
||
# Response: [{
|
||
# "num", "name", "stream_type", "stream_id", "stream_icon",
|
||
# "epg_channel_id", "added", "custom_sid", "tv_archive",
|
||
# "direct_source", "tv_archive_duration", "category_id",
|
||
# "category_ids", "thumbnail"
|
||
# }]
|
||
#
|
||
# 6. get_vod_streams — Список VOD (фильмов)
|
||
# Params: category_id (опц.), params[offset], params[items_per_page]
|
||
# Response: [{
|
||
# "num", "name", "title", "year", "stream_type", "stream_id",
|
||
# "stream_icon", "rating", "rating_5based", "added", "plot",
|
||
# "cast", "director", "genre", "release_date", "youtube_trailer",
|
||
# "episode_run_time", "category_id", "category_ids",
|
||
# "container_extension", "custom_sid", "direct_source"
|
||
# }]
|
||
#
|
||
# 7. get_series — Список сериалов
|
||
# Params: category_id (опц.)
|
||
# Response: [{
|
||
# "num", "name", "title", "year", "stream_type", "series_id",
|
||
# "cover", "plot", "cast", "director", "genre", "release_date",
|
||
# "releaseDate", "last_modified", "rating", "rating_5based",
|
||
# "backdrop_path", "youtube_trailer", "episode_run_time",
|
||
# "category_id", "category_ids"
|
||
# }]
|
||
#
|
||
# 8. get_series_info — Детальная информация о сериале
|
||
# Params: series_id (обяз.)
|
||
# Response: {
|
||
# "seasons": [{ "cover", "cover_big", ... }],
|
||
# "info": { "name", "title", "year", "cover", "plot", ... },
|
||
# "episodes": { "1": [{ "id", "episode_num", "title", ... }] }
|
||
# }
|
||
#
|
||
# 9. get_vod_info — Детальная информация о VOD
|
||
# Params: vod_id (обяз.)
|
||
# Response: {
|
||
# "info": { "tmdb_id", "episode_run_time", "releasedate",
|
||
# "cover_big", "movie_image", "rating", "backdrop_path",
|
||
# "subtitles": [{ "index", "language", "title" }]
|
||
# },
|
||
# "movie_data": { "stream_id", "name", "title", "year",
|
||
# "added", "category_id", "category_ids",
|
||
# "container_extension", "custom_sid", "direct_source"
|
||
# }
|
||
# }
|
||
#
|
||
# 10. get_epg — Полная EPG-программа для потока(ов)
|
||
# Params: stream_id (обяз., одиночный или через запятую)
|
||
# from_now (опц., фильтр только будущие)
|
||
# Response (single): [{ "title", "description", "start", "end" }]
|
||
# Response (multi): { "123": [...], "456": [...] }
|
||
#
|
||
# 11. get_short_epg — Текущая и ближайшие программы
|
||
# Params: stream_id (обяз.), limit (опц., default=4)
|
||
# Response: { "epg_listings": [{ "title", "description",
|
||
# "start", "stop", "end", "start_timestamp", "stop_timestamp",
|
||
# "end_timestamp" }] }
|
||
#
|
||
# 12. get_simple_data_table — EPG с архивной информацией
|
||
# Params: stream_id (обяз.)
|
||
# Response: { "epg_listings": [{
|
||
# "title", "description", "start", "end",
|
||
# "start_timestamp", "stop_timestamp",
|
||
# "now_playing", "has_archive"
|
||
# }] }
|
||
#
|
||
# ───────────────────────────────────────────────────────────────────
|
||
# Числовые алиасы action
|
||
# ───────────────────────────────────────────────────────────────────
|
||
#
|
||
# 200 → get_vod_categories 205 → get_short_epg
|
||
# 201 → get_live_categories 206 → get_series_categories
|
||
# 202 → get_live_streams 207 → get_simple_data_table
|
||
# 203 → get_vod_streams 208 → get_series
|
||
# 204 → get_series_info 209 → get_vod_info
|
||
#
|
||
# ───────────────────────────────────────────────────────────────────
|
||
# Panel API (legacy)
|
||
# ───────────────────────────────────────────────────────────────────
|
||
#
|
||
# URL: /panel_api.php (вместо /player_api.php)
|
||
# Требует: настройка legacy_panel_api=1
|
||
# Без action: возвращает default info + автозагрузку каналов
|
||
# С action: аналогично player_api
|
||
#
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
|
||
# ─── Цвета ────────────────────────────────────────────────────────
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[0;33m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
# ─── Аргументы ────────────────────────────────────────────────────
|
||
|
||
if [[ $# -lt 3 ]]; then
|
||
echo -e "${BOLD}Usage:${NC} $0 <base_url> <username> <password>"
|
||
echo ""
|
||
echo " base_url — URL сервера (https://example.com)"
|
||
echo " username — логин пользователя"
|
||
echo " password — пароль пользователя"
|
||
echo ""
|
||
echo " Env vars: STREAM_ID, VOD_ID, SERIES_ID, CATEGORY_ID, VERBOSE, TIMEOUT"
|
||
exit 1
|
||
fi
|
||
|
||
BASE_URL="${1%/}"
|
||
USERNAME="$2"
|
||
PASSWORD="$3"
|
||
|
||
STREAM_ID="${STREAM_ID:-}"
|
||
VOD_ID="${VOD_ID:-}"
|
||
SERIES_ID="${SERIES_ID:-}"
|
||
CATEGORY_ID="${CATEGORY_ID:-}"
|
||
VERBOSE="${VERBOSE:-0}"
|
||
TIMEOUT="${TIMEOUT:-10}"
|
||
|
||
API_URL="${BASE_URL}/player_api.php"
|
||
AUTH="username=${USERNAME}&password=${PASSWORD}"
|
||
|
||
# ─── Счётчики ─────────────────────────────────────────────────────
|
||
|
||
PASS=0
|
||
FAIL=0
|
||
SKIP=0
|
||
WARN=0
|
||
|
||
# ─── Утилиты ──────────────────────────────────────────────────────
|
||
|
||
log_pass() { echo -e " ${GREEN}✓ PASS${NC} $1"; ((PASS++)) || true; }
|
||
log_fail() { echo -e " ${RED}✗ FAIL${NC} $1"; ((FAIL++)) || true; }
|
||
log_skip() { echo -e " ${YELLOW}○ SKIP${NC} $1"; ((SKIP++)) || true; }
|
||
log_warn() { echo -e " ${YELLOW}⚠ WARN${NC} $1"; ((WARN++)) || true; }
|
||
log_head() { echo -e "\n${CYAN}${BOLD}═══ $1 ═══${NC}"; }
|
||
|
||
# ─── HTTP-запрос с проверкой ──────────────────────────────────────
|
||
#
|
||
# api_test <test_name> <url_params> <required_keys...>
|
||
#
|
||
# Выполняет GET-запрос, проверяет:
|
||
# 1. HTTP 200
|
||
# 2. Content-Type: application/json
|
||
# 3. Ответ — валидный JSON
|
||
# 4. Наличие обязательных ключей (если указаны)
|
||
#
|
||
# Возвращает тело ответа через глобальный RESPONSE.
|
||
|
||
RESPONSE=""
|
||
HTTP_CODE=""
|
||
|
||
api_get() {
|
||
local url="${API_URL}?${AUTH}&${1}"
|
||
local tmp_headers
|
||
tmp_headers=$(mktemp)
|
||
trap "rm -f '$tmp_headers'" RETURN
|
||
|
||
RESPONSE=$(curl -s -w '\n%{http_code}' \
|
||
--max-time "$TIMEOUT" \
|
||
-D "$tmp_headers" \
|
||
"$url" 2>/dev/null) || {
|
||
RESPONSE=""
|
||
HTTP_CODE="000"
|
||
return 1
|
||
}
|
||
|
||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||
RESPONSE=$(echo "$RESPONSE" | sed '$d')
|
||
|
||
return 0
|
||
}
|
||
|
||
api_test() {
|
||
local test_name="$1"
|
||
local params="$2"
|
||
shift 2
|
||
local required_keys=("$@")
|
||
|
||
api_get "$params" || true
|
||
|
||
# HTTP status
|
||
if [[ "$HTTP_CODE" != "200" ]]; then
|
||
log_fail "${test_name} — HTTP ${HTTP_CODE}"
|
||
if [[ "$VERBOSE" == "1" ]] && [[ -n "$RESPONSE" ]]; then
|
||
echo " Response: ${RESPONSE:0:200}"
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
# Valid JSON
|
||
if ! echo "$RESPONSE" | python3 -m json.tool > /dev/null 2>&1; then
|
||
log_fail "${test_name} — invalid JSON"
|
||
if [[ "$VERBOSE" == "1" ]]; then
|
||
echo " Response: ${RESPONSE:0:300}"
|
||
fi
|
||
return 1
|
||
fi
|
||
|
||
# Required keys
|
||
local missing=()
|
||
for key in "${required_keys[@]}"; do
|
||
if [[ -z "$key" ]]; then
|
||
continue
|
||
fi
|
||
if ! echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
data = json.load(sys.stdin)
|
||
keys = '${key}'.split('.')
|
||
obj = data
|
||
for k in keys:
|
||
if isinstance(obj, dict) and k in obj:
|
||
obj = obj[k]
|
||
elif isinstance(obj, list) and len(obj) > 0 and k in obj[0]:
|
||
obj = obj[0][k]
|
||
else:
|
||
sys.exit(1)
|
||
" 2>/dev/null; then
|
||
missing+=("$key")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
log_warn "${test_name} — missing keys: ${missing[*]}"
|
||
if [[ "$VERBOSE" == "1" ]]; then
|
||
echo " Response: ${RESPONSE:0:500}"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
log_pass "$test_name"
|
||
|
||
if [[ "$VERBOSE" == "1" ]]; then
|
||
local len=${#RESPONSE}
|
||
echo " (${len} bytes)"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# Проверить что ответ — JSON-массив
|
||
assert_array() {
|
||
local test_name="$1"
|
||
if echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if isinstance(d,list) else 1)" 2>/dev/null; then
|
||
log_pass "${test_name} — is array"
|
||
else
|
||
log_fail "${test_name} — expected array"
|
||
fi
|
||
}
|
||
|
||
# Проверить что ответ — JSON-объект
|
||
assert_object() {
|
||
local test_name="$1"
|
||
if echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if isinstance(d,dict) else 1)" 2>/dev/null; then
|
||
log_pass "${test_name} — is object"
|
||
else
|
||
log_fail "${test_name} — expected object"
|
||
fi
|
||
}
|
||
|
||
# Получить количество элементов в ответе
|
||
response_count() {
|
||
echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else len(d.keys()))" 2>/dev/null || echo "?"
|
||
}
|
||
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
# ТЕСТЫ
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
|
||
echo -e "${BOLD}Player API Test Suite${NC}"
|
||
echo -e "URL: ${API_URL}"
|
||
echo -e "User: ${USERNAME}"
|
||
echo -e "Date: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo ""
|
||
|
||
# ─── 0. Connectivity ─────────────────────────────────────────────
|
||
|
||
log_head "0. Connectivity"
|
||
|
||
api_get "" || true
|
||
if [[ "$HTTP_CODE" == "000" ]]; then
|
||
echo -e "${RED}Cannot connect to ${API_URL}${NC}"
|
||
echo "Check URL and network connectivity."
|
||
exit 1
|
||
fi
|
||
log_pass "Server reachable (HTTP ${HTTP_CODE})"
|
||
|
||
# ─── 1. Auth — Default Info ──────────────────────────────────────
|
||
|
||
log_head "1. Authentication + Default Info (no action)"
|
||
|
||
api_test "default info" "" \
|
||
"user_info.username" \
|
||
"user_info.status" \
|
||
"user_info.exp_date" \
|
||
"user_info.max_connections" \
|
||
"user_info.allowed_output_formats" \
|
||
"server_info.version" \
|
||
"server_info.url" \
|
||
"server_info.port" \
|
||
"server_info.timestamp_now"
|
||
|
||
# Проверяем username в ответе
|
||
if echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
assert d['user_info']['username'] == '${USERNAME}'
|
||
" 2>/dev/null; then
|
||
log_pass "username matches"
|
||
else
|
||
log_fail "username mismatch in response"
|
||
fi
|
||
|
||
# ─── 2. Auth errors ──────────────────────────────────────────────
|
||
|
||
log_head "2. Auth Error Handling"
|
||
|
||
# Неверные креды
|
||
local_url="${API_URL}?username=__invalid__&password=__invalid__"
|
||
local_resp=$(curl -s -w '\n%{http_code}' --max-time "$TIMEOUT" "$local_url" 2>/dev/null) || true
|
||
local_code=$(echo "$local_resp" | tail -1)
|
||
|
||
if [[ "$local_code" != "200" ]] || echo "$local_resp" | head -1 | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
sys.exit(0 if 'error' in str(d).lower() or d.get('user_info',{}).get('auth') != 1 else 1)
|
||
" 2>/dev/null; then
|
||
log_pass "invalid credentials rejected"
|
||
else
|
||
log_warn "invalid credentials may not be properly rejected"
|
||
fi
|
||
|
||
# Пустые креды
|
||
local_url="${API_URL}?username=&password="
|
||
local_resp=$(curl -s --max-time "$TIMEOUT" "$local_url" 2>/dev/null) || true
|
||
log_pass "empty credentials handled (no crash)"
|
||
|
||
# ─── 3. Categories ───────────────────────────────────────────────
|
||
|
||
log_head "3. Categories"
|
||
|
||
api_test "get_live_categories" "action=get_live_categories" \
|
||
"category_id" "category_name"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_live_categories"
|
||
echo -e " count: $(response_count)"
|
||
fi
|
||
|
||
api_test "get_vod_categories" "action=get_vod_categories" \
|
||
"category_id" "category_name"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_vod_categories"
|
||
echo -e " count: $(response_count)"
|
||
fi
|
||
|
||
api_test "get_series_categories" "action=get_series_categories" \
|
||
"category_id" "category_name"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_series_categories"
|
||
echo -e " count: $(response_count)"
|
||
fi
|
||
|
||
# Числовые алиасы
|
||
api_test "numeric alias 201=get_live_categories" "action=201"
|
||
api_test "numeric alias 200=get_vod_categories" "action=200"
|
||
api_test "numeric alias 206=get_series_categories" "action=206"
|
||
|
||
# ─── 4. Live Streams ─────────────────────────────────────────────
|
||
|
||
log_head "4. Live Streams"
|
||
|
||
api_test "get_live_streams" "action=get_live_streams" \
|
||
"stream_id" "name" "stream_type" "category_id"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_live_streams"
|
||
count=$(response_count)
|
||
echo -e " count: ${count}"
|
||
|
||
# Сохраняем первый stream_id для EPG тестов
|
||
if [[ -z "$STREAM_ID" ]]; then
|
||
STREAM_ID=$(echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
if d: print(d[0]['stream_id'])
|
||
else: print('')
|
||
" 2>/dev/null) || true
|
||
if [[ -n "$STREAM_ID" ]]; then
|
||
echo -e " auto-detected STREAM_ID=${STREAM_ID}"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# С фильтром по категории
|
||
if [[ -n "$CATEGORY_ID" ]]; then
|
||
api_test "get_live_streams (category_id=${CATEGORY_ID})" \
|
||
"action=get_live_streams&category_id=${CATEGORY_ID}" \
|
||
"stream_id"
|
||
else
|
||
log_skip "get_live_streams + category_id (set CATEGORY_ID env)"
|
||
fi
|
||
|
||
# Числовой алиас
|
||
api_test "numeric alias 202=get_live_streams" "action=202"
|
||
|
||
# ─── 5. VOD Streams ──────────────────────────────────────────────
|
||
|
||
log_head "5. VOD Streams"
|
||
|
||
api_test "get_vod_streams" "action=get_vod_streams" \
|
||
"stream_id" "name" "container_extension"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_vod_streams"
|
||
count=$(response_count)
|
||
echo -e " count: ${count}"
|
||
|
||
# Сохраняем первый vod_id
|
||
if [[ -z "$VOD_ID" ]]; then
|
||
VOD_ID=$(echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
if d: print(d[0]['stream_id'])
|
||
else: print('')
|
||
" 2>/dev/null) || true
|
||
if [[ -n "$VOD_ID" ]]; then
|
||
echo -e " auto-detected VOD_ID=${VOD_ID}"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
api_test "numeric alias 203=get_vod_streams" "action=203"
|
||
|
||
# ─── 6. Series ────────────────────────────────────────────────────
|
||
|
||
log_head "6. Series"
|
||
|
||
api_test "get_series" "action=get_series" \
|
||
"series_id" "name" "title"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_series"
|
||
count=$(response_count)
|
||
echo -e " count: ${count}"
|
||
|
||
# Сохраняем первый series_id
|
||
if [[ -z "$SERIES_ID" ]]; then
|
||
SERIES_ID=$(echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
if d: print(d[0]['series_id'])
|
||
else: print('')
|
||
" 2>/dev/null) || true
|
||
if [[ -n "$SERIES_ID" ]]; then
|
||
echo -e " auto-detected SERIES_ID=${SERIES_ID}"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
api_test "numeric alias 208=get_series" "action=208"
|
||
|
||
# ─── 7. Series Info ───────────────────────────────────────────────
|
||
|
||
log_head "7. Series Info"
|
||
|
||
if [[ -n "$SERIES_ID" ]]; then
|
||
api_test "get_series_info (id=${SERIES_ID})" \
|
||
"action=get_series_info&series_id=${SERIES_ID}" \
|
||
"info.name" "seasons"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_object "get_series_info"
|
||
# Проверяем episodes
|
||
has_episodes=$(echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
print('yes' if 'episodes' in d and d['episodes'] else 'no')
|
||
" 2>/dev/null) || true
|
||
if [[ "$has_episodes" == "yes" ]]; then
|
||
log_pass "get_series_info has episodes"
|
||
else
|
||
log_warn "get_series_info — no episodes found"
|
||
fi
|
||
fi
|
||
|
||
api_test "numeric alias 204=get_series_info" \
|
||
"action=204&series_id=${SERIES_ID}"
|
||
else
|
||
log_skip "get_series_info (no SERIES_ID available)"
|
||
log_skip "numeric alias 204 (no SERIES_ID)"
|
||
fi
|
||
|
||
# ─── 8. VOD Info ──────────────────────────────────────────────────
|
||
|
||
log_head "8. VOD Info"
|
||
|
||
if [[ -n "$VOD_ID" ]]; then
|
||
api_test "get_vod_info (id=${VOD_ID})" \
|
||
"action=get_vod_info&vod_id=${VOD_ID}" \
|
||
"info" "movie_data.stream_id"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_object "get_vod_info"
|
||
# Проверяем subtitles
|
||
has_subtitles=$(echo "$RESPONSE" | python3 -c "
|
||
import json, sys
|
||
d = json.load(sys.stdin)
|
||
print('yes' if 'subtitles' in d.get('info', {}) else 'no')
|
||
" 2>/dev/null) || true
|
||
if [[ "$has_subtitles" == "yes" ]]; then
|
||
log_pass "get_vod_info has subtitles field"
|
||
else
|
||
log_warn "get_vod_info — no subtitles field"
|
||
fi
|
||
fi
|
||
|
||
api_test "numeric alias 209=get_vod_info" \
|
||
"action=209&vod_id=${VOD_ID}"
|
||
else
|
||
log_skip "get_vod_info (no VOD_ID available)"
|
||
log_skip "numeric alias 209 (no VOD_ID)"
|
||
fi
|
||
|
||
# ─── 9. EPG ───────────────────────────────────────────────────────
|
||
|
||
log_head "9. EPG"
|
||
|
||
if [[ -n "$STREAM_ID" ]]; then
|
||
api_test "get_epg (stream_id=${STREAM_ID})" \
|
||
"action=get_epg&stream_id=${STREAM_ID}"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_array "get_epg (single)"
|
||
echo -e " entries: $(response_count)"
|
||
fi
|
||
|
||
# EPG from_now
|
||
api_test "get_epg + from_now" \
|
||
"action=get_epg&stream_id=${STREAM_ID}&from_now=1"
|
||
|
||
# Short EPG
|
||
api_test "get_short_epg (stream_id=${STREAM_ID})" \
|
||
"action=get_short_epg&stream_id=${STREAM_ID}" \
|
||
"epg_listings"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_object "get_short_epg"
|
||
fi
|
||
|
||
# Short EPG with limit
|
||
api_test "get_short_epg + limit=2" \
|
||
"action=get_short_epg&stream_id=${STREAM_ID}&limit=2" \
|
||
"epg_listings"
|
||
|
||
# Simple data table
|
||
api_test "get_simple_data_table (stream_id=${STREAM_ID})" \
|
||
"action=get_simple_data_table&stream_id=${STREAM_ID}" \
|
||
"epg_listings"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_object "get_simple_data_table"
|
||
fi
|
||
|
||
# Числовой алиас
|
||
api_test "numeric alias 205=get_short_epg" \
|
||
"action=205&stream_id=${STREAM_ID}"
|
||
api_test "numeric alias 207=get_simple_data_table" \
|
||
"action=207&stream_id=${STREAM_ID}"
|
||
else
|
||
log_skip "get_epg (no STREAM_ID available — set STREAM_ID env)"
|
||
log_skip "get_short_epg (no STREAM_ID)"
|
||
log_skip "get_simple_data_table (no STREAM_ID)"
|
||
log_skip "numeric aliases 205, 207 (no STREAM_ID)"
|
||
fi
|
||
|
||
# ─── 10. Multi-stream EPG ────────────────────────────────────────
|
||
|
||
log_head "10. Multi-stream EPG"
|
||
|
||
if [[ -n "$STREAM_ID" ]]; then
|
||
# Multi через запятую
|
||
api_test "get_epg multi (stream_id=${STREAM_ID},${STREAM_ID})" \
|
||
"action=get_epg&stream_id=${STREAM_ID},${STREAM_ID}"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
assert_object "get_epg multi"
|
||
fi
|
||
|
||
api_test "get_short_epg multi" \
|
||
"action=get_short_epg&stream_id=${STREAM_ID},${STREAM_ID}"
|
||
else
|
||
log_skip "multi-stream EPG (no STREAM_ID)"
|
||
fi
|
||
|
||
# ─── 11. Pagination ──────────────────────────────────────────────
|
||
|
||
log_head "11. Pagination"
|
||
|
||
api_test "get_live_streams offset=0, limit=3" \
|
||
"action=get_live_streams¶ms[offset]=0¶ms[items_per_page]=3"
|
||
if [[ "$HTTP_CODE" == "200" ]]; then
|
||
count=$(response_count)
|
||
if [[ "$count" -le 3 ]] 2>/dev/null; then
|
||
log_pass "pagination limit respected (got ${count})"
|
||
else
|
||
log_warn "pagination: expected ≤3, got ${count}"
|
||
fi
|
||
fi
|
||
|
||
api_test "get_vod_streams offset=0, limit=2" \
|
||
"action=get_vod_streams¶ms[offset]=0¶ms[items_per_page]=2"
|
||
|
||
# ─── 12. Edge Cases ──────────────────────────────────────────────
|
||
|
||
log_head "12. Edge Cases"
|
||
|
||
# Неизвестный action → default info
|
||
api_test "unknown action → default info" \
|
||
"action=nonexistent_action" \
|
||
"user_info" "server_info"
|
||
|
||
# get_vod_info без vod_id → пустой info
|
||
api_test "get_vod_info without vod_id" \
|
||
"action=get_vod_info" \
|
||
"info"
|
||
|
||
# get_series_info без series_id
|
||
api_test "get_series_info without series_id" \
|
||
"action=get_series_info"
|
||
|
||
# get_epg без stream_id → пустой массив
|
||
api_test "get_epg without stream_id" \
|
||
"action=get_epg"
|
||
|
||
# Несуществующий stream_id
|
||
api_test "get_epg invalid stream_id" \
|
||
"action=get_epg&stream_id=999999999"
|
||
|
||
# Несуществующий vod_id
|
||
api_test "get_vod_info invalid vod_id" \
|
||
"action=get_vod_info&vod_id=999999999" \
|
||
"info"
|
||
|
||
# ─── 13. Panel API (legacy) ──────────────────────────────────────
|
||
|
||
log_head "13. Panel API (legacy)"
|
||
|
||
PANEL_URL="${BASE_URL}/panel_api.php?${AUTH}"
|
||
panel_resp=$(curl -s -w '\n%{http_code}' --max-time "$TIMEOUT" "$PANEL_URL" 2>/dev/null) || true
|
||
panel_code=$(echo "$panel_resp" | tail -1)
|
||
|
||
if [[ "$panel_code" == "200" ]]; then
|
||
panel_body=$(echo "$panel_resp" | sed '$d')
|
||
if echo "$panel_body" | python3 -m json.tool > /dev/null 2>&1; then
|
||
log_pass "panel_api.php returns valid JSON (HTTP 200)"
|
||
else
|
||
log_warn "panel_api.php returns HTTP 200 but invalid JSON"
|
||
fi
|
||
else
|
||
log_warn "panel_api.php returned HTTP ${panel_code} (may be disabled)"
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
# РЕЗУЛЬТАТЫ
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
|
||
echo ""
|
||
echo -e "${BOLD}═══════════════════════════════════════════════${NC}"
|
||
echo -e "${BOLD} Results${NC}"
|
||
echo -e "${BOLD}═══════════════════════════════════════════════${NC}"
|
||
echo -e " ${GREEN}PASS:${NC} ${PASS}"
|
||
echo -e " ${RED}FAIL:${NC} ${FAIL}"
|
||
echo -e " ${YELLOW}WARN:${NC} ${WARN}"
|
||
echo -e " ${YELLOW}SKIP:${NC} ${SKIP}"
|
||
TOTAL=$((PASS + FAIL + WARN + SKIP))
|
||
echo -e " ${BOLD}TOTAL: ${TOTAL}${NC}"
|
||
echo ""
|
||
|
||
if [[ "$FAIL" -gt 0 ]]; then
|
||
echo -e "${RED}${BOLD}Some tests FAILED!${NC}"
|
||
exit 1
|
||
else
|
||
echo -e "${GREEN}${BOLD}All tests passed.${NC}"
|
||
exit 0
|
||
fi
|