Compare commits

...

18 Commits

Author SHA1 Message Date
jxxghp
a1e6fd88a9 更新 version.py 2025-04-06 07:53:29 +08:00
jxxghp
e72ff867fc fix 115 pickcode 2025-04-05 09:29:08 +08:00
jxxghp
8512641984 更新 scraper.py 2025-04-04 22:13:14 +08:00
jxxghp
f1aa64d191 fix episodes group 2025-04-04 12:17:42 +08:00
jxxghp
347262538f fix episodes group 2025-04-04 08:59:12 +08:00
jxxghp
82510d60ca 更新 __init__.py 2025-04-03 22:48:29 +08:00
jxxghp
6104cd04c3 更新 context.py 2025-04-03 20:32:56 +08:00
jxxghp
44eb58426a feat:支持指定剧集组识别和刮削 2025-04-03 18:43:04 +08:00
jxxghp
078b60cc1e feat:支持指定剧集组识别和刮削 2025-04-03 18:35:02 +08:00
jxxghp
21e120a4f8 refactor:减少一次接口查询 2025-04-03 10:43:31 +08:00
jxxghp
439b834aa8 更新 version.py 2025-04-02 18:39:50 +08:00
jxxghp
ddbe8324be README增加开发说明 2025-03-30 11:36:19 +08:00
jxxghp
8ffe93113b README增加开发说明 2025-03-30 09:53:34 +08:00
jxxghp
8b31b7cb8a v2.3.6-1
- 修复媒体服务器库存检索问题
- 继续优化搜索页面
2025-03-30 09:23:46 +08:00
jxxghp
e09e21caa9 Merge pull request #4067 from cddjr/fix_media_exists 2025-03-30 02:48:19 +08:00
景大侠
20b145c679 继续修复媒体缺失问题 2025-03-30 02:41:24 +08:00
jxxghp
c5730cf1ad Merge pull request #4065 from cddjr/fix_v235_emby_bug 2025-03-29 23:18:34 +08:00
景大侠
f16b038463 修复v2.3.5引入的emby误报媒体缺失的bug 2025-03-29 23:15:58 +08:00
34 changed files with 378 additions and 154 deletions

View File

