Merge branch 'dev' of https://github.com/jxxghp/MoviePilot into feature/security

This commit is contained in:
InfinityPacer
2024-10-13 00:13:35 +08:00
51 changed files with 409 additions and 279 deletions

View File

@@ -1,4 +1,3 @@
import json
from typing import List, Any
from fastapi import APIRouter, Depends

View File

@@ -1,4 +1,3 @@
import json
from datetime import timedelta
from typing import Any, List

View File

@@ -14,7 +14,7 @@ from app.db import get_db
from app.db.models import User
from app.db.models.message import Message
from app.db.user_oper import get_current_active_superuser
from app.helper.serviceconfig import ServiceConfigHelper
from app.helper.service import ServiceConfigHelper
from app.log import logger
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
from app.schemas.types import MessageChannel

View File

@@ -1,4 +1,3 @@
import json
from pathlib import Path
from typing import Any, Optional

View File

@@ -22,7 +22,7 @@ async def webhook_message(background_tasks: BackgroundTasks,
_: str = Depends(verify_apitoken)
) -> Any:
"""
Webhook响应配置请求中需要添加参数token=API_TOKEN&source=消息配置
Webhook响应配置请求中需要添加参数token=API_TOKEN&source=媒体服务器
"""
body = await request.body()
form = await request.form()
@@ -35,7 +35,7 @@ async def webhook_message(background_tasks: BackgroundTasks,
def webhook_message(background_tasks: BackgroundTasks,
request: Request, _: str = Depends(verify_apitoken)) -> Any:
"""
Webhook响应配置请求中需要添加参数token=API_TOKEN&source=消息配置
Webhook响应配置请求中需要添加参数token=API_TOKEN&source=媒体服务器
"""
args = request.query_params
background_tasks.add_task(start_webhook_chain, None, None, args)

View File

@@ -17,7 +17,7 @@ from app.core.module import ModuleManager
from app.db.message_oper import MessageOper
from app.db.user_oper import UserOper
from app.helper.message import MessageHelper
from app.helper.serviceconfig import ServiceConfigHelper
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
@@ -101,7 +101,7 @@ class ChainBase(metaclass=ABCMeta):
try:
module_name = module.get_name()
except Exception as err:
logger.error(f"获取模块名称出错:{str(err)}")
logger.debug(f"获取模块名称出错:{str(err)}")
module_name = module_id
try:
func = getattr(module, method)

View File

@@ -1,11 +1,10 @@
import json
import threading
from typing import List, Union, Optional, Generator
from app import schemas
from app.chain import ChainBase
from app.db.mediaserver_oper import MediaServerOper
from app.helper.serviceconfig import ServiceConfigHelper
from app.helper.service import ServiceConfigHelper
from app.log import logger
lock = threading.Lock()

View File

@@ -1,5 +1,4 @@
import copy
import json
import re
from typing import Any, Optional, Dict, Union

View File

@@ -127,11 +127,11 @@ class TmdbChain(ChainBase, metaclass=Singleton):
return None
@cached(cache=TTLCache(maxsize=1, ttl=3600))
def get_trending_wallpapers(self, num: int = 10) -> Optional[List[str]]:
def get_trending_wallpapers(self, num: int = 10) -> List[str]:
"""
获取所有流行壁纸
"""
infos = self.tmdb_trending()
if infos:
return [info.backdrop_path for info in infos if info and info.backdrop_path][:num]
return None
return []

View File

@@ -5,7 +5,7 @@ from app.core.config import settings
from app.core.event import eventmanager
from app.helper.module import ModuleHelper
from app.log import logger
from app.schemas.types import EventType
from app.schemas.types import EventType, ModuleType
from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton
@@ -124,6 +124,17 @@ class ModuleManager(metaclass=Singleton):
and ObjectUtils.check_method(getattr(module, method)):
yield module
def get_running_type_modules(self, module_type: ModuleType) -> Generator:
"""
获取指定类型的模块列表
"""
if not self._running_modules:
return []
for _, module in self._running_modules.items():
if hasattr(module, 'get_type') \
and module.get_type() == module_type:
yield module
def get_module(self, module_id: str) -> Any:
"""
根据模块id获取模块

View File

@@ -1,4 +1,3 @@
import json
from typing import Optional
from sqlalchemy.orm import Session

View File

@@ -1,9 +1,7 @@
import json
from typing import Any
from app.db import DbOper
from app.db.models.plugindata import PluginData
from app.utils.object import ObjectUtils
class PluginDataOper(DbOper):

View File

@@ -1,4 +1,3 @@
import json
from datetime import datetime
from typing import Tuple, List

View File

@@ -1,10 +1,8 @@
import json
from typing import Any, Union
from app.db import DbOper
from app.db.models.systemconfig import SystemConfig
from app.schemas.types import SystemConfigKey
from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton

View File

@@ -1,4 +1,3 @@
import json
import time
from typing import Any, List

View File

@@ -1,4 +1,3 @@
import json
from typing import Optional
from fastapi import Depends, HTTPException

View File

@@ -1,10 +1,8 @@
import json
from typing import Any, Union, Dict, Optional
from app.db import DbOper
from app.db.models.userconfig import UserConfig
from app.schemas.types import UserConfigKey
from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton

View File

@@ -1,8 +1,8 @@
from typing import Optional
from app.helper.servicebase import ServiceBaseHelper
from app.helper.service import ServiceBaseHelper
from app.schemas import DownloaderConf, ServiceInfo
from app.schemas.types import SystemConfigKey
from app.schemas.types import SystemConfigKey, ModuleType
class DownloaderHelper(ServiceBaseHelper[DownloaderConf]):
@@ -14,29 +14,24 @@ class DownloaderHelper(ServiceBaseHelper[DownloaderConf]):
super().__init__(
config_key=SystemConfigKey.Downloaders,
conf_type=DownloaderConf,
modules=["QbittorrentModule", "TransmissionModule"]
module_type=ModuleType.Downloader
)
def is_qbittorrent(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
def is_downloader(
self,
service_type: Optional[str] = None,
service: Optional[ServiceInfo] = None,
name: Optional[str] = None,
) -> bool:
"""
判断指定的下载器是否为 qbittorrent 类型,需要传入 `service` 或 `name` 中的任一参数
通用的下载器类型判断方法
:param service_type: 下载器的类型名称(如 'qbittorrent', 'transmission'
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 qbittorrent,返回 True否则返回 False
:return: 如果服务类型或实例为指定类型,返回 True否则返回 False
"""
if not service:
service = self.get_service(name=name)
return service.type == "qbittorrent" if service else False
# 如果未提供 service 则通过 name 获取服务
service = service or self.get_service(name=name)
def is_transmission(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的下载器是否为 transmission 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 transmission返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "transmission" if service else False
# 判断服务类型是否为指定类型
return bool(service and service.type == service_type)

View File

@@ -1,8 +1,8 @@
from typing import Optional
from app.helper.servicebase import ServiceBaseHelper
from app.helper.service import ServiceBaseHelper
from app.schemas import MediaServerConf, ServiceInfo
from app.schemas.types import SystemConfigKey
from app.schemas.types import SystemConfigKey, ModuleType
class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
@@ -14,41 +14,24 @@ class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
super().__init__(
config_key=SystemConfigKey.MediaServers,
conf_type=MediaServerConf,
modules=["PlexModule", "EmbyModule", "JellyfinModule"]
module_type=ModuleType.MediaServer
)
def is_plex(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
def is_media_server(
self,
service_type: Optional[str] = None,
service: Optional[ServiceInfo] = None,
name: Optional[str] = None,
) -> bool:
"""
判断指定的媒体服务器是否为 Plex 类型,需要传入 `service` 或 `name` 中的任一参数
通用的媒体服务器类型判断方法
:param service_type: 媒体服务器的类型名称(如 'plex', 'emby', 'jellyfin'
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 plex,返回 True否则返回 False
:return: 如果服务类型或实例为指定类型,返回 True否则返回 False
"""
if not service:
service = self.get_service(name=name)
return service.type == "plex" if service else False
# 如果未提供 service 则通过 name 获取服务
service = service or self.get_service(name=name)
def is_emby(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的媒体服务器是否为 Emby 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 emby返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "emby" if service else False
def is_jellyfin(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的媒体服务器是否为 Jellyfin 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 jellyfin返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "jellyfin" if service else False
# 判断服务类型是否为指定类型
return bool(service and service.type == service_type)

View File

@@ -1,8 +1,8 @@
from typing import Optional
from app.helper.servicebase import ServiceBaseHelper
from app.helper.service import ServiceBaseHelper
from app.schemas import NotificationConf, ServiceInfo
from app.schemas.types import SystemConfigKey
from app.schemas.types import SystemConfigKey, ModuleType
class NotificationHelper(ServiceBaseHelper[NotificationConf]):
@@ -14,84 +14,25 @@ class NotificationHelper(ServiceBaseHelper[NotificationConf]):
super().__init__(
config_key=SystemConfigKey.Notifications,
conf_type=NotificationConf,
modules=[
"WechatModule",
"WebPushModule",
"VoceChatModule",
"TelegramModule",
"SynologyChatModule",
"SlackModule"
]
module_type=ModuleType.Notification
)
def is_wechat(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
def is_notification(
self,
service_type: Optional[str] = None,
service: Optional[ServiceInfo] = None,
name: Optional[str] = None,
) -> bool:
"""
判断指定的消息通知服务是否为 Wechat 类型,需要传入 `service` 或 `name` 中的任一参数
通用的消息通知服务类型判断方法
:param service_type: 消息通知服务的类型名称(如 'wechat', 'voicechat', 'telegram', 等)
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 wechat,返回 True否则返回 False
:return: 如果服务类型或实例为指定类型,返回 True否则返回 False
"""
if not service:
service = self.get_service(name=name)
return service.type == "wechat" if service else False
# 如果未提供 service 则通过 name 获取服务
service = service or self.get_service(name=name)
def is_webpush(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的消息通知服务是否为 WebPush 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 webpush返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "webpush" if service else False
def is_voicechat(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的消息通知服务是否为 VoiceChat 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 voicechat返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "voicechat" if service else False
def is_telegram(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的消息通知服务是否为 Telegram 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 telegram返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "telegram" if service else False
def is_synologychat(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的消息通知服务是否为 SynologyChat 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 synologychat返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "synologychat" if service else False
def is_slack(self, service: Optional[ServiceInfo] = None, name: Optional[str] = None) -> bool:
"""
判断指定的消息通知服务是否为 Slack 类型,需要传入 `service` 或 `name` 中的任一参数
:param service: 要判断的服务信息
:param name: 服务的名称
:return: 如果服务类型为 slack返回 True否则返回 False。
"""
if not service:
service = self.get_service(name=name)
return service.type == "slack" if service else False
# 判断服务类型是否为指定类型
return bool(service and service.type == service_type)

View File

@@ -1,23 +1,83 @@
from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator
from app.core.module import ModuleManager
from app.helper.serviceconfig import ServiceConfigHelper
from app.schemas import ServiceInfo
from app.schemas.types import SystemConfigKey
from app.db.systemconfig_oper import SystemConfigOper
from app.schemas import DownloaderConf, MediaServerConf, NotificationConf, NotificationSwitchConf, ServiceInfo
from app.schemas.types import NotificationType, SystemConfigKey, ModuleType
TConf = TypeVar("TConf")
class ServiceConfigHelper:
"""
配置帮助类获取不同类型的服务配置
"""
@staticmethod
def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
"""
通用获取配置的方法根据 config_key 获取相应的配置并返回指定类型的配置列表
:param config_key: 系统配置的 key
:param conf_type: 用于实例化配置对象的类类型
:return: 配置对象列表
"""
config_data = SystemConfigOper().get(config_key)
if not config_data:
return []
# 直接使用 conf_type 来实例化配置对象
return [conf_type(**conf) for conf in config_data]
@staticmethod
def get_downloader_configs() -> List[DownloaderConf]:
"""
获取下载器的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.Downloaders, DownloaderConf)
@staticmethod
def get_mediaserver_configs() -> List[MediaServerConf]:
"""
获取媒体服务器的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.MediaServers, MediaServerConf)
@staticmethod
def get_notification_configs() -> List[NotificationConf]:
"""
获取消息通知渠道的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.Notifications, NotificationConf)
@staticmethod
def get_notification_switches() -> List[NotificationSwitchConf]:
"""
获取消息通知场景的开关
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.NotificationSwitchs, NotificationSwitchConf)
@staticmethod
def get_notification_switch(mtype: NotificationType) -> Optional[str]:
"""
获取指定类型的消息通知场景的开关
"""
switchs = ServiceConfigHelper.get_notification_switches()
for switch in switchs:
if switch.type == mtype.value:
return switch.action
return None
class ServiceBaseHelper(Generic[TConf]):
"""
通用服务帮助类抽象获取配置和服务实例的通用逻辑
"""
def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf], modules: List[str]):
def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf], module_type: ModuleType):
self.modulemanager = ModuleManager()
self.config_key = config_key
self.conf_type = conf_type
self.modules = modules
self.module_type = module_type
def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]:
"""
@@ -47,8 +107,8 @@ class ServiceBaseHelper(Generic[TConf]):
迭代所有模块的实例及其对应的配置返回 ServiceInfo 实例
"""
configs = self.get_configs()
for module_name in self.modules:
module = self.modulemanager.get_running_module(module_name)
modules = self.modulemanager.get_running_type_modules(self.module_type)
for module in modules:
if not module:
continue
module_instances = module.get_instances()
@@ -81,9 +141,10 @@ class ServiceBaseHelper(Generic[TConf]):
return {
service_info.name: service_info
for service_info in self.iterate_module_instances()
if service_info.config and
(type_filter is None or service_info.type == type_filter) and
(name_filters_set is None or service_info.name in name_filters_set)
if service_info.config and (
type_filter is None or service_info.type == type_filter
) and (
name_filters_set is None or service_info.name in name_filters_set)
}
def get_service(self, name: str, type_filter: Optional[str] = None) -> Optional[ServiceInfo]:

View File

@@ -1,65 +0,0 @@
from typing import List, Type, Optional
from app.db.systemconfig_oper import SystemConfigOper
from app.schemas import DownloaderConf, MediaServerConf, NotificationConf, NotificationSwitchConf
from app.schemas.types import SystemConfigKey, NotificationType
class ServiceConfigHelper:
"""
配置帮助类,获取不同类型的服务配置
"""
@staticmethod
def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
"""
通用获取配置的方法,根据 config_key 获取相应的配置并返回指定类型的配置列表
:param config_key: 系统配置的 key
:param conf_type: 用于实例化配置对象的类类型
:return: 配置对象列表
"""
config_data = SystemConfigOper().get(config_key)
if not config_data:
return []
# 直接使用 conf_type 来实例化配置对象
return [conf_type(**conf) for conf in config_data]
@staticmethod
def get_downloader_configs() -> List[DownloaderConf]:
"""
获取下载器的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.Downloaders, DownloaderConf)
@staticmethod
def get_mediaserver_configs() -> List[MediaServerConf]:
"""
获取媒体服务器的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.MediaServers, MediaServerConf)
@staticmethod
def get_notification_configs() -> List[NotificationConf]:
"""
获取消息通知渠道的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.Notifications, NotificationConf)
@staticmethod
def get_notification_switches() -> List[NotificationSwitchConf]:
"""
获取消息通知场景的开关
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.NotificationSwitchs, NotificationSwitchConf)
@staticmethod
def get_notification_switch(mtype: NotificationType) -> Optional[str]:
"""
获取指定类型的消息通知场景的开关
"""
switchs = ServiceConfigHelper.get_notification_switches()
for switch in switchs:
if switch.type == mtype.value:
return switch.action
return None

View File

@@ -1,8 +1,9 @@
from abc import abstractmethod, ABCMeta
from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable, Any
from app.helper.serviceconfig import ServiceConfigHelper
from app.helper.service import ServiceConfigHelper
from app.schemas import Notification, MessageChannel, NotificationConf, MediaServerConf, DownloaderConf
from app.schemas.types import ModuleType
class _ModuleBase(metaclass=ABCMeta):
@@ -34,6 +35,14 @@ class _ModuleBase(metaclass=ABCMeta):
"""
pass
@staticmethod
@abstractmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
pass
@abstractmethod
def stop(self) -> None:
"""

View File

@@ -7,6 +7,7 @@ from app.core.meta import MetaBase
from app.log import logger
from app.modules import _ModuleBase
from app.modules.bangumi.bangumi import BangumiApi
from app.schemas.types import ModuleType
from app.utils.http import RequestUtils
@@ -37,6 +38,13 @@ class BangumiModule(_ModuleBase):
def get_name() -> str:
return "Bangumi"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaRecognize
def recognize_media(self, bangumiid: int = None,
**kwargs) -> Optional[MediaInfo]:
"""

