This commit is contained in:
DengDai
2025-12-08 14:45:14 +08:00
commit 519589f8f5
60 changed files with 8191 additions and 0 deletions

View File

41
backend/routes/actions.py Normal file
View File

@@ -0,0 +1,41 @@
from flask import Blueprint, jsonify, request
from config import config_manager
from utils.placeholders import ServicePlaceholder, long_running_task
actions_bp = Blueprint('actions', __name__, url_prefix='/api')
@actions_bp.route('/downloader/add', methods=['POST'])
def add_download_task():
# 逻辑: 根据配置,将下载链接添加到 qBittorrent 或 Transmission
# 返回格式: {"message": "任务添加成功"}
data = request.get_json()
if not data or 'download_url' not in data:
return jsonify({"error": "缺少 download_url"}), 400
downloader_type = config_manager.get('downloader')
config = config_manager.get(downloader_type)
manager = ServicePlaceholder(config)
success, message = manager.add_task(data['download_url'])
if success:
return jsonify({"message": f"任务已成功添加至 {downloader_type}"})
else:
return jsonify({"error": message}), 500
@actions_bp.route('/scrape', methods=['POST'])
def scrape_media():
# 逻辑: 接收媒体路径,在后台线程中启动刮削任务
# 返回格式 (202 Accepted): {"message": "刮削任务已加入队列..."}
data = request.get_json()
media_path = data.get('media_path')
if not media_path:
return jsonify({"error": "缺少 media_path"}), 400
douban_config = config_manager.get('douban')
manager = ServicePlaceholder(douban_config)
# 在后台线程中执行刮削
long_running_task(manager.scrape, media_path)
# 立即返回 202 Accepted 响应
return jsonify({"message": "刮削任务已加入队列,请稍后刷新查看结果"}), 202

View File

@@ -0,0 +1,53 @@
from flask import Blueprint, jsonify
from config import config_manager
from utils.placeholders import ServicePlaceholder
from utils.plex_utils import PlexManager
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/api/dashboard')
@dashboard_bp.route('/initial_check', methods=['GET'])
def initial_check():
# 逻辑: 检查配置文件中是否至少有一个关键项被配置例如ptskit的URL
# 返回格式: {"is_configured": true | false}
is_configured = bool(config_manager.get('ptskit', {}).get('url'))
return jsonify({"is_configured": is_configured})
@dashboard_bp.route('/stats', methods=['GET'])
def get_stats():
# 逻辑: 分别连接 qBittorrent, Transmission, Emby 等服务获取它们的项目总数
# 返回格式: {"local_skits": int, "fnos_items": int, ...}
qb_manager = ServicePlaceholder(config_manager.get('qbittorrent'))
plex_config = config_manager.get('plex')
plex_manager = PlexManager(plex_config)
plex_items = plex_manager.get_stats()
return jsonify({
"local_skits": 15,
"fnos_items": 12,
"emby_items": 12,
"plex_items": plex_items,
"qb_total": qb_manager.get_stats(),
"tr_total": 4
})
@dashboard_bp.route('/component_statuses', methods=['GET'])
def get_component_statuses():
# 逻辑: 对每个配置的服务进行一次连接测试,并返回其状态
# 返回格式: {"qbittorrent": {"status": "success" | "error" | "unconfigured", "message": str}, ...}
plex_config = config_manager.get('plex')
plex_status = {"status": "unconfigured", "message": "未配置"}
if plex_config and plex_config.get('host'):
plex_manager = PlexManager(plex_config)
success, message = plex_manager.test_connection()
plex_status = {"status": "success" if success else "error", "message": message}
return jsonify({
"douban_cookie": {"status": "success", "message": "Cookie已配置"},
"ptskit_cookie": {"status": "success", "message": "Cookie已配置"},
"local_path": {"status": "success", "message": "已挂载"},
"qbittorrent": {"status": "success", "message": "连接成功 v4.5.2"},
"transmission": {"status": "error", "message": "连接失败: 认证错误"},
"fnos": {"status": "unconfigured", "message": "未配置"},
"emby": {"status": "success", "message": "连接成功"},
"plex": plex_status
})

102
backend/routes/media.py Normal file
View File

