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

View File

@@ -0,0 +1,54 @@
import requests
import time
import os
class DoubanManager:
def __init__(self, config):
self.cookie = config.get('cookie', '')
def test_connection(self):
if not self.cookie:
return False, "未配置 Cookie"
# 简单测试,可以访问一个需要登录的页面
headers = {'Cookie': self.cookie}
try:
res = requests.get('https://www.douban.com/mine/', headers=headers, timeout=10)
if '登录' in res.text:
return False, "Cookie 已失效"
return True, "Cookie 有效"
except Exception as e:
return False, f"网络请求失败: {e}"
def scrape(self, media_path):
"""
模拟耗时的刮削任务
:param media_path: 要刮削的媒体文件夹路径
"""
print(f"开始刮削: {media_path}")
# 1. 提取剧名
series_name = os.path.basename(media_path)
print(f"提取剧名: {series_name}")
# 2. 在豆瓣搜索 (模拟)
time.sleep(5)
print("模拟豆瓣搜索中...")
# 3. 获取信息并写入 NFO 和海报 (模拟)
time.sleep(5)
nfo_path = os.path.join(media_path, 'movie.nfo')
poster_path = os.path.join(media_path, 'poster.jpg')
with open(nfo_path, 'w', encoding='utf-8') as f:
f.write(f"<movie><title>{series_name}</title><plot>这是一个模拟刮削的剧情简介。</plot></movie>")
# 模拟下载海报
try:
# 使用一个占位图
img_data = requests.get("https://via.placeholder.com/300x450.png?text=Poster").content
with open(poster_path, 'wb') as handler:
handler.write(img_data)
except Exception as e:
print(f"下载海报失败: {e}")
print(f"刮削完成: {media_path}")

View File

View File

@@ -0,0 +1,36 @@
# skit_panel_backend/utils/pagination.py
def paginate(data_list: list, page: int, page_size: int):
"""
一个简单的列表分页工具。
:param data_list: 要分页的完整列表。
:param page: 当前页码 (从1开始)。
:param page_size: 每页的项目数。
:return: 一个包含分页后数据和总数的字典。
"""
if not isinstance(data_list, list):
# 如果输入不是列表,返回空结果
return {"total": 0, "list": []}
total = len(data_list)
# 确保页码和页面大小是正整数
page = max(1, page)
page_size = max(1, page_size)
start = (page - 1) * page_size
# 防止切片索引越界
if start >= total:
return {"total": total, "list": []}
end = start + page_size
paginated_list = data_list[start:end]
return {
"total": total,
"list": paginated_list
}

View File

@@ -0,0 +1,45 @@
import time
import threading
"""
这个文件包含了所有与外部服务交互的模拟类。
在实际开发中,您应该将每个类移动到自己的文件中(如 qbittorrent_utils.py
并实现与真实服务的交互逻辑。
"""
def long_running_task(target, *args, **kwargs):
"""一个辅助函数,用于在后台线程中运行耗时任务"""
thread = threading.Thread(target=target, args=args, kwargs=kwargs)
thread.daemon = True
thread.start()
class ServicePlaceholder:
def __init__(self, config):
self.config = config
def test_connection(self):
# 模拟:检查配置是否存在
if self.config and all(self.config.values()):
return True, "模拟连接成功"
return False, "模拟连接失败:配置不完整"
def get_stats(self):
# 模拟返回一个统计数字
return 42
def get_library(self, page=1, page_size=20, keyword=None):
# 模拟返回一个分页的媒体库列表
return {
"total": 50,
"list": [{"id": f"item-{i}", "title": f"服务器上的短剧 {i}", "year": 2024} for i in range(page_size)]
}
def add_task(self, url):
# 模拟添加任务
return True, "模拟:任务添加成功"
def scrape(self, path):
# 模拟一个耗时的刮削过程
print(f"[后台任务] 开始刮削: {path}")
time.sleep(10)
print(f"[后台任务] 刮削完成: {path}")

141
backend/utils/plex_utils.py Normal file
View File