View File

@@ -14,7 +14,7 @@ from app.modules.douban.apiv2 import DoubanApi
from app.modules.douban.douban_cache import DoubanCache
from app.modules.douban.scraper import DoubanScraper
from app.schemas import MediaPerson, APIRateLimitException
from app.schemas.types import MediaType
from app.schemas.types import MediaType, ModuleType
from app.utils.common import retry
from app.utils.http import RequestUtils
from app.utils.limit import rate_limit_exponential
@@ -51,6 +51,13 @@ class DoubanModule(_ModuleBase):
def get_name() -> str:
return "豆瓣"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaRecognize
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
doubanid: str = None,

View File

@@ -5,7 +5,6 @@ from app.core.context import MediaInfo
from app.log import logger
from app.modules import _ModuleBase, _MediaServerBase
from app.modules.emby.emby import Emby
from app.schemas import MediaServerConf
from app.schemas.types import MediaType
@@ -22,6 +21,10 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
def get_name() -> str:
return "Emby"
@staticmethod
def get_type() -> str:
return "mediaserver"
def stop(self):
pass
@@ -78,7 +81,10 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
server: Emby = self.get_instance(source)
if not server:
return None
return server.get_webhook_message(form, args)
result = server.get_webhook_message(form, args)
if result:
result.server_name = source
return result
for server in self.get_instances().values():
if server:

