Files
SupervisorAI/live_catalog/live_catalog_backend.py

237 lines
8.7 KiB
Python

import os
import urllib.parse
import socket
import json
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
# ========== 硬编码的树形数据 ==========
# 节点结构:
# {
# "id": 1,
# "name": "河北省",
# "parent_id": None, # None 表示根节点
# "is_leaf": False,
# "stream_url": None # 叶子节点才会有值
# }
nodes = {
1: {"id": 1, "name": "河北省", "parent_id": None, "is_leaf": False, "stream_url": None},
2: {"id": 2, "name": "河南省", "parent_id": None, "is_leaf": False, "stream_url": None},
3: {"id": 3, "name": "石家庄市", "parent_id": 1, "is_leaf": False, "stream_url": None},
4: {"id": 4, "name": "保定市", "parent_id": 1, "is_leaf": False, "stream_url": None},
5: {"id": 5, "name": "郑州市", "parent_id": 2, "is_leaf": False, "stream_url": None},
6: {"id": 6, "name": "长安区", "parent_id": 3, "is_leaf": True,
"stream_url": "http://localhost:8355/stream.m3u8"},
7: {"id": 7, "name": "桥西区", "parent_id": 3, "is_leaf": True,
"stream_url": "https://example.com/live/qiaoxi.m3u8"},
8: {"id": 8, "name": "竞秀区", "parent_id": 4, "is_leaf": True,
"stream_url": "https://example.com/live/jingxiu.m3u8"},
9: {"id": 9, "name": "莲池区", "parent_id": 4, "is_leaf": True,
"stream_url": "https://example.com/live/lianchi.m3u8"},
10: {"id": 10, "name": "中原区", "parent_id": 5, "is_leaf": True,
"stream_url": "https://example.com/live/zhongyuan.m3u8"},
}
def get_children(parent_id):
"""返回父节点下的直接子节点列表"""
return [node for node in nodes.values() if node["parent_id"] == parent_id]
def get_node(node_id):
"""根据 id 获取节点详情"""
return nodes.get(node_id)
def get_stream_url(node_id):
"""获取叶子节点的视频流地址"""
node = nodes.get(node_id)
if node and node["is_leaf"]:
return node["stream_url"]
return None
# ========== HTTP 处理器 ==========
class APIHandler(SimpleHTTPRequestHandler):
# 设置超时
timeout = 30
# MIME 类型映射
MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.mp3': 'audio/mpeg',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.txt': 'text/plain; charset=utf-8',
}
def log_message(self, format, *args):
print(f"[{self.log_date_time_string()}] {self.address_string()} - {format % args}")
def send_json_response(self, data, status=200):
"""统一返回 JSON 格式"""
self.send_response(status)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
def send_error_json(self, message, status=400):
"""返回错误 JSON"""
self.send_json_response({"error": message}, status)
def do_GET(self):
try:
parsed_path = urllib.parse.urlparse(self.path)
path = parsed_path.path
query = parsed_path.query
# 新 API 路由 (RESTful)
if path.startswith('/api/roots'):
# GET /api/roots
roots = get_children(None)
self.send_json_response(roots)
return
elif path.startswith('/api/children/'):
# GET /api/children/3
try:
node_id = int(path.split('/')[-1])
except (ValueError, IndexError):
self.send_error_json("Invalid node id", 400)
return
children = get_children(node_id)
self.send_json_response(children)
return
elif path.startswith('/api/node/'):
# GET /api/node/3
try:
node_id = int(path.split('/')[-1])
except (ValueError, IndexError):
self.send_error_json("Invalid node id", 400)
return
node = get_node(node_id)
if node is None:
self.send_error_json("Node not found", 404)
return
self.send_json_response(node)
return
elif path.startswith('/api/stream/'):
# GET /api/stream/6
try:
node_id = int(path.split('/')[-1])
except (ValueError, IndexError):
self.send_error_json("Invalid node id", 400)
return
url = get_stream_url(node_id)
if url is None:
self.send_error_json("Stream not found or node is not a leaf", 404)
return
self.send_json_response({"url": url})
return
# 静态文件服务(与原逻辑一致)
if path == '/' or path == '/index.html':
self.serve_file('index.html', query='api=1')
return
else:
filename = path.lstrip('/')
if os.path.exists(filename):
self.serve_static_file(filename)
else:
self.send_error(404, 'Not Found')
return
except Exception as e:
print(f"Error handling request: {e}")
self.send_error(500, 'Internal Server Error')
def serve_file(self, filename, query=None):
"""与原代码相同的文件服务(保留原逻辑)"""
try:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
if os.path.exists(file_path):
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
with open(file_path, 'rb') as f:
content = f.read()
if query:
try:
content = content.decode('utf-8')
# 替换原有 js 中的 apiParam 赋值语句(兼容旧前端)
content = content.replace(
"const apiParam = urlParams.get('api') || '1';",
f"const apiParam = '{query.split('=')[1]}';"
)
content = content.encode('utf-8')
except Exception as e:
print(f"Error modifying HTML content: {e}")
self.wfile.write(content)
else:
self.send_error(404, f'{filename} not found')
except Exception as e:
print(f"Error serving file: {e}")
raise
def serve_static_file(self, filename):
"""与原代码相同的静态文件服务"""
try:
ext = os.path.splitext(filename)[1].lower()
content_type = self.MIME_TYPES.get(ext, 'application/octet-stream')
self.send_response(200)
self.send_header('Content-type', content_type)
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
with open(filename, 'rb') as f:
self.wfile.write(f.read())
except Exception as e:
print(f"Error serving static file: {e}")
self.send_error(500, 'Internal Server Error')
def check_port_available(port):
"""检查端口是否可用"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(('', port))
return True
except socket.error:
return False
def run():
port = 18369
if not check_port_available(port):
print(f"错误: 端口 {port} 已被占用")
return
server_address = ('', port)
httpd = ThreadingHTTPServer(server_address, APIHandler)
print(f'Server running on http://localhost:{port}')
print('API endpoints:')
print(' GET /api/roots - 获取所有根节点')
print(' GET /api/children/<id> - 获取指定节点的子节点')
print(' GET /api/node/<id> - 获取节点详情')
print(' GET /api/stream/<id> - 获取视频流地址')
print('静态文件服务: 访问 / 或 /index.html')
print('按 Ctrl+C 停止服务器')
try:
httpd.serve_forever()
except KeyboardInterrupt:
print('\n服务器已停止')
httpd.server_close()
if __name__ == '__main__':
run()