From 078b60cc1e1c9043e5a5d4aca6182129b2461b44 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 3 Apr 2025 18:35:02 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=94=AF=E6=8C=81=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=E5=89=A7=E9=9B=86=E7=BB=84=E8=AF=86=E5=88=AB=E5=92=8C?= =?UTF-8?q?=E5=88=AE=E5=89=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/subscribe.py | 1 + app/api/endpoints/tmdb.py | 4 +- app/api/endpoints/transfer.py | 1 + app/chain/__init__.py | 5 +- app/chain/download.py | 33 +++++----- app/chain/media.py | 14 +++-- app/chain/subscribe.py | 33 +++++++--- app/chain/tmdb.py | 5 +- app/chain/transfer.py | 18 ++++-- app/core/context.py | 4 +- app/db/models/downloadhistory.py | 2 + app/db/models/subscribe.py | 4 ++ app/db/models/subscribehistory.py | 2 + app/db/models/transferhistory.py | 2 + app/db/subscribe_oper.py | 2 + app/db/transferhistory_oper.py | 2 + app/modules/douban/__init__.py | 4 +- app/modules/themoviedb/__init__.py | 80 +++++++++++++++++++++--- app/modules/themoviedb/scraper.py | 10 ++- app/modules/themoviedb/tmdbapi.py | 81 +++++++++++++------------ app/schemas/context.py | 4 +- app/schemas/history.py | 4 ++ app/schemas/subscribe.py | 6 ++ app/schemas/transfer.py | 2 + database/versions/4b544f5d3b07_2_1_3.py | 33 ++++++++++ 25 files changed, 256 insertions(+), 100 deletions(-) create mode 100644 database/versions/4b544f5d3b07_2_1_3.py diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index bd2360db..e16c8d76 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -83,6 +83,7 @@ def create_subscribe( doubanid=subscribe_in.doubanid, bangumiid=subscribe_in.bangumiid, mediaid=subscribe_in.mediaid, + episode_group=subscribe_in.episode_group, username=current_user.name, best_version=subscribe_in.best_version, save_path=subscribe_in.save_path, diff --git a/app/api/endpoints/tmdb.py b/app/api/endpoints/tmdb.py index 7db37d1e..701caddb 100644 --- a/app/api/endpoints/tmdb.py +++ b/app/api/endpoints/tmdb.py @@ -114,9 +114,9 @@ def tmdb_person_credits(person_id: int, @router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode]) -def tmdb_season_episodes(tmdbid: int, season: int, +def tmdb_season_episodes(tmdbid: int, season: int, episode_group: Optional[str] = None, _: schemas.TokenPayload = Depends(verify_token)) -> Any: """ 根据TMDBID查询某季的所有信信息 """ - return TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season) + return TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season, episode_group=episode_group) diff --git a/app/api/endpoints/transfer.py b/app/api/endpoints/transfer.py index 3122a6f1..9399cbc5 100644 --- a/app/api/endpoints/transfer.py +++ b/app/api/endpoints/transfer.py @@ -146,6 +146,7 @@ def manual_transfer(transer_item: ManualTransferItem, doubanid=transer_item.doubanid, mtype=mtype, season=transer_item.season, + episode_group=transer_item.episode_group, transfer_type=transer_item.transfer_type, epformat=epformat, min_filesize=transer_item.min_filesize, diff --git a/app/chain/__init__.py b/app/chain/__init__.py index bfb287f1..131eb71e 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -150,6 +150,7 @@ class ChainBase(metaclass=ABCMeta): tmdbid: Optional[int] = None, doubanid: Optional[str] = None, bangumiid: Optional[int] = None, + episode_group: Optional[str] = None, cache: bool = True) -> Optional[MediaInfo]: """ 识别媒体信息,不含Fanart图片 @@ -158,6 +159,7 @@ class ChainBase(metaclass=ABCMeta): :param tmdbid: tmdbid :param doubanid: 豆瓣ID :param bangumiid: BangumiID + :param episode_group: 剧集组 :param cache: 是否使用缓存 :return: 识别的媒体信息,包括剧集信息 """ @@ -173,7 +175,8 @@ class ChainBase(metaclass=ABCMeta): doubanid = None bangumiid = None return self.run_module("recognize_media", meta=meta, mtype=mtype, - tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid, cache=cache) + tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid, + episode_group=episode_group, cache=cache) def match_doubaninfo(self, name: str, imdbid: Optional[str] = None, mtype: Optional[MediaType] = None, year: Optional[str] = None, season: Optional[int] = None, diff --git a/app/chain/download.py b/app/chain/download.py index ec7a485b..5e64dd3b 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -209,7 +209,6 @@ class DownloadChain(ChainBase): save_path: Optional[str] = None, userid: Union[str, int] = None, username: Optional[str] = None, - media_category: Optional[str] = None, label: Optional[str] = None) -> Optional[str]: """ 下载及发送通知 @@ -222,9 +221,13 @@ class DownloadChain(ChainBase): :param save_path: 保存路径 :param userid: 用户ID :param username: 调用下载的用户名/插件名 - :param media_category: 自定义媒体类别 :param label: 自定义标签 """ + _torrent = context.torrent_info + _media = context.media_info + _meta = context.meta_info + _site_downloader = _torrent.site_downloader + # 发送资源下载事件,允许外部拦截下载 event_data = ResourceDownloadEventData( context=context, @@ -236,7 +239,7 @@ class DownloadChain(ChainBase): "save_path": save_path, "userid": userid, "username": username, - "media_category": media_category + "media_category": _media.category } ) # 触发资源下载事件 @@ -250,15 +253,11 @@ class DownloadChain(ChainBase): f"Reason: {event_data.reason}") return None - _torrent = context.torrent_info - _media = context.media_info - _meta = context.meta_info - _site_downloader = _torrent.site_downloader - # 补充完整的media数据 if not _media.genre_ids: new_media = self.recognize_media(mtype=_media.type, tmdbid=_media.tmdb_id, - doubanid=_media.douban_id, bangumiid=_media.bangumi_id) + doubanid=_media.douban_id, bangumiid=_media.bangumi_id, + episode_group=_media.episode_group) if new_media: _media = new_media @@ -355,7 +354,8 @@ class DownloadChain(ChainBase): username=username, channel=channel.value if channel else None, date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), - media_category=media_category, + media_category=_media.category, + episode_group=_media.episode_group, note={"source": source} ) @@ -423,7 +423,6 @@ class DownloadChain(ChainBase): source: Optional[str] = None, userid: Optional[str] = None, username: Optional[str] = None, - media_category: Optional[str] = None, downloader: Optional[str] = None ) -> Tuple[List[Context], Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]: """ @@ -435,7 +434,6 @@ class DownloadChain(ChainBase): :param source: 来源(消息通知、订阅、手工下载等) :param userid: 用户ID :param username: 调用下载的用户名/插件名 - :param media_category: 自定义媒体类别 :param downloader: 下载器 :return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id/douban_id] = {season: NotExistMediaInfo} """ @@ -524,7 +522,7 @@ class DownloadChain(ChainBase): logger.info(f"开始下载电影 {context.torrent_info.title} ...") if self.download_single(context, save_path=save_path, channel=channel, source=source, userid=userid, username=username, - media_category=media_category, downloader=downloader): + downloader=downloader): # 下载成功 logger.info(f"{context.torrent_info.title} 添加下载成功") downloaded_list.append(context) @@ -609,8 +607,7 @@ class DownloadChain(ChainBase): source=source, userid=userid, username=username, - media_category=media_category, - downloader=downloader, + downloader=downloader ) else: # 下载 @@ -618,7 +615,6 @@ class DownloadChain(ChainBase): download_id = self.download_single(context, save_path=save_path, channel=channel, source=source, userid=userid, username=username, - media_category=media_category, downloader=downloader) if download_id: @@ -690,7 +686,6 @@ class DownloadChain(ChainBase): download_id = self.download_single(context, save_path=save_path, channel=channel, source=source, userid=userid, username=username, - media_category=media_category, downloader=downloader) if download_id: # 下载成功 @@ -780,7 +775,6 @@ class DownloadChain(ChainBase): source=source, userid=userid, username=username, - media_category=media_category, downloader=downloader ) if not download_id: @@ -866,7 +860,8 @@ class DownloadChain(ChainBase): # 补充媒体信息 mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type, tmdbid=mediainfo.tmdb_id, - doubanid=mediainfo.douban_id) + doubanid=mediainfo.douban_id, + episode_group=mediainfo.episode_group) if not mediainfo: logger.error(f"媒体信息识别失败!") return False, {} diff --git a/app/chain/media.py b/app/chain/media.py index 43ab4ca9..1fcc0e84 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -42,13 +42,13 @@ class MediaChain(ChainBase, metaclass=Singleton): """ return self.run_module("metadata_nfo", meta=meta, mediainfo=mediainfo, season=season, episode=episode) - def recognize_by_meta(self, metainfo: MetaBase) -> Optional[MediaInfo]: + def recognize_by_meta(self, metainfo: MetaBase, episode_group: Optional[str] = None) -> Optional[MediaInfo]: """ 根据主副标题识别媒体信息 """ title = metainfo.title # 识别媒体信息 - mediainfo: MediaInfo = self.recognize_media(meta=metainfo) + mediainfo: MediaInfo = self.recognize_media(meta=metainfo, episode_group=episode_group) if not mediainfo: # 尝试使用辅助识别,如果有注册响应事件的话 if eventmanager.check(ChainEventType.NameRecognize): @@ -112,7 +112,7 @@ class MediaChain(ChainBase, metaclass=Singleton): # 重新识别 return self.recognize_media(meta=org_meta) - def recognize_by_path(self, path: str) -> Optional[Context]: + def recognize_by_path(self, path: str, episode_group: Optional[str] = None) -> Optional[Context]: """ 根据文件路径识别媒体信息 """ @@ -121,7 +121,7 @@ class MediaChain(ChainBase, metaclass=Singleton): # 元数据 file_meta = MetaInfoPath(file_path) # 识别媒体信息 - mediainfo = self.recognize_media(meta=file_meta) + mediainfo = self.recognize_media(meta=file_meta, episode_group=episode_group) if not mediainfo: # 尝试使用辅助识别,如果有注册响应事件的话 if eventmanager.check(ChainEventType.NameRecognize): @@ -474,7 +474,8 @@ class MediaChain(ChainBase, metaclass=Singleton): if not file_meta.begin_episode: logger.warn(f"{filepath.name} 无法识别文件集数!") return - file_mediainfo = self.recognize_media(meta=file_meta, tmdbid=mediainfo.tmdb_id) + file_mediainfo = self.recognize_media(meta=file_meta, tmdbid=mediainfo.tmdb_id, + episode_group=mediainfo.episode_group) if not file_mediainfo: logger.warn(f"{filepath.name} 无法识别文件媒体信息!") return @@ -483,7 +484,8 @@ class MediaChain(ChainBase, metaclass=Singleton): if overwrite or not self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path): # 获取集的nfo文件 episode_nfo = self.metadata_nfo(meta=file_meta, mediainfo=file_mediainfo, - season=file_meta.begin_season, episode=file_meta.begin_episode) + season=file_meta.begin_season, + episode=file_meta.begin_episode) if episode_nfo: # 保存或上传nfo文件到上级目录 if not parent: diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index f7552efa..546c4469 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -60,6 +60,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton): doubanid: Optional[str] = None, bangumiid: Optional[int] = None, mediaid: Optional[str] = None, + episode_group: Optional[str] = None, season: Optional[int] = None, channel: MessageChannel = None, source: Optional[str] = None, @@ -117,7 +118,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton): mediainfo = __get_event_meida(mediaid, metainfo) else: # 使用TMDBID识别 - mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid, cache=False) + mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid, + episode_group=episode_group, cache=False) else: if doubanid: # 豆瓣识别模式,不使用缓存 @@ -134,7 +136,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton): # 使用名称识别兜底 if not mediainfo: - mediainfo = self.recognize_media(meta=metainfo) + mediainfo = self.recognize_media(meta=metainfo, episode_group=episode_group) # 识别失败 if not mediainfo: @@ -153,6 +155,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton): tmdbid=mediainfo.tmdb_id, doubanid=mediainfo.douban_id, bangumiid=mediainfo.bangumi_id, + episode_group=episode_group, cache=False) if not mediainfo: logger.error(f"媒体信息识别失败!") @@ -207,7 +210,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton): 'save_path': self.__get_default_subscribe_config(mediainfo.type, "save_path") if not kwargs.get( "save_path") else kwargs.get("save_path"), 'filter_groups': self.__get_default_subscribe_config(mediainfo.type, "filter_groups") if not kwargs.get( - "filter_groups") else kwargs.get("filter_groups"), + "filter_groups") else kwargs.get("filter_groups") }) sid, err_msg = self.subscribeoper.add(mediainfo=mediainfo, season=season, username=username, **kwargs) if not sid: @@ -383,6 +386,11 @@ class SubscribeChain(ChainBase, metaclass=Singleton): logger.info( f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级') continue + # 更新订阅自定义属性 + if subscribe.media_category: + torrent_mediainfo.category = subscribe.media_category + if subscribe.episode_group: + torrent_mediainfo.episode_group = subscribe.episode_group matched_contexts.append(context) if not matched_contexts: @@ -398,7 +406,6 @@ class SubscribeChain(ChainBase, metaclass=Singleton): userid=subscribe.username, username=subscribe.username, save_path=subscribe.save_path, - media_category=subscribe.media_category, downloader=subscribe.downloader, source=self.get_subscribe_source_keyword(subscribe) ) @@ -603,9 +610,10 @@ class SubscribeChain(ChainBase, metaclass=Singleton): logger.debug(f'开始匹配站点:{domain},共缓存了 {len(contexts)} 个种子...') for context in contexts: # 提取信息 - torrent_meta = copy.deepcopy(context.meta_info) - torrent_mediainfo = copy.deepcopy(context.media_info) - torrent_info = context.torrent_info + _context = copy.deepcopy(context) + torrent_meta = _context.meta_info + torrent_mediainfo = _context.media_info + torrent_info = _context.torrent_info # 不在订阅站点范围的不处理 sub_sites = self.get_sub_sites(subscribe) @@ -736,7 +744,12 @@ class SubscribeChain(ChainBase, metaclass=Singleton): # 匹配成功 logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}') - _match_context.append(context) + # 自定义属性 + if subscribe.media_category: + torrent_mediainfo.category = subscribe.media_category + if subscribe.episode_group: + torrent_mediainfo.episode_group = subscribe.episode_group + _match_context.append(_context) if not _match_context: # 未匹配到资源 @@ -752,7 +765,6 @@ class SubscribeChain(ChainBase, metaclass=Singleton): userid=subscribe.username, username=subscribe.username, save_path=subscribe.save_path, - media_category=subscribe.media_category, downloader=subscribe.downloader, source=self.get_subscribe_source_keyword(subscribe) ) @@ -1274,7 +1286,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton): # 查询TMDB中的集信息 tmdb_episodes = self.tmdbchain.tmdb_episodes( tmdbid=subscribe.tmdbid, - season=subscribe.season + season=subscribe.season, + episode_group=subscribe.episode_group ) if tmdb_episodes: for episode in tmdb_episodes: diff --git a/app/chain/tmdb.py b/app/chain/tmdb.py index d476943d..2ca53c43 100644 --- a/app/chain/tmdb.py +++ b/app/chain/tmdb.py @@ -70,13 +70,14 @@ class TmdbChain(ChainBase, metaclass=Singleton): """ return self.run_module("tmdb_seasons", tmdbid=tmdbid) - def tmdb_episodes(self, tmdbid: int, season: int) -> List[schemas.TmdbEpisode]: + def tmdb_episodes(self, tmdbid: int, season: int, episode_group: Optional[str] = None) -> List[schemas.TmdbEpisode]: """ 根据TMDBID查询某季的所有信信息 :param tmdbid: TMDBID :param season: 季 + :param episode_group: 剧集组 """ - return self.run_module("tmdb_episodes", tmdbid=tmdbid, season=season) + return self.run_module("tmdb_episodes", tmdbid=tmdbid, season=season, episode_group=episode_group) def movie_similar(self, tmdbid: int) -> Optional[List[MediaInfo]]: """ diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 3398ef2c..09cc1dc4 100644 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -623,7 +623,8 @@ class TransferChain(ChainBase, metaclass=Singleton): # 下载记录中已存在识别信息 mediainfo: Optional[MediaInfo] = self.recognize_media(mtype=MediaType(download_history.type), tmdbid=download_history.tmdbid, - doubanid=download_history.doubanid) + doubanid=download_history.doubanid, + episode_group=download_history.episode_group) if mediainfo: # 更新自定义媒体类别 if download_history.media_category: @@ -681,7 +682,8 @@ class TransferChain(ChainBase, metaclass=Singleton): season_num = 1 task.episodes_info = self.tmdbchain.tmdb_episodes( tmdbid=task.mediainfo.tmdb_id, - season=season_num + season=season_num, + episode_group=task.mediainfo.episode_group ) # 查询整理目标目录 @@ -798,7 +800,8 @@ class TransferChain(ChainBase, metaclass=Singleton): # 按TMDBID识别 mediainfo = self.recognize_media(mtype=mtype, tmdbid=downloadhis.tmdbid, - doubanid=downloadhis.doubanid) + doubanid=downloadhis.doubanid, + episode_group=downloadhis.episode_group) if mediainfo: # 补充图片 self.obtain_images(mediainfo) @@ -1214,12 +1217,12 @@ class TransferChain(ChainBase, metaclass=Singleton): # 查询媒体信息 if mtype and mediaid: mediainfo = self.recognize_media(mtype=mtype, tmdbid=int(mediaid) if str(mediaid).isdigit() else None, - doubanid=mediaid) + doubanid=mediaid, episode_group=history.episode_group) if mediainfo: # 更新媒体图片 self.obtain_images(mediainfo=mediainfo) else: - mediainfo = self.mediachain.recognize_by_path(str(src_path)) + mediainfo = self.mediachain.recognize_by_path(str(src_path), episode_group=history.episode_group) if not mediainfo: return False, f"未识别到媒体信息,类型:{mtype.value},id:{mediaid}" # 重新执行整理 @@ -1252,6 +1255,7 @@ class TransferChain(ChainBase, metaclass=Singleton): doubanid: Optional[str] = None, mtype: MediaType = None, season: Optional[int] = None, + episode_group: Optional[str] = None, transfer_type: Optional[str] = None, epformat: EpisodeFormat = None, min_filesize: Optional[int] = 0, @@ -1269,6 +1273,7 @@ class TransferChain(ChainBase, metaclass=Singleton): :param doubanid: 豆瓣ID :param mtype: 媒体类型 :param season: 季度 + :param episode_group: 剧集组 :param transfer_type: 整理类型 :param epformat: 剧集格式 :param min_filesize: 最小文件大小(MB) @@ -1282,7 +1287,8 @@ class TransferChain(ChainBase, metaclass=Singleton): if tmdbid or doubanid: # 有输入TMDBID时单个识别 # 识别媒体信息 - mediainfo: MediaInfo = self.mediachain.recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype) + mediainfo: MediaInfo = self.mediachain.recognize_media(tmdbid=tmdbid, doubanid=doubanid, + mtype=mtype, episode_group=episode_group) if not mediainfo: return False, f"媒体信息识别失败,tmdbid:{tmdbid},doubanid:{doubanid},type: {mtype.value}" else: diff --git a/app/core/context.py b/app/core/context.py index de6450c5..8633c692 100644 --- a/app/core/context.py +++ b/app/core/context.py @@ -264,8 +264,10 @@ class MediaInfo: next_episode_to_air: dict = field(default_factory=dict) # 内容分级 content_rating: str = None - # 剧集组 + # 全部剧集组 episode_groups: List[dict] = field(default_factory=list) + # 剧集组 + episode_group: str = None def __post_init__(self): # 设置媒体信息 diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index 20eb8a4a..5ddd3145 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -52,6 +52,8 @@ class DownloadHistory(Base): note = Column(JSON) # 自定义媒体类别 media_category = Column(String) + # 剧集组 + episode_group = Column(String) @staticmethod @db_query diff --git a/app/db/models/subscribe.py b/app/db/models/subscribe.py index 8b049e48..052a5bbb 100644 --- a/app/db/models/subscribe.py +++ b/app/db/models/subscribe.py @@ -84,6 +84,10 @@ class Subscribe(Base): media_category = Column(String) # 过滤规则组 filter_groups = Column(JSON, default=list) + # 可选剧集组 + episode_groups = Column(JSON, default=list) + # 选择的剧集组 + episode_group = Column(String) @staticmethod @db_query diff --git a/app/db/models/subscribehistory.py b/app/db/models/subscribehistory.py index ab955984..e0fba88c 100644 --- a/app/db/models/subscribehistory.py +++ b/app/db/models/subscribehistory.py @@ -69,6 +69,8 @@ class SubscribeHistory(Base): media_category = Column(String) # 过滤规则组 filter_groups = Column(JSON, default=list) + # 剧集组 + episode_group = Column(String) @staticmethod @db_query diff --git a/app/db/models/transferhistory.py b/app/db/models/transferhistory.py index a4971622..4bb13ed3 100644 --- a/app/db/models/transferhistory.py +++ b/app/db/models/transferhistory.py @@ -56,6 +56,8 @@ class TransferHistory(Base): date = Column(String, index=True) # 文件清单,以JSON存储 files = Column(JSON, default=list) + # 剧集组 + episode_group = Column(String) @staticmethod @db_query diff --git a/app/db/subscribe_oper.py b/app/db/subscribe_oper.py index 04353ae5..97850a8d 100644 --- a/app/db/subscribe_oper.py +++ b/app/db/subscribe_oper.py @@ -29,10 +29,12 @@ class SubscribeOper(DbOper): tvdbid=mediainfo.tvdb_id, doubanid=mediainfo.douban_id, bangumiid=mediainfo.bangumi_id, + episode_group=mediainfo.episode_group, poster=mediainfo.get_poster_image(), backdrop=mediainfo.get_backdrop_image(), vote=mediainfo.vote_average, description=mediainfo.overview, + episode_groups=mediainfo.episode_groups, date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), **kwargs) subscribe.create(self._db) diff --git a/app/db/transferhistory_oper.py b/app/db/transferhistory_oper.py index b7adda63..49900b0b 100644 --- a/app/db/transferhistory_oper.py +++ b/app/db/transferhistory_oper.py @@ -177,6 +177,7 @@ class TransferHistoryOper(DbOper): image=mediainfo.get_poster_image(), downloader=downloader, download_hash=download_hash, + episode_group=mediainfo.episode_group, status=0, errmsg=transferinfo.message or '未知错误', files=transferinfo.file_list @@ -193,6 +194,7 @@ class TransferHistoryOper(DbOper): episodes=meta.episode, downloader=downloader, download_hash=download_hash, + episode_group=mediainfo.episode_group, status=0, errmsg="未识别到媒体信息" ) diff --git a/app/modules/douban/__init__.py b/app/modules/douban/__init__.py index cc518467..b66ed847 100644 --- a/app/modules/douban/__init__.py +++ b/app/modules/douban/__init__.py @@ -75,8 +75,8 @@ class DoubanModule(_ModuleBase): def recognize_media(self, meta: MetaBase = None, mtype: MediaType = None, - doubanid: str = None, - cache: bool = True, + doubanid: Optional[str] = None, + cache: Optional[bool] = True, **kwargs) -> Optional[MediaInfo]: """ 识别媒体信息 diff --git a/app/modules/themoviedb/__init__.py b/app/modules/themoviedb/__init__.py index 63092846..c3609ac5 100644 --- a/app/modules/themoviedb/__init__.py +++ b/app/modules/themoviedb/__init__.py @@ -1,3 +1,4 @@ +import re from typing import Optional, List, Tuple, Union, Dict import cn2an @@ -85,6 +86,7 @@ class TheMovieDbModule(_ModuleBase): def recognize_media(self, meta: MetaBase = None, mtype: MediaType = None, tmdbid: Optional[int] = None, + episode_group: Optional[str] = None, cache: Optional[bool] = True, **kwargs) -> Optional[MediaInfo]: """ @@ -92,6 +94,7 @@ class TheMovieDbModule(_ModuleBase): :param meta: 识别的元数据 :param mtype: 识别的媒体类型,与tmdbid配套 :param tmdbid: tmdbid + :param episode_group: 剧集组 :param cache: 是否使用缓存 :return: 识别的媒体信息,包括剧集信息 """ @@ -116,6 +119,11 @@ class TheMovieDbModule(_ModuleBase): meta.tmdbid = tmdbid cache_info = self.cache.get(meta) + # 查询剧集组 + group_episodes = [] + if episode_group: + group_episodes = self.tmdb.get_tv_group_episodes(episode_group) + # 识别匹配 if not cache_info or not cache: info = None @@ -143,7 +151,8 @@ class TheMovieDbModule(_ModuleBase): year=meta.year, mtype=meta.type, season_year=meta.year, - season_number=meta.begin_season) + season_number=meta.begin_season, + group_episodes=group_episodes) if not info: # 去掉年份再查一次 info = self.tmdb.match(name=name, @@ -157,7 +166,8 @@ class TheMovieDbModule(_ModuleBase): if not info: info = self.tmdb.match(name=name, year=meta.year, - mtype=MediaType.TV) + mtype=MediaType.TV, + group_episodes=group_episodes) if not info: # 去掉年份和类型再查一次 info = self.tmdb.match_multi(name=name) @@ -207,11 +217,61 @@ class TheMovieDbModule(_ModuleBase): logger.info(f"{tmdbid} TMDB识别结果:{mediainfo.type.value} " f"{mediainfo.title_year}") - # 补充剧集年份为季年份 + # 使用剧集组的集信息和年份 if mediainfo.type == MediaType.TV and mediainfo.episode_groups: - episode_years = self.tmdb.get_tv_episode_years(mediainfo.episode_groups) - if episode_years: - mediainfo.season_years = episode_years + if group_episodes: + # 指定剧集组时 + seasons = {} + season_info = [] + season_years = {} + for group_episode in group_episodes: + # 季 + season = group_episode.get("order") + # 集列表 + episodes = group_episode.get("episodes") + if not episodes: + continue + seasons[season] = [ep.get("episode_number") for ep in episodes] + season_info[season] = episodes + # 当前季第一季时间 + first_date = episodes[0].get("air_date") + if re.match(r"^\d{4}-\d{2}-\d{2}$", first_date): + season_years[season] = str(first_date).split("-")[0] + # 每季集清单 + if seasons: + mediainfo.seasons = seasons + mediainfo.number_of_seasons = len(seasons) + # 每季集详情 + if season_info: + mediainfo.season_info = season_info + # 每季年份 + if season_years: + mediainfo.season_years = season_years + # 所有剧集组 + mediainfo.episode_group = episode_group + mediainfo.episode_groups = group_episodes + else: + # 每季年份 + season_years = {} + for group in mediainfo.episode_groups: + if group.get('type') != 6: + # 只处理剧集部分 + continue + group_episodes = self.tmdb.get_tv_group_episodes(group.get('id')) + if not group_episodes: + continue + for group_episode in group_episodes: + season = group_episode.get('order') + episodes = group_episode.get('episodes') + if not episodes: + continue + # 当前季第一季时间 + first_date = episodes[0].get("air_date") + # 判断是不是日期格式 + if re.match(r"^\d{4}-\d{2}-\d{2}$", first_date): + season_years[season] = str(first_date).split("-")[0] + if season_years: + mediainfo.season_years = season_years return mediainfo else: logger.info(f"{meta.name if meta else tmdbid} 未匹配到TMDB媒体信息") @@ -431,13 +491,17 @@ class TheMovieDbModule(_ModuleBase): return [schemas.TmdbSeason(**season) for season in tmdb_info.get("seasons", []) if season.get("season_number")] - def tmdb_episodes(self, tmdbid: int, season: int) -> List[schemas.TmdbEpisode]: + def tmdb_episodes(self, tmdbid: int, season: int, episode_group: Optional[str] = None) -> List[schemas.TmdbEpisode]: """ 根据TMDBID查询某季的所有信信息 :param tmdbid: TMDBID :param season: 季 + :param episode_group: 剧集组 """ - season_info = self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season) + if episode_group: + season_info = self.tmdb.get_tv_group_episodes(episode_group) + else: + season_info = self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season) if not season_info or not season_info.get("episodes"): return [] return [schemas.TmdbEpisode(**episode) for episode in season_info.get("episodes")] diff --git a/app/modules/themoviedb/scraper.py b/app/modules/themoviedb/scraper.py index afe82482..960e7c03 100644 --- a/app/modules/themoviedb/scraper.py +++ b/app/modules/themoviedb/scraper.py @@ -32,7 +32,10 @@ class TmdbScraper: else: if season is not None: # 查询季信息 - seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season) + if mediainfo.episode_group: + seasoninfo = self.tmdb.get_tv_group_episodes(mediainfo.episode_group) + else: + seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season) if episode: # 集元数据文件 episodeinfo = self.__get_episode_detail(seasoninfo, meta.begin_episode) @@ -61,7 +64,10 @@ class TmdbScraper: # 只需要集的图片 if episode: # 集的图片 - seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season) + if mediainfo.episode_group: + seasoninfo = self.tmdb.get_tv_group_episodes(mediainfo.episode_group) + else: + seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season) if seasoninfo: episodeinfo = self.__get_episode_detail(seasoninfo, episode) if episodeinfo and episodeinfo.get("still_path"): diff --git a/app/modules/themoviedb/tmdbapi.py b/app/modules/themoviedb/tmdbapi.py index 8a3b0daa..6a97e23e 100644 --- a/app/modules/themoviedb/tmdbapi.py +++ b/app/modules/themoviedb/tmdbapi.py @@ -1,3 +1,4 @@ +import re import traceback from typing import Optional, List from urllib.parse import quote @@ -187,7 +188,8 @@ class TmdbApi: mtype: MediaType, year: Optional[str] = None, season_year: Optional[str] = None, - season_number: Optional[int] = None) -> Optional[dict]: + season_number: Optional[int] = None, + group_episodes: Optional[List[dict]] = None) -> Optional[dict]: """ 搜索tmdb中的媒体信息,匹配返回一条尽可能正确的信息 :param name: 检索的名称 @@ -195,6 +197,7 @@ class TmdbApi: :param year: 年份,如要是季集需要是首播年份(first_air_date) :param season_year: 当前季集年份 :param season_number: 季集,整数 + :param group_episodes: 集数组信息 :return: TMDB的INFO,同时会将mtype赋值到media_type中 """ if not self.search: @@ -222,7 +225,8 @@ class TmdbApi: f"正在识别{mtype.value}:{name}, 季集={season_number}, 季集年份={season_year} ...") info = self.__search_tv_by_season(name, season_year, - season_number) + season_number, + group_episodes) if not info: year_range = [year] if year: @@ -332,12 +336,14 @@ class TmdbApi: return tv return {} - def __search_tv_by_season(self, name: str, season_year: str, season_number: int) -> Optional[dict]: + def __search_tv_by_season(self, name: str, season_year: str, season_number: int, + group_episodes: Optional[List[dict]] = None) -> Optional[dict]: """ 根据电视剧的名称和季的年份及序号匹配TMDB :param name: 识别的文件名或者种子名 :param season_year: 季的年份 :param season_number: 季序号 + :param group_episodes: 集数组信息 :return: 匹配的媒体信息 """ @@ -345,12 +351,25 @@ class TmdbApi: if not tv_info: return False try: - seasons = self.__get_tv_seasons(tv_info) - for season, season_info in seasons.items(): - if season_info.get("air_date"): - if season_info.get("air_date")[0:4] == str(_season_year) \ - and season == int(season_number): - return True + if group_episodes: + for group_episode in group_episodes: + season = group_episode.get('order') + if season != season_number: + continue + episodes = group_episode.get('episodes') + if not episodes: + continue + first_date = episodes[0].get("air_date") + if re.match(r"^\d{4}-\d{2}-\d{2}$", first_date): + if str(_season_year) == str(first_date).split("-")[0]: + return True + else: + seasons = self.__get_tv_seasons(tv_info) + for season, season_info in seasons.items(): + if season_info.get("air_date"): + if season_info.get("air_date")[0:4] == str(_season_year) \ + and season == int(season_number): + return True except Exception as e1: logger.error(f"连接TMDB出错:{e1}") print(traceback.format_exc()) @@ -1317,6 +1336,19 @@ class TmdbApi: logger.error(str(e)) return [] + def get_tv_group_episodes(self, group_id: str) -> List[dict]: + """ + 获取电视剧剧集组集列表 + """ + if not self.tv: + return [] + try: + logger.debug(f"正在获取剧集组:{group_id}...") + return self.tv.group_episodes(group_id) or [] + except Exception as e: + logger.error(str(e)) + return [] + def get_person_detail(self, person_id: int) -> dict: """ 获取人物详情 @@ -1377,37 +1409,6 @@ class TmdbApi: """ self.tmdb.cache_clear() - def get_tv_episode_years(self, episode_groups: List[dict]) -> dict: - """ - 查询剧集组年份 - """ - try: - if not episode_groups: - return {} - episode_years = {} - for episode_group in episode_groups: - if episode_group.get('type') != 6: - # 只处理剧集部分 - continue - logger.debug(f"正在获取剧集组年份:{episode_group.get('id')}...") - group_episodes = self.tv.group_episodes(episode_group.get('id')) - if not group_episodes: - continue - for group_episode in group_episodes: - order = group_episode.get('order') - episodes = group_episode.get('episodes') - if not episodes: - continue - # 当前季第一季时间 - first_date = episodes[0].get("air_date") - if not first_date and str(first_date).split("-") != 3: - continue - episode_years[order] = str(first_date).split("-")[0] - return episode_years - except Exception as e: - logger.error(str(e)) - return {} - def close(self): """ 关闭连接 diff --git a/app/schemas/context.py b/app/schemas/context.py index b9086978..84a78429 100644 --- a/app/schemas/context.py +++ b/app/schemas/context.py @@ -170,8 +170,10 @@ class MediaInfo(BaseModel): runtime: Optional[int] = None # 下一集 next_episode_to_air: Optional[dict] = Field(default_factory=dict) - # 剧集组 + # 全部剧集组 episode_groups: Optional[list] = Field(default_factory=list) + # 剧集组 + episode_group: Optional[str] = None class TorrentInfo(BaseModel): diff --git a/app/schemas/history.py b/app/schemas/history.py index c2ec5e87..193b13b8 100644 --- a/app/schemas/history.py +++ b/app/schemas/history.py @@ -48,6 +48,8 @@ class DownloadHistory(BaseModel): note: Optional[Any] = None # 自定义媒体类别 media_category: Optional[str] = None + # 自定义剧集组 + episode_group: Optional[str] = None class Config: orm_mode = True @@ -86,6 +88,8 @@ class TransferHistory(BaseModel): image: Optional[str] = None # 下载器Hash download_hash: Optional[str] = None + # 自定义剧集组 + episode_group: Optional[str] = None # 状态 1-成功,0-失败 status: bool = True # 失败原因 diff --git a/app/schemas/subscribe.py b/app/schemas/subscribe.py index eb67c9a1..728ebdb3 100644 --- a/app/schemas/subscribe.py +++ b/app/schemas/subscribe.py @@ -73,6 +73,10 @@ class Subscribe(BaseModel): media_category: Optional[str] = None # 过滤规则组 filter_groups: Optional[List[str]] = Field(default_factory=list) + # 可选剧集组 + episode_groups: Optional[list] = Field(default_factory=list) + # 剧集组 + episode_group: str = None class Config: orm_mode = True @@ -130,6 +134,8 @@ class SubscribeShare(BaseModel): custom_words: Optional[str] = None # 自定义媒体类别 media_category: Optional[str] = None + # 自定义剧集组 + episode_group: Optional[str] = None # 复用人次 count: Optional[int] = 0 diff --git a/app/schemas/transfer.py b/app/schemas/transfer.py index 8a34e3f2..2edc7061 100644 --- a/app/schemas/transfer.py +++ b/app/schemas/transfer.py @@ -200,3 +200,5 @@ class ManualTransferItem(BaseModel): library_category_folder: Optional[bool] = None # 复用历史识别信息 from_history: Optional[bool] = False + # 剧集组 + episode_group: Optional[str] = None diff --git a/database/versions/4b544f5d3b07_2_1_3.py b/database/versions/4b544f5d3b07_2_1_3.py new file mode 100644 index 00000000..a364ae84 --- /dev/null +++ b/database/versions/4b544f5d3b07_2_1_3.py @@ -0,0 +1,33 @@ +"""2.1.3 + +Revision ID: 4b544f5d3b07 +Revises: 610bb05ddeef +Create Date: 2025-04-03 11:21:42.780337 + +""" +import contextlib + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import sqlite + +# revision identifiers, used by Alembic. +revision = '4b544f5d3b07' +down_revision = '610bb05ddeef' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with contextlib.suppress(Exception): + op.add_column('downloadhistory', sa.Column('episode_group', sa.String, nullable=True)) + op.add_column('subscribe', sa.Column('episode_groups', sa.JSON(), nullable=True)) + op.add_column('subscribe', sa.Column('episode_group', sa.String, nullable=True)) + op.add_column('subscribehistory', sa.Column('episode_group', sa.String, nullable=True)) + op.add_column('transferhistory', sa.Column('episode_group', sa.String, nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + pass