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