Files
iptvc/app/web/openapi.go
T
2026-07-19 19:14:55 +08:00

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": "Текст ошибки"},
},
},
},
},
},
},
}
}