@@ -0,0 +1,102 @@
from flask import Blueprint, jsonify, request
from config import config_manager
from utils.plex_utils import PlexManager
from plexapi.exceptions import Unauthorized, NotFound
from services.cache_service import CacheManager
from utils.pagination import paginate
media_bp = Blueprint('media', __name__, url_prefix='/api')
@media_bp.route('/ptskit/torrents', methods=['GET'])
def get_ptskit_torrents():
# 逻辑: 访问 PTSKIT 网站,解析种子列表页,支持分页和搜索
# 返回格式: {"total": int, "list": [{"id": str, "title": str, ...}]}
page = request.args.get('page', 1, type=int)
page_size = request.args.get('pageSize', 20, type=int)
keyword = request.args.get('keyword', '')
mock_list = [{
"id": f"torrent-{i + (page-1)*page_size}",
"title": f"[PTSKIT] 搜索'{keyword}'的结果 {i+1}",
"size": f"{i+1}.5 GB",
"seeders": 50 - i, "leechers": i,
"download_url": f"http://example.com/download.php?id={i}"
} for i in range(page_size)]
return jsonify({"total": 123, "list": mock_list})
@media_bp.route('/local-media', methods=['GET'])
def get_local_media():
# 逻辑: 扫描配置中的 skit_paths 目录,列出所有短剧文件夹
# 返回格式: {"total": int, "list": [{"id": str, "title": str, "path": str, ...}]}
page = request.args.get('page', 1, type=int)
page_size = request.args.get('pageSize', 20, type=int)
mock_list = [{
"id": f"local-{i + (page-1)*page_size}",
"title": f"本地短剧-{i+1}",
"path": f"/skits/本地短剧-{i+1}",
"poster": f"https://picsum.photos/seed/{i}/200/300",
"scraped": i % 2 == 0
} for i in range(page_size)]
return jsonify({"total": 88, "list": mock_list})
@media_bp.route('/<server_type>/library', methods=['GET'])
def get_media_server_library(server_type):
# 逻辑: 根据 server_type (emby, plex, fnos) 连接对应服务,获取媒体库
# 返回格式: {"total": int, "list": [{"id": str, "title": str, ...}]}
# if server_type not in ['emby', 'plex', 'fnos']:
# return jsonify({"error": "不支持的媒体服务器类型"}), 404
if server_type == 'plex':
try:
plex_config = config_manager.get('plex')
manager = PlexManager(plex_config)
libraries = manager.get_libraries()
return jsonify({"total": len(libraries), "list": libraries})
except ValueError as e: # 通常是未配置错误
return jsonify({"error": str(e)}), 400
except Unauthorized as e:
return jsonify({"error": f"Plex 认证失败: {e}"}), 401
except Exception as e:
return jsonify({"error": f"连接 Plex 时发生错误: {e}"}), 503
elif server_type in ['emby', 'fnos']:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('pageSize', 20, type=int)
mock_list = [{
"id": f"{server_type}-{i + (page-1)*page_size}",
"title": f"[{server_type.upper()}] 服务器上的短剧 {i+1}",
"year": 2023,
"poster": f"https://picsum.photos/seed/{server_type}{i}/200/300"
} for i in range(page_size)]
return jsonify({"total": 66, "list": mock_list})
return jsonify({"error": "不支持的媒体服务器类型"}), 404
@media_bp.route('/plex/libraries/<library_id>/items', methods=['GET'])
def get_plex_library_items(library_id):
page = request.args.get('page', 1, type=int)
page_size = request.args.get('pageSize', 20, type=int)
try:
# 1. 初始化缓存管理器,为每个库使用单独的缓存文件
# TTL 设置为 3660 秒1小时+1分钟确保比定时任务间隔稍长
cache_mgr = CacheManager(f"plex_lib_{library_id}.json", ttl_seconds=3660)
# 2. 初始化 Plex 管理器
plex_config = config_manager.get('plex')
manager = PlexManager(plex_config)
# 3. 使用 cache_mgr.get_data 获取数据
# 它会自动处理:读缓存,或在缓存失效时调用 manager.get_library_items
all_items = cache_mgr.get_data(manager.get_library_items, library_id=library_id)
# 4. 使用分页工具进行分页
paginated_data = paginate(all_items, page, page_size)
return jsonify(paginated_data)
except ValueError as e: # 通常是配置错误
return jsonify({"error": str(e)}), 400
except NotFound: # get_library_items 可能会在强制刷新时抛出
return jsonify({"error": f"ID为 '{library_id}' 的 Plex 媒体库不存在"}), 4404
except Unauthorized as e:
return jsonify({"error": f"Plex 认证失败: {e}"}), 401
except Exception as e:
return jsonify({"error": f"获取 Plex 项目时发生内部错误: {e}"}), 500

61
backend/routes/system.py Normal file
View File

@@ -0,0 +1,61 @@
from flask import Blueprint, jsonify, request
from config import config_manager
from utils.plex_utils import PlexManager
from utils.placeholders import ServicePlaceholder
system_bp = Blueprint('system', __name__, url_prefix='/api/system')
MANAGER_MAP = {
'plex': PlexManager,
# TODO: 当你实现其他 Manager 后,在这里添加它们
# 'qbittorrent': QBManager,
# 'transmission': TransmissionManager,
# 'emby': EmbyManager,
# 'fnos': FnosManager,
}
@system_bp.route('/config', methods=['GET'])
def get_system_config():
"""获取系统配置(屏蔽敏感信息)"""
return jsonify(config_manager.get_config(sensitive=False))
@system_bp.route('/config', methods=['POST'])
def save_system_config():
"""保存系统配置"""
new_config = request.get_json()
if not isinstance(new_config, dict):
return jsonify({"error": "无效的JSON格式"}), 400
success, error = config_manager.save_config(new_config)
if success:
return jsonify({"message": "配置保存成功!"})
else:
return jsonify({"error": f"保存失败: {error}"}), 500
@system_bp.route('/test_connection', methods=['POST'])
def test_connection():
"""动态测试指定服务的连接"""
data = request.get_json()
service_type = data.get('type')
service_config = data.get('config')
if not service_type or not service_config:
return jsonify({"error": "请求参数不完整"}), 400
# 3. 动态选择 Manager
# 从 MANAGER_MAP 中获取对应的类。
# 如果 service_type 不在映射中(例如 'emby' 还没实现),则默认使用 ServicePlaceholder
ManagerClass = MANAGER_MAP.get(service_type, ServicePlaceholder)
# 使用从前端实时传来的配置实例化 Manager
manager_instance = ManagerClass(service_config)
try:
# 调用实例的 test_connection 方法
# 我们新的 Manager 返回 (bool, str),正好符合这里的逻辑
success, message = manager_instance.test_connection()
return jsonify({"success": success, "message": message})
except Exception as e:
# 添加一个通用异常捕获,防止 Manager 内部的意外错误导致服务器崩溃
return jsonify({
"success": False,
"message": f"测试时发生内部错误: {str(e)}"
}), 500