This commit is contained in:
jxxghp
2023-06-06 07:15:17 +08:00
commit 4d06f86e62
217 changed files with 13959 additions and 0 deletions

58
app/chain/__init__.py Normal file
View File

@ -0,0 +1,58 @@
from abc import abstractmethod
from typing import Optional, Any
from app.core import Context, ModuleManager, EventManager
from app.log import logger
from app.utils.singleton import AbstractSingleton, Singleton
class _ChainBase(AbstractSingleton, metaclass=Singleton):
"""
处理链基类
"""
def __init__(self):
"""
公共初始化
"""
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 is_result_empty(ret):
"""
判断结果是否为空
"""
if isinstance(ret, tuple):
return all(value is None for value in ret)
else:
return result is None
logger.info(f"请求模块执行:{method} ...")
result = None
modules = self.modulemanager.get_modules(method)
for module in modules:
try:
if is_result_empty(result):
result = getattr(module, method)(*args, **kwargs)
else:
if isinstance(result, tuple):
temp = getattr(module, method)(*result)
else:
temp = getattr(module, method)(result)
if temp:
result = temp
except Exception as err:
logger.error(f"运行模块出错:{module.__class__.__name__} - {err}")
return result

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

383
app/chain/common.py Normal file
View File

@ -0,0 +1,383 @@
import re
from pathlib import Path
from typing import List, Optional, Tuple, Set
from app.chain import _ChainBase
from app.core import MediaInfo
from app.core import TorrentInfo, Context
from app.core.meta import MetaBase
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.utils.string import StringUtils
from app.utils.types import MediaType
class CommonChain(_ChainBase):
def __init__(self):
super().__init__()
self.torrent = TorrentHelper()
def process(self, *args, **kwargs) -> Optional[Context]:
pass
def post_message(self, title: str, text: str = None, image: str = None, userid: str = None):
"""
发送消息
"""
self.run_module('post_message', title=title, text=text, image=image, userid=userid)
def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo, userid: str = None):
"""
发送添加下载的消息
"""
msg_text = ""
if torrent.site_name:
msg_text = f"站点:{torrent.site_name}"
if meta.get_resource_type_string():
msg_text = f"{msg_text}\n质量:{meta.get_resource_type_string()}"
if torrent.size:
if str(torrent.size).isdigit():
size = StringUtils.str_filesize(torrent.size)
else:
size = torrent.size
msg_text = f"{msg_text}\n大小:{size}"
if torrent.title:
msg_text = f"{msg_text}\n种子:{torrent.title}"
if torrent.seeders:
msg_text = f"{msg_text}\n做种数:{torrent.seeders}"
msg_text = f"{msg_text}\n促销:{torrent.get_volume_factor_string()}"
if torrent.hit_and_run:
msg_text = f"{msg_text}\nHit&Run"
if torrent.description:
html_re = re.compile(r'<[^>]+>', re.S)
description = html_re.sub('', torrent.description)
torrent.description = re.sub(r'<[^>]+>', '', description)
msg_text = f"{msg_text}\n描述:{torrent.description}"
self.post_message(title=f"{mediainfo.get_title_string()}"
f"{meta.get_season_episode_string()} 开始下载",
text=msg_text,
image=mediainfo.get_message_image(),
userid=userid)
def batch_download(self,
contexts: List[Context],
need_tvs: dict = None,
userid: str = None) -> Tuple[List[Context], dict]:
"""
根据缺失数据,自动种子列表中组合择优下载
:param contexts: 资源上下文列表
:param need_tvs: 缺失的剧集信息
:param userid: 用户ID
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[mediainfo.tmdb_id] = [
{
"season": season,
"episodes": episodes,
"total_episodes": len(episodes)
}
]
"""
# 已下载的项目
downloaded_list: list = []
def __download_torrent(_torrent: TorrentInfo) -> Tuple[Optional[Path], list]:
"""
下载种子文件
:return: 种子路径,种子文件清单
"""
torrent_file, _, _, files, error_msg = self.torrent.download_torrent(
url=_torrent.enclosure,
cookie=_torrent.site_cookie,
ua=_torrent.site_ua,
proxy=_torrent.site_proxy)
if not torrent_file:
logger.error(f"下载种子文件失败:{_torrent.title} - {_torrent.enclosure}")
self.run_module('post_message',
title=f"{_torrent.title} 种子下载失败!",
text=f"错误信息:{error_msg}\n种子链接:{_torrent.enclosure}",
userid=userid)
return None, []
return torrent_file, files
def __download(_context: Context, _torrent_file: Path = None, _episodes: Set[int] = None) -> Optional[str]:
"""
下载及发送通知
"""
_torrent = _context.torrent_info
_media = _context.media_info
_meta = _context.meta_info
if not _torrent_file:
# 下载种子文件
_torrent_file, _ = __download_torrent(_torrent)
if not _torrent_file:
return
# 添加下载
_hash, error_msg = self.run_module("download",
torrent_path=_torrent_file,
mediainfo=_media,
episodes=_episodes)
if _hash:
# 下载成功
downloaded_list.append(_context)
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent, userid=userid)
else:
# 下载失败
logger.error(f"{_media.get_title_string()} 添加下载任务失败:"
f"{_torrent.title} - {_torrent.enclosure}{error_msg}")
self.run_module('post_message',
title="添加下载任务失败:%s %s"
% (_media.get_title_string(), _meta.get_season_episode_string()),
text=f"站点:{_torrent.site_name}\n"
f"种子名称:{_meta.org_string}\n"
f"种子链接:{_torrent.enclosure}\n"
f"错误信息:{error_msg}",
image=_media.get_message_image(),
userid=userid)
return _hash
def __update_seasons(tmdbid, need, current):
"""
更新need_tvs季数
"""
need = list(set(need).difference(set(current)))
for cur in current:
for nt in need_tvs.get(tmdbid):
if cur == nt.get("season") or (cur == 1 and not nt.get("season")):
need_tvs[tmdbid].remove(nt)
if not need_tvs.get(tmdbid):
need_tvs.pop(tmdbid)
return need
def __update_episodes(tmdbid, seq, need, current):
"""
更新need_tvs集数
"""
need = list(set(need).difference(set(current)))
if need:
need_tvs[tmdbid][seq]["episodes"] = need
else:
need_tvs[tmdbid].pop(seq)
if not need_tvs.get(tmdbid):
need_tvs.pop(tmdbid)
return need
def __get_season_episodes(tmdbid, season):
"""
获取需要的季的集数
"""
if not need_tvs.get(tmdbid):
return 0
for nt in need_tvs.get(tmdbid):
if season == nt.get("season"):
return nt.get("total_episodes")
return 0
# 如果是电影,直接下载
for context in contexts:
if context.media_info.type == MediaType.MOVIE:
__download(context)
# 电视剧整季匹配
if need_tvs:
# 先把整季缺失的拿出来,看是否刚好有所有季都满足的种子
need_seasons = {}
for need_tmdbid, need_tv in need_tvs.items():
for tv in need_tv:
if not tv:
continue
if not tv.get("episodes"):
if not need_seasons.get(need_tmdbid):
need_seasons[need_tmdbid] = []
need_seasons[need_tmdbid].append(tv.get("season") or 1)
# 查找整季包含的种子,只处理整季没集的种子或者是集数超过季的种子
for need_tmdbid, need_season in need_seasons.items():
for context in contexts:
media = context.media_info
meta = context.meta_info
torrent = context.torrent_info
if media.type == MediaType.MOVIE:
continue
item_season = meta.get_season_list()
if meta.get_episode_list():
continue
if need_tmdbid == media.tmdb_id:
if set(item_season).issubset(set(need_season)):
if len(item_season) == 1:
# 只有一季的可能是命名错误,需要打开种子鉴别,只有实际集数大于等于总集数才下载
torrent_path, torrent_files = __download_torrent(torrent)
if not torrent_path:
continue
torrent_episodes = self.torrent.get_torrent_episodes(torrent_files)
if not torrent_episodes \
or len(torrent_episodes) >= __get_season_episodes(need_tmdbid, item_season[0]):
_, download_id = __download(_context=context, _torrent_file=torrent_path)
else:
logger.info(
f"【Downloader】种子 {meta.org_string} 未含集数信息,解析文件数为 {len(torrent_episodes)}")
continue
else:
download_id = __download(context)
if download_id:
# 更新仍需季集
need_season = __update_seasons(tmdbid=need_tmdbid,
need=need_season,
current=item_season)
# 电视剧季内的集匹配
if need_tvs:
need_tv_list = list(need_tvs)
for need_tmdbid in need_tv_list:
need_tv = need_tvs.get(need_tmdbid)
if not need_tv:
continue
index = 0
for tv in need_tv:
need_season = tv.get("season") or 1
need_episodes = tv.get("episodes")
total_episodes = tv.get("total_episodes")
# 缺失整季的转化为缺失集进行比较
if not need_episodes:
need_episodes = list(range(1, total_episodes + 1))
for context in contexts:
media = context.media_info
meta = context.meta_info
if media.type == MediaType.MOVIE:
continue
if media.tmdb_id == need_tmdbid:
if context in downloaded_list:
continue
# 只处理单季含集的种子
item_season = meta.get_season_list()
if len(item_season) != 1 or item_season[0] != need_season:
continue
item_episodes = meta.get_episode_list()
if not item_episodes:
continue
# 为需要集的子集则下载
if set(item_episodes).issubset(set(need_episodes)):
download_id = __download(context)
if download_id:
# 更新仍需集数
need_episodes = __update_episodes(tmdbid=need_tmdbid,
need=need_episodes,
seq=index,
current=item_episodes)
index += 1
# 仍然缺失的剧集从整季中选择需要的集数文件下载仅支持QB和TR
if need_tvs:
need_tv_list = list(need_tvs)
for need_tmdbid in need_tv_list:
need_tv = need_tvs.get(need_tmdbid)
if not need_tv:
continue
index = 0
for tv in need_tv:
need_season = tv.get("season") or 1
need_episodes = tv.get("episodes")
if not need_episodes:
continue
for context in contexts:
media = context.media_info
meta = context.meta_info
torrent = context.torrent_info
if media.type == MediaType.MOVIE:
continue
if context in downloaded_list:
continue
if not need_episodes:
break
# 选中一个单季整季的或单季包括需要的所有集的
if media.tmdb_id == need_tmdbid \
and (not meta.get_episode_list()
or set(meta.get_episode_list()).intersection(set(need_episodes))) \
and len(meta.get_season_list()) == 1 \
and meta.get_season_list()[0] == need_season:
# 检查种子看是否有需要的集
torrent_path, torrent_files = __download_torrent(torrent)
if not torrent_path:
continue
# 种子全部集
torrent_episodes = self.torrent.get_torrent_episodes(torrent_files)
# 选中的集
selected_episodes = set(torrent_episodes).intersection(set(need_episodes))
if not selected_episodes:
logger.info(f"{meta.org_string} 没有需要的集,跳过...")
continue
# 添加下载
download_id = __download(_context=context,
_torrent_file=torrent_path,
_episodes=selected_episodes)
if not download_id:
continue
# 更新仍需集数
need_episodes = __update_episodes(tmdbid=need_tmdbid,
need=need_episodes,
seq=index,
current=selected_episodes)
index += 1
# 返回下载的资源,剩下没下完的
return downloaded_list, need_tvs
def get_no_exists_info(self, mediainfo: MediaInfo, no_exists: dict = None) -> Tuple[bool, dict]:
"""
检查媒体库,查询是否存在,对于剧集同时返回不存在的季集信息
:param mediainfo: 已识别的媒体信息
:param no_exists: 在调用该方法前已经存储的不存在的季集信息,有传入时该函数搜索的内容将会叠加后输出
:return: 当前媒体是否缺失,各标题总的季集和缺失的季集
"""
def __append_no_exists(_season: int, _episodes: list):
"""
添加不存在的季集信息
"""
if not no_exists.get(mediainfo.tmdb_id):
no_exists[mediainfo.tmdb_id] = [
{
"season": season,
"episodes": episodes,
"total_episodes": len(episodes)
}
]
else:
no_exists[mediainfo.tmdb_id].append({
"season": season,
"episodes": episodes,
"total_episodes": len(episodes)
})
if not no_exists:
no_exists = {}
if not mediainfo.seasons:
logger.error(f"媒体信息中没有季集信息:{mediainfo.get_title_string()}")
return False, {}
if mediainfo.type == MediaType.MOVIE:
# 电影
exists_movies = self.run_module("media_exists", mediainfo)
if exists_movies:
logger.info(f"媒体库中已存在电影:{mediainfo.get_title_string()}")
return True, {}
return False, {}
else:
# 电视剧
exists_tvs = self.run_module("media_exists", mediainfo)
if not exists_tvs:
# 所有剧集均缺失
for season, episodes in mediainfo.seasons.items():
# 添加不存在的季集信息
__append_no_exists(season, episodes)
return False, no_exists
else:
# 存在一些,检查缺失的季集
for season, episodes in mediainfo.seasons.items():
exist_seasons = exists_tvs.get("seasons")
if exist_seasons.get(season):
# 取差集
episodes = set(episodes).difference(set(exist_seasons['season']))
# 添加不存在的季集信息
__append_no_exists(season, episodes)
# 存在不完整的剧集
if no_exists:
return False, no_exists
# 全部存在
return True, no_exists