@@ -26,6 +26,34 @@
访问官方Wikihttps://wiki.movie-pilot.org
## 参与开发
需要 `Python 3.11``Node JS v20.12.1`
- 克隆主项目 [MoviePilot](https://github.com/jxxghp/MoviePilot)
```shell
git clone https://github.com/jxxghp/MoviePilot
```
- 克隆资源项目 [MoviePilot-Resources](https://github.com/jxxghp/MoviePilot-Resources) ,将 `resources` 目录下对应平台及版本的库 `.so`/`.pyd`/`.bin` 文件复制到 `app/helper` 目录
```shell
git clone https://github.com/jxxghp/MoviePilot-Resources
```
- 安装后端依赖,设置`app`为源代码根目录,运行 `main.py` 启动后端服务,默认监听端口:`3001`API文档地址`http://localhost:3001/docs`
```shell
pip install -r requirements.txt
python3 main.py
```
- 克隆前端项目 [MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)
```shell
git clone https://github.com/jxxghp/MoviePilot-Frontend
```
- 安装前端依赖,运行前端项目,访问:`http://localhost:5173`
```shell
yarn
yarn dev
```
- 参考 [插件开发指引](https://wiki.movie-pilot.org/zh/plugindev) 在 `app/plugins` 目录下开发插件代码
## 贡献者
<a href="https://github.com/jxxghp/MoviePilot/graphs/contributors">

View File

@@ -136,6 +136,24 @@ def category(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
return MediaChain().media_category() or {}
@router.get("/group/seasons/{episode_group}", summary="查询剧集组季信息", response_model=List[schemas.MediaSeason])
def group_seasons(episode_group: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询剧集组季信息themoviedb
"""
return TmdbChain().tmdb_group_seasons(group_id=episode_group)
@router.get("/groups/{tmdbid}", summary="查询媒体剧集组", response_model=List[dict])
def seasons(tmdbid: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询媒体剧集组列表themoviedb
"""
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, mtype=MediaType.TV)
if not mediainfo:
return []
return mediainfo.episode_groups
@router.get("/seasons", summary="查询媒体季信息", response_model=List[schemas.MediaSeason])
def seasons(mediaid: Optional[str] = None,
title: Optional[str] = None,

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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, {}

View File

@@ -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:

View File

@@ -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:
@@ -147,12 +149,13 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
season = 1
# 总集数
if not kwargs.get('total_episode'):
if not mediainfo.seasons:
if not mediainfo.seasons or episode_group:
# 补充媒体信息
mediainfo = self.recognize_media(mtype=mediainfo.type,
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:
@@ -323,6 +326,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -383,6 +387,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 +407,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)
)
@@ -574,6 +582,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -603,9 +612,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)
@@ -633,7 +643,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
if not torrent_mediainfo \
or (not torrent_mediainfo.tmdb_id and not torrent_mediainfo.douban_id):
# 重新识别媒体信息
torrent_mediainfo = self.recognize_media(meta=torrent_meta)
torrent_mediainfo = self.recognize_media(meta=torrent_meta,
episode_group=subscribe.episode_group)
if torrent_mediainfo:
# 更新种子缓存
context.media_info = torrent_mediainfo
@@ -736,7 +747,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 +768,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)
)
@@ -793,6 +808,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -1274,7 +1290,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:
@@ -1336,6 +1353,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(

View File

@@ -70,13 +70,21 @@ 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_group_seasons(self, group_id: str) -> List[schemas.TmdbSeason]:
"""
根据剧集组ID查询themoviedb所有季集信息
:param group_id: 剧集组ID
"""
return self.run_module("tmdb_group_seasons", group_id=group_id)
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]]:
"""

View File

@@ -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:

View File

@@ -264,6 +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):
# 设置媒体信息
@@ -454,6 +458,10 @@ class MediaInfo:
air_date = seainfo.get("air_date")
if air_date:
self.season_years[season] = air_date[:4]
# 剧集组
if info.get("episode_groups"):
self.episode_groups = info.pop("episode_groups").get("results") or []
# 海报
if info.get('poster_path'):
self.poster_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('poster_path')}"
@@ -773,6 +781,7 @@ class MediaInfo:
self.spoken_languages = []
self.networks = []
self.next_episode_to_air = {}
self.episode_groups = []
@dataclass

View File

@@ -52,6 +52,8 @@ class DownloadHistory(Base):
note = Column(JSON)
# 自定义媒体类别
media_category = Column(String)
# 剧集组
episode_group = Column(String)
@staticmethod
@db_query

View File

@@ -84,6 +84,8 @@ class Subscribe(Base):
media_category = Column(String)
# 过滤规则组
filter_groups = Column(JSON, default=list)
# 选择的剧集组
episode_group = Column(String)
@staticmethod
@db_query

View File

@@ -69,6 +69,8 @@ class SubscribeHistory(Base):
media_category = Column(String)
# 过滤规则组
filter_groups = Column(JSON, default=list)
# 剧集组
episode_group = Column(String)
@staticmethod
@db_query

View File

@@ -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

View File

@@ -29,6 +29,7 @@ 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,

View File

@@ -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="未识别到媒体信息"
)

View File

@@ -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]:
"""
识别媒体信息

View File

@@ -149,7 +149,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
else:
servers = self.get_instances().items()
for name, s in servers:
if not server:
if not s:
continue
if mediainfo.type == MediaType.MOVIE:
if itemid:

View File

