mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-13 23:16:46 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2096e8e0f | ||
|
|
75e80158e5 | ||
|
|
d42bd14288 | ||
|
|
28f6e7f9bb | ||
|
|
2aadbeaed7 | ||
|
|
3f6b4bf3f2 | ||
|
|
f73750fcf7 | ||
|
|
59df673eb5 | ||
|
|
e29ab92cd1 | ||
|
|
3777045a17 | ||
|
|
16165c0fcc | ||
|
|
4d377d5e04 | ||
|
|
76c84f9bac | ||
|
|
88f91152d6 | ||
|
|
dfdb88c5ac |
@@ -335,6 +335,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
:param _path: 元数据文件路径
|
||||
:param _content: 文件内容
|
||||
"""
|
||||
if not _fileitem or not _content or not _path:
|
||||
return
|
||||
tmp_file = settings.TEMP_PATH / _path.name
|
||||
tmp_file.write_bytes(_content)
|
||||
logger.info(f"保存文件:【{_fileitem.storage}】{_path}")
|
||||
@@ -356,6 +358,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
logger.info(f"{_url} 图片下载失败,请检查网络连通性!")
|
||||
except Exception as err:
|
||||
logger.error(f"{_url} 图片下载失败:{str(err)}!")
|
||||
return None
|
||||
|
||||
# 当前文件路径
|
||||
filepath = Path(fileitem.path)
|
||||
@@ -410,7 +413,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
# 下载图片
|
||||
content = __download_image(_url=attr_value)
|
||||
# 写入图片到当前目录
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
if content:
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
else:
|
||||
# 电视剧
|
||||
if fileitem.type == "file":
|
||||
@@ -447,7 +451,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
# 下载图片
|
||||
content = __download_image(image_url)
|
||||
# 保存图片文件到当前目录
|
||||
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
||||
if content:
|
||||
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
||||
|
||||
else:
|
||||
# 当前为目录,处理目录内的文件
|
||||
@@ -485,7 +490,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
# 下载图片
|
||||
content = __download_image(image_url)
|
||||
# 保存图片文件到当前目录
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
if content:
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
# 判断当前目录是不是剧集根目录
|
||||
if season_meta.name:
|
||||
# 当前目录有名称,生成tvshow nfo 和 tv图片
|
||||
@@ -511,6 +517,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
# 下载图片
|
||||
content = __download_image(image_url)
|
||||
# 保存图片文件到当前目录
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
if content:
|
||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
||||
|
||||
logger.info(f"{filepath.name} 刮削完成")
|
||||
|
||||
@@ -30,8 +30,8 @@ class MetaVideo(MetaBase):
|
||||
_episode_re = r"EP?(\d{2,4})$|^EP?(\d{1,4})$|^S\d{1,2}EP?(\d{1,4})$|S\d{2}EP?(\d{2,4})"
|
||||
_part_re = r"(^PART[0-9ABI]{0,2}$|^CD[0-9]{0,2}$|^DVD[0-9]{0,2}$|^DISK[0-9]{0,2}$|^DISC[0-9]{0,2}$)"
|
||||
_roman_numerals = r"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"
|
||||
_source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$"
|
||||
_effect_re = r"^REMUX$|^UHD$|^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$"
|
||||
_source_re = r"^BLURAY$|^HDTV$|^UHDTV$|^HDDVD$|^WEBRIP$|^DVDRIP$|^BDRIP$|^BLU$|^WEB$|^BD$|^HDRip$|^REMUX$|^UHD$|^REPACK$"
|
||||
_effect_re = r"^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$"
|
||||
_resources_type_re = r"%s|%s" % (_source_re, _effect_re)
|
||||
_name_no_begin_re = r"^[\[【].+?[\]】]"
|
||||
_name_no_chinese_re = r".*版|.*字幕"
|
||||
|
||||
@@ -9,17 +9,19 @@ class FormatParser(object):
|
||||
_split_chars = r"\.|\s+|\(|\)|\[|]|-|\+|【|】|/|~|;|&|\||#|_|「|」|~"
|
||||
|
||||
def __init__(self, eformat: str, details: str = None, part: str = None,
|
||||
offset: int = None, key: str = "ep"):
|
||||
offset: str = None, key: str = "ep"):
|
||||
"""
|
||||
:params eformat: 格式化字符串
|
||||
:params details: 格式化详情
|
||||
:params part: 分集
|
||||
:params offset: 偏移量
|
||||
:params offset: 偏移量 -10/EP*2
|
||||
:prams key: EP关键字
|
||||
"""
|
||||
self._format = eformat
|
||||
self._start_ep = None
|
||||
self._end_ep = None
|
||||
self.__offset = offset or "EP"
|
||||
self._key = key
|
||||
self._part = None
|
||||
if part:
|
||||
self._part = part
|
||||
@@ -34,8 +36,6 @@ class FormatParser(object):
|
||||
self._end_ep = int(tmp[0]) if int(tmp[0]) > int(tmp[1]) else int(tmp[1])
|
||||
else:
|
||||
self._start_ep = self._end_ep = int(tmp[0])
|
||||
self.__offset = int(offset) if offset else 0
|
||||
self._key = key
|
||||
|
||||
@property
|
||||
def format(self):
|
||||
@@ -77,15 +77,21 @@ class FormatParser(object):
|
||||
if self._start_ep is not None and self._start_ep == self._end_ep:
|
||||
if isinstance(self._start_ep, str):
|
||||
s, e = self._start_ep.split("-")
|
||||
start_ep = self.__offset.replace("EP", s)
|
||||
end_ep = self.__offset.replace("EP", e)
|
||||
if int(s) == int(e):
|
||||
return int(s) + self.__offset, None, self.part
|
||||
return int(s) + self.__offset, int(e) + self.__offset, self.part
|
||||
return self._start_ep + self.__offset, None, self.part
|
||||
return int(eval(start_ep)), None, self.part
|
||||
return int(eval(start_ep)), int(eval(end_ep)), self.part
|
||||
else:
|
||||
start_ep = self.__offset.replace("EP", str(self._start_ep))
|
||||
return int(eval(start_ep)), None, self.part
|
||||
if not self._format:
|
||||
return self._start_ep, self._end_ep, self.part
|
||||
s, e = self.__handle_single(file_name)
|
||||
return s + self.__offset if s is not None else None, \
|
||||
e + self.__offset if e is not None else None, self.part
|
||||
else:
|
||||
s, e = self.__handle_single(file_name)
|
||||
start_ep = self.__offset.replace("EP", str(s)) if s else None
|
||||
end_ep = self.__offset.replace("EP", str(e)) if e else None
|
||||
return int(eval(start_ep)) if start_ep else None, int(eval(end_ep)) if end_ep else None, self.part
|
||||
|
||||
def __handle_single(self, file: str) -> Tuple[Optional[int], Optional[int]]:
|
||||
"""
|
||||
|
||||
@@ -225,12 +225,13 @@ class RssHelper:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse(url, proxy: bool = False, timeout: int = 15) -> Union[List[dict], None]:
|
||||
def parse(url, proxy: bool = False, timeout: int = 15, headers: dict = None) -> Union[List[dict], None]:
|
||||
"""
|
||||
解析RSS订阅URL,获取RSS中的种子信息
|
||||
:param url: RSS地址
|
||||
:param proxy: 是否使用代理
|
||||
:param timeout: 请求超时
|
||||
:param headers: 自定义请求头
|
||||
:return: 种子信息列表,如为None代表Rss过期
|
||||
"""
|
||||
# 开始处理
|
||||
@@ -238,7 +239,8 @@ class RssHelper:
|
||||
if not url:
|
||||
return []
|
||||
try:
|
||||
ret = RequestUtils(proxies=settings.PROXY if proxy else None, timeout=timeout).get_res(url)
|
||||
ret = RequestUtils(proxies=settings.PROXY if proxy else None,
|
||||
timeout=timeout, headers=headers).get_res(url)
|
||||
if not ret:
|
||||
return []
|
||||
except Exception as err:
|
||||
|
||||
@@ -52,7 +52,7 @@ class DoubanCache(metaclass=Singleton):
|
||||
获取缓存KEY
|
||||
"""
|
||||
return f"[{meta.type.value if meta.type else '未知'}]" \
|
||||
f"{meta.name or meta.doubanid}-{meta.year}-{meta.begin_season}"
|
||||
f"{meta.doubanid or meta.name}-{meta.year}-{meta.begin_season}"
|
||||
|
||||
def get(self, meta: MetaBase):
|
||||
"""
|
||||
|
||||
@@ -462,6 +462,11 @@ class FileManagerModule(_ModuleBase):
|
||||
# 上传文件
|
||||
new_item = target_oper.upload(target_fileitem, filepath)
|
||||
if new_item:
|
||||
# 重命名为目标文件名
|
||||
if new_item.name != target_file.name:
|
||||
if target_oper.rename(new_item, target_file.name):
|
||||
new_item.name = target_file.name
|
||||
new_item.path = str(Path(new_item.path).parent / target_file.name)
|
||||
return new_item, ""
|
||||
else:
|
||||
return None, f"{fileitem.path} 上传 {target_storage} 失败"
|
||||
@@ -475,6 +480,11 @@ class FileManagerModule(_ModuleBase):
|
||||
# 上传文件
|
||||
new_item = target_oper.upload(target_fileitem, filepath)
|
||||
if new_item:
|
||||
# 重命名为目标文件名
|
||||
if new_item.name != target_file.name:
|
||||
if target_oper.rename(new_item, target_file.name):
|
||||
new_item.name = target_file.name
|
||||
new_item.path = str(Path(new_item.path).parent / target_file.name)
|
||||
# 删除源文件
|
||||
source_oper.delete(fileitem)
|
||||
return new_item, ""
|
||||
|
||||
@@ -222,7 +222,7 @@ class LocalStorage(StorageBase):
|
||||
软链接文件
|
||||
"""
|
||||
file_path = Path(fileitem.path)
|
||||
code, message = SystemUtils.copy(file_path, target_file)
|
||||
code, message = SystemUtils.softlink(file_path, target_file)
|
||||
if code != 0:
|
||||
logger.error(f"软链接文件失败:{message}")
|
||||
return False
|
||||
|
||||
@@ -468,6 +468,30 @@ class Jellyfin:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_item_path_by_id(self, item_id: str) -> Optional[str]:
|
||||
"""
|
||||
根据ItemId查询所在的Path
|
||||
:param item_id: 在Jellyfin中的ID
|
||||
:return: Path
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
url = f"{self._host}Items/{item_id}/PlaybackInfo"
|
||||
params = {"api_key": self._apikey}
|
||||
try:
|
||||
res = RequestUtils(timeout=10).get_res(url, params)
|
||||
if res:
|
||||
media_sources = res.json().get("MediaSources")
|
||||
if media_sources:
|
||||
return media_sources[0].get("Path")
|
||||
else:
|
||||
logger.error("Items/Id/PlaybackInfo 未获取到返回数据,不设置 Path")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("连接Items/Id/PlaybackInfo出错:" + str(e))
|
||||
return None
|
||||
return None
|
||||
|
||||
def generate_image_link(self, item_id: str, image_type: str, host_type: bool) -> Optional[str]:
|
||||
"""
|
||||
根据ItemId和imageType查询本地对应图片
|
||||
@@ -662,6 +686,8 @@ class Jellyfin:
|
||||
item_id=eventItem.item_id,
|
||||
image_type="Backdrop"
|
||||
)
|
||||
# jellyfin 的 webhook 不含 item_path,需要单独获取
|
||||
eventItem.item_path = self.get_item_path_by_id(eventItem.item_id)
|
||||
|
||||
return eventItem
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class TmdbCache(metaclass=Singleton):
|
||||
"""
|
||||
获取缓存KEY
|
||||
"""
|
||||
return f"[{meta.type.value if meta.type else '未知'}]{meta.name or meta.tmdbid}-{meta.year}-{meta.begin_season}"
|
||||
return f"[{meta.type.value if meta.type else '未知'}]{meta.tmdbid or meta.name}-{meta.year}-{meta.begin_season}"
|
||||
|
||||
def get(self, meta: MetaBase):
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
@@ -27,12 +26,6 @@ from app.schemas.types import EventType
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.timer import TimerUtils
|
||||
|
||||
# 获取 apscheduler 的日志记录器
|
||||
scheduler_logger = logging.getLogger('apscheduler')
|
||||
|
||||
# 设置日志级别为 WARNING
|
||||
scheduler_logger.setLevel(logging.WARNING)
|
||||
|
||||
|
||||
class SchedulerChain(ChainBase):
|
||||
pass
|
||||
@@ -436,23 +429,23 @@ class Scheduler(metaclass=Singleton):
|
||||
try:
|
||||
sid = f"{service['id']}"
|
||||
job_id = sid.split("|")[0]
|
||||
if job_id not in self._jobs:
|
||||
self._jobs[job_id] = {
|
||||
"func": service["func"],
|
||||
"name": service["name"],
|
||||
"pid": pid,
|
||||
"plugin_name": plugin_name,
|
||||
"running": False,
|
||||
}
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
service["trigger"],
|
||||
id=sid,
|
||||
name=service["name"],
|
||||
**service["kwargs"],
|
||||
kwargs={"job_id": job_id}
|
||||
)
|
||||
logger.info(f"注册插件{plugin_name}服务:{service['name']} - {service['trigger']}")
|
||||
self._jobs[job_id] = {
|
||||
"func": service["func"],
|
||||
"name": service["name"],
|
||||
"pid": pid,
|
||||
"plugin_name": plugin_name,
|
||||
"running": False,
|
||||
}
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
service["trigger"],
|
||||
id=sid,
|
||||
name=service["name"],
|
||||
**service["kwargs"],
|
||||
kwargs={"job_id": job_id},
|
||||
replace_existing=True
|
||||
)
|
||||
logger.info(f"注册插件{plugin_name}服务:{service['name']} - {service['trigger']}")
|
||||
except Exception as e:
|
||||
logger.error(f"注册插件{plugin_name}服务失败:{str(e)} - {service}")
|
||||
SchedulerChain().messagehelper.put(title=f"插件 {plugin_name} 服务注册失败",
|
||||
@@ -468,14 +461,25 @@ class Scheduler(metaclass=Singleton):
|
||||
with self._lock:
|
||||
# 获取插件名称
|
||||
plugin_name = PluginManager().get_plugin_attr(pid, "plugin_name")
|
||||
for job_id, service in self._jobs.copy().items():
|
||||
# 先从 _jobs 中查找匹配的服务
|
||||
jobs_to_remove = [(job_id, service) for job_id, service in self._jobs.items() if service.get("pid") == pid]
|
||||
if not jobs_to_remove:
|
||||
return
|
||||
for job_id, service in jobs_to_remove:
|
||||
try:
|
||||
if service.get("pid") == pid:
|
||||
self._jobs.pop(job_id, None)
|
||||
try:
|
||||
self._scheduler.remove_job(job_id)
|
||||
except JobLookupError:
|
||||
pass
|
||||
# 移除服务
|
||||
self._jobs.pop(job_id, None)
|
||||
# 在调度器中查找并移除对应的 job
|
||||
job_removed = False
|
||||
for job in list(self._scheduler.get_jobs()):
|
||||
job_id_from_service = job.id.split("|")[0]
|
||||
if job_id == job_id_from_service:
|
||||
try:
|
||||
self._scheduler.remove_job(job.id)
|
||||
job_removed = True
|
||||
except JobLookupError:
|
||||
pass
|
||||
if job_removed:
|
||||
logger.info(f"移除插件服务({plugin_name}):{service.get('name')}")
|
||||
except Exception as e:
|
||||
logger.error(f"移除插件服务失败:{str(e)} - {job_id}: {service}")
|
||||
|
||||
@@ -86,4 +86,4 @@ class EpisodeFormat(BaseModel):
|
||||
format: Optional[str] = None
|
||||
detail: Optional[str] = None
|
||||
part: Optional[str] = None
|
||||
offset: Optional[int] = None
|
||||
offset: Optional[str] = None
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.0.1'
|
||||
FRONTEND_VERSION = 'v2.0.1'
|
||||
APP_VERSION = 'v2.0.2'
|
||||
FRONTEND_VERSION = 'v2.0.2'
|
||||
|
||||
Reference in New Issue
Block a user