55
app/chain/cookiecloud.py Normal file
View File

@ -0,0 +1,55 @@
from typing import Tuple
from app.chain import _ChainBase
from app.core import settings
from app.db.sites import Sites
from app.helper.cookiecloud import CookieCloudHelper
from app.helper.sites import SitesHelper
from app.log import logger
class CookieCloudChain(_ChainBase):
"""
同步站点Cookie
"""
def __init__(self):
super().__init__()
self.sites = Sites()
self.siteshelper = SitesHelper()
self.cookiecloud = CookieCloudHelper(
server=settings.COOKIECLOUD_HOST,
key=settings.COOKIECLOUD_KEY,
password=settings.COOKIECLOUD_PASSWORD
)
def process(self) -> Tuple[bool, str]:
"""
通过CookieCloud同步站点Cookie
"""
logger.info("开始同步CookieCloud站点 ...")
cookies, msg = self.cookiecloud.download()
if not cookies:
logger.error(f"CookieCloud同步失败{msg}")
return False, msg
# 保存Cookie或新增站点
_update_count = 0
_add_count = 0
for domain, cookie in cookies.items():
if self.sites.exists(domain):
# 更新站点Cookie
self.sites.update_cookie(domain, cookie)
_update_count += 1
else:
# 获取站点信息
indexer = self.siteshelper.get_indexer(domain)
if indexer:
# 新增站点
self.sites.add(name=indexer.get("name"),
url=indexer.get("domain"),
domain=domain,
cookie=cookie)
_add_count += 1
ret_msg = f"更新了{_update_count}个站点,新增了{_add_count}个站点"
logger.info(f"CookieCloud同步成功{ret_msg}")
return True, ret_msg