View File

@@ -6,7 +6,7 @@ from app.core.context import MediaInfo, settings
from app.log import logger
from app.modules import _ModuleBase
from app.utils.http import RequestUtils
from app.schemas.types import MediaType
from app.schemas.types import MediaType, ModuleType
class FanartModule(_ModuleBase):
@@ -335,6 +335,13 @@ class FanartModule(_ModuleBase):
def get_name() -> str:
return "Fanart"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Other
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
获取图片

View File

@@ -17,7 +17,7 @@ from app.log import logger
from app.modules import _ModuleBase
from app.modules.filemanager.storages import StorageBase
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage
from app.schemas.types import MediaType
from app.schemas.types import MediaType, ModuleType
from app.utils.system import SystemUtils
lock = Lock()
@@ -44,6 +44,13 @@ class FileManagerModule(_ModuleBase):
def get_name() -> str:
return "文件整理"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Other
def stop(self):
pass

View File

@@ -7,6 +7,7 @@ from app.helper.rule import RuleHelper
from app.log import logger
from app.modules import _ModuleBase
from app.modules.filter.RuleParser import RuleParser
from app.schemas.types import ModuleType
from app.utils.string import StringUtils
@@ -159,6 +160,13 @@ class FilterModule(_ModuleBase):
def get_name() -> str:
return "过滤器"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Other
def stop(self):
pass

