diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index ebfe8079..f8ce17c9 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -398,6 +398,20 @@ def user_subscribes( return Subscribe.list_by_username(db, username) +@router.get("/files/{subscribe_id}", summary="订阅相关文件信息", response_model=List[schemas.SubscrbieInfo]) +def subscribe_files( + subscribe_id: int, + db: Session = Depends(get_db), + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 订阅相关文件信息 + """ + subscribe = Subscribe.get(db, subscribe_id) + if subscribe: + return SubscribeChain().subscribe_files_info(subscribe) + return schemas.SubscrbieInfo() + + @router.get("/{subscribe_id}", summary="订阅详情", response_model=schemas.Subscribe) def read_subscribe( subscribe_id: int, diff --git a/app/chain/__init__.py b/app/chain/__init__.py index 9e1d477d..7af8fcfa 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -459,6 +459,14 @@ class ChainBase(metaclass=ABCMeta): """ return self.run_module("media_exists", mediainfo=mediainfo, itemid=itemid) + def media_files(self, mediainfo: MediaInfo) -> Optional[List[FileItem]]: + """ + 获取媒体文件清单 + :param mediainfo: 识别的媒体信息 + :return: 媒体文件列表 + """ + return self.run_module("media_files", mediainfo=mediainfo) + def post_message(self, message: Notification) -> None: """ 发送消息 diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index bf2f67bd..2201e3f9 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -9,12 +9,14 @@ from app.chain import ChainBase from app.chain.download import DownloadChain from app.chain.media import MediaChain from app.chain.search import SearchChain +from app.chain.tmdb import TmdbChain from app.chain.torrents import TorrentsChain from app.core.config import settings from app.core.context import TorrentInfo, Context, MediaInfo from app.core.event import eventmanager, Event, EventManager from app.core.meta import MetaBase from app.core.metainfo import MetaInfo +from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.models.subscribe import Subscribe from app.db.site_oper import SiteOper from app.db.subscribe_oper import SubscribeOper @@ -24,7 +26,7 @@ from app.helper.message import MessageHelper from app.helper.subscribe import SubscribeHelper from app.helper.torrent import TorrentHelper from app.log import logger -from app.schemas import NotExistMediaInfo, Notification +from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType @@ -36,12 +38,14 @@ class SubscribeChain(ChainBase): def __init__(self): super().__init__() self.downloadchain = DownloadChain() + self.downloadhis = DownloadHistoryOper() self.searchchain = SearchChain() self.subscribeoper = SubscribeOper() self.subscribehistoryoper = SubscribeHistoryOper() self.subscribehelper = SubscribeHelper() self.torrentschain = TorrentsChain() self.mediachain = MediaChain() + self.tmdbchain = TmdbChain() self.message = MessageHelper() self.systemconfig = SystemConfigOper() self.torrenthelper = TorrentHelper() @@ -983,7 +987,8 @@ class SubscribeChain(ChainBase): begin_season: int, total_episode: int, start_episode: int, - downloaded_episodes: List[int] = None): + downloaded_episodes: List[int] = None + ) -> Dict[Union[int, str], Dict[int, NotExistMediaInfo]]: """ 根据订阅开始集数和总集数,结合TMDB信息计算当前订阅的缺失集数 :param subscribe_name: 订阅名称 @@ -1118,7 +1123,7 @@ class SubscribeChain(ChainBase): }) @staticmethod - def __get_default_subscribe_config(mtype: MediaType, default_config_key: str): + def __get_default_subscribe_config(mtype: MediaType, default_config_key: str) -> Optional[str]: """ 获取默认订阅配置 """ @@ -1155,3 +1160,110 @@ class SubscribeChain(ChainBase): "min_seeders": default_rule.get("min_seeders"), "min_seeders_time": default_rule.get("min_seeders_time"), } + + def subscribe_files_info(self, subscribe: Subscribe) -> Optional[SubscrbieInfo]: + """ + 订阅相关的下载和文件信息 + """ + if not subscribe: + return + + # 返回订阅数据 + subscribe_info = SubscrbieInfo( + id=subscribe.id, + name=subscribe.name, + year=subscribe.year, + type=subscribe.type, + tmdbid=subscribe.tmdbid, + doubanid=subscribe.doubanid, + season=subscribe.season, + poster=subscribe.poster, + backdrop=subscribe.backdrop, + vote=subscribe.vote, + description=subscribe.description, + episodes_info={} + ) + + # 所有集的数据 + episodes_info = {} + if subscribe.tmdbid and subscribe.type == MediaType.TV.value: + # 查询TMDB中的集信息 + tmdb_episodes = self.tmdbchain.tmdb_episodes( + tmdbid=subscribe.tmdb_id, + season=subscribe.season + ) + if tmdb_episodes: + for episode in tmdb_episodes: + episode_info = SubscribeEpisodeInfo() + episodes_info.title = episode.name + episodes_info.description = episode.overview + episodes_info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}" + episodes_info[episode.episode_number] = episode_info + elif subscribe.type == MediaType.TV.value: + # 根据开始结束集计算集信息 + for i in range(subscribe.start_episode or 1, subscribe.total_episode + 1): + episode_info = SubscribeEpisodeInfo() + episode_info.title = f'第 {i} 集' + episodes_info[i] = episode_info + else: + # 电影 + episode_info = SubscribeEpisodeInfo() + episode_info.title = subscribe.name + episodes_info[0] = episode_info + + # 所有下载记录 + download_his = self.downloadhis.get_by_mediaid(tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid) + if download_his: + for his in download_his: + # 种子链接 + torrent_url = f"【{his.torrent_site}】{his.torrent_name}" + # 查询下载文件 + files = self.downloadhis.get_files_by_hash(his.hash) + if files: + for file in files: + # 识别文件名 + file_meta = MetaInfo(file.filepath) + if subscribe.type == MediaType.TV.value: + episode_number = file_meta.begin_episode + if episode_number and episodes_info.get(episode_number): + episodes_info[episode_number].download_file = file.fullpath + episodes_info[episode_number].torrent = torrent_url + else: + episodes_info[0].download_file = file.fullpath + episodes_info[0].torrent = torrent_url + + # 生成元数据 + meta = MetaInfo(subscribe.name) + meta.year = subscribe.year + meta.begin_season = subscribe.season or None + try: + meta.type = MediaType(subscribe.type) + except ValueError: + logger.error(f'订阅 {subscribe.name} 类型错误:{subscribe.type}') + return subscribe_info + # 识别媒体信息 + mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, + tmdbid=subscribe.tmdbid, + doubanid=subscribe.doubanid, + cache=False) + if not mediainfo: + logger.warn( + f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}') + return subscribe_info + + # 所有媒体库文件记录 + library_fileitems = self.media_files(mediainfo) + if library_fileitems: + for fileitem in library_fileitems: + # 识别文件名 + file_meta = MetaInfo(fileitem.path) + if subscribe.type == MediaType.TV.value: + episode_number = file_meta.begin_episode + if episode_number and episodes_info.get(episode_number): + episodes_info[episode_number].library_file = fileitem.path + else: + episodes_info[0].library_file = fileitem.path + + # 更新订阅信息 + subscribe_info.episodes_info = episodes_info + return subscribe_info diff --git a/app/db/downloadhistory_oper.py b/app/db/downloadhistory_oper.py index a7fe1b64..dfe8b6cf 100644 --- a/app/db/downloadhistory_oper.py +++ b/app/db/downloadhistory_oper.py @@ -23,6 +23,14 @@ class DownloadHistoryOper(DbOper): """ return DownloadHistory.get_by_hash(self._db, download_hash) + def get_by_mediaid(self, tmdbid: int, doubanid: str) -> List[DownloadHistory]: + """ + 按媒体ID查询下载记录 + :param tmdbid: tmdbid + :param doubanid: doubanid + """ + return DownloadHistory.get_by_mediaid(self._db, tmdbid=tmdbid, doubanid=doubanid) + def add(self, **kwargs): """ 新增下载历史 diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index 27d5c94a..c6341a81 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -53,6 +53,12 @@ class DownloadHistory(Base): def get_by_hash(db: Session, download_hash: str): return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).first() + @staticmethod + @db_query + def get_by_mediaid(db: Session, tmdbid: int, doubanid: str): + return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid, + DownloadHistory.doubanid == doubanid).all() + @staticmethod @db_query def list_by_page(db: Session, page: int = 1, count: int = 30): diff --git a/app/modules/filemanager/__init__.py b/app/modules/filemanager/__init__.py index d1f14d08..b89ce762 100644 --- a/app/modules/filemanager/__init__.py +++ b/app/modules/filemanager/__init__.py @@ -1043,12 +1043,12 @@ class FileManagerModule(_ModuleBase): else: return Path(render_str) - def media_exists(self, mediainfo: MediaInfo, **kwargs) -> Optional[ExistMediaInfo]: + def media_files(self, mediainfo: MediaInfo) -> List[FileItem]: """ - 判断媒体文件是否存在于文件系统(网盘或本地文件),只支持标准媒体库结构 - :param mediainfo: 识别的媒体信息 - :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} + 获取对应媒体的媒体库文件列表 + :param mediainfo: 媒体信息 """ + ret_fileitems = [] # 检查本地媒体库 dest_dirs = DirectoryHelper().get_library_dirs() # 检查每一个媒体库目录 @@ -1059,8 +1059,6 @@ class FileManagerModule(_ModuleBase): continue # 媒体分类路径 dir_path = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dest_dir) - if not storage_oper.get_item(dir_path): - continue # 重命名格式 rename_format = settings.TV_RENAME_FORMAT \ if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT @@ -1079,30 +1077,45 @@ class FileManagerModule(_ModuleBase): if not media_path.exists(): continue # 检索媒体文件 - media_files = SystemUtils.list_files(directory=media_path, extensions=settings.RMT_MEDIAEXT) - if not media_files: + fileitem = storage_oper.get_item(media_path) + if not fileitem: continue - if mediainfo.type == MediaType.MOVIE: - # 电影存在任何文件为存在 - logger.info(f"{mediainfo.title_year} 在本地文件系统中找到了") - return ExistMediaInfo(type=MediaType.MOVIE) - else: - # 电视剧检索集数 - seasons: Dict[int, list] = {} - for media_file in media_files: - file_meta = MetaInfo(media_file.stem) - season_index = file_meta.begin_season or 1 - episode_index = file_meta.begin_episode - if not episode_index: - continue - if season_index not in seasons: - seasons[season_index] = [] + media_files = storage_oper.list(fileitem) + if media_files: + ret_fileitems.extend(media_files) + return ret_fileitems + + def media_exists(self, mediainfo: MediaInfo, **kwargs) -> Optional[ExistMediaInfo]: + """ + 判断媒体文件是否存在于文件系统(网盘或本地文件),只支持标准媒体库结构 + :param mediainfo: 识别的媒体信息 + :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} + """ + # 检查媒体库 + fileitems = self.media_files(mediainfo) + if not fileitems: + return None + + if mediainfo.type == MediaType.MOVIE: + # 电影存在任何文件为存在 + logger.info(f"{mediainfo.title_year} 在本地文件系统中找到了") + return ExistMediaInfo(type=MediaType.MOVIE) + else: + # 电视剧检索集数 + seasons: Dict[int, list] = {} + for fileitem in fileitems: + file_meta = MetaInfo(fileitem.basename) + season_index = file_meta.begin_season or 1 + episode_index = file_meta.begin_episode + if not episode_index: + continue + if season_index not in seasons: + seasons[season_index] = [] + if episode_index not in seasons[season_index]: seasons[season_index].append(episode_index) - # 返回剧集情况 - logger.info(f"{mediainfo.title_year} 在本地文件系统中找到了这些季集:{seasons}") - return ExistMediaInfo(type=MediaType.TV, seasons=seasons) - # 不存在 - return None + # 返回剧集情况 + logger.info(f"{mediainfo.title_year} 在本地文件系统中找到了这些季集:{seasons}") + return ExistMediaInfo(type=MediaType.TV, seasons=seasons) def __delete_version_files(self, target_storage: str, path: Path) -> bool: """ diff --git a/app/modules/slack/__init__.py b/app/modules/slack/__init__.py index c44d1e36..531a4277 100644 --- a/app/modules/slack/__init__.py +++ b/app/modules/slack/__init__.py @@ -248,7 +248,7 @@ class SlackModule(_ModuleBase, _MessageBase): continue client: Slack = self.get_client(conf.name) if client: - client.send_meidas_msg(title=message.title, medias=medias, userid=message.userid) + client.send_medias_msg(title=message.title, medias=medias, userid=message.userid) def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None: """ diff --git a/app/modules/slack/slack.py b/app/modules/slack/slack.py index 34134a3e..edcb6728 100644 --- a/app/modules/slack/slack.py +++ b/app/modules/slack/slack.py @@ -168,7 +168,7 @@ class Slack: logger.error(f"Slack消息发送失败: {msg_e}") return False, str(msg_e) - def send_meidas_msg(self, medias: List[MediaInfo], userid: str = "", title: str = "") -> Optional[bool]: + def send_medias_msg(self, medias: List[MediaInfo], userid: str = "", title: str = "") -> Optional[bool]: """ 发送列表类消息 """ diff --git a/app/modules/synologychat/__init__.py b/app/modules/synologychat/__init__.py index e6e70adc..f5d2e767 100644 --- a/app/modules/synologychat/__init__.py +++ b/app/modules/synologychat/__init__.py @@ -117,7 +117,7 @@ class SynologyChatModule(_ModuleBase, _MessageBase): continue client: SynologyChat = self.get_client(conf.name) if client: - client.send_meidas_msg(title=message.title, medias=medias, + client.send_medias_msg(title=message.title, medias=medias, userid=message.userid) def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None: diff --git a/app/modules/synologychat/synologychat.py b/app/modules/synologychat/synologychat.py index e6dd3c2b..bf692303 100644 --- a/app/modules/synologychat/synologychat.py +++ b/app/modules/synologychat/synologychat.py @@ -90,7 +90,7 @@ class SynologyChat: logger.error(f"SynologyChat发送消息错误:{str(msg_e)}") return False - def send_meidas_msg(self, medias: List[MediaInfo], userid: str = "", title: str = "") -> Optional[bool]: + def send_medias_msg(self, medias: List[MediaInfo], userid: str = "", title: str = "") -> Optional[bool]: """ 发送列表类消息 """ diff --git a/app/modules/telegram/__init__.py b/app/modules/telegram/__init__.py index 7e60117a..6a88548c 100644 --- a/app/modules/telegram/__init__.py +++ b/app/modules/telegram/__init__.py @@ -163,7 +163,7 @@ class TelegramModule(_ModuleBase, _MessageBase): continue client: Telegram = self.get_client(conf.name) if client: - client.send_meidas_msg(title=message.title, medias=medias, + client.send_medias_msg(title=message.title, medias=medias, userid=message.userid, link=message.link) def post_torrents_message(self, message: Notification, torrents: List[Context]) -> None: diff --git a/app/modules/telegram/telegram.py b/app/modules/telegram/telegram.py index 1d499996..e5906232 100644 --- a/app/modules/telegram/telegram.py +++ b/app/modules/telegram/telegram.py @@ -114,7 +114,7 @@ class Telegram: logger.error(f"发送消息失败:{msg_e}") return False - def send_meidas_msg(self, medias: List[MediaInfo], userid: str = "", + def send_medias_msg(self, medias: List[MediaInfo], userid: str = "", title: str = "", link: str = None) -> Optional[bool]: """ 发送媒体列表消息 diff --git a/app/schemas/subscribe.py b/app/schemas/subscribe.py index 2d2a52bb..e437228b 100644 --- a/app/schemas/subscribe.py +++ b/app/schemas/subscribe.py @@ -1,5 +1,5 @@ import json -from typing import Optional, List +from typing import Optional, List, Dict from pydantic import BaseModel, validator @@ -79,3 +79,45 @@ class Subscribe(BaseModel): class Config: orm_mode = True + + +class SubscribeEpisodeInfo(BaseModel): + # 种子地址 + torrent: Optional[str] = None + # 下载文件路程 + download_file: Optional[str] = None + # 媒体库文件路径 + library_file: Optional[str] = None + # 标题 + title: Optional[str] = None + # 描述 + description: Optional[str] = None + # 背景图 + backdrop: Optional[str] = None + + +class SubscrbieInfo(BaseModel): + # 订阅ID + id: Optional[int] = None + # 订阅名称 + name: Optional[str] = None + # 订阅年份 + year: Optional[str] = None + # 订阅类型 电影/电视剧 + type: Optional[str] = None + # 媒体ID + tmdbid: Optional[int] = None + doubanid: Optional[str] = None + bangumiid: Optional[int] = None + # 季号 + season: Optional[int] = None + # 海报 + poster: Optional[str] = None + # 背景图 + backdrop: Optional[str] = None + # 评分 + vote: Optional[int] = 0 + # 描述 + description: Optional[str] = None + # 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}} + episodes_info: Optional[Dict[int, SubscribeEpisodeInfo]] = {}