100
app/chain/douban_sync.py Normal file
View File

@ -0,0 +1,100 @@
from pathlib import Path
from app.chain import _ChainBase
from app.chain.common import CommonChain
from app.chain.search import SearchChain
from app.core import settings, MetaInfo
from app.db.subscribes import Subscribes
from app.helper.rss import RssHelper
from app.log import logger
class DoubanSyncChain(_ChainBase):
"""
同步豆瓣相看数据
"""
_interests_url: str = "https://www.douban.com/feed/people/%s/interests"
_cache_path: Path = settings.TEMP_PATH / "__doubansync_cache__"
def __init__(self):
super().__init__()
self.rsshelper = RssHelper()
self.common = CommonChain()
self.searchchain = SearchChain()
self.subscribes = Subscribes()
def process(self):
"""
通过用户RSS同步豆瓣相看数据
"""
if not settings.DOUBAN_USER_IDS:
return
# 读取缓存
caches = self._cache_path.read_text().split("\n") if self._cache_path.exists() else []
for user_id in settings.DOUBAN_USER_IDS.split(","):
# 同步每个用户的豆瓣数据
if not user_id:
continue
logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...")
url = self._interests_url % user_id
results = self.rsshelper.parse(url)
if not results:
logger.error(f"未获取到用户 {user_id} 豆瓣RSS数据{url}")
return
# 解析数据
for result in results:
dtype = result.get("title", "")[:2]
title = result.get("title", "")[2:]
if dtype not in ["想看"]:
continue
douban_id = result.get("link", "").split("/")[-2]
if not douban_id or douban_id in caches:
continue
# 根据豆瓣ID获取豆瓣数据
doubaninfo = self.run_module('douban_info', doubanid=douban_id)
if not doubaninfo:
logger.warn(f'未获取到豆瓣信息,标题:{title}豆瓣ID{douban_id}')
continue
# 识别媒体信息
meta = MetaInfo(doubaninfo.get("original_title") or doubaninfo.get("title"))
if doubaninfo.get("year"):
meta.year = doubaninfo.get("year")
mediainfo = self.run_module('recognize_media', meta=meta)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{title}豆瓣ID{douban_id}')
continue
# 加入缓存
caches.append(douban_id)
# 查询缺失的媒体信息
exist_flag, no_exists = self.common.get_no_exists_info(mediainfo=mediainfo)
if exist_flag:
logger.info(f'{mediainfo.get_title_string()} 媒体库中已存在')
continue
# 搜索
contexts = self.searchchain.process(meta=meta, mediainfo=mediainfo)
if not contexts:
logger.warn(f'{mediainfo.get_title_string()} 未搜索到资源')
continue
# 自动下载
_, lefts = self.common.batch_download(contexts=contexts, need_tvs=no_exists)
if not lefts:
# 全部下载完成
logger.info(f'{mediainfo.get_title_string()} 下载完成')
else:
# 未完成下载
logger.info(f'{mediainfo.get_title_string()} 未下载未完整,添加订阅 ...')
# 添加订阅
state, msg = self.subscribes.add(mediainfo,
season=meta.begin_season)
if state:
# 订阅成功
self.common.post_message(
title=f"{mediainfo.get_title_string()} 已添加订阅",
text="来自:豆瓣相看",
image=mediainfo.get_message_image())
logger.info(f"用户 {user_id} 豆瓣相看同步完成")
# 保存缓存
self._cache_path.write_text("\n".join(caches))

