init
This commit is contained in:
0
backend/utils/__init__.py
Normal file
0
backend/utils/__init__.py
Normal file
54
backend/utils/douban_utils.py
Normal file
54
backend/utils/douban_utils.py
Normal 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}")
|
||||
|
||||
0
backend/utils/media_server_utils.py
Normal file
0
backend/utils/media_server_utils.py
Normal file
36
backend/utils/pagination.py
Normal file
36
backend/utils/pagination.py
Normal 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
|
||||
}
|
||||
|
||||
45
backend/utils/placeholders.py
Normal file
45
backend/utils/placeholders.py
Normal 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
141
backend/utils/plex_utils.py
Normal 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}' 发送刷新请求")
|
||||
0
backend/utils/ptskit_utils.py
Normal file
0
backend/utils/ptskit_utils.py
Normal file
31
backend/utils/qbittorrent_utils.py
Normal file
31
backend/utils/qbittorrent_utils.py
Normal 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)
|
||||
0
backend/utils/transmission_utils.py
Normal file
0
backend/utils/transmission_utils.py
Normal file
Reference in New Issue
Block a user