View File

@@ -18,7 +18,7 @@ from app.modules.indexer.spider.tnode import TNodeSpider
from app.modules.indexer.spider.torrentleech import TorrentLeech
from app.modules.indexer.spider.yema import YemaSpider
from app.schemas import SiteUserData
from app.schemas.types import MediaType
from app.schemas.types import MediaType, ModuleType
from app.utils.string import StringUtils
@@ -40,6 +40,13 @@ class IndexerModule(_ModuleBase):
def get_name() -> str:
return "站点索引"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Indexer
def stop(self):
pass

View File

@@ -5,8 +5,7 @@ from app.core.context import MediaInfo
from app.log import logger
from app.modules import _ModuleBase, _MediaServerBase
from app.modules.jellyfin.jellyfin import Jellyfin
from app.schemas import MediaServerConf
from app.schemas.types import MediaType
from app.schemas.types import MediaType, ModuleType
class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
@@ -22,6 +21,13 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
def get_name() -> str:
return "Jellyfin"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaServer
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
@@ -78,7 +84,10 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
server: Jellyfin = self.get_instance(source)
if not server:
return None
return server.get_webhook_message(body)
result = server.get_webhook_message(body)
if result:
result.server_name = source
return result
for server in self.get_instances().values():
if server:

View File

@@ -5,8 +5,7 @@ from app.core.context import MediaInfo
from app.log import logger
from app.modules import _ModuleBase, _MediaServerBase
from app.modules.plex.plex import Plex
from app.schemas import MediaServerConf
from app.schemas.types import MediaType
from app.schemas.types import MediaType, ModuleType
class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
@@ -22,6 +21,13 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
def get_name() -> str:
return "Plex"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaServer
def stop(self):
pass
@@ -64,7 +70,10 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
server: Plex = self.get_instance(source)
if not server:
return None
return server.get_webhook_message(form)
result = server.get_webhook_message(form)
if result:
result.server_name = source
return result
for server in self.get_instances().values():
if server:

View File

@@ -12,7 +12,7 @@ from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus
from app.schemas.types import TorrentStatus, ModuleType
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
@@ -30,6 +30,13 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
def get_name() -> str:
return "Qbittorrent"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Downloader
def stop(self):
pass

View File

@@ -82,9 +82,9 @@ class Qbittorrent:
logger.error(f"qbittorrent 连接出错:{str(err)}")
return None
def get_torrents(self, ids: Union[str, list] = None,
status: Union[str, list] = None,
tags: Union[str, list] = None) -> Tuple[List[TorrentDictionary], bool]:
def get_torrents(self, ids: Optional[Union[str, list]] = None,
status: Optional[str] = None,
tags: Optional[Union[str, list]] = None) -> Tuple[List[TorrentDictionary], bool]:
"""
获取种子列表
return: 种子列表, 是否发生异常
@@ -117,7 +117,7 @@ class Qbittorrent:
if not self.qbc:
return None
# completed会包含移动状态 改为获取seeding状态 包含活动上传, 正在做种, 及强制做种
torrents, error = self.get_torrents(status=["seeding"], ids=ids, tags=tags)
torrents, error = self.get_torrents(status="seeding", ids=ids, tags=tags)
return None if error else torrents or []
def get_downloading_torrents(self, ids: Union[str, list] = None,
@@ -129,7 +129,7 @@ class Qbittorrent:
if not self.qbc:
return None
torrents, error = self.get_torrents(ids=ids,
status=["downloading"],
status="downloading",
tags=tags)
return None if error else torrents or []
@@ -195,7 +195,7 @@ class Qbittorrent:
logger.error(f"设置强制作种出错:{str(err)}")
def __get_last_add_torrentid_by_tag(self, tags: Union[str, list],
status: Union[str, list] = None) -> Optional[str]:
status: Optional[str] = None) -> Optional[str]:
"""
根据种子的下载链接获取下载中或暂停的钟子的ID
:return: 种子ID
@@ -211,7 +211,7 @@ class Qbittorrent:
return None
def get_torrent_id_by_tag(self, tags: Union[str, list],
status: Union[str, list] = None) -> Optional[str]:
status: Optional[str] = None) -> Optional[str]:
"""
通过标签多次尝试获取刚添加的种子ID并移除标签
"""