33
app/chain/identify.py Normal file
View File

@ -0,0 +1,33 @@
from typing import Optional
from app.chain import _ChainBase
from app.core import Context, MetaInfo, 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 = self.run_module('prepare_recognize', title=title, subtitle=subtitle)
if result:
title, subtitle = result
# 识别元数据
metainfo = MetaInfo(title, subtitle)
# 识别媒体信息
mediainfo: MediaInfo = self.run_module('recognize_media', meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta=metainfo)
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.get_title_string()}')
# 更新媒体图片
self.run_module('obtain_image', mediainfo=mediainfo)
# 返回上下文
return Context(meta=metainfo, mediainfo=mediainfo, title=title, subtitle=subtitle)

73
app/chain/search.py Normal file
View File

@ -0,0 +1,73 @@
from typing import Optional, List
from app.chain import _ChainBase
from app.chain.common import CommonChain
from app.core import Context, MetaInfo, MediaInfo, TorrentInfo
from app.core.meta import MetaBase
from app.helper.sites import SitesHelper
from app.log import logger
class SearchChain(_ChainBase):
"""
站点资源搜索处理链
"""
def __init__(self):
super().__init__()
self.common = CommonChain()
self.siteshelper = SitesHelper()
def process(self, meta: MetaBase, mediainfo: MediaInfo,
keyword: str = None) -> Optional[List[Context]]:
"""
根据媒体信息,执行搜索
:param meta: 元数据
:param mediainfo: 媒体信息
:param keyword: 搜索关键词
"""
# 执行搜索
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
torrents: List[TorrentInfo] = self.run_module(
'search_torrents',
mediainfo=mediainfo,
keyword=keyword,
sites=self.siteshelper.get_indexers()
)
if not torrents:
logger.warn(f'{keyword or mediainfo.title} 未搜索到资源')
return []
# 过滤不匹配的资源
_match_torrents = []
if mediainfo:
for torrent in torrents:
# 比对IMDBID
if torrent.imdbid \
and mediainfo.imdb_id \
and torrent.imdbid == mediainfo.imdb_id:
_match_torrents.append(torrent)
continue
# 识别
torrent_meta = MetaInfo(torrent.title, torrent.description)
# 识别媒体信息
torrent_mediainfo: MediaInfo = self.run_module('recognize_media', meta=torrent_meta)
if not torrent_mediainfo:
logger.warn(f'未识别到媒体信息,标题:{torrent.title}')
continue
# 过滤
if torrent_mediainfo.tmdb_id == mediainfo.tmdb_id \
and torrent_mediainfo.type == mediainfo.type:
_match_torrents.append(torrent)
else:
_match_torrents = torrents
# 过滤种子
result = self.run_module("filter_torrents", torrent_list=_match_torrents)
if result is not None:
_match_torrents = result
if not _match_torrents:
logger.warn(f'{keyword or mediainfo.title} 没有符合过滤条件的资源')
return []
# 组装上下文返回
return [Context(meta=MetaInfo(torrent.title),
mediainfo=mediainfo,
torrentinfo=torrent) for torrent in _match_torrents]

