fix downloaders && mediaservers && notifications

This commit is contained in:
jxxghp
2024-07-02 07:16:33 +08:00
parent b4e1e911fc
commit 9a07d88d41
15 changed files with 142 additions and 190 deletions
+9 -11
View File
@@ -1,5 +1,4 @@
import pickle import pickle
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict
@@ -23,6 +22,8 @@ class SearchChain(ChainBase):
""" """
站点资源搜索处理链 站点资源搜索处理链
""" """
__result_temp_file = "__search_result__"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -53,9 +54,9 @@ class SearchChain(ChainBase):
} }
} }
results = self.process(mediainfo=mediainfo, area=area, no_exists=no_exists) results = self.process(mediainfo=mediainfo, area=area, no_exists=no_exists)
# 保存结果 # 保存到本地文件
bytes_results = pickle.dumps(results) bytes_results = pickle.dumps(results)
self.systemconfig.set(SystemConfigKey.SearchResults, bytes_results) self.save_cache(bytes_results, self.__result_temp_file)
return results return results
def search_by_title(self, title: str, page: int = 0, site: int = None) -> List[Context]: def search_by_title(self, title: str, page: int = 0, site: int = None) -> List[Context]:
@@ -77,23 +78,20 @@ class SearchChain(ChainBase):
# 组装上下文 # 组装上下文
contexts = [Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description), contexts = [Context(meta_info=MetaInfo(title=torrent.title, subtitle=torrent.description),
torrent_info=torrent) for torrent in torrents] torrent_info=torrent) for torrent in torrents]
# 保存结果 # 保存到本地文件
bytes_results = pickle.dumps(contexts) bytes_results = pickle.dumps(contexts)
self.systemconfig.set(SystemConfigKey.SearchResults, bytes_results) self.save_cache(bytes_results, self.__result_temp_file)
return contexts return contexts
def last_search_results(self) -> List[Context]: def last_search_results(self) -> List[Context]:
""" """
获取上次搜索结果 获取上次搜索结果
""" """
results = self.systemconfig.get(SystemConfigKey.SearchResults) # 读取本地文件缓存
results = self.load_cache(self.__result_temp_file)
if not results: if not results:
return [] return []
try: return results
return pickle.loads(results)
except Exception as e:
logger.error(f'加载搜索结果失败:{str(e)} - {traceback.format_exc()}')
return []
def process(self, mediainfo: MediaInfo, def process(self, mediainfo: MediaInfo,
keyword: str = None, keyword: str = None,
-109
View File
@@ -95,100 +95,10 @@ class Settings(BaseSettings):
AUTH_SITE: str = "" AUTH_SITE: str = ""
# 交互搜索自动下载用户ID,使用,分割 # 交互搜索自动下载用户ID,使用,分割
AUTO_DOWNLOAD_USER: Optional[str] = None AUTO_DOWNLOAD_USER: Optional[str] = None
# 消息通知渠道 telegram/wechat/slack/synologychat/vocechat/webpush,多个通知渠道用,分隔
MESSAGER: str = "webpush"
# WeChat企业ID
WECHAT_CORPID: Optional[str] = None
# WeChat应用Secret
WECHAT_APP_SECRET: Optional[str] = None
# WeChat应用ID
WECHAT_APP_ID: Optional[str] = None
# WeChat代理服务器
WECHAT_PROXY: str = "https://qyapi.weixin.qq.com"
# WeChat Token
WECHAT_TOKEN: Optional[str] = None
# WeChat EncodingAESKey
WECHAT_ENCODING_AESKEY: Optional[str] = None
# WeChat 管理员
WECHAT_ADMINS: Optional[str] = None
# Telegram Bot Token
TELEGRAM_TOKEN: Optional[str] = None
# Telegram Chat ID
TELEGRAM_CHAT_ID: Optional[str] = None
# Telegram 用户ID,使用,分隔
TELEGRAM_USERS: str = ""
# Telegram 管理员ID,使用,分隔
TELEGRAM_ADMINS: str = ""
# Slack Bot User OAuth Token
SLACK_OAUTH_TOKEN: str = ""
# Slack App-Level Token
SLACK_APP_TOKEN: str = ""
# Slack 频道名称
SLACK_CHANNEL: str = ""
# SynologyChat Webhook
SYNOLOGYCHAT_WEBHOOK: str = ""
# SynologyChat Token
SYNOLOGYCHAT_TOKEN: str = ""
# VoceChat地址
VOCECHAT_HOST: str = ""
# VoceChat ApiKey
VOCECHAT_API_KEY: str = ""
# VoceChat 频道ID
VOCECHAT_CHANNEL_ID: str = ""
# 下载器 qbittorrent/transmission,启用多个下载器时使用,分隔,只有第一个会被默认使用
DOWNLOADER: str = "qbittorrent"
# 下载器监控开关
DOWNLOADER_MONITOR: bool = True
# Qbittorrent地址,IP:PORT
QB_HOST: Optional[str] = None
# Qbittorrent用户名
QB_USER: Optional[str] = None
# Qbittorrent密码
QB_PASSWORD: Optional[str] = None
# Qbittorrent分类自动管理
QB_CATEGORY: bool = False
# Qbittorrent按顺序下载
QB_SEQUENTIAL: bool = True
# Qbittorrent忽略队列限制,强制继续
QB_FORCE_RESUME: bool = False
# Transmission地址,IP:PORT
TR_HOST: Optional[str] = None
# Transmission用户名
TR_USER: Optional[str] = None
# Transmission密码
TR_PASSWORD: Optional[str] = None
# 种子标签 # 种子标签
TORRENT_TAG: str = "MOVIEPILOT" TORRENT_TAG: str = "MOVIEPILOT"
# 下载站点字幕 # 下载站点字幕
DOWNLOAD_SUBTITLE: bool = True DOWNLOAD_SUBTITLE: bool = True
# 媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割
MEDIASERVER: str = "emby"
# 媒体服务器同步间隔(小时)
MEDIASERVER_SYNC_INTERVAL: Optional[int] = 6
# 媒体服务器同步黑名单,多个媒体库名称,分割
MEDIASERVER_SYNC_BLACKLIST: Optional[str] = None
# EMBY服务器地址,IP:PORT
EMBY_HOST: Optional[str] = None
# EMBY外网地址,http(s)://DOMAIN:PORT,未设置时使用EMBY_HOST
EMBY_PLAY_HOST: Optional[str] = None
# EMBY Api Key
EMBY_API_KEY: Optional[str] = None
# Jellyfin服务器地址,IP:PORT
JELLYFIN_HOST: Optional[str] = None
# Jellyfin外网地址,http(s)://DOMAIN:PORT,未设置时使用JELLYFIN_HOST
JELLYFIN_PLAY_HOST: Optional[str] = None
# Jellyfin Api Key
JELLYFIN_API_KEY: Optional[str] = None
# Plex服务器地址,IP:PORT
PLEX_HOST: Optional[str] = None
# Plex外网地址,http(s)://DOMAIN:PORT,未设置时使用PLEX_HOST
PLEX_PLAY_HOST: Optional[str] = None
# Plex Token
PLEX_TOKEN: Optional[str] = None
# 转移方式 link/copy/move/softlink
TRANSFER_TYPE: str = "copy"
# 是否同盘优先
TRANSFER_SAME_DISK: bool = True
# CookieCloud是否启动本地服务 # CookieCloud是否启动本地服务
COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False
# CookieCloud服务器地址 # CookieCloud服务器地址
@@ -243,7 +153,6 @@ class Settings(BaseSettings):
@validator("SUBSCRIBE_RSS_INTERVAL", @validator("SUBSCRIBE_RSS_INTERVAL",
"COOKIECLOUD_INTERVAL", "COOKIECLOUD_INTERVAL",
"MEDIASERVER_SYNC_INTERVAL",
"META_CACHE_EXPIRE", "META_CACHE_EXPIRE",
pre=True, always=True) pre=True, always=True)
def convert_int(cls, value): def convert_int(cls, value):
@@ -335,24 +244,6 @@ class Settings(BaseSettings):
} }
return {} return {}
@property
def DEFAULT_DOWNLOADER(self):
"""
默认下载器
"""
if not self.DOWNLOADER:
return None
return next((d for d in settings.DOWNLOADER.split(",") if d), None)
@property
def DOWNLOADERS(self):
"""
下载器列表
"""
if not self.DOWNLOADER:
return []
return [d for d in settings.DOWNLOADER.split(",") if d]
@property @property
def VAPID(self): def VAPID(self):
return { return {
+20
View File
@@ -0,0 +1,20 @@
from app.db.systemconfig_oper import SystemConfigOper
from app.schemas.types import SystemConfigKey
class DownloaderHelper:
"""
下载器帮助类
"""
def __init__(self):
self.systemconfig = SystemConfigOper()
def get_downloaders(self) -> dict:
"""
获取下载器
"""
downloader_conf: dict = self.systemconfig.get(SystemConfigKey.Downloaders)
if not downloader_conf:
return {}
return downloader_conf
+20
View File
@@ -0,0 +1,20 @@
from app.db.systemconfig_oper import SystemConfigOper
from app.schemas.types import SystemConfigKey
class MediaServerHelper:
"""
媒体服务器帮助类
"""
def __init__(self):
self.systemconfig = SystemConfigOper()
def get_mediaservers(self) -> dict:
"""
获取媒体服务器
"""
mediaserver_conf: dict = self.systemconfig.get(SystemConfigKey.MediaServers)
if not mediaserver_conf:
return {}
return mediaserver_conf
+20
View File
@@ -0,0 +1,20 @@
from app.db.systemconfig_oper import SystemConfigOper
from app.schemas.types import SystemConfigKey
class NotificationHelper:
"""
消息通知渠道帮助类
"""
def __init__(self):
self.systemconfig = SystemConfigOper()
def get_notifications(self) -> dict:
"""
获取消息通知渠道
"""
notification_conf: dict = self.systemconfig.get(SystemConfigKey.Notifications)
if not notification_conf:
return {}
return notification_conf
+4 -4
View File
@@ -15,14 +15,14 @@ from app.utils.http import RequestUtils
class Emby: class Emby:
def __init__(self): def __init__(self, host: str = None, play_host: str = None, apikey: str = None):
self._host = settings.EMBY_HOST self._host = host
if self._host: if self._host:
self._host = RequestUtils.standardize_base_url(self._host) self._host = RequestUtils.standardize_base_url(self._host)
self._playhost = settings.EMBY_PLAY_HOST self._playhost = play_host
if self._playhost: if self._playhost:
self._playhost = RequestUtils.standardize_base_url(self._playhost) self._playhost = RequestUtils.standardize_base_url(self._playhost)
self._apikey = settings.EMBY_API_KEY self._apikey = apikey
self.user = self.get_user(settings.SUPERUSER) self.user = self.get_user(settings.SUPERUSER)
self.folders = self.get_emby_folders() self.folders = self.get_emby_folders()
self.serverid = self.get_server_id() self.serverid = self.get_server_id()
+4 -4
View File
@@ -12,14 +12,14 @@ from app.utils.http import RequestUtils
class Jellyfin: class Jellyfin:
def __init__(self): def __init__(self, host: str = None, play_host: str = None, apikey: str = None):
self._host = settings.JELLYFIN_HOST self._host = host
if self._host: if self._host:
self._host = RequestUtils.standardize_base_url(self._host) self._host = RequestUtils.standardize_base_url(self._host)
self._playhost = settings.JELLYFIN_PLAY_HOST self._playhost = play_host
if self._playhost: if self._playhost:
self._playhost = RequestUtils.standardize_base_url(self._playhost) self._playhost = RequestUtils.standardize_base_url(self._playhost)
self._apikey = settings.JELLYFIN_API_KEY self._apikey = apikey
self.user = self.get_user(settings.SUPERUSER) self.user = self.get_user(settings.SUPERUSER)
self.serverid = self.get_server_id() self.serverid = self.get_server_id()
+4 -4
View File
@@ -19,14 +19,14 @@ class Plex:
_plex = None _plex = None
_session = None _session = None
def __init__(self): def __init__(self, host: str = None, play_host: str = None, token: str = None):
self._host = settings.PLEX_HOST self._host = host
if self._host: if self._host:
self._host = RequestUtils.standardize_base_url(self._host) self._host = RequestUtils.standardize_base_url(self._host)
self._playhost = settings.PLEX_PLAY_HOST self._playhost = play_host
if self._playhost: if self._playhost:
self._playhost = RequestUtils.standardize_base_url(self._playhost) self._playhost = RequestUtils.standardize_base_url(self._playhost)
self._token = settings.PLEX_TOKEN self._token = token
if self._host and self._token: if self._host and self._token:
try: try:
self._plex = PlexServer(self._host, self._token) self._plex = PlexServer(self._host, self._token)
+4 -7
View File
@@ -8,7 +8,6 @@ from qbittorrentapi.transfer import TransferInfoDictionary
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.utils.string import StringUtils
class Qbittorrent: class Qbittorrent:
@@ -23,12 +22,10 @@ class Qbittorrent:
""" """
若不设置参数,则创建配置文件设置的下载器 若不设置参数,则创建配置文件设置的下载器
""" """
if host and port: self._host = host
self._host, self._port = host, port self._port = port
else: self._username = username
self._host, self._port = StringUtils.get_domain_address(address=settings.QB_HOST, prefix=True) self._password = password
self._username = username if username else settings.QB_USER
self._password = password if password else settings.QB_PASSWORD
if self._host and self._port: if self._host and self._port:
self.qbc = self.__login_qbittorrent() self.qbc = self.__login_qbittorrent()
+7 -8
View File
@@ -21,22 +21,21 @@ class Slack:
_client: WebClient = None _client: WebClient = None
_service: SocketModeHandler = None _service: SocketModeHandler = None
_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}" _ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}"
_channel = ""
def __init__(self): def __init__(self, oauth_token: str, app_token: str, channel: str = ""):
if not settings.SLACK_OAUTH_TOKEN or not settings.SLACK_APP_TOKEN:
return
try: try:
slack_app = App(token=settings.SLACK_OAUTH_TOKEN, slack_app = App(token=oauth_token,
ssl_check_enabled=False, ssl_check_enabled=False,
url_verification_enabled=False) url_verification_enabled=False)
except Exception as err: except Exception as err:
logger.error(f"Slack初始化失败: {str(err)}") logger.error(f"Slack初始化失败: {str(err)}")
return return
self._client = slack_app.client self._client = slack_app.client
self._channel = channel
# 注册消息响应 # 注册消息响应
@slack_app.event("message") @slack_app.event("message")
@@ -72,7 +71,7 @@ class Slack:
try: try:
self._service = SocketModeHandler( self._service = SocketModeHandler(
slack_app, slack_app,
settings.SLACK_APP_TOKEN app_token
) )
self._service.connect() self._service.connect()
logger.info("Slack消息接收服务启动") logger.info("Slack消息接收服务启动")
@@ -337,7 +336,7 @@ class Slack:
if conversation_id: if conversation_id:
break break
for channel in result["channels"]: for channel in result["channels"]:
if channel.get("name") == (settings.SLACK_CHANNEL or "全体"): if channel.get("name") == (self._channel or "全体"):
conversation_id = channel.get("id") conversation_id = channel.get("id")
break break
except Exception as e: except Exception as e:
+3 -3
View File
@@ -24,14 +24,14 @@ class Telegram:
_event = Event() _event = Event()
_bot: telebot.TeleBot = None _bot: telebot.TeleBot = None
def __init__(self): def __init__(self, token: str = None, chat_id: str = None):
""" """
初始化参数 初始化参数
""" """
# Token # Token
self._telegram_token = settings.TELEGRAM_TOKEN self._telegram_token = token
# Chat Id # Chat Id
self._telegram_chat_id = settings.TELEGRAM_CHAT_ID self._telegram_chat_id = chat_id
# 初始化机器人 # 初始化机器人
if self._telegram_token and self._telegram_chat_id: if self._telegram_token and self._telegram_chat_id:
# bot # bot
+5 -9
View File
@@ -1,12 +1,10 @@
from typing import Optional, Union, Tuple, List, Dict from typing import Optional, Union, Tuple, List
import transmission_rpc import transmission_rpc
from transmission_rpc import Client, Torrent, File from transmission_rpc import Client, Torrent, File
from transmission_rpc.session import SessionStats, Session from transmission_rpc.session import SessionStats, Session
from app.core.config import settings
from app.log import logger from app.log import logger
from app.utils.string import StringUtils
class Transmission: class Transmission:
@@ -28,12 +26,10 @@ class Transmission:
""" """
若不设置参数,则创建配置文件设置的下载器 若不设置参数,则创建配置文件设置的下载器
""" """
if host and port: self._host = host
self._host, self._port = host, port self._port = port
else: self._username = username
self._host, self._port = StringUtils.get_domain_address(address=settings.TR_HOST, prefix=False) self._password = password
self._username = username if username else settings.TR_USER
self._password = password if password else settings.TR_PASSWORD
if self._host and self._port: if self._host and self._port:
self.trc = self.__login_transmission() self.trc = self.__login_transmission()
+4 -5
View File
@@ -2,7 +2,6 @@ import re
import threading import threading
from typing import Optional, List from typing import Optional, List
from app.core.config import settings
from app.core.context import MediaInfo, Context from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.log import logger from app.log import logger
@@ -23,18 +22,18 @@ class VoceChat:
# 请求对象 # 请求对象
_client = None _client = None
def __init__(self): def __init__(self, host: str = None, apikey: str = None, channel_id: str = None):
""" """
初始化 初始化
""" """
self._host = settings.VOCECHAT_HOST self._host = host
if self._host: if self._host:
if not self._host.endswith("/"): if not self._host.endswith("/"):
self._host += "/" self._host += "/"
if not self._host.startswith("http"): if not self._host.startswith("http"):
self._playhost = "http://" + self._host self._playhost = "http://" + self._host
self._apikey = settings.VOCECHAT_API_KEY self._apikey = apikey
self._channel_id = settings.VOCECHAT_CHANNEL_ID self._channel_id = channel_id
if self._apikey and self._host and self._channel_id: if self._apikey and self._host and self._channel_id:
self._client = RequestUtils(headers={ self._client = RequestUtils(headers={
"content-type": "text/markdown", "content-type": "text/markdown",
+16 -8
View File
@@ -4,7 +4,6 @@ import threading
from datetime import datetime from datetime import datetime
from typing import Optional, List, Dict from typing import Optional, List, Dict
from app.core.config import settings
from app.core.context import MediaInfo, Context from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.log import logger from app.log import logger
@@ -28,25 +27,34 @@ class WeChat:
_appsecret = None _appsecret = None
# 企业微信AppID # 企业微信AppID
_appid = None _appid = None
# 代理
_proxy = None
# 企业微信发送消息URL # 企业微信发送消息URL
_send_msg_url = f"{settings.WECHAT_PROXY}/cgi-bin/message/send?access_token=%s" _send_msg_url = "/cgi-bin/message/send?access_token=%s"
# 企业微信获取TokenURL # 企业微信获取TokenURL
_token_url = f"{settings.WECHAT_PROXY}/cgi-bin/gettoken?corpid=%s&corpsecret=%s" _token_url = "/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
# 企业微信创新菜单URL # 企业微信创新菜单URL
_create_menu_url = f"{settings.WECHAT_PROXY}/cgi-bin/menu/create?access_token=%s&agentid=%s" _create_menu_url = "/cgi-bin/menu/create?access_token=%s&agentid=%s"
def __init__(self): def __init__(self, corpid: str = None, appsecret: str = None, appid: str = None, proxy: str = None):
""" """
初始化 初始化
""" """
self._corpid = settings.WECHAT_CORPID self._corpid = corpid
self._appsecret = settings.WECHAT_APP_SECRET self._appsecret = appsecret
self._appid = settings.WECHAT_APP_ID self._appid = appid
self._proxy = proxy or "https://qyapi.weixin.qq.com"
if self._corpid and self._appsecret and self._appid: if self._corpid and self._appsecret and self._appid:
self.__get_access_token() self.__get_access_token()
if self._proxy:
self._proxy = self._proxy.rstrip("/")
self._send_msg_url = f"{self._proxy}/{self._send_msg_url}"
self._token_url = f"{self._proxy}/{self._token_url}"
self._create_menu_url = f"{self._proxy}/{self._create_menu_url}"
def get_state(self): def get_state(self):
""" """
获取状态 获取状态
+22 -18
View File
@@ -58,16 +58,24 @@ class EventType(Enum):
# 系统配置Key字典 # 系统配置Key字典
class SystemConfigKey(Enum): class SystemConfigKey(Enum):
# 用户已安装的插件 # 下载器配置
UserInstalledPlugins = "UserInstalledPlugins" Downloaders = "Downloaders"
# 搜索结果 # 媒体服务器配置
SearchResults = "SearchResults" MediaServers = "MediaServers"
# 消息通知配置
Notifications = "Notifications"
# 下载目录配置
DownloadDirectories = "DownloadDirectories"
# 媒体库目录配置
LibraryDirectories = "LibraryDirectories"
# 阿里云盘认证参数
UserAliyunParams = "UserAliyunParams"
# 115网盘认证参数
User115Params = "User115Params"
# 搜索站点范围 # 搜索站点范围
IndexerSites = "IndexerSites" IndexerSites = "IndexerSites"
# 订阅站点范围 # 订阅站点范围
RssSites = "RssSites" RssSites = "RssSites"
# 种子优先级规则
TorrentsPriority = "TorrentsPriority"
# 通知消息渠道设置 # 通知消息渠道设置
NotificationChannels = "NotificationChannels" NotificationChannels = "NotificationChannels"
# 自定义制作组/字幕组 # 自定义制作组/字幕组
@@ -76,6 +84,10 @@ class SystemConfigKey(Enum):
Customization = "Customization" Customization = "Customization"
# 自定义识别词 # 自定义识别词
CustomIdentifiers = "CustomIdentifiers" CustomIdentifiers = "CustomIdentifiers"
# 转移屏蔽词
TransferExcludeWords = "TransferExcludeWords"
# 种子优先级规则
TorrentsPriority = "TorrentsPriority"
# 搜索优先级规则 # 搜索优先级规则
SearchFilterRules = "SearchFilterRules" SearchFilterRules = "SearchFilterRules"
# 订阅优先级规则 # 订阅优先级规则
@@ -86,22 +98,14 @@ class SystemConfigKey(Enum):
DefaultFilterRules = "DefaultFilterRules" DefaultFilterRules = "DefaultFilterRules"
# 默认搜索过滤规则 # 默认搜索过滤规则
DefaultSearchFilterRules = "DefaultSearchFilterRules" DefaultSearchFilterRules = "DefaultSearchFilterRules"
# 转移屏蔽词
TransferExcludeWords = "TransferExcludeWords"
# 插件安装统计
PluginInstallReport = "PluginInstallReport"
# 订阅统计 # 订阅统计
SubscribeReport = "SubscribeReport" SubscribeReport = "SubscribeReport"
# 用户自定义CSS # 用户自定义CSS
UserCustomCSS = "UserCustomCSS" UserCustomCSS = "UserCustomCSS"
# 下载目录定义 # 用户已安装的插件
DownloadDirectories = "DownloadDirectories" UserInstalledPlugins = "UserInstalledPlugins"
# 媒体库目录定义 # 插件安装统计
LibraryDirectories = "LibraryDirectories" PluginInstallReport = "PluginInstallReport"
# 阿里云盘认证参数
UserAliyunParams = "UserAliyunParams"
# 115网盘认证参数
User115Params = "User115Params"
# 处理进度Key字典 # 处理进度Key字典