wip7
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
//go:embed static
|
||||
var staticFS 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
|
||||
|
||||
// staticFileServer раздаёт встроенные статические файлы (JS, CSS)
|
||||
var staticFileServer 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))
|
||||
|
||||
staticSub, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
panic("cannot create sub-FS for static: " + err.Error())
|
||||
}
|
||||
staticFileServer = http.FileServer(http.FS(staticSub))
|
||||
}
|
||||
// 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": "Текст ошибки"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user