@@ -0,0 +1,141 @@
import logging
from plexapi.server import PlexServer
from plexapi.exceptions import Unauthorized, NotFound
log = logging.getLogger(__name__)
class PlexManager:
"""
用于与 Plex Media Server 交互的工具类。
- 适配项目 config.json 格式。
- 在发生错误时抛出异常,而不是返回错误字典。
- 成功时直接返回数据。
"""
def __init__(self, config):
"""
根据配置初始化 PlexManager。
:param config: 来自 config.json 的 'plex' 部分。
"""
self.config = config
self.token = config.get('token')
host = config.get('host')
port = config.get('port')
use_ssl = config.get('use_ssl', False)
if not all([host, port, self.token]):
self.baseurl = None
else:
scheme = 'https' if use_ssl else 'http'
self.baseurl = f"{scheme}://{host}:{port}"
self.server = None
log.debug(f"PlexManager initialized for URL: {self.baseurl}")
def _connect(self):
"""
连接到 Plex 服务器。如果连接已建立则直接返回。
如果失败,则会抛出异常。
"""
if self.server:
return
if not self.baseurl:
raise ValueError("Plex 未配置 (host, port, token)")
try:
log.debug(f"尝试连接到 Plex: {self.baseurl}")
# plexapi 内部有重试机制,但我们可以设置一个合理的超时
self.server = PlexServer(self.baseurl, self.token, timeout=10)
# 访问一个属性来验证连接
_ = self.server.friendlyName
log.info(f"成功连接到 Plex 服务器: {self.server.friendlyName}")
except Unauthorized:
log.error("Plex 连接失败: Token 无效或权限不足")
raise # 将原始异常重新抛出
except Exception as e:
log.error(f"Plex 连接失败: {e}", exc_info=True)
self.server = None # 连接失败时重置 server 对象
raise # 重新抛出,让调用者处理
def test_connection(self):
"""
测试与 Plex 的连接,并返回一个易于前端展示的状态元组。
:return: (bool, str) -> (是否成功, 消息)
"""
try:
self._connect()
message = f"连接成功: {self.server.friendlyName} (v{self.server.version})"
return True, message
except Exception as e:
return False, f"连接失败: {e}"
def get_stats(self):
"""获取所有媒体库的项目总数,用于仪表盘。"""
try:
self._connect()
return len(self.server.library.sections())
except Exception:
return 0 # 如果连接失败返回0
def get_libraries(self):
"""
获取媒体库列表。
:return: list of dicts, 每个字典代表一个媒体库
"""
self._connect()
libraries = []
log.debug("正在获取 Plex 媒体库列表")
for section in self.server.library.sections():
libraries.append({
'id': section.key,
'name': section.title,
'type': section.type,
'path': ', '.join(section.locations)
})
log.debug(f"成功获取 {len(libraries)} 个 Plex 媒体库")
return libraries
def get_library_items(self, library_id):
"""
获取指定媒体库中的所有项目。
此方法会抛出 NotFound 或 ValueError 异常,需要调用者捕获。
:param library_id: 媒体库的 ID (key)
:return: list of dicts, 每个字典代表一个媒体项目
"""
self._connect()
log.debug(f"正在获取 Plex 媒体库 '{library_id}' 的项目")
# plexapi v4.12.0+ sectionByID 接受字符串或整数
section = self.server.library.sectionByID(library_id)
items = []
if section.type not in ['movie', 'show']:
log.warning(f"跳过不支持的 Plex 库类型: {section.type}")
return items
for item in section.all():
item_data = {
'id': item.ratingKey,
'title': item.title,
'type': item.type,
'year': item.year,
'summary': item.summary,
'poster': self.server.url(item.thumb, True) if item.thumb else None,
'path': ', '.join(item.locations),
}
items.append(item_data)
log.debug(f"从库 '{section.title}' 获取到 {len(items)} 个项目")
return items
def refresh_library(self, library_id):
"""
发送刷新指定媒体库的请求。
:param library_id: 媒体库的 ID (key)
"""
self._connect()
log.debug(f"正在请求刷新 Plex 媒体库: {library_id}")
section = self.server.library.sectionByID(library_id)
section.update()
log.info(f"已向 Plex 媒体库 '{section.title}' 发送刷新请求")

View File

View File

@@ -0,0 +1,31 @@
import qbittorrentapi
class QBittorrentManager:
def __init__(self, config):
self.client = qbittorrentapi.Client(**config)
def test_connection(self):
try:
self.client.auth_log_in()
version = self.client.app.version
api_version = self.client.app.webapi_version
return True, f"连接成功qBittorrent 版本: {version} (API: {api_version})"
except qbittorrentapi.LoginFailed as e:
return False, f"登录失败: {e}"
except Exception as e:
return False, f"连接失败: {e}"
def get_stats(self):
try:
self.client.auth_log_in()
return len(self.client.torrents_info())
except:
return 0
def add_torrent(self, url):
try:
self.client.auth_log_in()
result = self.client.torrents_add(urls=url)
return result == "Ok.", "任务添加成功" if result == "Ok." else "任务添加失败"
except Exception as e:
return False, str(e)

View File