mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-14 23:16:48 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73eba90f2f | ||
|
|
62e74f6fd1 | ||
|
|
4375e48840 | ||
|
|
a1d6e94e90 | ||
|
|
1f44e13ff0 | ||
|
|
d2992f9ced | ||
|
|
950337bccc | ||
|
|
757c3be359 | ||
|
|
269ab9adfc | ||
|
|
bd241a5164 | ||
|
|
3d92b57f24 | ||
|
|
70d8cb3697 | ||
|
|
9e4ec5841c | ||
|
|
682f4fe608 | ||
|
|
ce8a077e07 | ||
|
|
d5f63bcdb3 | ||
|
|
5c3756fd1b | ||
|
|
99939e1a3d | ||
|
|
56742ace11 | ||
|
|
742cb7a8da |
@@ -22,7 +22,7 @@ from app.helper.service import ServiceConfigHelper
|
||||
from app.log import logger
|
||||
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \
|
||||
WebhookEventInfo, TmdbEpisode, MediaPerson, FileItem, TransferDirectoryConf
|
||||
from app.schemas.types import TorrentStatus, MediaType, MediaImageType, EventType
|
||||
from app.schemas.types import TorrentStatus, MediaType, MediaImageType, EventType, MessageChannel
|
||||
from app.utils.object import ObjectUtils
|
||||
|
||||
|
||||
@@ -641,6 +641,19 @@ class ChainBase(metaclass=ABCMeta):
|
||||
return self.messagequeue.send_message("post_torrents_message", message=message, torrents=torrents,
|
||||
immediately=True if message.userid else False)
|
||||
|
||||
def delete_message(self, channel: MessageChannel, source: str,
|
||||
message_id: Union[str, int], chat_id: Optional[Union[str, int]] = None) -> bool:
|
||||
"""
|
||||
删除消息
|
||||
:param channel: 消息渠道
|
||||
:param source: 消息源(指定特定的消息模块)
|
||||
:param message_id: 消息ID
|
||||
:param chat_id: 聊天ID(如群组ID)
|
||||
:return: 删除是否成功
|
||||
"""
|
||||
return self.run_module("delete_message", channel=channel, source=source,
|
||||
message_id=message_id, chat_id=chat_id)
|
||||
|
||||
def metadata_img(self, mediainfo: MediaInfo,
|
||||
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[dict]:
|
||||
"""
|
||||
|
||||
@@ -427,7 +427,7 @@ class MediaChain(ChainBase):
|
||||
"""
|
||||
try:
|
||||
logger.info(f"正在下载图片:{_url} ...")
|
||||
r = RequestUtils(proxies=settings.PROXY).get_res(url=_url)
|
||||
r = RequestUtils(proxies=settings.PROXY, ua=settings.USER_AGENT).get_res(url=_url)
|
||||
if r:
|
||||
return r.content
|
||||
else:
|
||||
@@ -506,7 +506,9 @@ class MediaChain(ChainBase):
|
||||
# 根据图片类型检查开关
|
||||
if 'poster' in image_name.lower():
|
||||
should_scrape = scraping_switchs.get('movie_poster', True)
|
||||
elif 'backdrop' in image_name.lower() or 'fanart' in image_name.lower():
|
||||
elif ('backdrop' in image_name.lower()
|
||||
or 'fanart' in image_name.lower()
|
||||
or 'background' in image_name.lower()):
|
||||
should_scrape = scraping_switchs.get('movie_backdrop', True)
|
||||
elif 'logo' in image_name.lower():
|
||||
should_scrape = scraping_switchs.get('movie_logo', True)
|
||||
@@ -700,7 +702,9 @@ class MediaChain(ChainBase):
|
||||
# 根据电视剧图片类型检查开关
|
||||
if 'poster' in image_name.lower():
|
||||
should_scrape = scraping_switchs.get('tv_poster', True)
|
||||
elif 'backdrop' in image_name.lower() or 'fanart' in image_name.lower():
|
||||
elif ('backdrop' in image_name.lower()
|
||||
or 'fanart' in image_name.lower()
|
||||
or 'background' in image_name.lower()):
|
||||
should_scrape = scraping_switchs.get('tv_backdrop', True)
|
||||
elif 'banner' in image_name.lower():
|
||||
should_scrape = scraping_switchs.get('tv_banner', True)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import gc
|
||||
import re
|
||||
from typing import Any, Optional, Dict, Union, List
|
||||
|
||||
@@ -133,10 +132,10 @@ class MessageChain(ChainBase):
|
||||
"""
|
||||
# 申明全局变量
|
||||
global _current_page, _current_meta, _current_media
|
||||
# 加载缓存
|
||||
user_cache: Dict[str, dict] = self.load_cache(self._cache_file) or {}
|
||||
# 处理消息
|
||||
logger.info(f'收到用户消息内容,用户:{userid},内容:{text}')
|
||||
# 加载缓存
|
||||
user_cache: Dict[str, dict] = self.load_cache(self._cache_file) or {}
|
||||
# 保存消息
|
||||
if not text.startswith('CALLBACK:'):
|
||||
self.messagehelper.put(
|
||||
@@ -272,6 +271,18 @@ class MessageChain(ChainBase):
|
||||
"type": "Torrent",
|
||||
"items": contexts
|
||||
}
|
||||
_current_page = 0
|
||||
# 保存缓存
|
||||
self.save_cache(user_cache, self._cache_file)
|
||||
# 删除原消息
|
||||
if (original_message_id and original_chat_id and
|
||||
ChannelCapabilityManager.supports_deletion(channel)):
|
||||
self.delete_message(
|
||||
channel=channel,
|
||||
source=source,
|
||||
message_id=original_message_id,
|
||||
chat_id=original_chat_id
|
||||
)
|
||||
# 发送种子数据
|
||||
logger.info(f"搜索到 {len(contexts)} 条数据,开始发送选择消息 ...")
|
||||
self.__post_torrents_message(channel=channel,
|
||||
@@ -279,9 +290,7 @@ class MessageChain(ChainBase):
|
||||
title=mediainfo.title,
|
||||
items=contexts[:self._page_size],
|
||||
userid=userid,
|
||||
total=len(contexts),
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id)
|
||||
total=len(contexts))
|
||||
|
||||
elif cache_type in ["Subscribe", "ReSubscribe"]:
|
||||
# 订阅或洗版媒体
|
||||
@@ -467,10 +476,12 @@ class MessageChain(ChainBase):
|
||||
logger.info(f"搜索到 {len(medias)} 条相关媒体信息")
|
||||
# 记录当前状态
|
||||
_current_meta = meta
|
||||
# 保存缓存
|
||||
user_cache[userid] = {
|
||||
'type': action,
|
||||
'items': medias
|
||||
}
|
||||
self.save_cache(user_cache, self._cache_file)
|
||||
_current_page = 0
|
||||
_current_media = None
|
||||
# 发送媒体列表
|
||||
@@ -478,9 +489,7 @@ class MessageChain(ChainBase):
|
||||
source=source,
|
||||
title=meta.name,
|
||||
items=medias[:self._page_size],
|
||||
userid=userid, total=len(medias),
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id)
|
||||
userid=userid, total=len(medias))
|
||||
else:
|
||||
# 广播事件
|
||||
self.eventmanager.send_event(
|
||||
@@ -493,15 +502,6 @@ class MessageChain(ChainBase):
|
||||
}
|
||||
)
|
||||
|
||||
# 保存缓存
|
||||
self.save_cache(user_cache, self._cache_file)
|
||||
|
||||
# 清理内存
|
||||
user_cache.clear()
|
||||
del user_cache
|
||||
|
||||
gc.collect()
|
||||
|
||||
def _handle_callback(self, text: str, channel: MessageChannel, source: str,
|
||||
userid: Union[str, int], username: str,
|
||||
original_message_id: Optional[Union[str, int]] = None,
|
||||
@@ -509,6 +509,9 @@ class MessageChain(ChainBase):
|
||||
"""
|
||||
处理按钮回调
|
||||
"""
|
||||
|
||||
global _current_media
|
||||
|
||||
# 提取回调数据
|
||||
callback_data = text[9:] # 去掉 "CALLBACK:" 前缀
|
||||
logger.info(f"处理按钮回调:{callback_data}")
|
||||
@@ -533,101 +536,20 @@ class MessageChain(ChainBase):
|
||||
return
|
||||
|
||||
# 解析系统回调数据
|
||||
if callback_data.startswith("page_"):
|
||||
# 翻页操作
|
||||
self._handle_page_callback(callback_data=callback_data, channel=channel,
|
||||
source=source, userid=userid,
|
||||
original_message_id=original_message_id, original_chat_id=original_chat_id)
|
||||
elif callback_data.startswith("select_"):
|
||||
# 选择操作或翻页操作
|
||||
if callback_data in ["select_p", "select_n"]:
|
||||
# 翻页操作
|
||||
page_text = callback_data.split("_")[1] # 提取 "p" 或 "n"
|
||||
self.handle_message(channel=channel, source=source,
|
||||
userid=userid, username=username,
|
||||
text=page_text,
|
||||
original_message_id=original_message_id, original_chat_id=original_chat_id)
|
||||
else:
|
||||
# 选择操作
|
||||
self._handle_select_callback(callback_data=callback_data, channel=channel,
|
||||
source=source, userid=userid, username=username)
|
||||
elif callback_data.startswith("download_"):
|
||||
# 下载操作
|
||||
self._handle_download_callback(callback_data=callback_data, channel=channel,
|
||||
source=source, userid=userid, username=username)
|
||||
elif callback_data.startswith("subscribe_"):
|
||||
# 订阅操作
|
||||
self._handle_subscribe_callback(callback_data=callback_data, channel=channel,
|
||||
source=source, userid=userid, username=username)
|
||||
else:
|
||||
# 其他自定义回调
|
||||
logger.info(f"未知的回调数据:{callback_data}")
|
||||
|
||||
def _handle_page_callback(self, callback_data: str, channel: MessageChannel, source: str,
|
||||
userid: Union[str, int], original_message_id: Optional[Union[str, int]],
|
||||
original_chat_id: Optional[str]):
|
||||
"""
|
||||
处理翻页回调
|
||||
"""
|
||||
try:
|
||||
page = int(callback_data.split("_")[1])
|
||||
|
||||
# 获取当前页面
|
||||
global _current_page
|
||||
|
||||
# 判断是上一页还是下一页
|
||||
if page < _current_page:
|
||||
# 上一页,调用原来的 "p" 逻辑
|
||||
self.handle_message(channel=channel, source=source, userid=userid,
|
||||
username="", text="p",
|
||||
original_message_id=original_message_id, original_chat_id=original_chat_id)
|
||||
elif page > _current_page:
|
||||
# 下一页,调用原来的 "n" 逻辑
|
||||
self.handle_message(channel=channel, source=source, userid=userid,
|
||||
username="", text="n",
|
||||
original_message_id=original_message_id, original_chat_id=original_chat_id)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"处理翻页回调失败:{e}")
|
||||
|
||||
def _handle_select_callback(self, callback_data: str, channel: MessageChannel, source: str,
|
||||
userid: Union[str, int], username: str) -> None:
|
||||
"""
|
||||
处理选择回调
|
||||
"""
|
||||
try:
|
||||
index = int(callback_data.split("_")[1])
|
||||
# 调用原有的数字选择逻辑
|
||||
self.handle_message(channel=channel, source=source, userid=userid, username=username, text=str(index + 1))
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"处理选择回调失败:{e}")
|
||||
|
||||
def _handle_download_callback(self, callback_data: str, channel: MessageChannel, source: str,
|
||||
userid: Union[str, int], username: str) -> None:
|
||||
"""
|
||||
处理下载回调
|
||||
"""
|
||||
try:
|
||||
if callback_data == "download_auto":
|
||||
# 自动选择下载
|
||||
self.handle_message(channel=channel, source=source, userid=userid, username=username, text="0")
|
||||
else:
|
||||
index = int(callback_data.split("_")[1])
|
||||
self.handle_message(channel=channel, source=source, userid=userid, username=username,
|
||||
text=str(index + 1))
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"处理下载回调失败:{e}")
|
||||
|
||||
def _handle_subscribe_callback(self, callback_data: str, channel: MessageChannel, source: str,
|
||||
userid: Union[str, int], username: str) -> None:
|
||||
"""
|
||||
处理订阅回调
|
||||
"""
|
||||
try:
|
||||
index = int(callback_data.split("_")[1])
|
||||
self.handle_message(channel=channel, source=source, userid=userid, username=username, text=str(index + 1))
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"处理订阅回调失败:{e}")
|
||||
page_text = callback_data.split("_", 1)[1]
|
||||
self.handle_message(channel=channel, source=source, userid=userid, username=username,
|
||||
text=page_text,
|
||||
original_message_id=original_message_id, original_chat_id=original_chat_id)
|
||||
except IndexError:
|
||||
logger.error(f"回调数据格式错误:{callback_data}")
|
||||
self.post_message(Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="回调数据格式错误,请检查!"
|
||||
))
|
||||
|
||||
def __auto_download(self, channel: MessageChannel, source: str, cache_list: list[Context],
|
||||
userid: Union[str, int], username: str,
|
||||
@@ -742,7 +664,7 @@ class MessageChain(ChainBase):
|
||||
|
||||
buttons.append([{
|
||||
"text": button_text,
|
||||
"callback_data": f"select_{_current_page * self._page_size + i}"
|
||||
"callback_data": f"select_{i + 1}"
|
||||
}])
|
||||
else:
|
||||
# 多按钮一行的情况,使用简化文本
|
||||
@@ -750,7 +672,7 @@ class MessageChain(ChainBase):
|
||||
|
||||
current_row.append({
|
||||
"text": button_text,
|
||||
"callback_data": f"select_{_current_page * self._page_size + i}"
|
||||
"callback_data": f"select_{i + 1}"
|
||||
})
|
||||
|
||||
# 如果当前行已满或者是最后一个按钮,添加到按钮列表
|
||||
@@ -762,9 +684,9 @@ class MessageChain(ChainBase):
|
||||
if total > self._page_size:
|
||||
page_buttons = []
|
||||
if _current_page > 0:
|
||||
page_buttons.append({"text": "⬅️ 上一页", "callback_data": "select_p"})
|
||||
page_buttons.append({"text": "⬅️ 上一页", "callback_data": "page_p"})
|
||||
if (_current_page + 1) * self._page_size < total:
|
||||
page_buttons.append({"text": "下一页 ➡️", "callback_data": "select_n"})
|
||||
page_buttons.append({"text": "下一页 ➡️", "callback_data": "page_n"})
|
||||
if page_buttons:
|
||||
buttons.append(page_buttons)
|
||||
|
||||
@@ -821,7 +743,7 @@ class MessageChain(ChainBase):
|
||||
max_per_row = ChannelCapabilityManager.get_max_buttons_per_row(channel)
|
||||
|
||||
# 自动选择按钮
|
||||
buttons.append([{"text": "🤖 自动选择下载", "callback_data": "download_auto"}])
|
||||
buttons.append([{"text": "🤖 自动选择下载", "callback_data": "download_0"}])
|
||||
|
||||
# 为每个种子项创建下载按钮
|
||||
current_row = []
|
||||
@@ -837,7 +759,7 @@ class MessageChain(ChainBase):
|
||||
|
||||
buttons.append([{
|
||||
"text": button_text,
|
||||
"callback_data": f"download_{_current_page * self._page_size + i}"
|
||||
"callback_data": f"download_{i + 1}"
|
||||
}])
|
||||
else:
|
||||
# 多按钮一行的情况,使用简化文本
|
||||
@@ -845,7 +767,7 @@ class MessageChain(ChainBase):
|
||||
|
||||
current_row.append({
|
||||
"text": button_text,
|
||||
"callback_data": f"download_{_current_page * self._page_size + i}"
|
||||
"callback_data": f"download_{i + 1}"
|
||||
})
|
||||
|
||||
# 如果当前行已满或者是最后一个按钮,添加到按钮列表
|
||||
@@ -857,9 +779,9 @@ class MessageChain(ChainBase):
|
||||
if total > self._page_size:
|
||||
page_buttons = []
|
||||
if _current_page > 0:
|
||||
page_buttons.append({"text": "⬅️ 上一页", "callback_data": "select_p"})
|
||||
page_buttons.append({"text": "⬅️ 上一页", "callback_data": "page_p"})
|
||||
if (_current_page + 1) * self._page_size < total:
|
||||
page_buttons.append({"text": "下一页 ➡️", "callback_data": "select_n"})
|
||||
page_buttons.append({"text": "下一页 ➡️", "callback_data": "page_n"})
|
||||
if page_buttons:
|
||||
buttons.append(page_buttons)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from app.chain.site import SiteChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.system import SystemChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.event import Event as ManagerEvent, eventmanager, Event
|
||||
from app.core.plugin import PluginManager
|
||||
from app.helper.message import MessageHelper
|
||||
@@ -162,10 +161,6 @@ class Command(metaclass=Singleton):
|
||||
"""
|
||||
初始化菜单命令
|
||||
"""
|
||||
if settings.DEV:
|
||||
logger.debug("Development mode active. Skipping command initialization.")
|
||||
return
|
||||
|
||||
# 使用线程池提交后台任务,避免引起阻塞
|
||||
ThreadHelper().submit(self.__init_commands_background, pid)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.core.meta.releasegroup import ReleaseGroupsMatcher
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.tokens import Tokens
|
||||
from app.core.meta.streamingplatform import StreamingPlatforms
|
||||
|
||||
|
||||
class MetaVideo(MetaBase):
|
||||
@@ -31,7 +32,7 @@ class MetaVideo(MetaBase):
|
||||
_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$|^REMUX$|^UHD$"
|
||||
_effect_re = r"^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$|^HLG$|^HDR10(\+|Plus)$"
|
||||
_effect_re = r"^SDR$|^HDR\d*$|^DOLBY$|^DOVI$|^DV$|^3D$|^REPACK$|^HLG$|^HDR10(\+|Plus)$|^EDR$|^HQ$"
|
||||
_resources_type_re = r"%s|%s" % (_source_re, _effect_re)
|
||||
_name_no_begin_re = r"^[\[【].+?[\]】]"
|
||||
_name_no_chinese_re = r".*版|.*字幕"
|
||||
@@ -51,7 +52,7 @@ class MetaVideo(MetaBase):
|
||||
_resources_pix_re = r"^[SBUHD]*(\d{3,4}[PI]+)|\d{3,4}X(\d{3,4})"
|
||||
_resources_pix_re2 = r"(^[248]+K)"
|
||||
_video_encode_re = r"^(H26[45])$|^(x26[45])$|^AVC$|^HEVC$|^VC\d?$|^MPEG\d?$|^Xvid$|^DivX$|^AV1$|^HDR\d*$|^AVS(\+|[23])$"
|
||||
_audio_encode_re = r"^DTS\d?$|^DTSHD$|^DTSHDMA$|^Atmos$|^TrueHD\d?$|^AC3$|^\dAudios?$|^DDP\d?$|^DD\+\d?$|^DD\d?$|^LPCM\d?$|^AAC\d?$|^FLAC\d?$|^HD\d?$|^MA\d?$|^HR\d?$|^Opus\d?$|^Vorbis\d?$"
|
||||
_audio_encode_re = r"^DTS\d?$|^DTSHD$|^DTSHDMA$|^Atmos$|^TrueHD\d?$|^AC3$|^\dAudios?$|^DDP\d?$|^DD\+\d?$|^DD\d?$|^LPCM\d?$|^AAC\d?$|^FLAC\d?$|^HD\d?$|^MA\d?$|^HR\d?$|^Opus\d?$|^Vorbis\d?$|^AV[3S]A$"
|
||||
|
||||
def __init__(self, title: str, subtitle: str = None, isfile: bool = False):
|
||||
"""
|
||||
@@ -66,6 +67,8 @@ class MetaVideo(MetaBase):
|
||||
original_title = title
|
||||
self._source = ""
|
||||
self._effect = []
|
||||
self.web_source = None
|
||||
self._index = 0
|
||||
# 判断是否纯数字命名
|
||||
if isfile \
|
||||
and title.isdigit() \
|
||||
@@ -93,9 +96,12 @@ class MetaVideo(MetaBase):
|
||||
# 拆分tokens
|
||||
tokens = Tokens(title)
|
||||
self.tokens = tokens
|
||||
# 实例化StreamingPlatforms对象
|
||||
streaming_platforms = StreamingPlatforms()
|
||||
# 解析名称、年份、季、集、资源类型、分辨率等
|
||||
token = tokens.get_next()
|
||||
while token:
|
||||
self._index += 1 # 更新当前处理的token索引
|
||||
# Part
|
||||
self.__init_part(token)
|
||||
# 标题
|
||||
@@ -116,6 +122,9 @@ class MetaVideo(MetaBase):
|
||||
# 资源类型
|
||||
if self._continue_flag:
|
||||
self.__init_resource_type(token)
|
||||
# 流媒体平台
|
||||
if self._continue_flag:
|
||||
self.__init_web_source(token, streaming_platforms)
|
||||
# 视频编码
|
||||
if self._continue_flag:
|
||||
self.__init_video_encode(token)
|
||||
@@ -131,6 +140,9 @@ class MetaVideo(MetaBase):
|
||||
self.resource_effect = " ".join(self._effect)
|
||||
if self._source:
|
||||
self.resource_type = self._source.strip()
|
||||
# 添加流媒体平台
|
||||
if self.web_source:
|
||||
self.resource_type = f"{self.web_source} {self.resource_type}"
|
||||
# 提取原盘DIY
|
||||
if self.resource_type and "BluRay" in self.resource_type:
|
||||
if (self.subtitle and re.findall(r'D[Ii]Y', self.subtitle)) \
|
||||
@@ -574,6 +586,57 @@ class MetaVideo(MetaBase):
|
||||
self._effect.append(effect)
|
||||
self._last_token = effect.upper()
|
||||
|
||||
def __init_web_source(self, token: str, streaming_platforms: StreamingPlatforms):
|
||||
"""
|
||||
识别流媒体平台
|
||||
"""
|
||||
if not self.name:
|
||||
return
|
||||
|
||||
platform_name = None
|
||||
query_range = 1
|
||||
|
||||
prev_token = None
|
||||
prev_idx = self._index - 2
|
||||
if 0 <= prev_idx < len(self.tokens.tokens):
|
||||
prev_token = self.tokens.tokens[prev_idx]
|
||||
|
||||
next_token = self.tokens.peek()
|
||||
|
||||
if streaming_platforms.is_streaming_platform(token):
|
||||
platform_name = streaming_platforms.get_streaming_platform_name(token)
|
||||
else:
|
||||
for adjacent_token, is_next in [(prev_token, False), (next_token, True)]:
|
||||
if not adjacent_token or platform_name:
|
||||
continue
|
||||
|
||||
for separator in [" ", "-"]:
|
||||
if is_next:
|
||||
combined_token = f"{token}{separator}{adjacent_token}"
|
||||
else:
|
||||
combined_token = f"{adjacent_token}{separator}{token}"
|
||||
|
||||
if streaming_platforms.is_streaming_platform(combined_token):
|
||||
platform_name = streaming_platforms.get_streaming_platform_name(combined_token)
|
||||
query_range = 2
|
||||
if is_next:
|
||||
self.tokens.get_next()
|
||||
break
|
||||
|
||||
if not platform_name:
|
||||
return
|
||||
|
||||
web_tokens = ["WEB", "DL", "WEBDL", "WEBRIP"]
|
||||
match_start_idx = self._index - query_range
|
||||
match_end_idx = self._index - 1
|
||||
start_index = max(0, match_start_idx - query_range)
|
||||
end_index = min(len(self.tokens.tokens), match_end_idx + 1 + query_range)
|
||||
tokens_to_check = self.tokens.tokens[start_index:end_index]
|
||||
|
||||
if any(tok and tok.upper() in web_tokens for tok in tokens_to_check):
|
||||
self.web_source = platform_name
|
||||
self._continue_flag = False
|
||||
|
||||
def __init_video_encode(self, token: str):
|
||||
"""
|
||||
识别视频编码
|
||||
|
||||
104
app/core/meta/streamingplatform.py
Normal file
104
app/core/meta/streamingplatform.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
class StreamingPlatforms(metaclass=Singleton):
|
||||
"""
|
||||
流媒体平台简称与全称。
|
||||
"""
|
||||
STREAMING_PLATFORMS: List[Tuple[str, str]] = [
|
||||
("AMZN", "Amazon"),
|
||||
("NF", "Netflix"),
|
||||
("ATVP", "Apple TV+"),
|
||||
("iT", "iTunes"),
|
||||
("DSNP", "Disney+"),
|
||||
("HS", "Hotstar"),
|
||||
("APPS", "Disney+ MENA"),
|
||||
("PMTP", "Paramount+"),
|
||||
("HMAX", "Max"),
|
||||
("", "Max"),
|
||||
("HULU", "Hulu"),
|
||||
("MA", "Movies Anywhere"),
|
||||
("BCORE", "Bravia Core"),
|
||||
("MS", "Microsoft Store"),
|
||||
("SHO", "Showtime"),
|
||||
("STAN", "Stan"),
|
||||
("PCOK", "Peacock"),
|
||||
("SKST", "SkyShowtime"),
|
||||
("NOW", "Now TV"),
|
||||
("FXTL", "Foxtel Now"),
|
||||
("BNGE", "Binge"),
|
||||
("CRKL", "Crackle"),
|
||||
("RKTN", "Rakuten TV"),
|
||||
("ALL4", "All 4"),
|
||||
("AS", "Adult Swim"),
|
||||
("BRTB", "Brtb TV"),
|
||||
("CNLP", "Canal+"),
|
||||
("CRIT", "Criterion Channel"),
|
||||
("DSCP", "Discovery+"),
|
||||
("", "ESPN"),
|
||||
("FOOD", "Food Network"),
|
||||
("MUBI", "Mubi"),
|
||||
("PLAY", "Google Play"),
|
||||
("YT", "YouTube"),
|
||||
("", "friDay"),
|
||||
("", "KKTV"),
|
||||
("", "ofiii"),
|
||||
("", "LiTV"),
|
||||
("", "MyVideo"),
|
||||
("Hami", "Hami Video"),
|
||||
("", "meWATCH"),
|
||||
("CATCHPLAY", "CATCHPLAY+"),
|
||||
("", "LINE TV"),
|
||||
("VIU", "Viu"),
|
||||
("IQ", ""),
|
||||
("", "WeTV"),
|
||||
("ABMA", "Abema"),
|
||||
("ADN", ""),
|
||||
("AT-X", ""),
|
||||
("Baha", ""),
|
||||
("BG", "B-Global"),
|
||||
("CR", "Crunchyroll"),
|
||||
("", "DMM"),
|
||||
("FOD", ""),
|
||||
("FUNi", "Funimation"),
|
||||
("HIDI", "HIDIVE"),
|
||||
("UNXT", "U-NEXT"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""初始化流媒体平台匹配器"""
|
||||
self._lookup_cache = {}
|
||||
self._build_cache()
|
||||
|
||||
def _build_cache(self) -> None:
|
||||
"""
|
||||
构建查询缓存。
|
||||
"""
|
||||
self._lookup_cache.clear()
|
||||
for short_name, full_name in self.STREAMING_PLATFORMS:
|
||||
canonical_name = full_name or short_name
|
||||
if not canonical_name:
|
||||
continue
|
||||
|
||||
aliases = {short_name, full_name}
|
||||
for alias in aliases:
|
||||
if alias:
|
||||
self._lookup_cache[alias.upper()] = canonical_name
|
||||
|
||||
def get_streaming_platform_name(self, platform_code: str) -> Optional[str]:
|
||||
"""
|
||||
根据流媒体平台简称或全称获取标准名称。
|
||||
"""
|
||||
if platform_code is None:
|
||||
return None
|
||||
return self._lookup_cache.get(platform_code.upper())
|
||||
|
||||
def is_streaming_platform(self, name: str) -> bool:
|
||||
"""
|
||||
判断给定的字符串是否为已知的流媒体平台代码或名称。
|
||||
"""
|
||||
if name is None:
|
||||
return False
|
||||
return name.upper() in self._lookup_cache
|
||||
@@ -19,7 +19,6 @@ from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.module import ModuleHelper
|
||||
from app.helper.plugin import PluginHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
@@ -124,19 +123,8 @@ class PluginManager(metaclass=Singleton):
|
||||
|
||||
# 已安装插件
|
||||
installed_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
# 扫描插件目录
|
||||
if pid:
|
||||
# 加载指定插件
|
||||
plugins = ModuleHelper.load_with_pre_filter(
|
||||
"app.plugins",
|
||||
filter_func=lambda name, obj: check_module(obj) and name == pid
|
||||
)
|
||||
else:
|
||||
# 加载已安装插件
|
||||
plugins = ModuleHelper.load(
|
||||
"app.plugins",
|
||||
filter_func=lambda name, obj: check_module(obj) and name in installed_plugins
|
||||
)
|
||||
# 扫描插件目录,只加载符合条件的插件
|
||||
plugins = self._load_selective_plugins(pid, installed_plugins, check_module)
|
||||
# 排序
|
||||
plugins.sort(key=lambda x: x.plugin_order if hasattr(x, "plugin_order") else 0)
|
||||
for plugin in plugins:
|
||||
@@ -216,6 +204,80 @@ class PluginManager(metaclass=Singleton):
|
||||
self._running_plugins = {}
|
||||
logger.info("插件停止完成")
|
||||
|
||||
@staticmethod
|
||||
def _load_selective_plugins(pid: Optional[str], installed_plugins: List[str],
|
||||
check_module_func: Callable) -> List[Any]:
|
||||
"""
|
||||
选择性加载插件,只import符合条件的插件
|
||||
:param pid: 指定插件ID,为空则加载所有已安装插件
|
||||
:param installed_plugins: 已安装插件列表
|
||||
:param check_module_func: 模块检查函数
|
||||
:return: 插件类列表
|
||||
"""
|
||||
import importlib
|
||||
|
||||
plugins = []
|
||||
plugins_dir = settings.ROOT_PATH / "app" / "plugins"
|
||||
|
||||
if not plugins_dir.exists():
|
||||
logger.warning(f"插件目录不存在:{plugins_dir}")
|
||||
return plugins
|
||||
|
||||
# 确定需要加载的插件目录名称列表
|
||||
if pid:
|
||||
# 加载指定插件
|
||||
target_plugins = [pid.lower()]
|
||||
else:
|
||||
# 加载已安装插件
|
||||
target_plugins = [plugin_id.lower() for plugin_id in installed_plugins]
|
||||
|
||||
if not target_plugins:
|
||||
logger.debug("没有需要加载的插件")
|
||||
return plugins
|
||||
|
||||
# 扫描plugins目录
|
||||
_loaded_modules = set()
|
||||
for plugin_dir in plugins_dir.iterdir():
|
||||
if not plugin_dir.is_dir() or plugin_dir.name.startswith('_'):
|
||||
continue
|
||||
|
||||
# 检查是否是需要加载的插件
|
||||
if plugin_dir.name not in target_plugins:
|
||||
logger.debug(f"跳过插件目录:{plugin_dir.name}(不在加载列表中)")
|
||||
continue
|
||||
|
||||
# 检查__init__.py是否存在
|
||||
init_file = plugin_dir / "__init__.py"
|
||||
if not init_file.exists():
|
||||
logger.debug(f"跳过插件目录:{plugin_dir.name}(缺少__init__.py)")
|
||||
continue
|
||||
|
||||
try:
|
||||
# 构建模块名
|
||||
module_name = f"app.plugins.{plugin_dir.name}"
|
||||
logger.debug(f"正在导入插件模块:{module_name}")
|
||||
|
||||
# 导入模块
|
||||
module = importlib.import_module(module_name)
|
||||
importlib.reload(module)
|
||||
|
||||
# 检查模块中的类
|
||||
for name, obj in module.__dict__.items():
|
||||
if name.startswith('_') or not isinstance(obj, type):
|
||||
continue
|
||||
if name in _loaded_modules:
|
||||
continue
|
||||
if check_module_func(obj):
|
||||
_loaded_modules.add(name)
|
||||
plugins.append(obj)
|
||||
logger.debug(f"找到符合条件的插件类:{name}")
|
||||
break
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"加载插件 {plugin_dir.name} 失败:{str(err)} - {traceback.format_exc()}")
|
||||
|
||||
return plugins
|
||||
|
||||
@property
|
||||
def running_plugins(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -243,6 +305,7 @@ class PluginManager(metaclass=Singleton):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in ['DEV', 'PLUGIN_AUTO_RELOAD']:
|
||||
return
|
||||
logger.info("配置变更,重新加载插件文件修改监测...")
|
||||
self.reload_monitor()
|
||||
|
||||
def reload_monitor(self):
|
||||
@@ -350,8 +413,7 @@ class PluginManager(metaclass=Singleton):
|
||||
# 确定需要安装的插件
|
||||
plugins_to_install = [
|
||||
plugin for plugin in online_plugins
|
||||
if plugin.id in install_plugins
|
||||
and not self.is_plugin_exists(plugin.id, plugin.plugin_version)
|
||||
if plugin.id in install_plugins and not self.is_plugin_exists(plugin.id, plugin.plugin_version)
|
||||
]
|
||||
|
||||
if not plugins_to_install:
|
||||
|
||||
@@ -68,6 +68,7 @@ def enable_doh(enable: bool):
|
||||
else:
|
||||
socket.getaddrinfo = _orig_getaddrinfo
|
||||
|
||||
|
||||
class DohHelper(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
enable_doh(settings.DOH_ENABLE)
|
||||
|
||||
@@ -32,6 +32,7 @@ class SystemHelper:
|
||||
if event_data.key not in ['DEBUG', 'LOG_LEVEL', 'LOG_MAX_FILE_SIZE', 'LOG_BACKUP_COUNT',
|
||||
'LOG_FILE_FORMAT', 'LOG_CONSOLE_FORMAT']:
|
||||
return
|
||||
logger.info("配置变更,更新日志设置...")
|
||||
logger.update_loggers()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -29,6 +29,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.MediaServers.value]:
|
||||
return
|
||||
logger.info("配置变更,重新初始化Emby模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -399,28 +399,30 @@ class FanartModule(_ModuleBase):
|
||||
if not mediainfo.get_image(season_image):
|
||||
mediainfo.set_image(season_image, image_obj.get('url'))
|
||||
else:
|
||||
|
||||
# 其他图片,优先环境变量指定语言,再like最多
|
||||
def pick_best_image(images):
|
||||
def __pick_best_image(_images):
|
||||
lang_env = settings.FANART_LANG
|
||||
if lang_env:
|
||||
langs = [lang.strip() for lang in lang_env.split(",") if lang.strip()]
|
||||
for lang in langs:
|
||||
lang_images = [img for img in images if img.get('lang') == lang]
|
||||
lang_images = [img for img in _images if img.get('lang') == lang]
|
||||
if lang_images:
|
||||
lang_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||||
return lang_images[0]
|
||||
# 没设置或没找到,按原逻辑 zh、en、like最多
|
||||
zh_images = [img for img in images if img.get('lang') == 'zh']
|
||||
zh_images = [img for img in _images if img.get('lang') == 'zh']
|
||||
if zh_images:
|
||||
zh_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||||
return zh_images[0]
|
||||
en_images = [img for img in images if img.get('lang') == 'en']
|
||||
en_images = [img for img in _images if img.get('lang') == 'en']
|
||||
if en_images:
|
||||
en_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||||
return en_images[0]
|
||||
images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||||
return images[0]
|
||||
image_obj = pick_best_image(images)
|
||||
_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||||
return _images[0]
|
||||
|
||||
image_obj = __pick_best_image(images)
|
||||
# 设置图片,没有图片才设置
|
||||
if not mediainfo.get_image(image_name):
|
||||
mediainfo.set_image(image_name, image_obj.get('url'))
|
||||
|
||||
@@ -30,6 +30,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.MediaServers.value]:
|
||||
return
|
||||
logger.info("配置变更,重新初始化Jellyfin模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -30,6 +30,7 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.MediaServers.value]:
|
||||
return
|
||||
logger.info("配置变更,重新初始化Plex模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -36,6 +36,7 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Downloaders.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载Qbittorrent模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -32,6 +32,7 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Notifications.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载Slack模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
@@ -222,13 +223,13 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
||||
# 使用CALLBACK前缀标识按钮回调
|
||||
text = f"CALLBACK:{callback_data}"
|
||||
username = msg_json.get("user", {}).get("name")
|
||||
|
||||
|
||||
# 获取原消息信息用于编辑
|
||||
message_info = msg_json.get("message", {})
|
||||
# Slack消息的时间戳作为消息ID
|
||||
message_ts = message_info.get("ts")
|
||||
channel_id = msg_json.get("channel", {}).get("id") or msg_json.get("container", {}).get("channel_id")
|
||||
|
||||
|
||||
logger.info(f"收到来自 {client_config.name} 的Slack按钮回调:"
|
||||
f"userid={userid}, username={username}, callback_data={callback_data}")
|
||||
|
||||
@@ -320,3 +321,26 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
|
||||
userid=message.userid, buttons=message.buttons,
|
||||
original_message_id=message.original_message_id,
|
||||
original_chat_id=message.original_chat_id)
|
||||
|
||||
def delete_message(self, channel: MessageChannel, source: str,
|
||||
message_id: str, chat_id: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除消息
|
||||
:param channel: 消息渠道
|
||||
:param source: 指定的消息源
|
||||
:param message_id: 消息ID(Slack中为时间戳)
|
||||
:param chat_id: 聊天ID(频道ID)
|
||||
:return: 删除是否成功
|
||||
"""
|
||||
success = False
|
||||
for conf in self.get_configs().values():
|
||||
if channel != self._channel:
|
||||
break
|
||||
if source != conf.name:
|
||||
continue
|
||||
client: Slack = self.get_instance(conf.name)
|
||||
if client:
|
||||
result = client.delete_msg(message_id=message_id, chat_id=chat_id)
|
||||
if result:
|
||||
success = True
|
||||
return success
|
||||
|
||||
@@ -13,18 +13,16 @@ from app.core.metainfo import MetaInfo
|
||||
from app.log import logger
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
lock = Lock()
|
||||
|
||||
|
||||
class Slack:
|
||||
|
||||
_client: WebClient = None
|
||||
_service: SocketModeHandler = None
|
||||
_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}"
|
||||
_channel = ""
|
||||
|
||||
def __init__(self, SLACK_OAUTH_TOKEN: Optional[str] = None, SLACK_APP_TOKEN: Optional[str] = None,
|
||||
def __init__(self, SLACK_OAUTH_TOKEN: Optional[str] = None, SLACK_APP_TOKEN: Optional[str] = None,
|
||||
SLACK_CHANNEL: Optional[str] = None, **kwargs):
|
||||
|
||||
if not SLACK_OAUTH_TOKEN or not SLACK_APP_TOKEN:
|
||||
@@ -160,7 +158,7 @@ class Slack:
|
||||
"emoji": True
|
||||
},
|
||||
"url": button["url"],
|
||||
"action_id": f"actionId-url-{len(elements)}"
|
||||
"action_id": f"actionId-url-{button.get('text', 'url')}-{len(elements)}"
|
||||
})
|
||||
else:
|
||||
# 回调按钮
|
||||
@@ -197,7 +195,7 @@ class Slack:
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
# 判断是编辑消息还是发送新消息
|
||||
if original_message_id and original_chat_id:
|
||||
# 编辑消息
|
||||
@@ -258,7 +256,7 @@ class Slack:
|
||||
"type": "divider"
|
||||
})
|
||||
index = 1
|
||||
|
||||
|
||||
# 如果有自定义按钮,先添加所有媒体项,然后添加统一的按钮
|
||||
if buttons:
|
||||
# 添加媒体列表(不带单独的选择按钮)
|
||||
@@ -288,7 +286,7 @@ class Slack:
|
||||
}
|
||||
)
|
||||
index += 1
|
||||
|
||||
|
||||
# 添加统一的自定义按钮(在所有媒体项之后)
|
||||
for button_row in buttons:
|
||||
elements = []
|
||||
@@ -302,7 +300,7 @@ class Slack:
|
||||
"emoji": True
|
||||
},
|
||||
"url": button["url"],
|
||||
"action_id": f"actionId-url-{len(elements)}"
|
||||
"action_id": f"actionId-url-{button.get('text', 'url')}-{len(elements)}"
|
||||
})
|
||||
else:
|
||||
elements.append({
|
||||
@@ -366,7 +364,7 @@ class Slack:
|
||||
}
|
||||
)
|
||||
index += 1
|
||||
|
||||
|
||||
# 判断是编辑消息还是发送新消息
|
||||
if original_message_id and original_chat_id:
|
||||
# 编辑消息
|
||||
@@ -423,7 +421,7 @@ class Slack:
|
||||
}]
|
||||
# 列表
|
||||
index = 1
|
||||
|
||||
|
||||
# 如果有自定义按钮,先添加种子列表,然后添加统一的按钮
|
||||
if buttons:
|
||||
# 添加种子列表(不带单独的选择按钮)
|
||||
@@ -433,9 +431,9 @@ class Slack:
|
||||
meta = MetaInfo(torrent.title, torrent.description)
|
||||
link = torrent.page_url
|
||||
title_text = f"{meta.season_episode} " \
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group}"
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group}"
|
||||
title_text = re.sub(r"\s+", " ", title_text).strip()
|
||||
free = torrent.volume_factor
|
||||
seeder = f"{torrent.seeders}↑"
|
||||
@@ -453,7 +451,7 @@ class Slack:
|
||||
}
|
||||
)
|
||||
index += 1
|
||||
|
||||
|
||||
# 添加统一的自定义按钮
|
||||
for button_row in buttons:
|
||||
elements = []
|
||||
@@ -467,7 +465,7 @@ class Slack:
|
||||
"emoji": True
|
||||
},
|
||||
"url": button["url"],
|
||||
"action_id": f"actionId-url-{len(elements)}"
|
||||
"action_id": f"actionId-url-{button.get('text', 'url')}-{len(elements)}"
|
||||
})
|
||||
else:
|
||||
elements.append({
|
||||
@@ -493,9 +491,9 @@ class Slack:
|
||||
meta = MetaInfo(torrent.title, torrent.description)
|
||||
link = torrent.page_url
|
||||
title_text = f"{meta.season_episode} " \
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group}"
|
||||
f"{meta.resource_term} " \
|
||||
f"{meta.video_term} " \
|
||||
f"{meta.release_group}"
|
||||
title_text = re.sub(r"\s+", " ", title_text).strip()
|
||||
free = torrent.volume_factor
|
||||
seeder = f"{torrent.seeders}↑"
|
||||
@@ -530,7 +528,7 @@ class Slack:
|
||||
}
|
||||
)
|
||||
index += 1
|
||||
|
||||
|
||||
# 判断是编辑消息还是发送新消息
|
||||
if original_message_id and original_chat_id:
|
||||
# 编辑消息
|
||||
@@ -552,6 +550,43 @@ class Slack:
|
||||
logger.error(f"Slack消息发送失败: {msg_e}")
|
||||
return False
|
||||
|
||||
def delete_msg(self, message_id: str, chat_id: Optional[str] = None) -> Optional[bool]:
|
||||
"""
|
||||
删除Slack消息
|
||||
:param message_id: 消息时间戳(Slack消息ID)
|
||||
:param chat_id: 频道ID
|
||||
:return: 删除是否成功
|
||||
"""
|
||||
if not self._client:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 确定要删除消息的频道ID
|
||||
if chat_id:
|
||||
target_channel = chat_id
|
||||
else:
|
||||
target_channel = self.__find_public_channel()
|
||||
|
||||
if not target_channel:
|
||||
logger.error("无法确定要删除消息的Slack频道")
|
||||
return False
|
||||
|
||||
# 删除消息
|
||||
result = self._client.chat_delete(
|
||||
channel=target_channel,
|
||||
ts=message_id
|
||||
)
|
||||
|
||||
if result.get("ok"):
|
||||
logger.info(f"成功删除Slack消息: channel={target_channel}, ts={message_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"删除Slack消息失败: {result.get('error', 'unknown error')}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"删除Slack消息异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def __find_public_channel(self):
|
||||
"""
|
||||
查找公共频道
|
||||
|
||||
@@ -30,6 +30,7 @@ class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Notifications.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载SynologyChat模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -36,6 +36,7 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Notifications.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载Telegram模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
@@ -286,6 +287,29 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
|
||||
original_message_id=message.original_message_id,
|
||||
original_chat_id=message.original_chat_id)
|
||||
|
||||
def delete_message(self, channel: MessageChannel, source: str,
|
||||
message_id: int, chat_id: Optional[int] = None) -> bool:
|
||||
"""
|
||||
删除消息
|
||||
:param channel: 消息渠道
|
||||
:param source: 指定的消息源
|
||||
:param message_id: 消息ID
|
||||
:param chat_id: 聊天ID
|
||||
:return: 删除是否成功
|
||||
"""
|
||||
success = False
|
||||
for conf in self.get_configs().values():
|
||||
if channel != self._channel:
|
||||
break
|
||||
if source != conf.name:
|
||||
continue
|
||||
client: Telegram = self.get_instance(conf.name)
|
||||
if client:
|
||||
result = client.delete_msg(message_id=message_id, chat_id=chat_id)
|
||||
if result:
|
||||
success = True
|
||||
return success
|
||||
|
||||
def register_commands(self, commands: Dict[str, dict]):
|
||||
"""
|
||||
注册命令,实现这个函数接收系统可用的命令菜单
|
||||
|
||||
@@ -336,6 +336,35 @@ class Telegram:
|
||||
logger.error(f"回应回调查询失败:{str(e)}")
|
||||
return False
|
||||
|
||||
def delete_msg(self, message_id: int, chat_id: Optional[int] = None) -> Optional[bool]:
|
||||
"""
|
||||
删除Telegram消息
|
||||
:param message_id: 消息ID
|
||||
:param chat_id: 聊天ID
|
||||
:return: 删除是否成功
|
||||
"""
|
||||
if not self._telegram_token or not self._telegram_chat_id:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 确定要删除消息的聊天ID
|
||||
if chat_id:
|
||||
target_chat_id = chat_id
|
||||
else:
|
||||
target_chat_id = self._telegram_chat_id
|
||||
|
||||
# 删除消息
|
||||
result = self._bot.delete_message(chat_id=target_chat_id, message_id=int(message_id))
|
||||
if result:
|
||||
logger.info(f"成功删除Telegram消息: chat_id={target_chat_id}, message_id={message_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"删除Telegram消息失败: chat_id={target_chat_id}, message_id={message_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"删除Telegram消息异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def __edit_message(self, chat_id: str, message_id: int, text: str,
|
||||
buttons: Optional[List[List[dict]]] = None,
|
||||
image: Optional[str] = None) -> Optional[bool]:
|
||||
@@ -352,7 +381,7 @@ class Telegram:
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
|
||||
# 创建按钮键盘
|
||||
reply_markup = None
|
||||
if buttons:
|
||||
|
||||
@@ -36,6 +36,7 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Downloaders.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载Transmission模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -34,6 +34,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
|
||||
event_data: schemas.ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.MediaServers.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载飞牛影视模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -31,6 +31,7 @@ class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Notifications.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载VoceChat模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -31,6 +31,7 @@ class WebPushModule(_ModuleBase, _MessageBase):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Notifications.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载WebPush模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -35,6 +35,7 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Notifications.value]:
|
||||
return
|
||||
logger.info("配置变更,重新加载Wechat模块...")
|
||||
self.init_module()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -91,6 +91,7 @@ class Monitor(metaclass=Singleton):
|
||||
event_data: ConfigChangeEventData = event.event_data
|
||||
if event_data.key not in [SystemConfigKey.Directories.value]:
|
||||
return
|
||||
logger.info("配置变更事件触发,重新初始化目录监控...")
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
|
||||
@@ -68,6 +68,7 @@ class Scheduler(metaclass=Singleton):
|
||||
if event_data.key not in ['DEV', 'COOKIECLOUD_INTERVAL', 'MEDIASERVER_SYNC_INTERVAL', 'SUBSCRIBE_SEARCH',
|
||||
'SUBSCRIBE_MODE', 'SUBSCRIBE_RSS_INTERVAL', 'SITEDATA_REFRESH_INTERVAL']:
|
||||
return
|
||||
logger.info(f"配置项 {event_data.key} 变更,重新初始化定时服务...")
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
|
||||
@@ -145,6 +145,8 @@ class ChannelCapability(Enum):
|
||||
MENU_COMMANDS = "menu_commands"
|
||||
# 支持消息编辑
|
||||
MESSAGE_EDITING = "message_editing"
|
||||
# 支持消息删除
|
||||
MESSAGE_DELETION = "message_deletion"
|
||||
# 支持回调查询
|
||||
CALLBACK_QUERIES = "callback_queries"
|
||||
# 支持富文本
|
||||
@@ -182,6 +184,7 @@ class ChannelCapabilityManager:
|
||||
ChannelCapability.INLINE_BUTTONS,
|
||||
ChannelCapability.MENU_COMMANDS,
|
||||
ChannelCapability.MESSAGE_EDITING,
|
||||
ChannelCapability.MESSAGE_DELETION,
|
||||
ChannelCapability.CALLBACK_QUERIES,
|
||||
ChannelCapability.RICH_TEXT,
|
||||
ChannelCapability.IMAGES,
|
||||
@@ -205,6 +208,8 @@ class ChannelCapabilityManager:
|
||||
channel=MessageChannel.Slack,
|
||||
capabilities={
|
||||
ChannelCapability.INLINE_BUTTONS,
|
||||
ChannelCapability.MESSAGE_EDITING,
|
||||
ChannelCapability.MESSAGE_DELETION,
|
||||
ChannelCapability.CALLBACK_QUERIES,
|
||||
ChannelCapability.RICH_TEXT,
|
||||
ChannelCapability.IMAGES,
|
||||
@@ -290,6 +295,13 @@ class ChannelCapabilityManager:
|
||||
"""
|
||||
return cls.supports_capability(channel, ChannelCapability.MESSAGE_EDITING)
|
||||
|
||||
@classmethod
|
||||
def supports_deletion(cls, channel: MessageChannel) -> bool:
|
||||
"""
|
||||
检查渠道是否支持消息删除
|
||||
"""
|
||||
return cls.supports_capability(channel, ChannelCapability.MESSAGE_DELETION)
|
||||
|
||||
@classmethod
|
||||
def get_max_buttons_per_row(cls, channel: MessageChannel) -> int:
|
||||
"""
|
||||
|
||||
@@ -36,3 +36,7 @@ class Tokens:
|
||||
return None
|
||||
else:
|
||||
return self._tokens[index]
|
||||
|
||||
@property
|
||||
def tokens(self):
|
||||
return self._tokens
|
||||
|
||||
@@ -153,7 +153,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S01",
|
||||
"episode": "E02",
|
||||
"restype": "WEB-DL",
|
||||
"restype": "B-Global WEB-DL",
|
||||
"pix": "1080p",
|
||||
"video_codec": "x264",
|
||||
"audio_codec": "AAC"
|
||||
@@ -569,7 +569,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S02",
|
||||
"episode": "E05",
|
||||
"restype": "WEB-DL",
|
||||
"restype": "Crunchyroll WEB-DL",
|
||||
"pix": "1080p",
|
||||
"video_codec": "x264",
|
||||
"audio_codec": "AAC"
|
||||
@@ -649,7 +649,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "",
|
||||
"episode": "",
|
||||
"restype": "WEBRip",
|
||||
"restype": "Netflix WEBRip",
|
||||
"pix": "1080p",
|
||||
"video_codec": "H264",
|
||||
"audio_codec": "DDP 5.1"
|
||||
@@ -681,7 +681,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S01",
|
||||
"episode": "E16",
|
||||
"restype": "WEB-DL",
|
||||
"restype": "KKTV WEB-DL",
|
||||
"pix": "1080p",
|
||||
"video_codec": "x264",
|
||||
"audio_codec": "AAC"
|
||||
@@ -921,7 +921,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S06",
|
||||
"episode": "E06",
|
||||
"restype": "WEBRip",
|
||||
"restype": "Max WEBRip",
|
||||
"pix": "1080p",
|
||||
"video_codec": "x264",
|
||||
"audio_codec": "DD 5.1"
|
||||
@@ -937,7 +937,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S06",
|
||||
"episode": "E05",
|
||||
"restype": "WEBRip",
|
||||
"restype": "Max WEBRip",
|
||||
"pix": "1080p",
|
||||
"video_codec": "x264",
|
||||
"audio_codec": "DD 5.1"
|
||||
@@ -969,7 +969,7 @@ meta_cases = [{
|
||||
"part": "",
|
||||
"season": "S02",
|
||||
"episode": "",
|
||||
"restype": "WEB-DL",
|
||||
"restype": "Netflix WEB-DL",
|
||||
"pix": "2160p",
|
||||
"video_codec": "H265",
|
||||
"audio_codec": "DDP 5.1 Atmos"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import unittest
|
||||
|
||||
from tests.test_bluray import BluRayTest
|
||||
from tests.test_metainfo import MetaInfoTest
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -10,9 +9,6 @@ if __name__ == '__main__':
|
||||
suite.addTest(MetaInfoTest('test_metainfo'))
|
||||
suite.addTest(MetaInfoTest('test_emby_format_ids'))
|
||||
|
||||
# 测试蓝光目录识别
|
||||
suite.addTest(BluRayTest())
|
||||
|
||||
# 运行测试
|
||||
runner = unittest.TextTestRunner()
|
||||
runner.run(suite)
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from unittest import TestCase
|
||||
|
||||
from app import schemas
|
||||
from app.chain.storage import StorageChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from tests.cases.files import bluray_files
|
||||
|
||||
|
||||
class MockTransferHistoryOper(TransferHistoryOper):
|
||||
def __init__(self):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.history = []
|
||||
|
||||
def get_by_src(self, src, storage=None):
|
||||
self.history.append(src)
|
||||
return TransferHistory()
|
||||
|
||||
|
||||
class MockStorage(StorageChain):
|
||||
def __init__(self, files: list):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.__root = schemas.FileItem(
|
||||
path="/", name="", type="dir", extension="", size=0
|
||||
)
|
||||
self.__all = {self.__root.path: self.__root}
|
||||
|
||||
def __build_child(parent: schemas.FileItem, files: list[dict]):
|
||||
parent.children = []
|
||||
for item in files:
|
||||
children = item.get("children")
|
||||
sep = "" if parent.path.endswith("/") else "/"
|
||||
name: str = item["name"]
|
||||
file_item = schemas.FileItem(
|
||||
path=f"{parent.path}{sep}{name}",
|
||||
name=name,
|
||||
extension=Path(name).suffix[1:],
|
||||
basename=Path(name).stem,
|
||||
type="file" if children is None else "dir",
|
||||
size=item.get("size", 0),
|
||||
)
|
||||
parent.children.append(file_item)
|
||||
self.__all[file_item.path] = file_item
|
||||
if children is not None:
|
||||
__build_child(file_item, children)
|
||||
|
||||
__build_child(self.__root, files)
|
||||
|
||||
def list_files(
|
||||
self, fileitem: schemas.FileItem, recursion: bool = False
|
||||
) -> Optional[List[schemas.FileItem]]:
|
||||
if fileitem.type != "dir":
|
||||
return None
|
||||
if recursion:
|
||||
result = []
|
||||
file_path = f"{fileitem.path}/"
|
||||
for path, item in self.__all.items():
|
||||
if path.startswith(file_path):
|
||||
result.append(item)
|
||||
return result
|
||||
else:
|
||||
return fileitem.children
|
||||
|
||||
def get_file_item(self, storage: str, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
根据路径获取文件项
|
||||
"""
|
||||
path_posix = path.as_posix()
|
||||
return self.__all.get(path_posix)
|
||||
|
||||
|
||||
class MockTransferChain(TransferChain):
|
||||
def __init__(self, storage: MockStorage):
|
||||
# pylint: disable=super-init-not-called
|
||||
|
||||
self.transferhis = MockTransferHistoryOper()
|
||||
self.systemconfig = SystemConfigOper()
|
||||
self.storagechain = storage
|
||||
|
||||
def test(self, path: str):
|
||||
self.transferhis.history.clear()
|
||||
self.do_transfer(
|
||||
force=False,
|
||||
background=False,
|
||||
fileitem=self.storagechain.get_file_item(None, Path(path)),
|
||||
)
|
||||
return self.transferhis.history
|
||||
|
||||
|
||||
class BluRayTest(TestCase):
|
||||
def __init__(self, methodName="test"):
|
||||
super().__init__(methodName)
|
||||
|
||||
def setUp(self) -> None:
|
||||
pass
|
||||
|
||||
def tearDown(self) -> None:
|
||||
pass
|
||||
|
||||
def test(self):
|
||||
transfer = MockTransferChain(MockStorage(bluray_files))
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2055)",
|
||||
"/FOLDER/Digimon/Digimon (2099)",
|
||||
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2055)",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon/Digimon (2055)"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2055)",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon/Digimon (2055)/BDMV"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2055)",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon/Digimon (2055)/BDMV/STREAM"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2055)",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon/Digimon (2055)/BDMV/STREAM/00001.m2ts"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon/Digimon (2199)"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||
],
|
||||
transfer.test("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Pokemon.2029.mp4",
|
||||
],
|
||||
transfer.test("/FOLDER/Pokemon.2029.mp4"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"/FOLDER/Digimon/Digimon (2055)",
|
||||
"/FOLDER/Digimon/Digimon (2099)",
|
||||
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||
"/FOLDER/Pokemon (2016)",
|
||||
"/FOLDER/Pokemon (2021)",
|
||||
"/FOLDER/Pokemon (2028)/Pokemon.2028.mkv",
|
||||
"/FOLDER/Pokemon.2029.mp4",
|
||||
],
|
||||
transfer.test("/FOLDER"),
|
||||
)
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.5.7'
|
||||
FRONTEND_VERSION = 'v2.5.7'
|
||||
APP_VERSION = 'v2.5.8'
|
||||
FRONTEND_VERSION = 'v2.5.8'
|
||||
|
||||
Reference in New Issue
Block a user