View File

@@ -2,12 +2,12 @@ import json
import re
from typing import Optional, Union, List, Tuple, Any
from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.slack.slack import Slack
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
class SlackModule(_ModuleBase, _MessageBase[Slack]):
@@ -23,6 +23,13 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
def get_name() -> str:
return "Slack"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Notification
def stop(self):
"""
停止模块

View File

@@ -10,6 +10,7 @@ from app.core.context import Context
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.modules import _ModuleBase
from app.schemas.types import ModuleType
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
@@ -32,6 +33,13 @@ class SubtitleModule(_ModuleBase):
def get_name() -> str:
return "站点字幕"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Other
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass

View File

@@ -5,6 +5,7 @@ from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.synologychat.synologychat import SynologyChat
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
@@ -20,6 +21,13 @@ class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
def get_name() -> str:
return "Synology Chat"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Notification
def stop(self):
pass

View File

@@ -1,12 +1,12 @@
import json
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.telegram.telegram import Telegram
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
@@ -22,6 +22,13 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
def get_name() -> str:
return "Telegram"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Notification
def stop(self):
"""
停止模块

View File

@@ -13,7 +13,7 @@ from app.modules.themoviedb.scraper import TmdbScraper
from app.modules.themoviedb.tmdb_cache import TmdbCache
from app.modules.themoviedb.tmdbapi import TmdbApi
from app.schemas import MediaPerson
from app.schemas.types import MediaType, MediaImageType
from app.schemas.types import MediaType, MediaImageType, ModuleType
from app.utils.http import RequestUtils
@@ -41,6 +41,13 @@ class TheMovieDbModule(_ModuleBase):
def get_name() -> str:
return "TheMovieDb"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaRecognize
def stop(self):
self.cache.save()
self.tmdb.close()

View File

