This commit is contained in:
jxxghp
2023-06-17 17:34:18 +08:00
parent f85e960fa9
commit acdec220f7
42 changed files with 423 additions and 253 deletions

View File

@@ -1,19 +1,18 @@
import traceback
from abc import abstractmethod
from pathlib import Path
from typing import Optional, Any, Tuple, List, Set, Union, Dict
from ruamel.yaml import CommentedMap
from app.core.context import Context
from app.core.event import EventManager
from app.core.module import ModuleManager
from app.core.context import MediaInfo, TorrentInfo
from app.core.event import EventManager
from app.core.meta import MetaBase
from app.core.module import ModuleManager
from app.log import logger
from app.schemas.context import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent
from app.utils.singleton import AbstractSingleton, Singleton
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent
from app.schemas.types import TorrentStatus, MediaType
from app.utils.singleton import AbstractSingleton, Singleton
class ChainBase(AbstractSingleton, metaclass=Singleton):
@@ -28,14 +27,7 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
self.modulemanager = ModuleManager()
self.eventmanager = EventManager()
@abstractmethod
def process(self, *args, **kwargs) -> Optional[Context]:
"""
处理链,返回上下文
"""
pass
def run_module(self, method: str, *args, **kwargs) -> Any:
def __run_module(self, method: str, *args, **kwargs) -> Any:
"""
运行包含该方法的所有模块,然后返回结果
"""
@@ -69,84 +61,84 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
def prepare_recognize(self, title: str,
subtitle: str = None) -> Tuple[str, str]:
return self.run_module("prepare_recognize", title=title, subtitle=subtitle)
return self.__run_module("prepare_recognize", title=title, subtitle=subtitle)
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
tmdbid: int = None) -> Optional[MediaInfo]:
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
return self.__run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
def obtain_image(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
return self.run_module("obtain_image", mediainfo=mediainfo)
return self.__run_module("obtain_image", mediainfo=mediainfo)
def douban_info(self, doubanid: str) -> Optional[dict]:
return self.run_module("douban_info", doubanid=doubanid)
return self.__run_module("douban_info", doubanid=doubanid)
def tvdb_info(self, tvdbid: int) -> Optional[dict]:
return self.run_module("tvdb_info", tvdbid=tvdbid)
return self.__run_module("tvdb_info", tvdbid=tvdbid)
def message_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:
return self.run_module("message_parser", body=body, form=form, args=args)
return self.__run_module("message_parser", body=body, form=form, args=args)
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:
return self.run_module("webhook_parser", body=body, form=form, args=args)
return self.__run_module("webhook_parser", body=body, form=form, args=args)
def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]:
return self.run_module("search_medias", meta=meta)
return self.__run_module("search_medias", meta=meta)
def search_torrents(self, mediainfo: Optional[MediaInfo], sites: List[CommentedMap],
keyword: str = None) -> Optional[List[TorrentInfo]]:
return self.run_module("search_torrents", mediainfo=mediainfo, sites=sites, keyword=keyword)
return self.__run_module("search_torrents", mediainfo=mediainfo, sites=sites, keyword=keyword)
def refresh_torrents(self, sites: List[CommentedMap]) -> Optional[List[TorrentInfo]]:
return self.run_module("refresh_torrents", sites=sites)
return self.__run_module("refresh_torrents", sites=sites)
def filter_torrents(self, torrent_list: List[TorrentInfo],
season_episodes: Dict[int, list] = None) -> List[TorrentInfo]:
return self.run_module("filter_torrents", torrent_list=torrent_list, season_episodes=season_episodes)
return self.__run_module("filter_torrents", torrent_list=torrent_list, season_episodes=season_episodes)
def download(self, torrent_path: Path, cookie: str,
episodes: Set[int] = None) -> Optional[Tuple[Optional[str], str]]:
return self.run_module("download", torrent_path=torrent_path, cookie=cookie, episodes=episodes)
return self.__run_module("download", torrent_path=torrent_path, cookie=cookie, episodes=episodes)
def download_added(self, context: Context, torrent_path: Path) -> None:
return self.run_module("download_added", context=context, torrent_path=torrent_path)
return self.__run_module("download_added", context=context, torrent_path=torrent_path)
def list_torrents(self, status: TorrentStatus = None,
hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
return self.run_module("list_torrents", status=status, hashs=hashs)
return self.__run_module("list_torrents", status=status, hashs=hashs)
def transfer(self, path: Path, mediainfo: MediaInfo) -> Optional[TransferInfo]:
return self.run_module("transfer", path=path, mediainfo=mediainfo)
return self.__run_module("transfer", path=path, mediainfo=mediainfo)
def transfer_completed(self, hashs: Union[str, list], transinfo: TransferInfo) -> None:
return self.run_module("transfer_completed", hashs=hashs, transinfo=transinfo)
return self.__run_module("transfer_completed", hashs=hashs, transinfo=transinfo)
def remove_torrents(self, hashs: Union[str, list]) -> bool:
return self.run_module("remove_torrents", hashs=hashs)
return self.__run_module("remove_torrents", hashs=hashs)
def media_exists(self, mediainfo: MediaInfo) -> Optional[ExistMediaInfo]:
return self.run_module("media_exists", mediainfo=mediainfo)
return self.__run_module("media_exists", mediainfo=mediainfo)
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]:
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
return self.__run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
def post_message(self, title: str, text: str = None,
image: str = None, userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_message", title=title, text=text, image=image, userid=userid)
return self.__run_module("post_message", title=title, text=text, image=image, userid=userid)
def post_medias_message(self, title: str, items: List[MediaInfo],
userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_medias_message", title=title, items=items, userid=userid)
return self.__run_module("post_medias_message", title=title, items=items, userid=userid)
def post_torrents_message(self, title: str, items: List[Context],
mediainfo: MediaInfo,
userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_torrents_message", title=title, mediainfo=mediainfo,
items=items, userid=userid)
return self.__run_module("post_torrents_message", title=title, mediainfo=mediainfo,
items=items, userid=userid)
def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None:
return self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
return self.__run_module("scrape_metadata", path=path, mediainfo=mediainfo)
def register_commands(self, commands: dict) -> None:
return self.run_module("register_commands", commands=commands)
return self.__run_module("register_commands", commands=commands)

View File

@@ -12,7 +12,7 @@ from app.helper.rss import RssHelper
from app.log import logger
class DoubanSyncChain(ChainBase):
class DoubanChain(ChainBase):
"""
同步豆瓣想看数据
"""
@@ -28,7 +28,7 @@ class DoubanSyncChain(ChainBase):
self.searchchain = SearchChain()
self.subscribechain = SubscribeChain()
def process(self):
def sync(self):
"""
通过用户RSS同步豆瓣想看数据
"""
@@ -80,8 +80,7 @@ class DoubanSyncChain(ChainBase):
continue
logger.info(f'{mediainfo.title_year} 媒体库中不存在,开始搜索 ...')
# 搜索
contexts = self.searchchain.process(meta=meta,
mediainfo=mediainfo,
contexts = self.searchchain.process(mediainfo=mediainfo,
no_exists=no_exists)
if not contexts:
logger.warn(f'{mediainfo.title_year} 未搜索到资源')
@@ -95,12 +94,12 @@ class DoubanSyncChain(ChainBase):
# 未完成下载
logger.info(f'{mediainfo.title_year} 未下载未完整,添加订阅 ...')
# 添加订阅
self.subscribechain.process(title=mediainfo.title,
year=mediainfo.year,
mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
season=meta.begin_season,
username="豆瓣想看")
self.subscribechain.add(title=mediainfo.title,
year=mediainfo.year,
mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
season=meta.begin_season,
username="豆瓣想看")
logger.info(f"用户 {user_id} 豆瓣想看同步完成")
# 保存缓存

View File

@@ -8,7 +8,7 @@ from app.core.meta import MetaBase
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.schemas.context import ExistMediaInfo, NotExistMediaInfo
from app.schemas import ExistMediaInfo, NotExistMediaInfo
from app.schemas.types import MediaType, TorrentStatus, EventType
from app.utils.string import StringUtils
@@ -20,9 +20,6 @@ class DownloadChain(ChainBase):
self.torrent = TorrentHelper()
self.downloadhis = DownloadHistoryOper()
def process(self, *args, **kwargs) -> Optional[Context]:
pass
def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo, userid: str = None):
"""
发送添加下载的消息

View File

@@ -1,34 +0,0 @@
from typing import Optional
from app.chain import ChainBase
from app.core.metainfo import MetaInfo
from app.core.context import Context, MediaInfo
from app.log import logger
class IdentifyChain(ChainBase):
"""
识别处理链
"""
def process(self, title: str, subtitle: str = None) -> Optional[Context]:
"""
识别媒体信息
"""
logger.info(f'开始识别媒体信息,标题:{title},副标题:{subtitle} ...')
# 识别前预处理
result: Optional[tuple] = self.prepare_recognize(title=title, subtitle=subtitle)
if result:
title, subtitle = result
# 识别元数据
metainfo = MetaInfo(title, subtitle)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta=metainfo)
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_image(mediainfo=mediainfo)
# 返回上下文
return Context(meta=metainfo, mediainfo=mediainfo, title=title, subtitle=subtitle)

68
app/chain/media.py Normal file
View File

@@ -0,0 +1,68 @@
from typing import Optional, List, Tuple
from app.chain import ChainBase
from app.core.context import Context, MediaInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.log import logger
from app.utils.string import StringUtils
class MediaChain(ChainBase):
"""
识别处理链
"""
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
"""
识别媒体信息
"""
logger.info(f'开始识别媒体信息,标题:{title},副标题:{subtitle} ...')
# 识别前预处理
result: Optional[tuple] = self.prepare_recognize(title=title, subtitle=subtitle)
if result:
title, subtitle = result
# 识别元数据
metainfo = MetaInfo(title, subtitle)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta=metainfo)
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_image(mediainfo=mediainfo)
# 返回上下文
return Context(meta=metainfo, mediainfo=mediainfo, title=title, subtitle=subtitle)
def search(self, title: str) -> Tuple[MetaBase, List[MediaInfo]]:
"""
搜索媒体信息
:param title: 搜索内容
:return: 识别元数据,媒体信息列表
"""
# 提取要素
mtype, key_word, season_num, episode_num, year, content = StringUtils.get_keyword(title)
# 识别
meta = MetaInfo(content)
if not meta.name:
logger.warn(f'{title} 未识别到元数据!')
return meta, []
# 合并信息
if mtype:
meta.type = mtype
if season_num:
meta.begin_season = season_num
if episode_num:
meta.begin_episode = episode_num
if year:
meta.year = year
# 开始搜索
logger.info(f"开始搜索媒体信息:{meta.name}")
medias: Optional[List[MediaInfo]] = self.search_medias(meta=meta)
if not medias:
logger.warn(f"{meta.name} 没有找到对应的媒体信息!")
return meta, []
logger.info(f"{content} 搜索到 {len(medias)} 条相关媒体信息")
# 识别的元数据,媒体信息列表
return meta, medias

View File

@@ -1,16 +1,16 @@
from typing import Any
from app.chain.download import *
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.chain.subscribe import SubscribeChain
from app.core.context import MediaInfo
from app.core.event import EventManager
from app.core.metainfo import MetaInfo
from app.log import logger
from app.schemas.types import EventType
class UserMessageChain(ChainBase):
class MessageChain(ChainBase):
"""
外来消息处理链
"""
@@ -30,6 +30,7 @@ class UserMessageChain(ChainBase):
self.downloadchain = DownloadChain()
self.subscribechain = SubscribeChain()
self.searchchain = SearchChain()
self.medtachain = MediaChain()
self.torrent = TorrentHelper()
self.eventmanager = EventManager()
@@ -93,16 +94,16 @@ class UserMessageChain(ChainBase):
# 发送缺失的媒体信息
if no_exists:
# 发送消息
messages = [f"{no_exist.get('season')} 季缺失 {len(no_exist.get('episodes')) or no_exist.get('total_episodes')}"
for no_exist in no_exists.get(mediainfo.tmdb_id)]
messages = [
f"{no_exist.get('season')} 季缺失 {len(no_exist.get('episodes')) or no_exist.get('total_episodes')}"
for no_exist in no_exists.get(mediainfo.tmdb_id)]
self.post_message(title=f"{mediainfo.title_year}\n" + "\n".join(messages))
# 搜索种子,过滤掉不需要的剧集,以便选择
logger.info(f"{mediainfo.title_year} 媒体库中不存在,开始搜索 ...")
self.post_message(
title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...", userid=userid)
# 开始搜索
contexts = self.searchchain.process(meta=self._current_meta,
mediainfo=mediainfo,
contexts = self.searchchain.process(mediainfo=mediainfo,
no_exists=no_exists)
if not contexts:
# 没有数据
@@ -129,13 +130,13 @@ class UserMessageChain(ChainBase):
elif cache_type == "Subscribe":
# 订阅媒体
mediainfo: MediaInfo = cache_list[int(text) - 1]
self.subscribechain.process(title=mediainfo.title,
year=mediainfo.year,
mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
season=self._current_meta.begin_season,
userid=userid,
username=username)
self.subscribechain.add(title=mediainfo.title,
year=mediainfo.year,
mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
season=self._current_meta.begin_season,
userid=userid,
username=username)
elif cache_type == "Torrent":
if int(text) == 0:
# 自动选择下载
@@ -158,13 +159,13 @@ class UserMessageChain(ChainBase):
# 未完成下载
logger.info(f'{self._current_media.title_year} 未下载未完整,添加订阅 ...')
# 添加订阅
self.subscribechain.process(title=self._current_media.title,
year=self._current_media.year,
mtype=self._current_media.type,
tmdbid=self._current_media.tmdb_id,
season=self._current_meta.begin_season,
userid=userid,
username=username)
self.subscribechain.add(title=self._current_media.title,
year=self._current_media.year,
mtype=self._current_media.type,
tmdbid=self._current_media.tmdb_id,
season=self._current_meta.begin_season,
userid=userid,
username=username)
else:
# 下载种子
context: Context = cache_list[int(text) - 1]
@@ -245,31 +246,19 @@ class UserMessageChain(ChainBase):
# 搜索
content = re.sub(r"(搜索|下载)[:\s]*", "", text)
action = "Search"
# 提取要素
mtype, key_word, season_num, episode_num, year, title = StringUtils.get_keyword(content)
# 搜索
meta, medias = self.medtachain.search(content)
# 识别
meta = MetaInfo(title)
if not meta.name:
self.post_message(title="无法识别输入内容!", userid=userid)
return
# 合并信息
if mtype:
meta.type = mtype
if season_num:
meta.begin_season = season_num
if episode_num:
meta.begin_episode = episode_num
if year:
meta.year = year
# 记录当前状态
self._current_meta = meta
# 开始搜索
logger.info(f"开始搜索:{meta.name}")
medias: Optional[List[MediaInfo]] = self.search_medias(meta=meta)
if not medias:
self.post_message(title=f"{meta.name} 没有找到对应的媒体信息!", userid=userid)
return
logger.info(f"搜索到 {len(medias)} 条相关媒体信息")
# 记录当前状态
self._current_meta = meta
self._user_cache[userid] = {
'type': action,
'items': medias

View File

@@ -3,13 +3,12 @@ from typing import Optional, List, Dict
from app.chain import ChainBase
from app.core.config import settings
from app.core.context import Context, MediaInfo, TorrentInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.helper.sites import SitesHelper
from app.log import logger
from app.schemas.context import NotExistMediaInfo
from app.utils.string import StringUtils
from app.schemas import NotExistMediaInfo
from app.schemas.types import MediaType
from app.utils.string import StringUtils
class SearchChain(ChainBase):
@@ -21,12 +20,23 @@ class SearchChain(ChainBase):
super().__init__()
self.siteshelper = SitesHelper()
def process(self, meta: MetaBase, mediainfo: MediaInfo,
def search_by_tmdbid(self, tmdbid: int, mtype: str = None) -> Optional[List[Context]]:
"""
根据TMDB ID搜索资源不过滤本地存在的内容
:param tmdbid: TMDB ID
:param mtype: 媒体,电影 or 电视剧
"""
mediainfo = self.recognize_media(tmdbid=tmdbid, mtype=mtype)
if not mediainfo:
logger.error(f'{tmdbid} 媒体信息识别失败!')
return None
return self.process(mediainfo=mediainfo)
def process(self, mediainfo: MediaInfo,
keyword: str = None,
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None) -> Optional[List[Context]]:
"""
根据媒体信息,执行搜索
:param meta: 元数据
根据媒体信息,搜索种子资源
:param mediainfo: 媒体信息
:param keyword: 搜索关键词
:param no_exists: 缺失的媒体信息
@@ -124,6 +134,6 @@ class SearchChain(ChainBase):
_match_torrents = torrents
logger.info(f"匹配完成,共匹配到 {len(_match_torrents)} 个资源")
# 组装上下文返回
return [Context(meta=MetaInfo(torrent.title),
return [Context(meta=MetaInfo(title=torrent.title, subtitle=torrent.description),
mediainfo=mediainfo,
torrentinfo=torrent) for torrent in _match_torrents]

View File

@@ -7,7 +7,7 @@ from app.helper.cookie import CookieHelper
from app.log import logger
class SiteMessageChain(ChainBase):
class SiteChain(ChainBase):
"""
站点远程管理处理链
"""
@@ -20,7 +20,7 @@ class SiteMessageChain(ChainBase):
self._siteoper = SiteOper()
self._cookiehelper = CookieHelper()
def process(self, userid: Union[str, int] = None):
def list(self, userid: Union[str, int] = None):
"""
查询所有站点发送消息
"""
@@ -63,7 +63,7 @@ class SiteMessageChain(ChainBase):
"is_active": False
})
# 重新发送消息
self.process()
self.list()
def enable(self, arg_str, userid: Union[str, int] = None):
"""
@@ -84,7 +84,7 @@ class SiteMessageChain(ChainBase):
"is_active": True
})
# 重新发送消息
self.process()
self.list()
def get_cookie(self, arg_str: str, userid: Union[str, int] = None):
"""

View File

@@ -9,7 +9,7 @@ from app.core.config import settings
from app.db.subscribe_oper import SubscribeOper
from app.helper.sites import SitesHelper
from app.log import logger
from app.schemas.context import NotExistMediaInfo
from app.schemas import NotExistMediaInfo
from app.utils.string import StringUtils
from app.schemas.types import MediaType
@@ -29,13 +29,13 @@ class SubscribeChain(ChainBase):
self.subscribehelper = SubscribeOper()
self.siteshelper = SitesHelper()
def process(self, title: str, year: str,
mtype: MediaType = None,
tmdbid: int = None,
season: int = None,
userid: str = None,
username: str = None,
**kwargs) -> Optional[int]:
def add(self, title: str, year: str,
mtype: MediaType = None,
tmdbid: int = None,
season: int = None,
userid: str = None,
username: str = None,
**kwargs) -> Optional[int]:
"""
识别媒体信息并添加订阅
"""
@@ -153,8 +153,7 @@ class SubscribeChain(ChainBase):
)
# 搜索
contexts = self.searchchain.process(meta=meta,
mediainfo=mediainfo,
contexts = self.searchchain.process(mediainfo=mediainfo,
keyword=subscribe.keyword,
no_exists=no_exists)
if not contexts:

View File

@@ -9,7 +9,7 @@ from app.core.metainfo import MetaInfo
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.models.downloadhistory import DownloadHistory
from app.log import logger
from app.schemas.context import TransferInfo, TransferTorrent
from app.schemas import TransferInfo, TransferTorrent
from app.schemas.types import TorrentStatus, EventType, MediaType
from app.utils.string import StringUtils

View File

@@ -6,12 +6,12 @@ from app.utils.http import WebUtils
from app.schemas.types import EventType
class WebhookMessageChain(ChainBase):
class WebhookChain(ChainBase):
"""
响应Webhook事件
"""
def process(self, body: Any, form: Any, args: Any) -> None:
def message(self, body: Any, form: Any, args: Any) -> None:
"""
处理Webhook报文并发送消息
"""