mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-14 07:26:50 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dab7fbe66 | ||
|
|
62c06b6593 | ||
|
|
000b62969f | ||
|
|
b4473bb4a7 | ||
|
|
2c0e06d599 | ||
|
|
d2c55e8ed3 | ||
|
|
714abaa25a | ||
|
|
0017eb987b | ||
|
|
e5a0894692 | ||
|
|
a8e00e9f0f | ||
|
|
77a4c271ae | ||
|
|
014b77c3c7 | ||
|
|
076e241056 | ||
|
|
7ce57cc67a | ||
|
|
da0343283a | ||
|
|
d5f7f1ba91 | ||
|
|
8761c82afe | ||
|
|
13023141bc | ||
|
|
4dd2038625 | ||
|
|
06a32b0e9d |
@@ -2,7 +2,7 @@ from fastapi import APIRouter
|
||||
|
||||
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
||||
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
||||
transfer, mediaserver, bangumi, storage, discover, recommend, workflow
|
||||
transfer, mediaserver, bangumi, storage, discover, recommend, workflow, torrent
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
||||
@@ -27,3 +27,4 @@ api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"])
|
||||
api_router.include_router(discover.router, prefix="/discover", tags=["discover"])
|
||||
api_router.include_router(recommend.router, prefix="/recommend", tags=["recommend"])
|
||||
api_router.include_router(workflow.router, prefix="/workflow", tags=["workflow"])
|
||||
api_router.include_router(torrent.router, prefix="/torrent", tags=["torrent"])
|
||||
|
||||
@@ -149,11 +149,12 @@ def seasons(tmdbid: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any
|
||||
"""
|
||||
查询媒体剧集组列表(themoviedb)
|
||||
"""
|
||||
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, mtype=MediaType.TV)
|
||||
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, mtype=MediaType.TV)
|
||||
if not mediainfo:
|
||||
return []
|
||||
return mediainfo.episode_groups
|
||||
|
||||
|
||||
@router.get("/seasons", summary="查询媒体季信息", response_model=List[schemas.MediaSeason])
|
||||
def seasons(mediaid: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
|
||||
@@ -500,6 +500,8 @@ def uninstall_plugin(plugin_id: str,
|
||||
plugin_manager.plugins.pop(plugin_id, None)
|
||||
except Exception as e:
|
||||
logger.error(f"删除插件分身目录 {plugin_base_dir} 失败: {str(e)}")
|
||||
# 从插件文件夹中移除该插件
|
||||
_remove_plugin_from_folders(plugin_id)
|
||||
# 移除插件
|
||||
plugin_manager.remove_plugin(plugin_id)
|
||||
return schemas.Response(success=True)
|
||||
@@ -523,9 +525,101 @@ def clone_plugin(plugin_id: str,
|
||||
)
|
||||
|
||||
if success:
|
||||
# 注册插件服务
|
||||
reload_plugin(message)
|
||||
# 将分身插件添加到原插件所在的文件夹中
|
||||
_add_clone_to_plugin_folder(plugin_id, message)
|
||||
return schemas.Response(success=True, message="插件分身创建成功")
|
||||
else:
|
||||
return schemas.Response(success=False, message=message)
|
||||
except Exception as e:
|
||||
logger.error(f"创建插件分身失败:{str(e)}")
|
||||
return schemas.Response(success=False, message=f"创建插件分身失败:{str(e)}")
|
||||
|
||||
|
||||
def _add_clone_to_plugin_folder(original_plugin_id: str, clone_plugin_id: str):
|
||||
"""
|
||||
将分身插件添加到原插件所在的文件夹中
|
||||
:param original_plugin_id: 原插件ID
|
||||
:param clone_plugin_id: 分身插件ID
|
||||
"""
|
||||
try:
|
||||
config_oper = SystemConfigOper()
|
||||
# 获取插件文件夹配置
|
||||
folders = config_oper.get(SystemConfigKey.PluginFolders) or {}
|
||||
|
||||
# 查找原插件所在的文件夹
|
||||
target_folder = None
|
||||
for folder_name, folder_data in folders.items():
|
||||
if isinstance(folder_data, dict) and 'plugins' in folder_data:
|
||||
# 新格式:{"plugins": [...], "order": ..., "icon": ...}
|
||||
if original_plugin_id in folder_data['plugins']:
|
||||
target_folder = folder_name
|
||||
break
|
||||
elif isinstance(folder_data, list):
|
||||
# 旧格式:直接是插件列表
|
||||
if original_plugin_id in folder_data:
|
||||
target_folder = folder_name
|
||||
break
|
||||
|
||||
# 如果找到了原插件所在的文件夹,则将分身插件也添加到该文件夹中
|
||||
if target_folder:
|
||||
folder_data = folders[target_folder]
|
||||
if isinstance(folder_data, dict) and 'plugins' in folder_data:
|
||||
# 新格式
|
||||
if clone_plugin_id not in folder_data['plugins']:
|
||||
folder_data['plugins'].append(clone_plugin_id)
|
||||
logger.info(f"已将分身插件 {clone_plugin_id} 添加到文件夹 '{target_folder}' 中")
|
||||
elif isinstance(folder_data, list):
|
||||
# 旧格式
|
||||
if clone_plugin_id not in folder_data:
|
||||
folder_data.append(clone_plugin_id)
|
||||
logger.info(f"已将分身插件 {clone_plugin_id} 添加到文件夹 '{target_folder}' 中")
|
||||
|
||||
# 保存更新后的文件夹配置
|
||||
config_oper.set(SystemConfigKey.PluginFolders, folders)
|
||||
else:
|
||||
logger.info(f"原插件 {original_plugin_id} 不在任何文件夹中,分身插件 {clone_plugin_id} 将保持独立")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理插件文件夹时出错:{str(e)}")
|
||||
# 文件夹处理失败不影响插件分身创建的整体流程
|
||||
|
||||
|
||||
def _remove_plugin_from_folders(plugin_id: str):
|
||||
"""
|
||||
从所有文件夹中移除指定的插件
|
||||
:param plugin_id: 要移除的插件ID
|
||||
"""
|
||||
try:
|
||||
config_oper = SystemConfigOper()
|
||||
# 获取插件文件夹配置
|
||||
folders = config_oper.get(SystemConfigKey.PluginFolders) or {}
|
||||
|
||||
# 标记是否有修改
|
||||
modified = False
|
||||
|
||||
# 遍历所有文件夹,移除指定插件
|
||||
for folder_name, folder_data in folders.items():
|
||||
if isinstance(folder_data, dict) and 'plugins' in folder_data:
|
||||
# 新格式:{"plugins": [...], "order": ..., "icon": ...}
|
||||
if plugin_id in folder_data['plugins']:
|
||||
folder_data['plugins'].remove(plugin_id)
|
||||
logger.info(f"已从文件夹 '{folder_name}' 中移除插件 {plugin_id}")
|
||||
modified = True
|
||||
elif isinstance(folder_data, list):
|
||||
# 旧格式:直接是插件列表
|
||||
if plugin_id in folder_data:
|
||||
folder_data.remove(plugin_id)
|
||||
logger.info(f"已从文件夹 '{folder_name}' 中移除插件 {plugin_id}")
|
||||
modified = True
|
||||
|
||||
# 如果有修改,保存更新后的文件夹配置
|
||||
if modified:
|
||||
config_oper.set(SystemConfigKey.PluginFolders, folders)
|
||||
else:
|
||||
logger.debug(f"插件 {plugin_id} 不在任何文件夹中,无需移除")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从文件夹中移除插件时出错:{str(e)}")
|
||||
# 文件夹处理失败不影响插件卸载的整体流程
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from app import schemas
|
||||
from app.api.endpoints.plugin import register_plugin_api
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.command import Command
|
||||
@@ -17,13 +18,13 @@ from app.db.models.site import Site
|
||||
from app.db.models.siteicon import SiteIcon
|
||||
from app.db.models.sitestatistic import SiteStatistic
|
||||
from app.db.models.siteuserdata import SiteUserData
|
||||
from app.db.site_oper import SiteOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import get_current_active_superuser
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.utils.string import StringUtils
|
||||
from startup.plugins_initializer import register_plugin_api
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -395,6 +396,21 @@ def auth_site(
|
||||
return schemas.Response(success=status, message=msg)
|
||||
|
||||
|
||||
@router.get("/mapping", summary="获取站点域名到名称的映射", response_model=schemas.Response)
|
||||
def site_mapping(_: User = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
获取站点域名到名称的映射关系
|
||||
"""
|
||||
try:
|
||||
sites = SiteOper().list()
|
||||
mapping = {}
|
||||
for site in sites:
|
||||
mapping[site.domain] = site.name
|
||||
return schemas.Response(success=True, data=mapping)
|
||||
except Exception as e:
|
||||
return schemas.Response(success=False, message=f"获取映射失败:{str(e)}")
|
||||
|
||||
|
||||
@router.get("/{site_id}", summary="站点详情", response_model=schemas.Site)
|
||||
def read_site(
|
||||
site_id: int,
|
||||
|
||||
@@ -162,47 +162,50 @@ def rename(fileitem: schemas.FileItem,
|
||||
"""
|
||||
if not new_name:
|
||||
return schemas.Response(success=False, message="新名称为空")
|
||||
|
||||
# 重命名目录内文件
|
||||
if recursive:
|
||||
transferchain = TransferChain()
|
||||
media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT
|
||||
# 递归修改目录内文件(智能识别命名)
|
||||
sub_files: List[schemas.FileItem] = StorageChain().list_files(fileitem)
|
||||
if sub_files:
|
||||
# 开始进度
|
||||
progress = ProgressHelper()
|
||||
progress.start(ProgressKey.BatchRename)
|
||||
total = len(sub_files)
|
||||
handled = 0
|
||||
for sub_file in sub_files:
|
||||
handled += 1
|
||||
progress.update(value=handled / total * 100,
|
||||
text=f"正在处理 {sub_file.name} ...",
|
||||
key=ProgressKey.BatchRename)
|
||||
if sub_file.type == "dir":
|
||||
continue
|
||||
if not sub_file.extension:
|
||||
continue
|
||||
if f".{sub_file.extension.lower()}" not in media_exts:
|
||||
continue
|
||||
sub_path = Path(f"{fileitem.path}{sub_file.name}")
|
||||
meta = MetaInfoPath(sub_path)
|
||||
mediainfo = transferchain.recognize_media(meta)
|
||||
if not mediainfo:
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息")
|
||||
new_path = transferchain.recommend_name(meta=meta, mediainfo=mediainfo)
|
||||
if not new_path:
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=False, message=f"{sub_path.name} 未识别到新名称")
|
||||
ret: schemas.Response = rename(fileitem=sub_file,
|
||||
new_name=Path(new_path).name,
|
||||
recursive=False)
|
||||
if not ret.success:
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=False, message=f"{sub_path.name} 重命名失败!")
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
# 重命名自己
|
||||
result = StorageChain().rename_file(fileitem, new_name)
|
||||
if result:
|
||||
if recursive:
|
||||
transferchain = TransferChain()
|
||||
media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT
|
||||
# 递归修改目录内文件(智能识别命名)
|
||||
sub_files: List[schemas.FileItem] = StorageChain().list_files(fileitem)
|
||||
if sub_files:
|
||||
# 开始进度
|
||||
progress = ProgressHelper()
|
||||
progress.start(ProgressKey.BatchRename)
|
||||
total = len(sub_files)
|
||||
handled = 0
|
||||
for sub_file in sub_files:
|
||||
handled += 1
|
||||
progress.update(value=handled / total * 100,
|
||||
text=f"正在处理 {sub_file.name} ...",
|
||||
key=ProgressKey.BatchRename)
|
||||
if sub_file.type == "dir":
|
||||
continue
|
||||
if not sub_file.extension:
|
||||
continue
|
||||
if f".{sub_file.extension.lower()}" not in media_exts:
|
||||
continue
|
||||
sub_path = Path(f"{fileitem.path}{sub_file.name}")
|
||||
meta = MetaInfoPath(sub_path)
|
||||
mediainfo = transferchain.recognize_media(meta)
|
||||
if not mediainfo:
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息")
|
||||
new_path = transferchain.recommend_name(meta=meta, mediainfo=mediainfo)
|
||||
if not new_path:
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=False, message=f"{sub_path.name} 未识别到新名称")
|
||||
ret: schemas.Response = rename(fileitem=sub_file,
|
||||
new_name=Path(new_path).name,
|
||||
recursive=False)
|
||||
if not ret.success:
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=False, message=f"{sub_path.name} 重命名失败!")
|
||||
progress.end(ProgressKey.BatchRename)
|
||||
return schemas.Response(success=True)
|
||||
return schemas.Response(success=False)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from app.helper.progress import ProgressHelper
|
||||
from app.helper.rule import RuleHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.system import SystemHelper
|
||||
from app.log import logger
|
||||
from app.monitor import Monitor
|
||||
from app.scheduler import Scheduler
|
||||
@@ -37,7 +38,6 @@ from app.utils.crypto import HashUtils
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.security import SecurityUtils
|
||||
from app.utils.url import UrlUtils
|
||||
from app.helper.system import SystemHelper
|
||||
from version import APP_VERSION
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
199
app/api/endpoints/torrent.py
Normal file
199
app/api/endpoints/torrent.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app import schemas
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.db.models import User
|
||||
from app.db.user_oper import get_current_active_superuser
|
||||
from app.utils.crypto import HashUtils
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/cache", summary="获取种子缓存", response_model=schemas.Response)
|
||||
def torrents_cache(_: User = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
获取当前种子缓存数据
|
||||
"""
|
||||
torrents_chain = TorrentsChain()
|
||||
|
||||
# 获取spider和rss两种缓存
|
||||
if settings.SUBSCRIBE_MODE == "rss":
|
||||
cache_info = torrents_chain.get_torrents("rss")
|
||||
else:
|
||||
cache_info = torrents_chain.get_torrents("spider")
|
||||
|
||||
# 统计信息
|
||||
torrent_count = sum(len(torrents) for torrents in cache_info.values())
|
||||
|
||||
# 转换为前端需要的格式
|
||||
torrent_data = []
|
||||
for domain, contexts in cache_info.items():
|
||||
for context in contexts:
|
||||
torrent_hash = HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}")
|
||||
torrent_data.append({
|
||||
"hash": torrent_hash,
|
||||
"domain": domain,
|
||||
"title": context.torrent_info.title,
|
||||
"description": context.torrent_info.description,
|
||||
"size": context.torrent_info.size,
|
||||
"pubdate": context.torrent_info.pubdate,
|
||||
"site_name": context.torrent_info.site_name,
|
||||
"media_name": context.media_info.title if context.media_info else "",
|
||||
"media_year": context.media_info.year if context.media_info else "",
|
||||
"media_type": context.media_info.type if context.media_info else "",
|
||||
"season_episode": context.meta_info.season_episode if context.meta_info else "",
|
||||
"resource_term": context.meta_info.resource_term if context.meta_info else "",
|
||||
"enclosure": context.torrent_info.enclosure,
|
||||
"page_url": context.torrent_info.page_url,
|
||||
"poster_path": context.media_info.get_poster_image() if context.media_info else "",
|
||||
"backdrop_path": context.media_info.get_backdrop_image() if context.media_info else ""
|
||||
})
|
||||
|
||||
return schemas.Response(success=True, data={
|
||||
"count": torrent_count,
|
||||
"sites": len(cache_info),
|
||||
"data": torrent_data
|
||||
})
|
||||
|
||||
|
||||
@router.delete("/cache/{domain}/{torrent_hash}", summary="删除指定种子缓存",
|
||||
response_model=schemas.Response)
|
||||
def delete_cache(domain: str, torrent_hash: str, _: User = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
删除指定的种子缓存
|
||||
:param domain: 站点域名
|
||||
:param torrent_hash: 种子hash(使用title+description的md5)
|
||||
:param _: 当前用户,必须是超级用户
|
||||
"""
|
||||
|
||||
torrents_chain = TorrentsChain()
|
||||
|
||||
try:
|
||||
# 获取当前缓存
|
||||
cache_data = torrents_chain.get_torrents()
|
||||
|
||||
if domain not in cache_data:
|
||||
return schemas.Response(success=False, message=f"站点 {domain} 缓存不存在")
|
||||
|
||||
# 查找并删除指定种子
|
||||
original_count = len(cache_data[domain])
|
||||
cache_data[domain] = [
|
||||
context for context in cache_data[domain]
|
||||
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}") != torrent_hash
|
||||
]
|
||||
|
||||
if len(cache_data[domain]) == original_count:
|
||||
return schemas.Response(success=False, message="未找到指定的种子")
|
||||
|
||||
# 保存更新后的缓存
|
||||
torrents_chain.save_cache(cache_data, torrents_chain.cache_file)
|
||||
|
||||
return schemas.Response(success=True, message="种子删除成功")
|
||||
except Exception as e:
|
||||
return schemas.Response(success=False, message=f"删除失败:{str(e)}")
|
||||
|
||||
|
||||
@router.delete("/cache", summary="清理种子缓存", response_model=schemas.Response)
|
||||
def clear_cache(_: User = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
清理所有种子缓存
|
||||
"""
|
||||
torrents_chain = TorrentsChain()
|
||||
|
||||
try:
|
||||
torrents_chain.clear_torrents()
|
||||
return schemas.Response(success=True, message="种子缓存清理完成")
|
||||
except Exception as e:
|
||||
return schemas.Response(success=False, message=f"清理失败:{str(e)}")
|
||||
|
||||
|
||||
@router.post("/cache/refresh", summary="刷新种子缓存", response_model=schemas.Response)
|
||||
def refresh_cache(_: User = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
刷新种子缓存
|
||||
"""
|
||||
from app.chain.torrents import TorrentsChain
|
||||
|
||||
torrents_chain = TorrentsChain()
|
||||
|
||||
try:
|
||||
result = torrents_chain.refresh()
|
||||
|
||||
# 统计刷新结果
|
||||
total_count = sum(len(torrents) for torrents in result.values())
|
||||
sites_count = len(result)
|
||||
|
||||
return schemas.Response(success=True, message=f"缓存刷新完成,共刷新 {sites_count} 个站点,{total_count} 个种子")
|
||||
except Exception as e:
|
||||
return schemas.Response(success=False, message=f"刷新失败:{str(e)}")
|
||||
|
||||
|
||||
@router.post("/cache/reidentify/{domain}/{torrent_hash}", summary="重新识别种子", response_model=schemas.Response)
|
||||
def reidentify_cache(domain: str, torrent_hash: str,
|
||||
tmdbid: Optional[int] = None, doubanid: Optional[str] = None,
|
||||
_: User = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
重新识别指定的种子
|
||||
:param domain: 站点域名
|
||||
:param torrent_hash: 种子hash(使用title+description的md5)
|
||||
:param tmdbid: 手动指定的TMDB ID
|
||||
:param doubanid: 手动指定的豆瓣ID
|
||||
:param _: 当前用户,必须是超级用户
|
||||
"""
|
||||
|
||||
torrents_chain = TorrentsChain()
|
||||
media_chain = MediaChain()
|
||||
|
||||
try:
|
||||
# 获取当前缓存
|
||||
cache_data = torrents_chain.get_torrents()
|
||||
|
||||
if domain not in cache_data:
|
||||
return schemas.Response(success=False, message=f"站点 {domain} 缓存不存在")
|
||||
|
||||
# 查找指定种子
|
||||
target_context = None
|
||||
for context in cache_data[domain]:
|
||||
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}") == torrent_hash:
|
||||
target_context = context
|
||||
break
|
||||
|
||||
if not target_context:
|
||||
return schemas.Response(success=False, message="未找到指定的种子")
|
||||
|
||||
# 重新识别
|
||||
meta = MetaInfo(title=target_context.torrent_info.title,
|
||||
subtitle=target_context.torrent_info.description)
|
||||
if tmdbid or doubanid:
|
||||
# 手动指定媒体信息
|
||||
mediainfo = MediaChain().recognize_media(meta=meta, tmdbid=tmdbid, doubanid=doubanid)
|
||||
else:
|
||||
# 自动重新识别
|
||||
mediainfo = media_chain.recognize_by_meta(meta)
|
||||
|
||||
if not mediainfo:
|
||||
# 创建空的媒体信息
|
||||
mediainfo = MediaInfo()
|
||||
else:
|
||||
# 清理多余数据
|
||||
mediainfo.clear()
|
||||
|
||||
# 更新上下文中的媒体信息
|
||||
target_context.media_info = mediainfo
|
||||
|
||||
# 保存更新后的缓存
|
||||
torrents_chain.save_cache(cache_data, TorrentsChain().cache_file)
|
||||
|
||||
return schemas.Response(success=True, message="重新识别完成", data={
|
||||
"media_name": mediainfo.title if mediainfo else "",
|
||||
"media_year": mediainfo.year if mediainfo else "",
|
||||
"media_type": mediainfo.type.value if mediainfo and mediainfo.type else ""
|
||||
})
|
||||
except Exception as e:
|
||||
return schemas.Response(success=False, message=f"重新识别失败:{str(e)}")
|
||||
@@ -38,6 +38,15 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
|
||||
self.mediachain = MediaChain()
|
||||
self.torrenthelper = TorrentHelper()
|
||||
|
||||
@property
|
||||
def cache_file(self) -> str:
|
||||
"""
|
||||
返回缓存文件列表
|
||||
"""
|
||||
if settings.SUBSCRIBE_MODE == 'spider':
|
||||
return self._spider_file
|
||||
return self._rss_file
|
||||
|
||||
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
|
||||
"""
|
||||
远程刷新订阅,发送消息
|
||||
|
||||
@@ -1118,7 +1118,7 @@ class PluginManager(metaclass=Singleton):
|
||||
logger.info(f"分身插件 {clone_id} 配置重新初始化完成")
|
||||
|
||||
logger.info(f"插件分身 {clone_id} 创建成功")
|
||||
return True, "插件分身创建成功"
|
||||
return True, clone_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建插件分身失败:{str(e)}")
|
||||
|
||||
@@ -59,8 +59,10 @@ class TransHandler:
|
||||
current_value.update(value)
|
||||
else:
|
||||
current_value[key] = value
|
||||
elif isinstance(current_value, bool):
|
||||
current_value = value
|
||||
elif isinstance(current_value, int):
|
||||
current_value += value
|
||||
current_value += (value or 0)
|
||||
else:
|
||||
current_value = value
|
||||
setattr(self.result, key, current_value)
|
||||
@@ -748,6 +750,16 @@ class TransHandler:
|
||||
else:
|
||||
logger.info(f"正在删除已存在的文件:{target_file}")
|
||||
target_file.unlink()
|
||||
else:
|
||||
exists_item = target_oper.get_item(target_file)
|
||||
if exists_item:
|
||||
if not over_flag:
|
||||
logger.warn(f"文件已存在:【{target_storage}】{target_file}")
|
||||
return None, f"【{target_storage}】{target_file} 已存在"
|
||||
else:
|
||||
logger.info(f"正在删除已存在的文件:【{target_storage}】{target_file}")
|
||||
target_oper.delete(exists_item)
|
||||
# 执行文件整理命令
|
||||
new_item, errmsg = self.__transfer_command(fileitem=fileitem,
|
||||
target_storage=target_storage,
|
||||
source_oper=source_oper,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from threading import Lock
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from app.core.config import settings
|
||||
@@ -8,39 +9,83 @@ from app.schemas.types import ModuleType, MediaRecognizeType
|
||||
|
||||
|
||||
class TheTvDbModule(_ModuleBase):
|
||||
tvdb: tvdb_v4_official.TVDB = None
|
||||
"""
|
||||
TVDB媒体信息匹配
|
||||
"""
|
||||
__timeout: int = 15
|
||||
tvdb: Optional[tvdb_v4_official.TVDB] = None
|
||||
__auth_lock = Lock()
|
||||
|
||||
def init_module(self) -> None:
|
||||
self._initialize_tvdb_session()
|
||||
pass
|
||||
|
||||
def _initialize_tvdb_session(self) -> None:
|
||||
def _initialize_tvdb_session(self, is_retry: bool = False) -> None:
|
||||
"""
|
||||
创建或刷新 TVDB 登录会话
|
||||
创建或刷新 TVDB 登录会话。
|
||||
:param is_retry: 是否是由于token失效后的重试登录
|
||||
"""
|
||||
action = "刷新" if is_retry else "创建"
|
||||
logger.info(f"开始{action}TVDB登录会话...")
|
||||
try:
|
||||
self.tvdb = tvdb_v4_official.TVDB(apikey=settings.TVDB_V4_API_KEY, pin=settings.TVDB_V4_API_PIN)
|
||||
if not settings.TVDB_V4_API_KEY:
|
||||
raise ConnectionError("TVDB API Key 未配置,无法初始化会话。")
|
||||
self.tvdb = tvdb_v4_official.TVDB(apikey=settings.TVDB_V4_API_KEY,
|
||||
pin=settings.TVDB_V4_API_PIN,
|
||||
proxy=settings.PROXY,
|
||||
timeout=self.__timeout)
|
||||
if self.tvdb:
|
||||
logger.info(f"TVDB登录会话{action}成功。")
|
||||
else:
|
||||
raise ValueError(f"TVDB登录会话{action}后实例仍为None。")
|
||||
except Exception as e:
|
||||
logger.error(f"TVDB 登录失败: {str(e)}")
|
||||
self.tvdb = None
|
||||
raise ConnectionError(f"TVDB登录会话{action}失败: {str(e)}") from e
|
||||
|
||||
def _handle_tvdb_call(self, func, *args, **kwargs):
|
||||
def _ensure_tvdb_session(self, is_retry: bool = False) -> None:
|
||||
"""
|
||||
确保TVDB会话存在。如果不存在或需要强制重新初始化,则进行初始化。
|
||||
:param is_retry: 是否重新初始化(例如token失效时)
|
||||
"""
|
||||
# 第一次检查 (无锁),提高性能,避免不必要锁竞争
|
||||
if not self.tvdb or is_retry:
|
||||
with self.__auth_lock:
|
||||
# 第二次检查 (有锁),防止多个线程都通过第一次检查后重复初始化
|
||||
if not self.tvdb or is_retry:
|
||||
self._initialize_tvdb_session(is_retry=is_retry)
|
||||
|
||||
def _handle_tvdb_call(self, method_name: str, *args, **kwargs):
|
||||
"""
|
||||
包裹 TVDB 调用,处理 token 失效情况并尝试重新初始化
|
||||
:param method_name: 要在 self.tvdb 实例上调用的方法的名称 (字符串)
|
||||
"""
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
self._ensure_tvdb_session()
|
||||
actual_method = getattr(self.tvdb, method_name)
|
||||
return actual_method(*args, **kwargs)
|
||||
except ValueError as e:
|
||||
# 检查错误信息中是否包含 token 失效相关描述
|
||||
if "Unauthorized" in str(e):
|
||||
logger.warning("TVDB Token 可能已失效,正在尝试重新登录...")
|
||||
self._initialize_tvdb_session()
|
||||
return func(*args, **kwargs)
|
||||
elif "NotFoundException" in str(e):
|
||||
logger.warning("TVDB 剧集不存在")
|
||||
try:
|
||||
self._ensure_tvdb_session(is_retry=True)
|
||||
actual_method = getattr(self.tvdb, method_name)
|
||||
return actual_method(*args, **kwargs)
|
||||
except ConnectionError as conn_err:
|
||||
logger.error(f"TVDB Token失效后重新登录失败: {conn_err}")
|
||||
raise
|
||||
elif "NotFoundException" in str(e) or "ID not found" in str(e):
|
||||
logger.warning(f"TVDB 资源未找到 (调用 {method_name}): {e}")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"TVDB 调用 ({method_name}) 时发生未处理的 ValueError: {str(e)}")
|
||||
raise
|
||||
except ConnectionError as e:
|
||||
logger.error(f"TVDB 连接会话错误: {str(e)}")
|
||||
raise
|
||||
except AttributeError as e:
|
||||
logger.error(f"TVDB 实例上没有方法 '{method_name}': {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"TVDB 调用出错: {str(e)}")
|
||||
logger.error(f"TVDB 调用时发生未知错误: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@@ -69,14 +114,16 @@ class TheTvDbModule(_ModuleBase):
|
||||
return 4
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
logger.info("TheTvDbModule 停止。正在清除 TVDB 会话。")
|
||||
with self.__auth_lock:
|
||||
self.tvdb = None
|
||||
|
||||
def test(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
测试模块连接性
|
||||
"""
|
||||
try:
|
||||
self._handle_tvdb_call(self.tvdb.get_series, 81189)
|
||||
self._handle_tvdb_call("get_series", 81189)
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
@@ -92,7 +139,7 @@ class TheTvDbModule(_ModuleBase):
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始获取TVDB信息: {tvdbid} ...")
|
||||
return self._handle_tvdb_call(self.tvdb.get_series_extended, tvdbid)
|
||||
return self._handle_tvdb_call("get_series_extended", tvdbid)
|
||||
except Exception as err:
|
||||
logger.error(f"获取TVDB信息失败: {str(err)}")
|
||||
return None
|
||||
@@ -105,8 +152,13 @@ class TheTvDbModule(_ModuleBase):
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始用标题搜索TVDB剧集: {title} ...")
|
||||
res = self._handle_tvdb_call(self.tvdb.search, title)
|
||||
return [item for item in res if item.get("type") == "series"]
|
||||
res = self._handle_tvdb_call("search", title)
|
||||
if res is None:
|
||||
return []
|
||||
if not isinstance(res, list):
|
||||
logger.warning(f"TVDB 搜索 '{title}' 未返回列表:{type(res)}")
|
||||
return []
|
||||
return [item for item in res if isinstance(item, dict) and item.get("type") == "series"]
|
||||
except Exception as err:
|
||||
logger.error(f"用标题搜索TVDB剧集失败: {str(err)}")
|
||||
logger.error(f"用标题搜索TVDB剧集失败 ({title}): {str(err)}")
|
||||
return []
|
||||
|
||||
@@ -4,74 +4,124 @@ __author__ = "Weylin Wagnon"
|
||||
__version__ = "1.0.12"
|
||||
|
||||
import json
|
||||
import string
|
||||
import urllib
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class Auth:
|
||||
def __init__(self, url, apikey, pin=""):
|
||||
loginInfo = {"apikey": apikey}
|
||||
if pin != "":
|
||||
loginInfo["pin"] = pin
|
||||
"""
|
||||
TVDB认证类
|
||||
"""
|
||||
|
||||
def __init__(self, url, apikey, pin="", proxy=None, timeout: int = 15):
|
||||
login_info = {"apikey": apikey}
|
||||
if pin != "":
|
||||
login_info["pin"] = pin
|
||||
|
||||
login_info_bytes = json.dumps(login_info, indent=2)
|
||||
|
||||
loginInfoBytes = json.dumps(loginInfo, indent=2).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=loginInfoBytes)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, data=loginInfoBytes) as response:
|
||||
res = json.load(response)
|
||||
self.token = res["data"]["token"]
|
||||
except HTTPError as e:
|
||||
res = json.load(e)
|
||||
raise Exception("Code:{}, {}".format(e, res["message"]))
|
||||
# 使用项目统一的RequestUtils类
|
||||
req_utils = RequestUtils(proxies=proxy, timeout=timeout)
|
||||
response = req_utils.post_res(
|
||||
url=url,
|
||||
data=login_info_bytes,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response and response.status_code == 200:
|
||||
result = response.json()
|
||||
self.token = result["data"]["token"]
|
||||
else:
|
||||
error_msg = f"登录失败,状态码: {response.status_code if response else 'None'}"
|
||||
if response:
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_msg = f"Code: {response.status_code}, {error_data.get('message', '未知错误')}"
|
||||
except Exception as err:
|
||||
error_msg = f"Code: {response.status_code}, 响应解析失败:{err}"
|
||||
raise Exception(error_msg)
|
||||
except Exception as e:
|
||||
raise Exception(f"TVDB认证失败: {str(e)}")
|
||||
|
||||
def get_token(self):
|
||||
"""
|
||||
获取认证token
|
||||
"""
|
||||
return self.token
|
||||
|
||||
|
||||
class Request:
|
||||
def __init__(self, auth_token):
|
||||
"""
|
||||
请求处理类
|
||||
"""
|
||||
|
||||
def __init__(self, auth_token, proxy=None, timeout=15):
|
||||
self.auth_token = auth_token
|
||||
self.links = None
|
||||
self.proxy = proxy
|
||||
self.timeout = timeout
|
||||
|
||||
def make_request(self, url, if_modified_since=None):
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", "Bearer {}".format(self.auth_token))
|
||||
"""
|
||||
向指定的 URL 发起请求并返回数据
|
||||
"""
|
||||
headers = {"Authorization": f"Bearer {self.auth_token}"}
|
||||
if if_modified_since:
|
||||
req.add_header("If-Modified-Since", "{}".format(if_modified_since))
|
||||
headers["If-Modified-Since"] = str(if_modified_since)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
res = json.load(response)
|
||||
except HTTPError as e:
|
||||
try:
|
||||
if e.code == HTTPStatus.NOT_MODIFIED:
|
||||
return {
|
||||
"code": HTTPStatus.NOT_MODIFIED.real,
|
||||
"message": "Not-Modified",
|
||||
}
|
||||
res = json.load(e)
|
||||
except:
|
||||
res = {}
|
||||
data = res.get("data", None)
|
||||
if data is not None and res.get("status", "failure") != "failure":
|
||||
self.links = res.get("links", None)
|
||||
return data
|
||||
msg = res.get("message", None)
|
||||
if not msg:
|
||||
msg = "UNKNOWN FAILURE"
|
||||
raise ValueError("failed to get " + url + "\n " + str(msg))
|
||||
# 使用项目统一的RequestUtils类
|
||||
req_utils = RequestUtils(proxies=self.proxy, timeout=self.timeout)
|
||||
response = req_utils.get_res(url=url, headers=headers)
|
||||
|
||||
if response is None:
|
||||
raise ValueError(f"获取 {url} 失败\n 网络连接失败")
|
||||
|
||||
if response.status_code == HTTPStatus.NOT_MODIFIED:
|
||||
return {
|
||||
"code": HTTPStatus.NOT_MODIFIED.real,
|
||||
"message": "Not-Modified",
|
||||
}
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
data = result.get("data", None)
|
||||
if data is not None and result.get("status", "failure") != "failure":
|
||||
self.links = result.get("links", None)
|
||||
return data
|
||||
|
||||
msg = result.get("message", "未知错误")
|
||||
raise ValueError(f"获取 {url} 失败\n {str(msg)}")
|
||||
else:
|
||||
# 处理其他HTTP错误状态码
|
||||
try:
|
||||
error_data = response.json()
|
||||
msg = error_data.get("message", f"HTTP {response.status_code}")
|
||||
except Exception as err:
|
||||
msg = f"HTTP {response.status_code} {err}"
|
||||
raise ValueError(f"获取 {url} 失败\n {str(msg)}")
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ValueError):
|
||||
raise
|
||||
raise ValueError(f"获取 {url} 失败\n {str(e)}")
|
||||
|
||||
|
||||
class Url:
|
||||
"""
|
||||
URL构建类
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = "https://api4.thetvdb.com/v4/"
|
||||
|
||||
def construct(
|
||||
self, url_sect, url_id=None, url_subsect=None, url_lang=None, **query
|
||||
):
|
||||
def construct(self, url_sect, url_id=None, url_subsect=None, url_lang=None, **kwargs):
|
||||
"""
|
||||
构建API URL
|
||||
"""
|
||||
url = self.base_url + url_sect
|
||||
if url_id:
|
||||
url += "/" + str(url_id)
|
||||
@@ -79,387 +129,487 @@ class Url:
|
||||
url += "/" + url_subsect
|
||||
if url_lang:
|
||||
url += "/" + url_lang
|
||||
if query:
|
||||
query = {var: val for var, val in query.items() if val is not None}
|
||||
if query:
|
||||
url += "?" + urllib.parse.urlencode(query)
|
||||
if kwargs:
|
||||
params = {var: val for var, val in kwargs.items() if val is not None}
|
||||
if params:
|
||||
url += "?" + urllib.parse.urlencode(params)
|
||||
return url
|
||||
|
||||
|
||||
class TVDB:
|
||||
def __init__(self, apikey: str, pin=""):
|
||||
"""
|
||||
TVDB API主类
|
||||
"""
|
||||
|
||||
def __init__(self, apikey: str, pin="", proxy=None, timeout: int = 15):
|
||||
self.url = Url()
|
||||
login_url = self.url.construct("login")
|
||||
self.auth = Auth(login_url, apikey, pin)
|
||||
self.auth = Auth(login_url, apikey, pin, proxy, timeout)
|
||||
auth_token = self.auth.get_token()
|
||||
self.request = Request(auth_token)
|
||||
self.request = Request(auth_token, proxy, timeout)
|
||||
|
||||
def get_req_links(self) -> dict:
|
||||
"""
|
||||
获取上一次请求返回的链接信息(例如分页链接)
|
||||
"""
|
||||
return self.request.links
|
||||
|
||||
def get_artwork_statuses(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of artwork statuses"""
|
||||
"""
|
||||
返回艺术图状态列表
|
||||
"""
|
||||
url = self.url.construct("artwork/statuses", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_artwork_types(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of artwork types"""
|
||||
"""
|
||||
返回艺术图类型列表
|
||||
"""
|
||||
url = self.url.construct("artwork/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_artwork(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an artwork dictionary"""
|
||||
"""
|
||||
返回单个艺术图信息的字典
|
||||
"""
|
||||
url = self.url.construct("artwork", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_artwork_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an artwork extended dictionary"""
|
||||
"""
|
||||
返回单个艺术图的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("artwork", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_awards(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of awards"""
|
||||
"""
|
||||
返回奖项列表
|
||||
"""
|
||||
url = self.url.construct("awards", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an award dictionary"""
|
||||
"""
|
||||
返回单个奖项信息的字典
|
||||
"""
|
||||
url = self.url.construct("awards", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an award extended dictionary"""
|
||||
"""
|
||||
返回单个奖项的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("awards", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_award_categories(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of award categories"""
|
||||
"""
|
||||
返回奖项类别列表
|
||||
"""
|
||||
url = self.url.construct("awards/categories", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award_category(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an award category dictionary"""
|
||||
"""
|
||||
返回单个奖项类别信息的字典
|
||||
"""
|
||||
url = self.url.construct("awards/categories", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_award_category_extended(
|
||||
self, id: int, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns an award category extended dictionary"""
|
||||
def get_award_category_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回单个奖项类别的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("awards/categories", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_content_ratings(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of content ratings"""
|
||||
"""
|
||||
返回内容分级列表
|
||||
"""
|
||||
url = self.url.construct("content/ratings", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_countries(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of countries"""
|
||||
"""
|
||||
返回国家列表
|
||||
"""
|
||||
url = self.url.construct("countries", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_companies(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of companies"""
|
||||
"""
|
||||
返回公司列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("companies", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_company_types(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of company types"""
|
||||
"""
|
||||
返回公司类型列表
|
||||
"""
|
||||
url = self.url.construct("companies/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_company(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a company dictionary"""
|
||||
"""
|
||||
返回单个公司信息的字典
|
||||
"""
|
||||
url = self.url.construct("companies", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_series(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of series"""
|
||||
"""
|
||||
返回剧集列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("series", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a series dictionary"""
|
||||
"""
|
||||
返回单个剧集信息的字典
|
||||
"""
|
||||
url = self.url.construct("series", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_by_slug(
|
||||
self, slug: string, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a series dictionary"""
|
||||
def get_series_by_slug(self, slug: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
通过 slug (别名) 返回单个剧集信息的字典
|
||||
"""
|
||||
url = self.url.construct("series/slug", slug, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_extended(
|
||||
self, id: int, meta=None, short=False, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a series extended dictionary"""
|
||||
def get_series_extended(self, id: int, meta=None, short=False, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回单个剧集的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("series", id, "extended", meta=meta, short=short)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_episodes(
|
||||
self,
|
||||
id: int,
|
||||
season_type: str = "default",
|
||||
page: int = 0,
|
||||
lang: str = None,
|
||||
meta=None,
|
||||
if_modified_since=None,
|
||||
**kwargs
|
||||
) -> dict:
|
||||
"""Returns a series episodes dictionary"""
|
||||
def get_series_episodes(self, id: int, season_type: str = "default", page: int = 0,
|
||||
lang: str = None, meta=None, if_modified_since=None, **kwargs) -> dict:
|
||||
"""
|
||||
返回指定剧集和季类型的各集信息字典 (可分页,可指定语言)
|
||||
"""
|
||||
url = self.url.construct(
|
||||
"series", id, "episodes/" + season_type, lang, page=page, meta=meta, **kwargs
|
||||
)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_translation(
|
||||
self, id: int, lang: str, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a series translation dictionary"""
|
||||
def get_series_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回剧集的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("series", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_artworks(
|
||||
self, id: int, lang: str, type=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a series record with an artwork array"""
|
||||
def get_series_artworks(self, id: int, lang: str, type=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回包含艺术图数组的剧集记录 (可指定语言和类型)
|
||||
"""
|
||||
url = self.url.construct("series", id, "artworks", lang=lang, type=type)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_series_nextAired(self, id: int, if_modified_since=None) -> dict:
|
||||
"""Returns a series extended dictionary"""
|
||||
def get_series_next_aired(self, id: int, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回剧集的下一播出信息字典
|
||||
"""
|
||||
url = self.url.construct("series", id, "nextAired")
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_movies(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of movies"""
|
||||
"""
|
||||
返回电影列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("movies", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a movie dictionary"""
|
||||
"""
|
||||
返回单个电影信息的字典
|
||||
"""
|
||||
url = self.url.construct("movies", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie_by_slug(
|
||||
self, slug: string, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a movie dictionary"""
|
||||
def get_movie_by_slug(self, slug: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
通过 slug (别名) 返回单个电影信息的字典
|
||||
"""
|
||||
url = self.url.construct("movies/slug", slug, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie_extended(
|
||||
self, id: int, meta=None, short=False, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a movie extended dictionary"""
|
||||
def get_movie_extended(self, id: int, meta=None, short=False, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回电影的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("movies", id, "extended", meta=meta, short=short)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_movie_translation(
|
||||
self, id: int, lang: str, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a movie translation dictionary"""
|
||||
def get_movie_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回电影的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("movies", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_seasons(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of seasons"""
|
||||
"""
|
||||
返回季列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("seasons", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a season dictionary"""
|
||||
"""
|
||||
返回单季信息的字典
|
||||
"""
|
||||
url = self.url.construct("seasons", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a season extended dictionary"""
|
||||
"""
|
||||
返回单季的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("seasons", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season_types(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of season types"""
|
||||
"""
|
||||
返回季类型列表
|
||||
"""
|
||||
url = self.url.construct("seasons/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_season_translation(
|
||||
self, id: int, lang: str, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns a seasons translation dictionary"""
|
||||
def get_season_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回季的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("seasons", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_episodes(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of episodes"""
|
||||
"""
|
||||
返回集列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("episodes", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_episode(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an episode dictionary"""
|
||||
"""
|
||||
返回单集信息的字典
|
||||
"""
|
||||
url = self.url.construct("episodes", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_episode_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns an episode extended dictionary"""
|
||||
"""
|
||||
返回单集的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("episodes", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_episode_translation(
|
||||
self, id: int, lang: str, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns an episode translation dictionary"""
|
||||
def get_episode_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回单集的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("episodes", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
get_episodes_translation = (
|
||||
get_episode_translation # Support the old name of the function.
|
||||
)
|
||||
# 兼容旧函数名。
|
||||
get_episodes_translation = get_episode_translation
|
||||
|
||||
def get_all_genders(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of genders"""
|
||||
"""
|
||||
返回性别列表
|
||||
"""
|
||||
url = self.url.construct("genders", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_genres(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of genres"""
|
||||
"""
|
||||
返回类型(流派)列表
|
||||
"""
|
||||
url = self.url.construct("genres", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_genre(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a genres dictionary"""
|
||||
"""
|
||||
返回单个类型(流派)信息的字典
|
||||
"""
|
||||
url = self.url.construct("genres", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_languages(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of languages"""
|
||||
"""
|
||||
返回语言列表
|
||||
"""
|
||||
url = self.url.construct("languages", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_people(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of people"""
|
||||
"""
|
||||
返回人物列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("people", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_person(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a people dictionary"""
|
||||
"""
|
||||
返回单个人物信息的字典
|
||||
"""
|
||||
url = self.url.construct("people", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_person_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a people extended dictionary"""
|
||||
"""
|
||||
返回单个人物的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("people", id, "extended", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_person_translation(
|
||||
self, id: int, lang: str, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns an people translation dictionary"""
|
||||
def get_person_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回人物的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("people", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_character(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a character dictionary"""
|
||||
"""
|
||||
返回角色信息的字典
|
||||
"""
|
||||
url = self.url.construct("characters", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_people_types(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of people types"""
|
||||
"""
|
||||
返回人物类型列表
|
||||
"""
|
||||
url = self.url.construct("people/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
get_all_people_types = get_people_types # Support the old function name
|
||||
# 兼容旧函数名
|
||||
get_all_people_types = get_people_types
|
||||
|
||||
def get_source_types(self, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of source types"""
|
||||
"""
|
||||
返回来源类型列表
|
||||
"""
|
||||
url = self.url.construct("sources/types", meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
get_all_sourcetypes = get_source_types # Support the old function name
|
||||
# 兼容旧函数名
|
||||
get_all_sourcetypes = get_source_types
|
||||
|
||||
# kwargs accepts args such as: page=2, action='update', type='artwork'
|
||||
def get_updates(self, since: int, **kwargs) -> list:
|
||||
"""Returns a list of updates"""
|
||||
"""
|
||||
返回更新列表
|
||||
"""
|
||||
url = self.url.construct("updates", since=since, **kwargs)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_all_tag_options(self, page=None, meta=None, if_modified_since=None) -> list:
|
||||
"""Returns a list of tag options"""
|
||||
"""
|
||||
返回标签选项列表 (可分页)
|
||||
"""
|
||||
url = self.url.construct("tags/options", page=page, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_tag_option(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a tag option dictionary"""
|
||||
"""
|
||||
返回单个标签选项信息的字典
|
||||
"""
|
||||
url = self.url.construct("tags/options", id, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_all_lists(self, page=None, meta=None) -> dict:
|
||||
"""
|
||||
返回所有公开的列表信息 (可分页)
|
||||
"""
|
||||
url = self.url.construct("lists", page=page, meta=meta)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_list(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回单个列表信息的字典
|
||||
"""
|
||||
url = self.url.construct("lists", id, meta=meta)
|
||||
return self.request.make_request(url), if_modified_since
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_list_by_slug(self, slug: string, meta=None, if_modified_since=None) -> dict:
|
||||
"""Returns a movie dictionary"""
|
||||
def get_list_by_slug(self, slug: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
通过 slug (别名) 返回单个列表信息的字典
|
||||
"""
|
||||
url = self.url.construct("lists/slug", slug, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_list_extended(self, id: int, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回单个列表的扩展信息字典
|
||||
"""
|
||||
url = self.url.construct("lists", id, "extended", meta=meta)
|
||||
return self.request.make_request(url), if_modified_since
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_list_translation(
|
||||
self, id: int, lang: str, meta=None, if_modified_since=None
|
||||
) -> dict:
|
||||
"""Returns an list translation dictionary"""
|
||||
def get_list_translation(self, id: int, lang: str, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回列表的指定语言翻译信息字典
|
||||
"""
|
||||
url = self.url.construct("lists", id, "translations", lang, meta=meta)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_inspiration_types(self, meta=None, if_modified_since=None) -> dict:
|
||||
"""
|
||||
返回灵感类型列表
|
||||
"""
|
||||
url = self.url.construct("inspiration/types", meta=meta)
|
||||
return self.request.make_request(url), if_modified_since
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def search(self, query, **kwargs) -> list:
|
||||
"""Returns a list of search results"""
|
||||
def search(self, query: str, **kwargs) -> list:
|
||||
"""
|
||||
根据查询字符串进行搜索,并返回结果列表
|
||||
"""
|
||||
url = self.url.construct("search", query=query, **kwargs)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def search_by_remote_id(self, remoteid: str) -> list:
|
||||
"""Returns a list of search results by remote id exact match"""
|
||||
"""
|
||||
通过外部 ID 精确匹配搜索,并返回结果列表
|
||||
"""
|
||||
url = self.url.construct("search/remoteid", remoteid)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_tags(self, slug: str, if_modified_since=None) -> dict:
|
||||
"""Returns a tag option dictionary"""
|
||||
"""
|
||||
返回具有指定 slug 的标签实体信息字典 (此方法基于的 /entities/{slug} 端点非标准,请谨慎使用)
|
||||
"""
|
||||
url = self.url.construct("entities", url_subsect=slug)
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_entities_types(self, if_modified_since=None) -> dict:
|
||||
"""Returns a entities types dictionary"""
|
||||
"""
|
||||
返回可用的实体类型列表
|
||||
"""
|
||||
url = self.url.construct("entities")
|
||||
return self.request.make_request(url, if_modified_since)
|
||||
|
||||
def get_user_by_id(self, id: int) -> dict:
|
||||
"""Returns a user info dictionary"""
|
||||
"""
|
||||
通过用户 ID 返回用户信息字典
|
||||
"""
|
||||
url = self.url.construct("user", id)
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_user(self) -> dict:
|
||||
"""Returns a user info dictionary"""
|
||||
"""
|
||||
返回当前认证的用户信息字典
|
||||
"""
|
||||
url = self.url.construct("user")
|
||||
return self.request.make_request(url)
|
||||
|
||||
def get_user_favorites(self) -> dict:
|
||||
"""Returns a user info dictionary"""
|
||||
"""
|
||||
返回当前认证用户的收藏夹信息字典
|
||||
"""
|
||||
url = self.url.construct('user/favorites')
|
||||
return self.request.make_request(url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tvdb = TVDB("ed2aa66b-7899-4677-92a7-67bc9ce3d93a")
|
||||
query = "romance in the alley"
|
||||
res = tvdb.search(query)
|
||||
print(res)
|
||||
|
||||
@@ -540,17 +540,18 @@ class Scheduler(metaclass=Singleton):
|
||||
self.remove_plugin_job(pid)
|
||||
# 获取插件服务列表
|
||||
with self._lock:
|
||||
plugin_manager = PluginManager()
|
||||
try:
|
||||
plugin_services = PluginManager().get_plugin_services(pid=pid)
|
||||
plugin_services = plugin_manager.get_plugin_services(pid=pid)
|
||||
except Exception as e:
|
||||
logger.error(f"运行插件 {pid} 服务失败:{str(e)} - {traceback.format_exc()}")
|
||||
return
|
||||
# 获取插件名称
|
||||
plugin_name = PluginManager().get_plugin_attr(pid, "plugin_name")
|
||||
plugin_name = plugin_manager.get_plugin_attr(pid, "plugin_name")
|
||||
# 开始注册插件服务
|
||||
for service in plugin_services:
|
||||
try:
|
||||
sid = f"{service['id']}"
|
||||
sid = f"{pid}_{service['id']}"
|
||||
job_id = sid.split("|")[0]
|
||||
self.remove_plugin_job(pid, job_id)
|
||||
self._jobs[job_id] = {
|
||||
@@ -599,8 +600,8 @@ class Scheduler(metaclass=Singleton):
|
||||
name = service.get("name")
|
||||
provider_name = service.get("provider_name")
|
||||
if service.get("running") and name and provider_name:
|
||||
if name not in added:
|
||||
added.append(name)
|
||||
if job_id not in added:
|
||||
added.append(job_id)
|
||||
schedulers.append(schemas.ScheduleInfo(
|
||||
id=job_id,
|
||||
name=name,
|
||||
@@ -609,11 +610,11 @@ class Scheduler(metaclass=Singleton):
|
||||
))
|
||||
# 获取其他待执行任务
|
||||
for job in jobs:
|
||||
if job.name not in added:
|
||||
added.append(job.name)
|
||||
job_id = job.id.split("|")[0]
|
||||
if job_id not in added:
|
||||
added.append(job_id)
|
||||
else:
|
||||
continue
|
||||
job_id = job.id.split("|")[0]
|
||||
service = self._jobs.get(job_id)
|
||||
if not service:
|
||||
continue
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.5.1'
|
||||
FRONTEND_VERSION = 'v2.5.1'
|
||||
APP_VERSION = 'v2.5.2'
|
||||
FRONTEND_VERSION = 'v2.5.2'
|
||||
|
||||
Reference in New Issue
Block a user