@@ -770,7 +770,7 @@ class TmdbApi:
logger.debug(f"{tmdbid} 查询结果:{tmdbinfo.get('title')}")
return tmdbinfo or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return None
def __get_tv_detail(self,
@@ -953,7 +953,7 @@ class TmdbApi:
logger.debug(f"{tmdbid} 查询结果:{tmdbinfo.get('name')}")
return tmdbinfo or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return None
def get_tv_season_detail(self, tmdbid: int, season: int):
@@ -1027,7 +1027,7 @@ class TmdbApi:
tmdbinfo = self.season.details(tv_id=tmdbid, season_num=season)
return tmdbinfo or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return {}
def get_tv_episode_detail(self, tmdbid: int, season: int, episode: int) -> dict:
@@ -1044,7 +1044,7 @@ class TmdbApi:
tmdbinfo = self.episode.details(tv_id=tmdbid, season_num=season, episode_num=episode)
return tmdbinfo or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return {}
def discover_movies(self, **kwargs) -> List[dict]:
@@ -1064,7 +1064,7 @@ class TmdbApi:
info['media_type'] = MediaType.MOVIE
return tmdbinfo or []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def discover_tvs(self, **kwargs) -> List[dict]:
@@ -1084,7 +1084,7 @@ class TmdbApi:
info['media_type'] = MediaType.TV
return tmdbinfo or []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def discover_trending(self, page: int = 1) -> List[dict]:
@@ -1097,7 +1097,7 @@ class TmdbApi:
logger.debug(f"正在获取流行趋势page={page} ...")
return self.trending.all_week(page=page)
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_movie_images(self, tmdbid: int) -> dict:
@@ -1110,7 +1110,7 @@ class TmdbApi:
logger.debug(f"正在获取电影图片:{tmdbid}...")
return self.movie.images(movie_id=tmdbid) or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return {}
def get_tv_images(self, tmdbid: int) -> dict:
@@ -1123,7 +1123,7 @@ class TmdbApi:
logger.debug(f"正在获取电视剧图片:{tmdbid}...")
return self.tv.images(tv_id=tmdbid) or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return {}
def get_movie_similar(self, tmdbid: int) -> List[dict]:
@@ -1136,7 +1136,7 @@ class TmdbApi:
logger.debug(f"正在获取相似电影:{tmdbid}...")
return self.movie.similar(movie_id=tmdbid) or []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_tv_similar(self, tmdbid: int) -> List[dict]:
@@ -1149,7 +1149,7 @@ class TmdbApi:
logger.debug(f"正在获取相似电视剧:{tmdbid}...")
return self.tv.similar(tv_id=tmdbid) or []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_movie_recommend(self, tmdbid: int) -> List[dict]:
@@ -1162,7 +1162,7 @@ class TmdbApi:
logger.debug(f"正在获取推荐电影:{tmdbid}...")
return self.movie.recommendations(movie_id=tmdbid) or []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_tv_recommend(self, tmdbid: int) -> List[dict]:
@@ -1175,7 +1175,7 @@ class TmdbApi:
logger.debug(f"正在获取推荐电视剧:{tmdbid}...")
return self.tv.recommendations(tv_id=tmdbid) or []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_movie_credits(self, tmdbid: int, page: int = 1, count: int = 24) -> List[dict]:
@@ -1192,7 +1192,7 @@ class TmdbApi:
return cast[(page - 1) * count: page * count]
return []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_tv_credits(self, tmdbid: int, page: int = 1, count: int = 24) -> List[dict]:
@@ -1209,7 +1209,7 @@ class TmdbApi:
return cast[(page - 1) * count: page * count]
return []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def get_person_detail(self, person_id: int) -> dict:
@@ -1242,7 +1242,7 @@ class TmdbApi:
logger.debug(f"正在获取人物详情:{person_id}...")
return self.person.details(person_id=person_id) or {}
except Exception as e:
print(str(e))
logger.error(str(e))
return {}
def get_person_credits(self, person_id: int, page: int = 1, count: int = 24) -> List[dict]:
@@ -1263,7 +1263,7 @@ class TmdbApi:
return cast[(page - 1) * count: page * count]
return []
except Exception as e:
print(str(e))
logger.error(str(e))
return []
def clear_cache(self):
@@ -1301,7 +1301,7 @@ class TmdbApi:
episode_years[order] = str(first_date).split("-")[0]
return episode_years
except Exception as e:
print(str(e))
logger.error(str(e))
return {}
def close(self):

View File

@@ -4,6 +4,7 @@ from app.core.config import settings
from app.log import logger
from app.modules import _ModuleBase
from app.modules.thetvdb import tvdbapi
from app.schemas.types import ModuleType
from app.utils.http import RequestUtils
@@ -20,6 +21,13 @@ class TheTvDbModule(_ModuleBase):
def get_name() -> str:
return "TheTvDb"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.MediaRecognize
def stop(self):
self.tvdb.close()

View File

@@ -12,7 +12,7 @@ from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase
from app.modules.transmission.transmission import Transmission
from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus
from app.schemas.types import TorrentStatus, ModuleType
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
@@ -30,6 +30,13 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
def get_name() -> str:
return "Transmission"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Downloader
def stop(self):
pass

View File

@@ -1,12 +1,12 @@
import json
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.config import settings
from app.core.context import Context, MediaInfo
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.vocechat.vocechat import VoceChat
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
@@ -22,6 +22,13 @@ class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
def get_name() -> str:
return "VoceChat"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Notification
def stop(self):
pass

View File

@@ -7,6 +7,7 @@ from app.core.config import global_vars, settings
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.schemas import Notification
from app.schemas.types import ModuleType
class WebPushModule(_ModuleBase, _MessageBase):
@@ -21,6 +22,13 @@ class WebPushModule(_ModuleBase, _MessageBase):
def get_name() -> str:
return "WebPush"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Notification
def stop(self):
pass

View File

@@ -7,6 +7,7 @@ from app.modules import _ModuleBase, _MessageBase
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
from app.modules.wechat.wechat import WeChat
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
from app.utils.dom import DomUtils
@@ -23,6 +24,13 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
def get_name() -> str:
return "微信"
@staticmethod
def get_type() -> ModuleType:
"""
获取模块类型
"""
return ModuleType.Notification
def stop(self):
pass

View File

@@ -139,6 +139,7 @@ class WebhookEventInfo(BaseModel):
"""
event: Optional[str] = None
channel: Optional[str] = None
server_name: Optional[str] = None
item_type: Optional[str] = None
item_name: Optional[str] = None
item_id: Optional[str] = None

View File

@@ -179,3 +179,19 @@ class StorageSchema(Enum):
Alipan = "alipan"
U115 = "u115"
Rclone = "rclone"
# 模块类型
class ModuleType(Enum):
# 下载器
Downloader = "downloader"
# 媒体服务器
MediaServer = "mediaserver"
# 消息服务
Notification = "notification"
# 媒体识别
MediaRecognize = "mediarecognize"
# 站点索引
Indexer = "indexer"
# 其它
Other = "other"

View File

@@ -185,7 +185,7 @@ class StringUtils:
忽略特殊字符
"""
# 需要忽略的特殊字符
CONVERT_EMPTY_CHARS = r"[、.。,,·:;!'\"“”()\[\]【】「」\-—\+\|\\_/&#~]"
CONVERT_EMPTY_CHARS = r"[、.。,,·:;!'\"“”()\[\]【】「」\-—\+\|\\_/&#~]"
if not text:
return text
if not isinstance(text, list):

View File

@@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List
from cachetools import TTLCache, cached
@@ -94,7 +94,7 @@ class WebUtils:
@staticmethod
@cached(cache=TTLCache(maxsize=1, ttl=3600))
def get_bing_wallpapers(num: int = 7) -> Optional[str]:
def get_bing_wallpapers(num: int = 7) -> List[str]:
"""
获取7天的Bing每日壁纸
"""
@@ -107,4 +107,4 @@ class WebUtils:
return [f"https://cn.bing.com{image.get('url')}" for image in result.get('images') or []]
except Exception as err:
print(str(err))
return None
return []

View File

@@ -23,7 +23,7 @@ APScheduler~=3.10.1
cryptography~=43.0.0
pytz~=2023.3
pycryptodome~=3.20.0
qbittorrent-api==2023.5.48
qbittorrent-api==2024.9.67
plexapi~=4.15.16
transmission-rpc~=4.3.0
Jinja2~=3.1.4

47
update
View File

@@ -192,6 +192,46 @@ function test_connectivity_github() {
esac
}
function compare_versions() {
local v1="$1"
local v2="$2"
local parts1=()
local parts2=()
IFS='.-' read -ra parts1 <<< "$v1"
IFS='.-' read -ra parts2 <<< "$v2"
local i
for ((i = 0; i < ${#parts1[@]} || i < ${#parts2[@]}; i++)); do
local part1="${parts1[i]:-0}"
local part2="${parts2[i]:-0}"
if [[ $part1 =~ ^[0-9]+$ && $part2 =~ ^[0-9]+$ ]]; then
if ((part1 > part2)); then
return 1
elif ((part1 < part2)); then
return 2
fi
else
# 定义一个非数字的排序顺序的优先级,特殊后缀版本比较
local order=(alpha beta rc stable)
local index1=-1
local index2=-1
for ((j = 0; j < ${#order[@]}; j++)); do
if [[ $part1 = "${order[j]}" ]]; then
index1=$j
fi
if [[ $part2 = "${order[j]}" ]]; then
index2=$j
fi
done
if ((index1 > index2)); then
return 1
elif ((index1 < index2)); then
return 2
fi
fi
done
return 0
}
if [[ "${MOVIEPILOT_AUTO_UPDATE}" = "true" ]] || [[ "${MOVIEPILOT_AUTO_UPDATE}" = "release" ]] || [[ "${MOVIEPILOT_AUTO_UPDATE}" = "dev" ]]; then
# 优先级:镜像站 > 全局 > 不代理
# pip
@@ -231,11 +271,14 @@ if [[ "${MOVIEPILOT_AUTO_UPDATE}" = "true" ]] || [[ "${MOVIEPILOT_AUTO_UPDATE}"
if [[ "${new_version}" == *v* ]]; then
release_version=${new_version}
INFO "最新版本号:${release_version}"
if [ "${current_version}" != "${release_version}" ]; then
# 使用版本号比较函数进行比较
if compare_versions "${current_version}" "${release_version}"; then
WARN "当前版本已是最新版本,跳过更新步骤..."
elif [ $? -eq 2 ]; then
INFO "发现新版本,开始自动升级..."
install_backend_and_download_resources "tags/${release_version}.zip"
else
INFO "未发现新版本,跳过更新步骤..."
WARN "当前版本高于远程版本,跳过更新步骤..."
fi
else
WARN "最新版本号获取失败,继续启动..."