195
app/chain/subscribe.py Normal file
View File

@ -0,0 +1,195 @@
from typing import Dict, List
from app.chain import _ChainBase
from app.chain.common import CommonChain
from app.chain.search import SearchChain
from app.core import MetaInfo, TorrentInfo, Context, MediaInfo
from app.db.subscribes import Subscribes
from app.helper.sites import SitesHelper
from app.log import logger
from app.utils.string import StringUtils
from app.utils.types import MediaType
class SubscribeChain(_ChainBase):
"""
订阅处理链
"""
# 站点最新种子缓存 {站点域名: 种子上下文}
_torrents_cache: Dict[str, List[Context]] = {}
def __init__(self):
super().__init__()
self.common = CommonChain()
self.searchchain = SearchChain()
self.subscribes = Subscribes()
self.siteshelper = SitesHelper()
def process(self, title: str,
mtype: MediaType = None,
tmdbid: str = None,
season: int = None,
username: str = None,
**kwargs) -> bool:
"""
识别媒体信息并添加订阅
"""
logger.info(f'开始添加订阅,标题:{title} ...')
# 识别前预处理
result = self.run_module('prepare_recognize', title=title)
if result:
title, _ = result
# 识别元数据
metainfo = MetaInfo(title)
if mtype:
metainfo.type = mtype
if season:
metainfo.type = MediaType.TV
metainfo.begin_season = season
# 识别媒体信息
mediainfo = self.run_module('recognize_media', meta=metainfo, tmdbid=tmdbid)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{title}tmdbid{tmdbid}')
return False
# 更新媒体图片
self.run_module('obtain_image', mediainfo=mediainfo)
# 添加订阅
state, err_msg = self.subscribes.add(mediainfo, season=season, **kwargs)
if state:
logger.info(f'{mediainfo.get_title_string()} {err_msg}')
else:
logger.error(f'{mediainfo.get_title_string()} 添加订阅成功')
self.common.post_message(title=f"{mediainfo.get_title_string()} 已添加订阅",
text="用户:{username}",
image=mediainfo.get_message_image())
# 返回结果
return state
def search(self, sid: int = None, state: str = 'N'):
"""
订阅搜索
:param sid: 订阅ID有值时只处理该订阅
:param state: 订阅状态 N:未搜索 R:已搜索
:return: 更新订阅状态为R或删除订阅
"""
if sid:
subscribes = [self.subscribes.get(sid)]
else:
subscribes = self.subscribes.list(state)
# 遍历订阅
for subscribe in subscribes:
# 如果状态为N则更新为R
if subscribe.state == 'N':
self.subscribes.update(subscribe.id, {'state': 'R'})
# 生成元数据
meta = MetaInfo(subscribe.name)
meta.year = subscribe.year
meta.begin_season = subscribe.season
meta.type = MediaType.MOVIE if subscribe.type == MediaType.MOVIE.value else MediaType.TV
# 识别媒体信息
mediainfo = self.run_module('recognize_media', meta=meta, tmdbid=subscribe.tmdbid)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}')
continue
# 查询缺失的媒体信息
exist_flag, no_exists = self.common.get_no_exists_info(mediainfo=mediainfo)
if exist_flag:
logger.info(f'{mediainfo.get_title_string()} 媒体库中已存在,完成订阅')
self.subscribes.delete(subscribe.id)
continue
# 搜索
contexts = self.searchchain.process(meta=meta, mediainfo=mediainfo, keyword=subscribe.keyword)
if not contexts:
logger.warn(f'{subscribe.keyword or subscribe.name} 未搜索到资源')
continue
# 自动下载
_, lefts = self.common.batch_download(contexts=contexts, need_tvs=no_exists)
if not lefts:
# 全部下载完成
logger.info(f'{mediainfo.get_title_string()} 下载完成,完成订阅')
self.subscribes.delete(subscribe.id)
else:
# 未完成下载
logger.info(f'{mediainfo.get_title_string()} 未下载未完整,继续订阅 ...')
def refresh(self):
"""
刷新站点最新资源
"""
# 所有站点索引
indexers = self.siteshelper.get_indexers()
# 遍历站点缓存资源
for indexer in indexers:
domain = StringUtils.get_url_domain(indexer.get("domain"))
torrents: List[TorrentInfo] = self.run_module("refresh_torrents", sites=[indexer])
if torrents:
self._torrents_cache[domain] = []
for torrent in torrents:
# 识别
meta = MetaInfo(torrent.title, torrent.description)
# 识别媒体信息
mediainfo = self.run_module('recognize_media', meta=meta)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{torrent.title}')
continue
# 上下文
context = Context(meta=meta, mediainfo=mediainfo, torrentinfo=torrent)
self._torrents_cache[domain].append(context)
# 从缓存中匹配订阅
self.match()
def match(self):
"""
从缓存中匹配订阅,并自动下载
"""
# 所有订阅
subscribes = self.subscribes.list('R')
# 遍历订阅
for subscribe in subscribes:
# 生成元数据
meta = MetaInfo(subscribe.name)
meta.year = subscribe.year
meta.begin_season = subscribe.season
meta.type = MediaType.MOVIE if subscribe.type == MediaType.MOVIE.value else MediaType.TV
# 识别媒体信息
mediainfo: MediaInfo = self.run_module('recognize_media', meta=meta, tmdbid=subscribe.tmdbid)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}')
continue
# 查询缺失的媒体信息
exist_flag, no_exists = self.common.get_no_exists_info(mediainfo=mediainfo)
if exist_flag:
logger.info(f'{mediainfo.get_title_string()} 媒体库中已存在,完成订阅')
self.subscribes.delete(subscribe.id)
continue
# 遍历缓存种子
_match_context = []
for domain, contexts in self._torrents_cache.items():
for context in contexts:
# 检查是否匹配
torrent_meta = context.meta_info
torrent_mediainfo = context.media_info
torrent_info = context.torrent_info
if torrent_mediainfo.tmdb_id == mediainfo.tmdb_id \
and torrent_mediainfo.type == mediainfo.type:
if meta.begin_season and meta.begin_season != torrent_meta.begin_season:
continue
# 匹配成功
logger.info(f'{mediainfo.get_title_string()} 匹配成功:{torrent_info.title}')
_match_context.append(context)
logger(f'{mediainfo.get_title_string()} 匹配完成,共匹配到{len(_match_context)}个资源')
if _match_context:
# 批量择优下载
_, lefts = self.common.batch_download(contexts=_match_context, need_tvs=no_exists)
if not lefts:
# 全部下载完成
logger.info(f'{mediainfo.get_title_string()} 下载完成,完成订阅')
self.subscribes.delete(subscribe.id)
else:
# 未完成下载,计算剩余集数
left_episodes = lefts.get(mediainfo.tmdb_id, {}).get("episodes", [])
logger.info(f'{mediainfo.get_title_string()} 未下载未完整,更新缺失集数为{len(left_episodes)} ...')
self.subscribes.update(subscribe.id, {
"lack_episode": len(left_episodes)
})

