433 lines
18 KiB
Go
433 lines
18 KiB
Go
/*
|
|
* Copyright (c) 2025-2026, Антон Аксенов
|
|
* This file is part of iptvc project
|
|
* MIT License: https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE
|
|
*/
|
|
|
|
package web
|
|
|
|
import (
|
|
"axenov/iptv-checker/app"
|
|
"embed"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"net/http"
|
|
)
|
|
|
|
//go:embed swagger
|
|
var swaggerFS embed.FS
|
|
|
|
// handleOpenAPISpec — отдаёт динамически сгенерированную OpenAPI 3.0 спецификацию
|
|
func (s *Server) handleOpenAPISpec(w http.ResponseWriter, r *http.Request) {
|
|
spec := s.generateOpenAPISpec()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(spec)
|
|
}
|
|
|
|
// swaggerFileServer раздаёт встроенные файлы Swagger UI (CSS, JS, HTML)
|
|
var swaggerFileServer http.Handler
|
|
|
|
func init() {
|
|
sub, err := fs.Sub(swaggerFS, "swagger")
|
|
if err != nil {
|
|
panic("cannot create sub-FS for swagger: " + err.Error())
|
|
}
|
|
swaggerFileServer = http.FileServer(http.FS(sub))
|
|
}
|
|
// generateOpenAPISpec строит OpenAPI 3.0.3 спецификацию динамически
|
|
func (s *Server) generateOpenAPISpec() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"openapi": "3.0.3",
|
|
"info": map[string]interface{}{
|
|
"title": s.cfg.Site.Header.Title + " API",
|
|
"version": app.VERSION,
|
|
"description": "API для проверки IPTV-плейлистов и доступности каналов.",
|
|
"license": map[string]interface{}{
|
|
"name": "MIT",
|
|
"url": "https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE",
|
|
},
|
|
},
|
|
"servers": []map[string]interface{}{
|
|
{
|
|
"url": s.cfg.Site.BaseUrl,
|
|
"description": "Текущий сервер",
|
|
},
|
|
},
|
|
"paths": map[string]interface{}{
|
|
"/api/playlists": map[string]interface{}{
|
|
"get": map[string]interface{}{
|
|
"tags": []string{"Playlists"},
|
|
"summary": "Список плейлистов",
|
|
"description": "Возвращает список всех плейлистов из ini-файла с результатами проверки.",
|
|
"responses": map[string]interface{}{
|
|
"200": map[string]interface{}{
|
|
"description": "Список плейлистов",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/PlaylistListResponse"},
|
|
},
|
|
},
|
|
},
|
|
"500": map[string]interface{}{
|
|
"description": "Ошибка загрузки плейлистов",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ErrorResponse"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/playlists/{code}": map[string]interface{}{
|
|
"get": map[string]interface{}{
|
|
"tags": []string{"Playlists"},
|
|
"summary": "Информация о плейлисте",
|
|
"description": "Возвращает информацию о плейлисте по его коду, включая группы, атрибуты и результаты проверки.",
|
|
"parameters": []map[string]interface{}{
|
|
{
|
|
"name": "code",
|
|
"in": "path",
|
|
"required": true,
|
|
"description": "Код плейлиста (из ini-файла)",
|
|
"schema": map[string]interface{}{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]interface{}{
|
|
"200": map[string]interface{}{
|
|
"description": "Информация о плейлисте",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/PlaylistResponse"},
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]interface{}{
|
|
"description": "Код плейлиста не передан",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ErrorResponse"},
|
|
},
|
|
},
|
|
},
|
|
"404": map[string]interface{}{
|
|
"description": "Плейлист не найден",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ErrorResponse"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/playlists/{code}/channels": map[string]interface{}{
|
|
"get": map[string]interface{}{
|
|
"tags": []string{"Playlists"},
|
|
"summary": "Каналы плейлиста",
|
|
"description": "Возвращает каналы плейлиста по его коду.",
|
|
"parameters": []map[string]interface{}{
|
|
{
|
|
"name": "code",
|
|
"in": "path",
|
|
"required": true,
|
|
"description": "Код плейлиста (из ini-файла)",
|
|
"schema": map[string]interface{}{"type": "string"},
|
|
},
|
|
},
|
|
"responses": map[string]interface{}{
|
|
"200": map[string]interface{}{
|
|
"description": "Каналы плейлиста",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ChannelListResponse"},
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]interface{}{
|
|
"description": "Код плейлиста не передан",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ErrorResponse"},
|
|
},
|
|
},
|
|
},
|
|
"404": map[string]interface{}{
|
|
"description": "Плейлист не найден",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ErrorResponse"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/version": map[string]interface{}{
|
|
"get": map[string]interface{}{
|
|
"tags": []string{"System"},
|
|
"summary": "Версии компонентов",
|
|
"description": "Возвращает версию iptvc и (при наличии) версию KeyDB/Redis.",
|
|
"responses": map[string]interface{}{
|
|
"200": map[string]interface{}{
|
|
"description": "Информация о версиях",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/VersionResponse"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/health": map[string]interface{}{
|
|
"get": map[string]interface{}{
|
|
"tags": []string{"System"},
|
|
"summary": "Состояние сервиса",
|
|
"description": "Возвращает состояние подключения к KeyDB/Redis и путь к ini-файлу.",
|
|
"responses": map[string]interface{}{
|
|
"200": map[string]interface{}{
|
|
"description": "Состояние сервиса",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/HealthResponse"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"/api/stats": map[string]interface{}{
|
|
"get": map[string]interface{}{
|
|
"tags": []string{"Statistics"},
|
|
"summary": "Статистика",
|
|
"description": "Возвращает агрегированную статистику по плейлистам и каналам.",
|
|
"responses": map[string]interface{}{
|
|
"200": map[string]interface{}{
|
|
"description": "Статистика",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/StatsResponse"},
|
|
},
|
|
},
|
|
},
|
|
"500": map[string]interface{}{
|
|
"description": "Ошибка загрузки плейлистов",
|
|
"content": map[string]interface{}{
|
|
"application/json": map[string]interface{}{
|
|
"schema": map[string]interface{}{"$ref": "#/components/schemas/ErrorResponse"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"components": map[string]interface{}{
|
|
"schemas": map[string]interface{}{
|
|
"PlaylistListResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Список плейлистов",
|
|
"properties": map[string]interface{}{
|
|
"timestamp": map[string]interface{}{"type": "integer", "description": "UNIX timestamp ответа"},
|
|
"items": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"$ref": "#/components/schemas/Playlist"},
|
|
"description": "Массив плейлистов",
|
|
},
|
|
},
|
|
},
|
|
"PlaylistResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Информация о плейлисте с результатами проверки",
|
|
"allOf": []map[string]interface{}{
|
|
{"$ref": "#/components/schemas/Playlist"},
|
|
},
|
|
},
|
|
"Playlist": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Информация о плейлисте",
|
|
"properties": map[string]interface{}{
|
|
"code": map[string]interface{}{"type": "string", "description": "Код плейлиста"},
|
|
"name": map[string]interface{}{"type": "string", "description": "Название плейлиста"},
|
|
"description": map[string]interface{}{"type": "string", "description": "Описание плейлиста"},
|
|
"url": map[string]interface{}{"type": "string", "description": "URL плейлиста"},
|
|
"source": map[string]interface{}{"type": "string", "description": "Источник плейлиста"},
|
|
"isOnline": map[string]interface{}{
|
|
"type": "boolean",
|
|
"nullable": true,
|
|
"description": "Доступность плейлиста (null если не проверялся)",
|
|
},
|
|
"onlineCount": map[string]interface{}{"type": "integer", "description": "Количество рабочих каналов"},
|
|
"offlineCount": map[string]interface{}{"type": "integer", "description": "Количество нерабочих каналов"},
|
|
"checkedAt": map[string]interface{}{"type": "integer", "description": "UNIX timestamp последней проверки"},
|
|
"content": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Содержимое плейлиста (только для оффлайн-плейлистов)",
|
|
},
|
|
"attributes": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"$ref": "#/components/schemas/Attribute"},
|
|
"description": "Атрибуты тега #EXTM3U",
|
|
},
|
|
"groups": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"$ref": "#/components/schemas/Group"},
|
|
"description": "Группы каналов",
|
|
},
|
|
"tags": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"type": "string"},
|
|
"description": "Теги плейлиста",
|
|
},
|
|
},
|
|
},
|
|
"ChannelListResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Список каналов плейлиста",
|
|
"properties": map[string]interface{}{
|
|
"timestamp": map[string]interface{}{"type": "integer", "description": "UNIX timestamp ответа"},
|
|
"items": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"$ref": "#/components/schemas/Channel"},
|
|
"description": "Массив каналов",
|
|
},
|
|
},
|
|
},
|
|
"Channel": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Информация о канале и статусе его проверки",
|
|
"properties": map[string]interface{}{
|
|
"id": map[string]interface{}{"type": "string", "description": "MD5-хэш URL канала"},
|
|
"title": map[string]interface{}{"type": "string", "description": "Название канала"},
|
|
"url": map[string]interface{}{"type": "string", "description": "URL потока канала"},
|
|
"groupId": map[string]interface{}{"type": "string", "description": "MD5-хэш названия группы"},
|
|
"hasToken": map[string]interface{}{"type": "boolean", "description": "Признак наличия токена/ключа в URL"},
|
|
"attributes": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"$ref": "#/components/schemas/Attribute"},
|
|
"description": "Атрибуты тега #EXTINF",
|
|
},
|
|
"status": map[string]interface{}{"type": "integer", "description": "HTTP-код статуса"},
|
|
"isOnline": map[string]interface{}{"type": "boolean", "description": "Доступность канала (status < 400)"},
|
|
"error": map[string]interface{}{"type": "string", "description": "Текст ошибки"},
|
|
"contentType": map[string]interface{}{"type": "string", "description": "MIME-тип ответа"},
|
|
"tags": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"type": "string"},
|
|
"description": "Теги канала",
|
|
},
|
|
"checkedAt": map[string]interface{}{"type": "integer", "description": "UNIX timestamp проверки"},
|
|
},
|
|
},
|
|
"Group": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Группа каналов",
|
|
"properties": map[string]interface{}{
|
|
"id": map[string]interface{}{"type": "string", "description": "MD5-хэш названия группы"},
|
|
"name": map[string]interface{}{"type": "string", "description": "Название группы"},
|
|
"attributes": map[string]interface{}{
|
|
"type": "array",
|
|
"items": map[string]interface{}{"$ref": "#/components/schemas/Attribute"},
|
|
"description": "Атрибуты тега #EXTGRP",
|
|
},
|
|
},
|
|
},
|
|
"Attribute": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Именованный атрибут тега",
|
|
"properties": map[string]interface{}{
|
|
"name": map[string]interface{}{"type": "string", "description": "Имя атрибута"},
|
|
"value": map[string]interface{}{"type": "string", "description": "Значение атрибута"},
|
|
},
|
|
},
|
|
"VersionResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Информация о версиях компонентов",
|
|
"properties": map[string]interface{}{
|
|
"timestamp": map[string]interface{}{"type": "integer", "description": "UNIX timestamp ответа"},
|
|
"iptvc": map[string]interface{}{"type": "string", "description": "Версия iptvc"},
|
|
"keydb": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Версия KeyDB/Redis (если подключён)",
|
|
},
|
|
},
|
|
},
|
|
"HealthResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Состояние сервиса",
|
|
"properties": map[string]interface{}{
|
|
"timestamp": map[string]interface{}{"type": "integer", "description": "UNIX timestamp ответа"},
|
|
"redis": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Состояние подключения к KeyDB/Redis",
|
|
"properties": map[string]interface{}{
|
|
"isConnected": map[string]interface{}{"type": "boolean", "description": "Подключение установлено"},
|
|
},
|
|
},
|
|
"iniFile": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Информация об ini-файле",
|
|
"properties": map[string]interface{}{
|
|
"path": map[string]interface{}{"type": "string", "description": "Путь к playlists.ini"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"StatsResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Агрегированная статистика",
|
|
"properties": map[string]interface{}{
|
|
"timestamp": map[string]interface{}{"type": "integer", "description": "UNIX timestamp ответа"},
|
|
"playlists": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Статистика по плейлистам",
|
|
"properties": map[string]interface{}{
|
|
"all": map[string]interface{}{"type": "integer"},
|
|
"online": map[string]interface{}{"type": "integer"},
|
|
"offline": map[string]interface{}{"type": "integer"},
|
|
"unknown": map[string]interface{}{"type": "integer"},
|
|
"adult": map[string]interface{}{"type": "integer"},
|
|
"hasCatchup": map[string]interface{}{"type": "integer"},
|
|
"hasTvg": map[string]interface{}{"type": "integer"},
|
|
"groupped": map[string]interface{}{"type": "integer"},
|
|
"latest": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"code": map[string]interface{}{"type": "string"},
|
|
"time": map[string]interface{}{"type": "integer"},
|
|
"timeFmt": map[string]interface{}{"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"channels": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Статистика по каналам",
|
|
"properties": map[string]interface{}{
|
|
"all": map[string]interface{}{"type": "integer"},
|
|
"online": map[string]interface{}{"type": "integer"},
|
|
"offline": map[string]interface{}{"type": "integer"},
|
|
"adult": map[string]interface{}{"type": "integer"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"ErrorResponse": map[string]interface{}{
|
|
"type": "object",
|
|
"description": "Ошибка",
|
|
"properties": map[string]interface{}{
|
|
"timestamp": map[string]interface{}{"type": "integer", "description": "UNIX timestamp ответа"},
|
|
"error": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"message": map[string]interface{}{"type": "string", "description": "Текст ошибки"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|