mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-30 04:01:54 +08:00
feat:支持指定剧集组识别和刮削
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]]:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
# 设置媒体信息
|
||||
|
||||
@@ -52,6 +52,8 @@ class DownloadHistory(Base):
|
||||
note = Column(JSON)
|
||||
# 自定义媒体类别
|
||||
media_category = Column(String)
|
||||
# 剧集组
|
||||
episode_group = Column(String)
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,6 +69,8 @@ class SubscribeHistory(Base):
|
||||
media_category = Column(String)
|
||||
# 过滤规则组
|
||||
filter_groups = Column(JSON, default=list)
|
||||
# 剧集组
|
||||
episode_group = Column(String)
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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="未识别到媒体信息"
|
||||
)
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
识别媒体信息
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
关闭连接
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
# 失败原因
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -200,3 +200,5 @@ class ManualTransferItem(BaseModel):
|
||||
library_category_folder: Optional[bool] = None
|
||||
# 复用历史识别信息
|
||||
from_history: Optional[bool] = False
|
||||
# 剧集组
|
||||
episode_group: Optional[str] = None
|
||||
|
||||
33
database/versions/4b544f5d3b07_2_1_3.py
Normal file
33
database/versions/4b544f5d3b07_2_1_3.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user