266
app/chain/user_message.py Normal file
View File

@ -0,0 +1,266 @@
from typing import Dict
from fastapi import Request
from app.chain import _ChainBase
from app.chain.common import *
from app.chain.search import SearchChain
from app.core import MediaInfo, TorrentInfo, MetaInfo
from app.db.subscribes import Subscribes
from app.log import logger
from app.utils.types import EventType
class UserMessageChain(_ChainBase):
"""
外来消息处理链
"""
# 缓存的用户数据 {userid: {type: str, items: list}}
_user_cache: Dict[str, dict] = {}
# 每页数据量
_page_size: int = 8
# 当前页面
_current_page: int = 0
# 当前元数据
_current_meta: Optional[MetaInfo] = None
# 当前媒体信息
_current_media: Optional[MediaInfo] = None
def __init__(self):
super().__init__()
self.common = CommonChain()
self.subscribes = Subscribes()
self.searchchain = SearchChain()
def process(self, request: Request, *args, **kwargs) -> None:
"""
识别消息内容,执行操作
"""
# 获取消息内容
info: dict = self.run_module('message_parser', request=request)
if not info:
return
# 用户ID
userid = info.get('userid')
if not userid:
logger.debug(f'未识别到用户ID{request}')
return
# 消息内容
text = str(info.get('text')).strip() if info.get('text') else None
if not text:
logger.debug(f'未识别到消息内容:{request}')
return
logger.info(f'收到用户消息内容,用户:{userid},内容:{text}')
if text.startswith('/'):
# 执行命令
self.eventmanager.send_event(
EventType.CommandExcute,
{
"cmd": text
}
)
elif text.isdigit():
# 缓存
cache_data: dict = self._user_cache.get(userid)
# 选择项目
if not cache_data \
or not cache_data.get('items') \
or len(cache_data.get('items')) < int(text):
# 发送消息
self.common.post_message(title="输入有误!", userid=userid)
return
# 缓存类型
cache_type: str = cache_data.get('type')
# 缓存列表
cache_list: list = cache_data.get('items')
# 选择
if cache_type == "Search":
mediainfo: MediaInfo = cache_list[int(text) - 1]
self._current_media = mediainfo
# 检查是否已存在
exists: list = self.run_module('media_exists', mediainfo=mediainfo)
if exists:
# 已存在
self.common.post_message(
title=f"{mediainfo.type.value} {mediainfo.get_title_string()} 媒体库中已存在", userid=userid)
return
# 搜索种子
contexts = self.searchchain.process(meta=self._current_meta, mediainfo=mediainfo)
if not contexts:
# 没有数据
self.common.post_message(title=f"{mediainfo.title} 未搜索到资源!", userid=userid)
return
# 更新缓存
self._user_cache[userid] = {
"type": "Torrent",
"items": contexts
}
self._current_page = 0
# 发送种子数据
self.__post_torrents_message(items=contexts[:self._page_size], userid=userid)
elif cache_type == "Subscribe":
# 订阅媒体
mediainfo: MediaInfo = cache_list[int(text) - 1]
self._current_media = mediainfo
state, msg = self.subscribes.add(mediainfo,
season=self._current_meta.begin_season,
episode=self._current_meta.begin_episode)
if state:
# 订阅成功
self.common.post_message(
title=f"{mediainfo.get_title_string()} 已添加订阅",
image=mediainfo.get_message_image(),
userid=userid)
else:
# 订阅失败
self.common.post_message(title=f"{mediainfo.title} 添加订阅失败:{msg}", userid=userid)
elif cache_type == "Torrent":
if int(text) == 0:
# 自动选择下载
# 查询缺失的媒体信息
exist_flag, no_exists = self.common.get_no_exists_info(mediainfo=self._current_media)
if exist_flag:
self.common.post_message(title=f"{self._current_media.get_title_string()} 媒体库中已存在",
userid=userid)
return
# 批量下载
self.common.batch_download(contexts=cache_list, need_tvs=no_exists, userid=userid)
else:
# 下载种子
torrent: TorrentInfo = cache_list[int(text) - 1]
# 识别种子信息
meta = MetaInfo(torrent.title)
# 预处理种子
torrent_file, msg = self.run_module("prepare_torrent", torrentinfo=torrent)
if not torrent_file:
# 下载失败
self.run_module('post_message',
title=f"{torrent.title} 种子下载失败!",
text=f"错误信息:{msg}\n种子链接:{torrent.enclosure}",
userid=userid)
return
# 添加下载
state, msg = self.run_module("download_torrent",
torrent_path=torrent_file,
mediainfo=self._current_media)
if not state:
# 下载失败
self.common.post_message(title=f"{torrent.title} 添加下载失败!",
text=f"错误信息:{msg}",
userid=userid)
return
# 下载成功,发送通知
self.common.post_download_message(meta=meta, mediainfo=self._current_media, torrent=torrent)
elif text.lower() == "p":
# 上一页
cache_data: dict = self._user_cache.get(userid)
if not cache_data:
# 没有缓存
self.common.post_message(title="输入有误!", userid=userid)
return
if self._current_page == 0:
# 第一页
self.common.post_message(title="已经是第一页了!", userid=userid)
return
cache_type: str = cache_data.get('type')
cache_list: list = cache_data.get('items')
# 减一页
self._current_page -= 1
if self._current_page == 0:
start = 0
end = self._page_size
else:
start = self._current_page * self._page_size
end = start + self._page_size
if cache_type == "Torrent":
# 发送种子数据
self.__post_torrents_message(items=cache_list[start:end], userid=userid)
else:
# 发送媒体数据
self.__post_medias_message(items=cache_list[start:end], userid=userid)
elif text.lower() == "n":
# 下一页
cache_data: dict = self._user_cache.get(userid)
if not cache_data:
# 没有缓存
self.common.post_message(title="输入有误!", userid=userid)
return
cache_type: str = cache_data.get('type')
cache_list: list = cache_data.get('items')
# 加一页
self._current_page += 1
cache_list = cache_list[self._current_page * self._page_size:]
if not cache_list:
# 没有数据
self.common.post_message(title="已经是最后一页了!", userid=userid)
return
else:
if cache_type == "Torrent":
# 发送种子数据
self.__post_torrents_message(items=cache_list, userid=userid)
else:
# 发送媒体数据
self.__post_medias_message(items=cache_list, userid=userid)
else:
# 搜索或订阅
if text.startswith("订阅"):
# 订阅
content = re.sub(r"订阅[:\s]*", "", text)
action = "Subscribe"
else:
# 搜索
content = re.sub(r"(搜索|下载)[:\s]*", "", text)
action = "Search"
# 提取要素
mtype, key_word, season_num, episode_num, year, title = StringUtils.get_keyword(content)
# 识别
meta = MetaInfo(title)
if not meta.get_name():
self.common.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
# 开始搜索
medias = self.run_module('search_medias', meta=meta)
if not medias:
self.common.post_message(title=f"{meta.get_name()} 没有找到对应的媒体信息!", userid=userid)
return
self._user_cache[userid] = {
'type': action,
'items': medias
}
self._current_page = 0
self._current_media = None
# 发送媒体列表
self.__post_medias_message(items=medias[:self._page_size], userid=userid)
def __post_medias_message(self, items: list, userid: str):
"""
发送媒体列表消息
"""
self.run_module('post_medias_message',
title="请回复数字选择对应媒体p上一页, n下一页",
items=items,
userid=userid)
def __post_torrents_message(self, items: list, userid: str):
"""
发送种子列表消息
"""
self.run_module('post_torrents_message',
title="请回复数字下载对应资源0自动选择, p上一页, n下一页",
items=items,
userid=userid)

View File

@ -0,0 +1,20 @@
from typing import Any
from app.chain import _ChainBase
class WebhookMessageChain(_ChainBase):
"""
响应Webhook事件
"""
def process(self, message: dict) -> None:
"""
处理Webhook报文并发送消息
"""
# 获取主体内容
info = self.run_module('webhook_parser', message=message)
if not info:
return
# 发送消息
self.run_module("post_message", title=info.get("title"), text=info.get("text"), image=info.get("image"))