@@ -410,7 +410,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
return oss2.utils.b64encode_as_string(json.dumps(cb).strip())
target_name = new_name or local_path.name
target_path = str(Path(target_dir.path) / target_name)
target_path = Path(target_dir.path) / target_name
# 计算文件特征值
file_size = local_path.stat().st_size
file_sha1 = self._calc_sha1(local_path)
@@ -441,7 +441,6 @@ class U115Pan(StorageBase, metaclass=Singleton):
# 结果
init_result = init_resp.get("data")
logger.debug(f"【115】上传 Step 1 初始化结果: {init_result}")
file_id = init_result.get("file_id")
# 回调信息
bucket_name = init_result.get("bucket")
object_name = init_result.get("object")
@@ -486,27 +485,13 @@ class U115Pan(StorageBase, metaclass=Singleton):
bucket_name = init_result.get("bucket")
if not object_name:
object_name = init_result.get("object")
if not file_id:
file_id = init_result.get("file_id")
if not callback:
callback = init_result.get("callback")
# Step 3: 秒传
if init_result.get("status") == 2:
logger.info(f"【115】{target_name} 秒传成功")
return schemas.FileItem(
storage=self.schema.value,
fileid=str(file_id),
parent_fileid=target_cid,
path=target_path,
name=target_name,
basename=Path(target_name).stem,
extension=Path(target_name).suffix[1:],
size=file_size,
type="file",
pickcode=pick_code,
modify_time=int(time.time())
)
return self.get_item(target_path)
# Step 4: 获取上传凭证
token_resp = self._request_api(
@@ -617,19 +602,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
logger.error(f"【115】{target_name} 上传失败: {e.status}, 错误码: {e.code}, 详情: {e.message}")
return None
# 返回结果
return schemas.FileItem(
storage=self.schema.value,
fileid=str(file_id),
parent_fileid = target_cid,
type="file",
path=target_path,
name=target_name,
basename=Path(target_name).stem,
extension=Path(target_name).suffix[1:],
size=file_size,
pickcode=pick_code,
modify_time=int(time.time())
)
return self.get_item(target_path)
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
"""
@@ -726,7 +699,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
type="file" if resp["file_category"] == "1" else "dir",
name=resp["file_name"],
basename=Path(resp["file_name"]).stem,
extension=Path(resp["file_name"]).suffix[1:],
extension=Path(resp["file_name"]).suffix[1:] if resp["file_category"] == "1" else None,
pickcode=resp["pick_code"],
size=StringUtils.num_filesize(resp['size']) if resp["file_category"] == "1" else None,
modify_time=resp["utime"]

View File

@@ -150,7 +150,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
else:
servers = self.get_instances().items()
for name, s in servers:
if not server:
if not s:
continue
if mediainfo.type == MediaType.MOVIE:
if itemid:

View File

@@ -153,7 +153,7 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
else:
servers = self.get_instances().items()
for name, s in servers:
if not server:
if not s:
continue
if mediainfo.type == MediaType.MOVIE:
if itemid:

View File

@@ -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_seasons = []
if episode_group:
group_seasons = self.tmdb.get_tv_group_seasons(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_seasons=group_seasons)
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_seasons=group_seasons)
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:
episode_years = self.tmdb.get_tv_episode_years(info.get("id"))
if episode_years:
mediainfo.season_years = episode_years
# 使用剧集组的集信息和年份
if mediainfo.type == MediaType.TV and mediainfo.episode_groups:
if group_seasons:
# 指定剧集组时
seasons = {}
season_info = []
season_years = {}
for group_season in group_seasons:
# 季
season = group_season.get("order")
# 集列表
episodes = group_season.get("episodes")
if not episodes:
continue
seasons[season] = [ep.get("episode_number") for ep in episodes]
season_info.append(group_season)
# 当前季第一季时间
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_seasons
else:
# 每季年份
season_years = {}
for group in mediainfo.episode_groups:
if group.get('type') != 6:
# 只处理剧集部分
continue
group_episodes = self.tmdb.get_tv_group_seasons(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媒体信息")
@@ -428,16 +488,36 @@ class TheMovieDbModule(_ModuleBase):
tmdb_info = self.tmdb.get_info(tmdbid=tmdbid, mtype=MediaType.TV)
if not tmdb_info:
return []
return [schemas.TmdbSeason(**season)
for season in tmdb_info.get("seasons", []) if season.get("season_number")]
return [schemas.TmdbSeason(**sea)
for sea in tmdb_info.get("seasons", []) if sea.get("season_number")]
def tmdb_episodes(self, tmdbid: int, season: int) -> List[schemas.TmdbEpisode]:
def tmdb_group_seasons(self, group_id: str) -> List[schemas.TmdbSeason]:
"""
根据TMDBID查询某季的所有信信息
根据剧集组ID查询themoviedb所有季集信息
:param group_id: 剧集组ID
"""
group_seasons = self.tmdb.get_tv_group_seasons(group_id)
if not group_seasons:
return []
return [schemas.TmdbSeason(
season_number=sea.get("order"),
name=sea.get("name"),
episode_count=len(sea.get("episodes") or []),
air_date=sea.get("episodes")[0].get("air_date") if sea.get("episodes") else None,
) for sea in group_seasons]
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_detail(episode_group, season=season)
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")]

View File

@@ -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_detail(mediainfo.episode_group, season=season)
else:
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, season=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_detail(mediainfo.episode_group, season)
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"):

View File

@@ -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_seasons: 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_seasons: 集数组信息
: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_seasons)
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_seasons: Optional[List[dict]] = None) -> Optional[dict]:
"""
根据电视剧的名称和季的年份及序号匹配TMDB
:param name: 识别的文件名或者种子名
:param season_year: 季的年份
:param season_number: 季序号
:param group_seasons: 集数组信息
: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_seasons:
for group_season in group_seasons:
season = group_season.get('order')
if season != season_number:
continue
episodes = group_season.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())
@@ -768,11 +787,11 @@ class TmdbApi:
def __get_movie_detail(self,
tmdbid: int,
append_to_response: Optional[str] = "images,"
"credits,"
"alternative_titles,"
"translations,"
"release_dates,"
"external_ids") -> Optional[dict]:
"credits,"
"alternative_titles,"
"translations,"
"release_dates,"
"external_ids") -> Optional[dict]:
"""
获取电影的详情
:param tmdbid: TMDB ID
@@ -881,11 +900,12 @@ class TmdbApi:
def __get_tv_detail(self,
tmdbid: int,
append_to_response: Optional[str] = "images,"
"credits,"
"alternative_titles,"
"translations,"
"content_ratings,"
"external_ids") -> Optional[dict]:
"credits,"
"alternative_titles,"
"translations,"
"content_ratings,"
"external_ids,"
"episode_groups") -> Optional[dict]:
"""
获取电视剧的详情
:param tmdbid: TMDB ID
@@ -1316,6 +1336,33 @@ class TmdbApi:
logger.error(str(e))
return []
def get_tv_group_seasons(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_tv_group_detail(self, group_id: str, season: int) -> dict:
"""
获取剧集组某个季的信息
"""
group_seasons = self.get_tv_group_seasons(group_id)
if not group_seasons:
return {}
for group_season in group_seasons:
if group_season.get('order') == season:
return group_season
return {}
def get_person_detail(self, person_id: int) -> dict:
"""
获取人物详情
@@ -1376,38 +1423,6 @@ class TmdbApi:
"""
self.tmdb.cache_clear()
def get_tv_episode_years(self, tv_id: int) -> dict:
"""
查询剧集组年份
"""
try:
episode_groups = self.tv.episode_groups(tv_id)
if not episode_groups:
return {}
episode_years = {}
for episode_group in episode_groups:
logger.debug(f"正在获取剧集组年份:{episode_group.get('id')}...")
if episode_group.get('type') != 6:
# 只处理剧集部分
continue
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):
"""
关闭连接

View File

@@ -172,7 +172,7 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
else:
servers = self.get_instances().items()
for name, s in servers:
if not server:
if not s:
continue
if mediainfo.type == MediaType.MOVIE:
if itemid:

View File

@@ -403,10 +403,14 @@ class Api:
)
else:
json_body = None
if params:
queries_unquoted = "&".join([f"{k}={v}" for k, v in params.items()])
else:
queries_unquoted = None
headers = {
"User-Agent": settings.USER_AGENT,
"Authorization": self._token,
"authx": self.__get_authx(api_path, json_body),
"authx": self.__get_authx(api_path, json_body or queries_unquoted),
}
if json_body is not None:
headers["Content-Type"] = "application/json"
@@ -425,4 +429,4 @@ class Api:
logger.error(f"请求接口 {api_path} 失败")
except Exception as e:
logger.error(f"请求接口 {api_path} 异常:" + str(e))
return None
return None

View File

@@ -170,6 +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):

View File

@@ -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
# 失败原因

View File

@@ -73,6 +73,8 @@ class Subscribe(BaseModel):
media_category: Optional[str] = None
# 过滤规则组
filter_groups: Optional[List[str]] = Field(default_factory=list)
# 剧集组
episode_group: str = None
class Config:
orm_mode = True
@@ -130,6 +132,8 @@ class SubscribeShare(BaseModel):
custom_words: Optional[str] = None
# 自定义媒体类别
media_category: Optional[str] = None
# 自定义剧集组
episode_group: Optional[str] = None
# 复用人次
count: Optional[int] = 0

View File

@@ -200,3 +200,5 @@ class ManualTransferItem(BaseModel):
library_category_folder: Optional[bool] = None
# 复用历史识别信息
from_history: Optional[bool] = False
# 剧集组
episode_group: Optional[str] = None

View File

@@ -0,0 +1,32 @@
"""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_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

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v2.3.6'
FRONTEND_VERSION = 'v2.3.6'
APP_VERSION = 'v2.3.8'
FRONTEND_VERSION = 'v2.3.8'