mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-11 09:59:51 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e855d8b9af | ||
|
|
171720e629 | ||
|
|
8aa6b33fba | ||
|
|
505fc803db | ||
|
|
b5146620a6 | ||
|
|
7d44f24347 | ||
|
|
4dccc6e860 | ||
|
|
ee6585c737 | ||
|
|
62e5e8a69f | ||
|
|
e942a99ff0 | ||
|
|
b3fe49684b | ||
|
|
dcf1985361 | ||
|
|
8f4f4cc004 | ||
|
|
f49baadb76 | ||
|
|
5233484fc5 | ||
|
|
84c4cc8b5d | ||
|
|
77036eccd8 | ||
|
|
dcdb08ec80 | ||
|
|
cd7f688e78 | ||
|
|
cb12a052ac | ||
|
|
995c359f20 | ||
|
|
690066ad32 | ||
|
|
73942e315a | ||
|
|
48badb3243 | ||
|
|
d5eb12cc4e | ||
|
|
7d7539df4c | ||
|
|
14a8f44f8c | ||
|
|
a7be470f33 | ||
|
|
a677169f60 | ||
|
|
b72ef4f2aa | ||
|
|
403054751b | ||
|
|
b3e5c734d4 | ||
|
|
5732125ff6 | ||
|
|
eb66cf7aad | ||
|
|
a317c35eab |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ app/helper/*.so
|
||||
app/helper/*.pyd
|
||||
app/helper/*.bin
|
||||
app/plugins/**
|
||||
!app/plugins/__init__.py
|
||||
config/user.db
|
||||
config/sites/**
|
||||
*.pyc
|
||||
|
||||
@@ -32,6 +32,7 @@ RUN apt-get update -y \
|
||||
haproxy \
|
||||
fuse3 \
|
||||
rsync \
|
||||
ffmpeg \
|
||||
&& \
|
||||
if [ "$(uname -m)" = "x86_64" ]; \
|
||||
then ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1; \
|
||||
|
||||
@@ -73,7 +73,7 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **AUTO_UPDATE_RESOURCE**:启动时自动检测和更新资源包(站点索引及认证等),`true`/`false`,默认`true`,需要能正常连接Github,仅支持Docker
|
||||
- **❗AUTH_SITE:** 认证站点(认证通过后才能使用站点相关功能),支持配置多个认证站点,使用`,`分隔,如:`iyuu,hhclub`,会依次执行认证操作,直到有一个站点认证成功。
|
||||
|
||||
配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数,认证资源`v1.0.2`支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`/`ptvicomo`/`agsvpt`
|
||||
配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数,认证资源`v1.1.1`支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`ptba`:1ptba /`icc2022`/`ptlsp`/`xingtan`/`ptvicomo`/`agsvpt`
|
||||
|
||||
| 站点 | 参数 |
|
||||
|:------------:|:-----------------------------------------------------:|
|
||||
@@ -86,12 +86,12 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
| hdfans | `HDFANS_UID`:用户ID<br/>`HDFANS_PASSKEY`:密钥 |
|
||||
| wintersakura | `WINTERSAKURA_UID`:用户ID<br/>`WINTERSAKURA_PASSKEY`:密钥 |
|
||||
| leaves | `LEAVES_UID`:用户ID<br/>`LEAVES_PASSKEY`:密钥 |
|
||||
| 1ptba | `1PTBA_UID`:用户ID<br/>`1PTBA_PASSKEY`:密钥 |
|
||||
| ptba | `PTBA_UID`:用户ID<br/>`PTBA_PASSKEY`:密钥 |
|
||||
| icc2022 | `ICC2022_UID`:用户ID<br/>`ICC2022_PASSKEY`:密钥 |
|
||||
| ptlsp | `PTLSP_UID`:用户ID<br/>`PTLSP_PASSKEY`:密钥 |
|
||||
| xingtan | `XINGTAN_UID`:用户ID<br/>`XINGTAN_PASSKEY`:密钥 |
|
||||
| ptvicomo | `PTVICOMO_UID`:用户ID<br/>`PTVICOMO_PASSKEY`:密钥 |
|
||||
| agsvpt | `AGSVPT_UID`:用户ID<br/>`AGSVPT_PASSKEY`:密钥 |
|
||||
| agsvpt | `AGSVPT_UID`:用户ID<br/>`AGSVPT_PASSKEY`:密钥 |
|
||||
|
||||
|
||||
### 2. **app.env配置文件**
|
||||
|
||||
@@ -42,17 +42,26 @@ def delete_download_history(history_in: schemas.DownloadHistory,
|
||||
def transfer_history(title: str = None,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
status: bool = None,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询转移历史记录
|
||||
"""
|
||||
if title == "失败":
|
||||
title = None
|
||||
status = False
|
||||
elif title == "成功":
|
||||
title = None
|
||||
status = True
|
||||
|
||||
if title:
|
||||
total = TransferHistory.count_by_title(db, title)
|
||||
result = TransferHistory.list_by_title(db, title, page, count)
|
||||
total = TransferHistory.count_by_title(db, title=title, status=status)
|
||||
result = TransferHistory.list_by_title(db, title=title, page=page,
|
||||
count=count, status=status)
|
||||
else:
|
||||
result = TransferHistory.list_by_page(db, page, count)
|
||||
total = TransferHistory.count(db)
|
||||
result = TransferHistory.list_by_page(db, page=page, count=count, status=status)
|
||||
total = TransferHistory.count(db, status=status)
|
||||
|
||||
return schemas.Response(success=True,
|
||||
data={
|
||||
|
||||
@@ -35,6 +35,14 @@ def all_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
elif plugin.get("has_update"):
|
||||
plugin["installed"] = False
|
||||
plugins.append(plugin)
|
||||
# 本地未安装的插件
|
||||
for plugin in local_plugins:
|
||||
if plugin["id"] not in installed_ids:
|
||||
plugins.append(plugin)
|
||||
elif plugin.get("has_update"):
|
||||
plugin["installed"] = False
|
||||
plugins.append(plugin)
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
|
||||
@@ -228,10 +228,11 @@ def read_rss_sites(db: Session = Depends(get_db)) -> List[dict]:
|
||||
"""
|
||||
# 选中的rss站点
|
||||
selected_sites = SystemConfigOper().get(SystemConfigKey.RssSites) or []
|
||||
|
||||
# 所有站点
|
||||
all_site = Site.list_order_by_pri(db)
|
||||
if not selected_sites or not all_site:
|
||||
return []
|
||||
if not selected_sites:
|
||||
return all_site
|
||||
|
||||
# 选中的rss站点
|
||||
rss_sites = [site for site in all_site if site and site.id in selected_sites]
|
||||
|
||||
@@ -82,6 +82,7 @@ def create_subscribe(
|
||||
doubanid=subscribe_in.doubanid,
|
||||
username=current_user.name,
|
||||
best_version=subscribe_in.best_version,
|
||||
save_path=subscribe_in.save_path,
|
||||
exist_ok=True)
|
||||
return schemas.Response(success=True if sid else False, message=message, data={
|
||||
"id": sid
|
||||
|
||||
@@ -47,9 +47,11 @@ def manual_transfer(path: str = None,
|
||||
:param _: Token校验
|
||||
"""
|
||||
force = False
|
||||
target = Path(target) if target else None
|
||||
transfer = TransferChain()
|
||||
if logid:
|
||||
# 查询历史记录
|
||||
history = TransferHistory.get(db, logid)
|
||||
history: TransferHistory = TransferHistory.get(db, logid)
|
||||
if not history:
|
||||
return schemas.Response(success=False, message=f"历史记录不存在,ID:{logid}")
|
||||
# 强制转移
|
||||
@@ -59,19 +61,16 @@ def manual_transfer(path: str = None,
|
||||
# 目的路径
|
||||
if history.dest and str(history.dest) != "None":
|
||||
# 删除旧的已整理文件
|
||||
TransferChain().delete_files(Path(history.dest))
|
||||
transfer.delete_files(Path(history.dest))
|
||||
if not target:
|
||||
target = history.dest
|
||||
target = transfer.get_root_path(path=history.dest,
|
||||
type_name=history.type,
|
||||
category=history.category)
|
||||
elif path:
|
||||
in_path = Path(path)
|
||||
else:
|
||||
return schemas.Response(success=False, message=f"缺少参数:path/logid")
|
||||
|
||||
if target and target != "None":
|
||||
target = Path(target)
|
||||
else:
|
||||
target = None
|
||||
|
||||
# 类型
|
||||
mtype = MediaType(type_name) if type_name else None
|
||||
# 自定义格式
|
||||
@@ -84,7 +83,7 @@ def manual_transfer(path: str = None,
|
||||
offset=episode_offset,
|
||||
)
|
||||
# 开始转移
|
||||
state, errormsg = TransferChain().manual_transfer(
|
||||
state, errormsg = transfer.manual_transfer(
|
||||
in_path=in_path,
|
||||
target=target,
|
||||
tmdbid=tmdbid,
|
||||
|
||||
@@ -421,15 +421,19 @@ class ChainBase(metaclass=ABCMeta):
|
||||
"""
|
||||
return self.run_module("post_torrents_message", message=message, torrents=torrents)
|
||||
|
||||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str) -> None:
|
||||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str,
|
||||
force_nfo: bool = False, force_img: bool = False) -> None:
|
||||
"""
|
||||
刮削元数据
|
||||
:param path: 媒体文件路径
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param transfer_type: 转移模式
|
||||
:param force_nfo: 强制刮削nfo
|
||||
:param force_img: 强制刮削图片
|
||||
:return: 成功或失败
|
||||
"""
|
||||
self.run_module("scrape_metadata", path=path, mediainfo=mediainfo, transfer_type=transfer_type)
|
||||
self.run_module("scrape_metadata", path=path, mediainfo=mediainfo,
|
||||
transfer_type=transfer_type, force_nfo=force_nfo, force_img=force_img)
|
||||
|
||||
def register_commands(self, commands: Dict[str, dict]) -> None:
|
||||
"""
|
||||
|
||||
@@ -329,7 +329,8 @@ class DownloadChain(ChainBase):
|
||||
save_path: str = None,
|
||||
channel: MessageChannel = None,
|
||||
userid: str = None,
|
||||
username: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||
username: str = None
|
||||
) -> Tuple[List[Context], Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
|
||||
"""
|
||||
根据缺失数据,自动种子列表中组合择优下载
|
||||
:param contexts: 资源上下文列表
|
||||
@@ -509,7 +510,7 @@ class DownloadChain(ChainBase):
|
||||
start_episode = tv.start_episode or 1
|
||||
# 缺失整季的转化为缺失集进行比较
|
||||
if not need_episodes:
|
||||
need_episodes = list(range(start_episode, total_episode))
|
||||
need_episodes = list(range(start_episode, total_episode + 1))
|
||||
# 循环种子
|
||||
for context in contexts:
|
||||
# 媒体信息
|
||||
@@ -640,7 +641,7 @@ class DownloadChain(ChainBase):
|
||||
mediainfo: MediaInfo,
|
||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||
totals: Dict[int, int] = None
|
||||
) -> Tuple[bool, Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||
) -> Tuple[bool, Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
|
||||
"""
|
||||
检查媒体库,查询是否存在,对于剧集同时返回不存在的季集信息
|
||||
:param meta: 元数据
|
||||
|
||||
@@ -199,7 +199,8 @@ class SubscribeChain(ChainBase):
|
||||
tmdbid=subscribe.tmdbid,
|
||||
doubanid=subscribe.doubanid)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||
logger.warn(
|
||||
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||
continue
|
||||
|
||||
# 非洗版状态
|
||||
@@ -235,12 +236,12 @@ class SubscribeChain(ChainBase):
|
||||
# 已存在
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
|
||||
continue
|
||||
|
||||
# 电视剧订阅处理缺失集
|
||||
if meta.type == MediaType.TV:
|
||||
# 使用订阅的总集数和开始集数替换no_exists
|
||||
# 实际缺失集与订阅开始结束集范围进行整合
|
||||
no_exists = self.__get_subscribe_no_exits(
|
||||
no_exists=no_exists,
|
||||
mediakey=mediakey,
|
||||
@@ -249,7 +250,7 @@ class SubscribeChain(ChainBase):
|
||||
start_episode=subscribe.start_episode,
|
||||
|
||||
)
|
||||
# 打印缺失集信息
|
||||
# 打印汇总缺失集信息
|
||||
if no_exists and no_exists.get(mediakey):
|
||||
no_exists_info = no_exists.get(mediakey).get(subscribe.season)
|
||||
if no_exists_info:
|
||||
@@ -279,13 +280,11 @@ class SubscribeChain(ChainBase):
|
||||
filter_rule=filter_rule)
|
||||
if not contexts:
|
||||
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
||||
if meta.type == MediaType.TV:
|
||||
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
|
||||
meta=meta, mediainfo=mediainfo)
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, lefts=no_exists)
|
||||
continue
|
||||
|
||||
# 过滤
|
||||
# 过滤搜索结果
|
||||
matched_contexts = []
|
||||
for context in contexts:
|
||||
torrent_meta = context.meta_info
|
||||
@@ -304,41 +303,30 @@ class SubscribeChain(ChainBase):
|
||||
if torrent_meta.episode_list:
|
||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 不是整季')
|
||||
continue
|
||||
# 优先级小于已下载优先级的不要
|
||||
# 洗版时,优先级小于已下载优先级的不要
|
||||
if subscribe.current_priority \
|
||||
and torrent_info.pri_order < subscribe.current_priority:
|
||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于已下载优先级')
|
||||
continue
|
||||
matched_contexts.append(context)
|
||||
|
||||
if not matched_contexts:
|
||||
logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源')
|
||||
# 非洗版未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
|
||||
meta=meta, mediainfo=mediainfo)
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, lefts=no_exists)
|
||||
continue
|
||||
|
||||
# 自动下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
||||
no_exists=no_exists, username=subscribe.username)
|
||||
# 更新已经下载的集数
|
||||
if downloads \
|
||||
and meta.type == MediaType.TV \
|
||||
and not subscribe.best_version:
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
downloads, lefts = self.downloadchain.batch_download(
|
||||
contexts=matched_contexts,
|
||||
no_exists=no_exists,
|
||||
username=subscribe.username,
|
||||
save_path=subscribe.save_path
|
||||
)
|
||||
|
||||
if downloads and not lefts:
|
||||
# 判断是否应完成订阅
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, downloads=downloads)
|
||||
else:
|
||||
# 未完成下载
|
||||
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
|
||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||
# 更新订阅剩余集数和时间
|
||||
update_date = True if downloads else False
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, update_date=update_date)
|
||||
# 判断是否应完成订阅
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
||||
downloads=downloads, lefts=lefts)
|
||||
|
||||
# 手动触发时发送系统消息
|
||||
if manual:
|
||||
@@ -347,35 +335,74 @@ class SubscribeChain(ChainBase):
|
||||
else:
|
||||
self.message.put('所有订阅搜索完成!')
|
||||
|
||||
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo,
|
||||
mediainfo: MediaInfo, downloads: List[Context] = None):
|
||||
def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaInfo,
|
||||
mediainfo: MediaInfo, downloads: List[Context]):
|
||||
"""
|
||||
判断是否应完成订阅
|
||||
更新订阅已下载资源的优先级
|
||||
"""
|
||||
if not downloads:
|
||||
return
|
||||
if not subscribe.best_version:
|
||||
# 全部下载完成
|
||||
logger.info(f'{mediainfo.title_year} 完成订阅')
|
||||
return
|
||||
# 当前下载资源的优先级
|
||||
priority = max([item.torrent_info.pri_order for item in downloads])
|
||||
if priority == 100:
|
||||
logger.info(f'{mediainfo.title_year} 洗版完成,删除订阅')
|
||||
self.subscribeoper.delete(subscribe.id)
|
||||
# 发送通知
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
|
||||
title=f'{mediainfo.title_year} {meta.season} 已洗版完成',
|
||||
image=mediainfo.get_message_image()))
|
||||
elif downloads:
|
||||
# 当前下载资源的优先级
|
||||
priority = max([item.torrent_info.pri_order for item in downloads])
|
||||
if priority == 100:
|
||||
logger.info(f'{mediainfo.title_year} 洗版完成,删除订阅')
|
||||
else:
|
||||
# 正在洗版,更新资源优先级
|
||||
logger.info(f'{mediainfo.title_year} 正在洗版,更新资源优先级为 {priority}')
|
||||
self.subscribeoper.update(subscribe.id, {
|
||||
"current_priority": priority
|
||||
})
|
||||
|
||||
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo, mediainfo: MediaInfo,
|
||||
downloads: List[Context] = None,
|
||||
lefts: Dict[Union[int | str], Dict[int, NotExistMediaInfo]] = None,
|
||||
force: bool = False):
|
||||
"""
|
||||
判断是否应完成订阅
|
||||
"""
|
||||
mediakey = subscribe.tmdbid or subscribe.doubanid
|
||||
# 是否有剩余集
|
||||
no_lefts = not lefts or not lefts.get(mediakey)
|
||||
# 是否完成订阅
|
||||
if not subscribe.best_version:
|
||||
# 非洗板
|
||||
if ((no_lefts and meta.type == MediaType.TV)
|
||||
or (downloads and meta.type == MediaType.MOVIE)
|
||||
or force):
|
||||
# 全部下载完成
|
||||
logger.info(f'{mediainfo.title_year} 完成订阅')
|
||||
self.subscribeoper.delete(subscribe.id)
|
||||
# 发送通知
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'{mediainfo.title_year} {meta.season} 已洗版完成',
|
||||
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
|
||||
image=mediainfo.get_message_image()))
|
||||
elif downloads and meta.type == MediaType.TV:
|
||||
# 电视剧更新已下载集数
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
# 更新订阅剩余集数和时间
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
||||
mediainfo=mediainfo, update_date=True)
|
||||
else:
|
||||
# 正在洗版,更新资源优先级
|
||||
logger.info(f'{mediainfo.title_year} 正在洗版,更新资源优先级')
|
||||
self.subscribeoper.update(subscribe.id, {
|
||||
"current_priority": priority
|
||||
})
|
||||
# 未下载到内容且不完整
|
||||
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
|
||||
if meta.type == MediaType.TV:
|
||||
# 更新订阅剩余集数
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
||||
mediainfo=mediainfo, update_date=False)
|
||||
elif downloads:
|
||||
# 洗板,下载到了内容,更新资源优先级
|
||||
self.update_subscribe_priority(subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, downloads=downloads)
|
||||
else:
|
||||
# 洗版,未下载到内容
|
||||
logger.info(f'{mediainfo.title_year} 继续洗版 ...')
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
@@ -495,9 +522,11 @@ class SubscribeChain(ChainBase):
|
||||
meta.type = MediaType(subscribe.type)
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
|
||||
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid)
|
||||
tmdbid=subscribe.tmdbid,
|
||||
doubanid=subscribe.doubanid)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||
logger.warn(
|
||||
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||
continue
|
||||
# 非洗版
|
||||
if not subscribe.best_version:
|
||||
@@ -532,12 +561,12 @@ class SubscribeChain(ChainBase):
|
||||
# 已存在
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
|
||||
continue
|
||||
|
||||
# 电视剧订阅
|
||||
if meta.type == MediaType.TV:
|
||||
# 使用订阅的总集数和开始集数替换no_exists
|
||||
# 整合实际缺失集与订阅开始集结束集
|
||||
no_exists = self.__get_subscribe_no_exits(
|
||||
no_exists=no_exists,
|
||||
mediakey=mediakey,
|
||||
@@ -546,7 +575,7 @@ class SubscribeChain(ChainBase):
|
||||
start_episode=subscribe.start_episode,
|
||||
|
||||
)
|
||||
# 打印缺失集信息
|
||||
# 打印汇总缺失集信息
|
||||
if no_exists and no_exists.get(mediakey):
|
||||
no_exists_info = no_exists.get(mediakey).get(subscribe.season)
|
||||
if no_exists_info:
|
||||
@@ -633,35 +662,33 @@ class SubscribeChain(ChainBase):
|
||||
filter_rule=filter_rule):
|
||||
continue
|
||||
|
||||
# 洗版时,优先级小于已下载优先级的不要
|
||||
if subscribe.best_version:
|
||||
if subscribe.current_priority \
|
||||
and torrent_info.pri_order < subscribe.current_priority:
|
||||
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于已下载优先级')
|
||||
continue
|
||||
|
||||
# 匹配成功
|
||||
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
|
||||
_match_context.append(context)
|
||||
|
||||
# 开始下载
|
||||
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
|
||||
if _match_context:
|
||||
# 批量择优下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context, no_exists=no_exists,
|
||||
username=subscribe.username)
|
||||
# 更新已经下载的集数
|
||||
if downloads and meta.type == MediaType.TV:
|
||||
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
|
||||
if not _match_context:
|
||||
# 未匹配到资源
|
||||
logger.info(f'{mediainfo.title_year} 未匹配到符合条件的资源')
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, lefts=no_exists)
|
||||
continue
|
||||
|
||||
if downloads and not lefts:
|
||||
# 判断是否要完成订阅
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, downloads=downloads)
|
||||
else:
|
||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||
update_date = True if downloads else False
|
||||
# 未完成下载,计算剩余集数
|
||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
|
||||
mediainfo=mediainfo, update_date=update_date)
|
||||
else:
|
||||
if meta.type == MediaType.TV:
|
||||
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
|
||||
meta=meta, mediainfo=mediainfo)
|
||||
# 开始批量择优下载
|
||||
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context,
|
||||
no_exists=no_exists,
|
||||
username=subscribe.username,
|
||||
save_path=subscribe.save_path)
|
||||
# 判断是否要完成订阅
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
|
||||
downloads=downloads, lefts=lefts)
|
||||
|
||||
def check(self):
|
||||
"""
|
||||
@@ -684,7 +711,8 @@ class SubscribeChain(ChainBase):
|
||||
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
|
||||
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||
logger.warn(
|
||||
f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}')
|
||||
continue
|
||||
# 对于电视剧,获取当前季的总集数
|
||||
episodes = mediainfo.seasons.get(subscribe.season) or []
|
||||
@@ -756,14 +784,15 @@ class SubscribeChain(ChainBase):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||
def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, NotExistMediaInfo]],
|
||||
subscribe: Subscribe,
|
||||
meta: MetaBase,
|
||||
mediainfo: MediaInfo,
|
||||
update_date: bool = False):
|
||||
"""
|
||||
更新订阅剩余集数
|
||||
"""
|
||||
if not lefts:
|
||||
return
|
||||
mediakey = subscribe.tmdbid or subscribe.doubanid
|
||||
left_seasons = lefts.get(mediakey)
|
||||
if left_seasons:
|
||||
@@ -786,9 +815,6 @@ class SubscribeChain(ChainBase):
|
||||
self.subscribeoper.update(subscribe.id, {
|
||||
"lack_episode": lack_episode
|
||||
})
|
||||
else:
|
||||
# 判断是否应完成订阅
|
||||
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||
|
||||
def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
|
||||
"""
|
||||
@@ -850,7 +876,7 @@ class SubscribeChain(ChainBase):
|
||||
self.remote_list(channel, userid)
|
||||
|
||||
@staticmethod
|
||||
def __get_subscribe_no_exits(no_exists: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||
def __get_subscribe_no_exits(no_exists: Dict[Union[int, str], Dict[int, NotExistMediaInfo]],
|
||||
mediakey: Union[str, int],
|
||||
begin_season: int,
|
||||
total_episode: int,
|
||||
|
||||
@@ -259,8 +259,8 @@ class TransferChain(ChainBase):
|
||||
)
|
||||
self.post_message(Notification(
|
||||
mtype=NotificationType.Manual,
|
||||
title=f"{file_path.name} 未识别到媒体信息,无法入库!\n"
|
||||
f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。"
|
||||
title=f"{file_path.name} 未识别到媒体信息,无法入库!",
|
||||
text=f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。"
|
||||
))
|
||||
# 计数
|
||||
processed_num += 1
|
||||
@@ -481,6 +481,24 @@ class TransferChain(ChainBase):
|
||||
text=errmsg, userid=userid))
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_root_path(path: str, type_name: str, category: str) -> Path:
|
||||
"""
|
||||
计算媒体库目录的根路径
|
||||
"""
|
||||
if not path or path == "None":
|
||||
return None
|
||||
index = -2
|
||||
if type_name != '电影':
|
||||
index = -3
|
||||
if category:
|
||||
index -= 1
|
||||
if '/' in path:
|
||||
retpath = '/'.join(path.split('/')[:index])
|
||||
else:
|
||||
retpath = '\\'.join(path.split('\\')[:index])
|
||||
return Path(retpath)
|
||||
|
||||
def re_transfer(self, logid: int, mtype: MediaType = None,
|
||||
mediaid: str = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
@@ -498,7 +516,7 @@ class TransferChain(ChainBase):
|
||||
src_path = Path(history.src)
|
||||
if not src_path.exists():
|
||||
return False, f"源目录不存在:{src_path}"
|
||||
dest_path = Path(history.dest) if history.dest else None
|
||||
dest_path = self.get_root_path(path=history.dest, type_name=history.type, category=history.category)
|
||||
# 查询媒体信息
|
||||
if mtype and mediaid:
|
||||
mediainfo = self.recognize_media(mtype=mtype, tmdbid=int(mediaid) if str(mediaid).isdigit() else None,
|
||||
|
||||
@@ -187,11 +187,11 @@ class Settings(BaseSettings):
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57"
|
||||
# 媒体库目录,多个目录使用,分隔
|
||||
LIBRARY_PATH: str = None
|
||||
# 电影媒体库目录名,默认"电影"
|
||||
LIBRARY_MOVIE_NAME: str = None
|
||||
# 电视剧媒体库目录名,默认"电视剧"
|
||||
LIBRARY_TV_NAME: str = None
|
||||
# 动漫媒体库目录名,默认"电视剧/动漫"
|
||||
# 电影媒体库目录名
|
||||
LIBRARY_MOVIE_NAME: str = "电影"
|
||||
# 电视剧媒体库目录名
|
||||
LIBRARY_TV_NAME: str = "电视剧"
|
||||
# 动漫媒体库目录名,不设置时使用电视剧目录
|
||||
LIBRARY_ANIME_NAME: str = None
|
||||
# 二级分类
|
||||
LIBRARY_CATEGORY: bool = True
|
||||
|
||||
@@ -60,12 +60,16 @@ def MetaInfoPath(path: Path) -> MetaBase:
|
||||
根据路径识别元数据
|
||||
:param path: 路径
|
||||
"""
|
||||
# 上级目录元数据
|
||||
dir_meta = MetaInfo(title=path.parent.name)
|
||||
# 文件元数据,不包含后缀
|
||||
file_meta = MetaInfo(title=path.stem)
|
||||
# 上级目录元数据
|
||||
dir_meta = MetaInfo(title=path.parent.name)
|
||||
# 合并元数据
|
||||
file_meta.merge(dir_meta)
|
||||
# 上上级目录元数据
|
||||
root_meta = MetaInfo(title=path.parent.parent.name)
|
||||
# 合并元数据
|
||||
file_meta.merge(root_meta)
|
||||
return file_meta
|
||||
|
||||
|
||||
@@ -120,7 +124,7 @@ def find_metainfo(title: str) -> Tuple[str, dict]:
|
||||
if doubanid and doubanid[0].isdigit():
|
||||
metainfo['doubanid'] = doubanid[0]
|
||||
# 查找媒体类型
|
||||
mtype = re.findall(r'(?<=type=)\d+', result)
|
||||
mtype = re.findall(r'(?<=type=)\w+', result)
|
||||
if mtype:
|
||||
match mtype[0]:
|
||||
case "movie":
|
||||
|
||||
@@ -65,6 +65,8 @@ class Subscribe(Base):
|
||||
best_version = Column(Integer, default=0)
|
||||
# 当前优先级
|
||||
current_priority = Column(Integer)
|
||||
# 保存路径
|
||||
save_path = Column(String)
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -48,17 +48,28 @@ class TransferHistory(Base):
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def list_by_title(db: Session, title: str, page: int = 1, count: int = 30):
|
||||
result = db.query(TransferHistory).filter(TransferHistory.title.like(f'%{title}%')).order_by(
|
||||
TransferHistory.date.desc()).offset((page - 1) * count).limit(
|
||||
count).all()
|
||||
def list_by_title(db: Session, title: str, page: int = 1, count: int = 30, status: bool = None):
|
||||
if status is not None:
|
||||
result = db.query(TransferHistory).filter(TransferHistory.title.like(f'%{title}%'),
|
||||
TransferHistory.status == status).order_by(
|
||||
TransferHistory.date.desc()).offset((page - 1) * count).limit(
|
||||
count).all()
|
||||
else:
|
||||
result = db.query(TransferHistory).filter(TransferHistory.title.like(f'%{title}%')).order_by(
|
||||
TransferHistory.date.desc()).offset((page - 1) * count).limit(
|
||||
count).all()
|
||||
return list(result)
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def list_by_page(db: Session, page: int = 1, count: int = 30):
|
||||
result = db.query(TransferHistory).order_by(TransferHistory.date.desc()).offset((page - 1) * count).limit(
|
||||
count).all()
|
||||
def list_by_page(db: Session, page: int = 1, count: int = 30, status: bool = None):
|
||||
if status is not None:
|
||||
result = db.query(TransferHistory).filter(TransferHistory.status == status).order_by(
|
||||
TransferHistory.date.desc()).offset((page - 1) * count).limit(
|
||||
count).all()
|
||||
else:
|
||||
result = db.query(TransferHistory).order_by(TransferHistory.date.desc()).offset((page - 1) * count).limit(
|
||||
count).all()
|
||||
return list(result)
|
||||
|
||||
@staticmethod
|
||||
@@ -92,13 +103,20 @@ class TransferHistory(Base):
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def count(db: Session):
|
||||
return db.query(func.count(TransferHistory.id)).first()[0]
|
||||
def count(db: Session, status: bool = None):
|
||||
if status is not None:
|
||||
return db.query(func.count(TransferHistory.id)).filter(TransferHistory.status == status).first()[0]
|
||||
else:
|
||||
return db.query(func.count(TransferHistory.id)).first()[0]
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def count_by_title(db: Session, title: str):
|
||||
return db.query(func.count(TransferHistory.id)).filter(TransferHistory.title.like(f'%{title}%')).first()[0]
|
||||
def count_by_title(db: Session, title: str, status: bool = None):
|
||||
if status is not None:
|
||||
return db.query(func.count(TransferHistory.id)).filter(TransferHistory.title.like(f'%{title}%'),
|
||||
TransferHistory.status == status).first()[0]
|
||||
else:
|
||||
return db.query(func.count(TransferHistory.id)).filter(TransferHistory.title.like(f'%{title}%')).first()[0]
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -588,12 +588,15 @@ class DoubanModule(_ModuleBase):
|
||||
return []
|
||||
return infos.get("subject_collection_items")
|
||||
|
||||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str) -> None:
|
||||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str,
|
||||
force_nfo: bool = False, force_img: bool = False) -> None:
|
||||
"""
|
||||
刮削元数据
|
||||
:param path: 媒体文件路径
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param transfer_type: 传输类型
|
||||
:param force_nfo: 是否强制刮削nfo
|
||||
:param force_img: 是否强制刮削图片
|
||||
:return: 成功或失败
|
||||
"""
|
||||
if settings.SCRAP_SOURCE != "douban":
|
||||
@@ -630,7 +633,9 @@ class DoubanModule(_ModuleBase):
|
||||
self.scraper.gen_scraper_files(meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
file_path=scrape_path,
|
||||
transfer_type=transfer_type)
|
||||
transfer_type=transfer_type,
|
||||
force_nfo=force_nfo,
|
||||
force_img=force_img)
|
||||
else:
|
||||
# 目录下的所有文件
|
||||
for file in SystemUtils.list_files(path, settings.RMT_MEDIAEXT):
|
||||
@@ -667,7 +672,9 @@ class DoubanModule(_ModuleBase):
|
||||
self.scraper.gen_scraper_files(meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
file_path=file,
|
||||
transfer_type=transfer_type)
|
||||
transfer_type=transfer_type,
|
||||
force_nfo=force_nfo,
|
||||
force_img=force_img)
|
||||
except Exception as e:
|
||||
logger.error(f"刮削文件 {file} 失败,原因:{str(e)}")
|
||||
logger.info(f"{path} 刮削完成")
|
||||
|
||||
@@ -14,53 +14,67 @@ from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
class DoubanScraper:
|
||||
|
||||
_transfer_type = settings.TRANSFER_TYPE
|
||||
_force_nfo = False
|
||||
_force_img = False
|
||||
|
||||
def gen_scraper_files(self, meta: MetaBase, mediainfo: MediaInfo,
|
||||
file_path: Path, transfer_type: str):
|
||||
file_path: Path, transfer_type: str,
|
||||
force_nfo: bool = False, force_img: bool = False):
|
||||
"""
|
||||
生成刮削文件
|
||||
:param meta: 元数据
|
||||
:param mediainfo: 媒体信息
|
||||
:param file_path: 文件路径或者目录路径
|
||||
:param transfer_type: 转输类型
|
||||
:param force_nfo: 强制生成NFO
|
||||
:param force_img: 强制生成图片
|
||||
"""
|
||||
|
||||
self._transfer_type = transfer_type
|
||||
self._force_nfo = force_nfo
|
||||
self._force_img = force_img
|
||||
|
||||
try:
|
||||
# 电影
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# 强制或者不已存在时才处理
|
||||
if not file_path.with_name("movie.nfo").exists() \
|
||||
and not file_path.with_suffix(".nfo").exists():
|
||||
if self._force_nfo or (not file_path.with_name("movie.nfo").exists()
|
||||
and not file_path.with_suffix(".nfo").exists()):
|
||||
# 生成电影描述文件
|
||||
self.__gen_movie_nfo_file(mediainfo=mediainfo,
|
||||
file_path=file_path)
|
||||
# 生成电影图片
|
||||
self.__save_image(url=mediainfo.poster_path,
|
||||
file_path=file_path.with_name(f"poster{Path(mediainfo.poster_path).suffix}"))
|
||||
image_path = file_path.with_name(f"poster{Path(mediainfo.poster_path).suffix}")
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=mediainfo.poster_path,
|
||||
file_path=image_path)
|
||||
# 背景图
|
||||
if mediainfo.backdrop_path:
|
||||
self.__save_image(url=mediainfo.backdrop_path,
|
||||
file_path=file_path.with_name(f"backdrop{Path(mediainfo.backdrop_path).suffix}"))
|
||||
image_path = file_path.with_name(f"backdrop{Path(mediainfo.backdrop_path).suffix}")
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=mediainfo.backdrop_path,
|
||||
file_path=image_path)
|
||||
# 电视剧
|
||||
else:
|
||||
# 不存在时才处理
|
||||
if not file_path.parent.with_name("tvshow.nfo").exists():
|
||||
if self._force_nfo or not file_path.parent.with_name("tvshow.nfo").exists():
|
||||
# 根目录描述文件
|
||||
self.__gen_tv_nfo_file(mediainfo=mediainfo,
|
||||
dir_path=file_path.parents[1])
|
||||
# 生成根目录图片
|
||||
self.__save_image(url=mediainfo.poster_path,
|
||||
file_path=file_path.with_name(f"poster{Path(mediainfo.poster_path).suffix}"))
|
||||
image_path = file_path.with_name(f"poster{Path(mediainfo.poster_path).suffix}")
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=mediainfo.poster_path,
|
||||
file_path=image_path)
|
||||
# 背景图
|
||||
if mediainfo.backdrop_path:
|
||||
self.__save_image(url=mediainfo.backdrop_path,
|
||||
file_path=file_path.with_name(f"backdrop{Path(mediainfo.backdrop_path).suffix}"))
|
||||
image_path = file_path.with_name(f"backdrop{Path(mediainfo.backdrop_path).suffix}")
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=mediainfo.backdrop_path,
|
||||
file_path=image_path)
|
||||
# 季目录NFO
|
||||
if not file_path.with_name("season.nfo").exists():
|
||||
if self._force_nfo or not file_path.with_name("season.nfo").exists():
|
||||
self.__gen_tv_season_nfo_file(mediainfo=mediainfo,
|
||||
season=meta.begin_season,
|
||||
season_path=file_path.parent)
|
||||
@@ -175,8 +189,6 @@ class DoubanScraper:
|
||||
"""
|
||||
下载图片并保存
|
||||
"""
|
||||
if file_path.exists():
|
||||
return
|
||||
if not url:
|
||||
return
|
||||
try:
|
||||
@@ -201,8 +213,6 @@ class DoubanScraper:
|
||||
"""
|
||||
保存NFO
|
||||
"""
|
||||
if file_path.exists():
|
||||
return
|
||||
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
|
||||
if self._transfer_type in ['rclone_move', 'rclone_copy']:
|
||||
self.__save_remove_file(file_path, xml_str)
|
||||
|
||||
@@ -846,7 +846,7 @@ class Emby(metaclass=Singleton):
|
||||
eventItem.overview = message.get('Item', {}).get('Overview')
|
||||
eventItem.percentage = message.get('TranscodingInfo', {}).get('CompletionPercentage')
|
||||
if not eventItem.percentage:
|
||||
if message.get('PlaybackInfo', {}).get('PositionTicks'):
|
||||
if message.get('PlaybackInfo', {}).get('PositionTicks') and message.get('Item', {}).get('RunTimeTicks'):
|
||||
eventItem.percentage = message.get('PlaybackInfo', {}).get('PositionTicks') / \
|
||||
message.get('Item', {}).get('RunTimeTicks') * 100
|
||||
if message.get('Session'):
|
||||
|
||||
@@ -45,10 +45,20 @@ class FileTransferModule(_ModuleBase):
|
||||
"""
|
||||
# 获取目标路径
|
||||
if not target:
|
||||
# 未指定目的目录,根据源目录选择一个媒体库
|
||||
target = self.get_target_path(in_path=path)
|
||||
elif not target.exists() or target.is_file():
|
||||
# 目的路径不存在或者是文件时,找对应的媒体库目录
|
||||
target = self.get_library_path(target)
|
||||
# 拼装媒体库一、二级子目录
|
||||
target = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target)
|
||||
else:
|
||||
# 指定了目的目录
|
||||
if target.is_file():
|
||||
logger.error(f"转移目标路径是一个文件 {target} 是一个文件")
|
||||
return TransferInfo(success=False,
|
||||
path=path,
|
||||
message=f"{target} 不是有效目录")
|
||||
# 只拼装二级子目录(不要一级目录)
|
||||
target = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target, typename_dir=False)
|
||||
|
||||
if not target:
|
||||
logger.error("未找到媒体库目录,无法转移文件")
|
||||
return TransferInfo(success=False,
|
||||
@@ -56,6 +66,7 @@ class FileTransferModule(_ModuleBase):
|
||||
message="未找到媒体库目录")
|
||||
else:
|
||||
logger.info(f"获取转移目标路径:{target}")
|
||||
|
||||
# 转移
|
||||
return self.transfer_media(in_path=path,
|
||||
in_meta=meta,
|
||||
@@ -333,33 +344,42 @@ class FileTransferModule(_ModuleBase):
|
||||
over_flag=over_flag)
|
||||
|
||||
@staticmethod
|
||||
def __get_dest_dir(mediainfo: MediaInfo, target_dir: Path) -> Path:
|
||||
def __get_dest_dir(mediainfo: MediaInfo, target_dir: Path, typename_dir: bool = True) -> Path:
|
||||
"""
|
||||
根据设置并装媒体库目录
|
||||
:param mediainfo: 媒体信息
|
||||
:target_dir: 媒体库根目录
|
||||
:typename_dir: 是否加上类型目录
|
||||
"""
|
||||
if not target_dir:
|
||||
return target_dir
|
||||
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# 电影
|
||||
if settings.LIBRARY_MOVIE_NAME:
|
||||
if typename_dir:
|
||||
# 目的目录加上类型和二级分类
|
||||
target_dir = target_dir / settings.LIBRARY_MOVIE_NAME / mediainfo.category
|
||||
else:
|
||||
# 目的目录加上类型和二级分类
|
||||
target_dir = target_dir / mediainfo.type.value / mediainfo.category
|
||||
# 目的目录加上二级分类
|
||||
target_dir = target_dir / mediainfo.category
|
||||
|
||||
if mediainfo.type == MediaType.TV:
|
||||
# 电视剧
|
||||
if settings.LIBRARY_ANIME_NAME \
|
||||
and mediainfo.genre_ids \
|
||||
if mediainfo.genre_ids \
|
||||
and set(mediainfo.genre_ids).intersection(set(settings.ANIME_GENREIDS)):
|
||||
# 动漫
|
||||
target_dir = target_dir / settings.LIBRARY_ANIME_NAME / mediainfo.category
|
||||
elif settings.LIBRARY_TV_NAME:
|
||||
# 电视剧
|
||||
target_dir = target_dir / settings.LIBRARY_TV_NAME / mediainfo.category
|
||||
if typename_dir:
|
||||
target_dir = target_dir / (settings.LIBRARY_ANIME_NAME
|
||||
or settings.LIBRARY_TV_NAME) / mediainfo.category
|
||||
else:
|
||||
target_dir = target_dir / mediainfo.category
|
||||
else:
|
||||
# 目的目录加上类型和二级分类
|
||||
target_dir = target_dir / mediainfo.type.value / mediainfo.category
|
||||
# 电视剧
|
||||
if typename_dir:
|
||||
target_dir = target_dir / settings.LIBRARY_TV_NAME / mediainfo.category
|
||||
else:
|
||||
target_dir = target_dir / mediainfo.category
|
||||
|
||||
return target_dir
|
||||
|
||||
def transfer_media(self,
|
||||
@@ -389,12 +409,8 @@ class FileTransferModule(_ModuleBase):
|
||||
if transfer_type not in ['rclone_copy', 'rclone_move']:
|
||||
# 检查目标路径
|
||||
if not target_dir.exists():
|
||||
return TransferInfo(success=False,
|
||||
path=in_path,
|
||||
message=f"{target_dir} 目标路径不存在")
|
||||
|
||||
# 媒体库目的目录
|
||||
target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir)
|
||||
logger.info(f"目标路径不存在,正在创建:{target_dir} ...")
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 重命名格式
|
||||
rename_format = settings.TV_RENAME_FORMAT \
|
||||
@@ -657,20 +673,19 @@ class FileTransferModule(_ModuleBase):
|
||||
continue
|
||||
if target_path:
|
||||
return target_path
|
||||
# 顺序匹配第1个满足空间存储要求的目录
|
||||
if in_path.exists():
|
||||
file_size = in_path.stat().st_size
|
||||
for path in dest_paths:
|
||||
if SystemUtils.free_space(path) > file_size:
|
||||
return path
|
||||
# 顺序匹配第1个满足空间存储要求的目录
|
||||
if in_path.exists():
|
||||
file_size = in_path.stat().st_size
|
||||
for path in dest_paths:
|
||||
if SystemUtils.free_space(path) > file_size:
|
||||
return path
|
||||
# 默认返回第1个
|
||||
return dest_paths[0]
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||
def media_exists(self, mediainfo: MediaInfo, **kwargs) -> Optional[ExistMediaInfo]:
|
||||
"""
|
||||
判断媒体文件是否存在于本地文件系统
|
||||
判断媒体文件是否存在于本地文件系统,只支持标准媒体库结构
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param itemid: 媒体服务器ItemID
|
||||
:return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}}
|
||||
"""
|
||||
if not settings.LIBRARY_PATHS:
|
||||
|
||||
@@ -197,9 +197,17 @@ class Telegram(metaclass=Singleton):
|
||||
raise Exception("发送图片消息失败")
|
||||
if ret:
|
||||
return True
|
||||
ret = self._bot.send_message(chat_id=userid or self._telegram_chat_id,
|
||||
text=caption,
|
||||
parse_mode="Markdown")
|
||||
# 按4096分段循环发送消息
|
||||
ret = None
|
||||
if len(caption) > 4095:
|
||||
for i in range(0, len(caption), 4095):
|
||||
ret = self._bot.send_message(chat_id=userid or self._telegram_chat_id,
|
||||
text=caption[i:i + 4095],
|
||||
parse_mode="Markdown")
|
||||
else:
|
||||
ret = self._bot.send_message(chat_id=userid or self._telegram_chat_id,
|
||||
text=caption,
|
||||
parse_mode="Markdown")
|
||||
if ret is None:
|
||||
raise Exception("发送文本消息失败")
|
||||
return True if ret else False
|
||||
|
||||
@@ -212,12 +212,15 @@ class TheMovieDbModule(_ModuleBase):
|
||||
|
||||
return [MediaInfo(tmdb_info=info) for info in results]
|
||||
|
||||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str) -> None:
|
||||
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str,
|
||||
force_nfo: bool = False, force_img: bool = False) -> None:
|
||||
"""
|
||||
刮削元数据
|
||||
:param path: 媒体文件路径
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param transfer_type: 转移类型
|
||||
:param force_nfo: 强制刮削nfo
|
||||
:param force_img: 强制刮削图片
|
||||
:return: 成功或失败
|
||||
"""
|
||||
if settings.SCRAP_SOURCE != "themoviedb":
|
||||
@@ -229,13 +232,17 @@ class TheMovieDbModule(_ModuleBase):
|
||||
scrape_path = path / path.name
|
||||
self.scraper.gen_scraper_files(mediainfo=mediainfo,
|
||||
file_path=scrape_path,
|
||||
transfer_type=transfer_type)
|
||||
transfer_type=transfer_type,
|
||||
force_nfo=force_nfo,
|
||||
force_img=force_img)
|
||||
elif path.is_file():
|
||||
# 单个文件
|
||||
logger.info(f"开始刮削媒体库文件:{path} ...")
|
||||
self.scraper.gen_scraper_files(mediainfo=mediainfo,
|
||||
file_path=path,
|
||||
transfer_type=transfer_type)
|
||||
transfer_type=transfer_type,
|
||||
force_nfo=force_nfo,
|
||||
force_img=force_img)
|
||||
else:
|
||||
# 目录下的所有文件
|
||||
logger.info(f"开始刮削目录:{path} ...")
|
||||
@@ -244,7 +251,9 @@ class TheMovieDbModule(_ModuleBase):
|
||||
continue
|
||||
self.scraper.gen_scraper_files(mediainfo=mediainfo,
|
||||
file_path=file,
|
||||
transfer_type=transfer_type)
|
||||
transfer_type=transfer_type,
|
||||
force_nfo=force_nfo,
|
||||
force_img=force_img)
|
||||
logger.info(f"{path} 刮削完成")
|
||||
|
||||
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str, with_original_language: str,
|
||||
|
||||
@@ -19,19 +19,26 @@ from app.utils.system import SystemUtils
|
||||
class TmdbScraper:
|
||||
tmdb = None
|
||||
_transfer_type = settings.TRANSFER_TYPE
|
||||
_force_nfo = False
|
||||
_force_img = False
|
||||
|
||||
def __init__(self, tmdb):
|
||||
self.tmdb = tmdb
|
||||
|
||||
def gen_scraper_files(self, mediainfo: MediaInfo, file_path: Path, transfer_type: str):
|
||||
def gen_scraper_files(self, mediainfo: MediaInfo, file_path: Path, transfer_type: str,
|
||||
force_nfo: bool = False, force_img: bool = False):
|
||||
"""
|
||||
生成刮削文件,包括NFO和图片,传入路径为文件路径
|
||||
:param mediainfo: 媒体信息
|
||||
:param file_path: 文件路径或者目录路径
|
||||
:param transfer_type: 传输类型
|
||||
:param force_nfo: 是否强制生成NFO
|
||||
:param force_img: 是否强制生成图片
|
||||
"""
|
||||
|
||||
self._transfer_type = transfer_type
|
||||
self._force_nfo = force_nfo
|
||||
self._force_img = force_img
|
||||
|
||||
def __get_episode_detail(_seasoninfo: dict, _episode: int):
|
||||
"""
|
||||
@@ -46,8 +53,8 @@ class TmdbScraper:
|
||||
# 电影,路径为文件名 名称/名称.xxx 或者蓝光原盘目录 名称/名称
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# 不已存在时才处理
|
||||
if not file_path.with_name("movie.nfo").exists() \
|
||||
and not file_path.with_suffix(".nfo").exists():
|
||||
if self._force_nfo or (not file_path.with_name("movie.nfo").exists()
|
||||
and not file_path.with_suffix(".nfo").exists()):
|
||||
# 生成电影描述文件
|
||||
self.__gen_movie_nfo_file(mediainfo=mediainfo,
|
||||
file_path=file_path)
|
||||
@@ -59,14 +66,16 @@ class TmdbScraper:
|
||||
and isinstance(attr_value, str) \
|
||||
and attr_value.startswith("http"):
|
||||
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
||||
self.__save_image(url=attr_value,
|
||||
file_path=file_path.with_name(image_name))
|
||||
image_path = file_path.with_name(image_name)
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=attr_value,
|
||||
file_path=image_path)
|
||||
# 电视剧,路径为每一季的文件名 名称/Season xx/名称 SxxExx.xxx
|
||||
else:
|
||||
# 识别
|
||||
meta = MetaInfo(file_path.stem)
|
||||
# 根目录不存在时才处理
|
||||
if not file_path.parent.with_name("tvshow.nfo").exists():
|
||||
if self._force_nfo or not file_path.parent.with_name("tvshow.nfo").exists():
|
||||
# 根目录描述文件
|
||||
self.__gen_tv_nfo_file(mediainfo=mediainfo,
|
||||
dir_path=file_path.parents[1])
|
||||
@@ -79,13 +88,15 @@ class TmdbScraper:
|
||||
and isinstance(attr_value, str) \
|
||||
and attr_value.startswith("http"):
|
||||
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
||||
self.__save_image(url=attr_value,
|
||||
file_path=file_path.parent.with_name(image_name))
|
||||
image_path = file_path.parent.with_name(image_name)
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=attr_value,
|
||||
file_path=image_path)
|
||||
# 查询季信息
|
||||
seasoninfo = self.tmdb.get_tv_season_detail(mediainfo.tmdb_id, meta.begin_season)
|
||||
if seasoninfo:
|
||||
# 季目录NFO
|
||||
if not file_path.with_name("season.nfo").exists():
|
||||
if self._force_nfo or not file_path.with_name("season.nfo").exists():
|
||||
self.__gen_tv_season_nfo_file(seasoninfo=seasoninfo,
|
||||
season=meta.begin_season,
|
||||
season_path=file_path.parent)
|
||||
@@ -96,7 +107,9 @@ class TmdbScraper:
|
||||
ext = Path(seasoninfo.get('poster_path')).suffix
|
||||
# URL
|
||||
url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{seasoninfo.get('poster_path')}"
|
||||
self.__save_image(url, file_path.parent.with_name(f"season{sea_seq}-poster{ext}"))
|
||||
image_path = file_path.parent.with_name(f"season{sea_seq}-poster{ext}")
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=url, file_path=image_path)
|
||||
# 季的其它图片
|
||||
for attr_name, attr_value in vars(mediainfo).items():
|
||||
if attr_value \
|
||||
@@ -106,13 +119,15 @@ class TmdbScraper:
|
||||
and isinstance(attr_value, str) \
|
||||
and attr_value.startswith("http"):
|
||||
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
||||
self.__save_image(url=attr_value,
|
||||
file_path=file_path.parent.with_name(image_name))
|
||||
image_path = file_path.parent.with_name(image_name)
|
||||
if self._force_img or not image_path.exists():
|
||||
self.__save_image(url=attr_value,
|
||||
file_path=image_path)
|
||||
# 查询集详情
|
||||
episodeinfo = __get_episode_detail(seasoninfo, meta.begin_episode)
|
||||
if episodeinfo:
|
||||
# 集NFO
|
||||
if not file_path.with_suffix(".nfo").exists():
|
||||
if self._force_nfo or not file_path.with_suffix(".nfo").exists():
|
||||
self.__gen_tv_episode_nfo_file(episodeinfo=episodeinfo,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
season=meta.begin_season,
|
||||
@@ -120,10 +135,11 @@ class TmdbScraper:
|
||||
file_path=file_path)
|
||||
# 集的图片
|
||||
episode_image = episodeinfo.get("still_path")
|
||||
if episode_image:
|
||||
image_path = file_path.with_name(file_path.stem + "-thumb").with_suffix(Path(episode_image).suffix)
|
||||
if episode_image and (self._force_img or not image_path.exists()):
|
||||
self.__save_image(
|
||||
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episode_image}",
|
||||
file_path.with_suffix(Path(episode_image).suffix))
|
||||
image_path)
|
||||
except Exception as e:
|
||||
logger.error(f"{file_path} 刮削失败:{str(e)}")
|
||||
|
||||
@@ -339,8 +355,6 @@ class TmdbScraper:
|
||||
"""
|
||||
下载图片并保存
|
||||
"""
|
||||
if file_path.exists():
|
||||
return
|
||||
try:
|
||||
logger.info(f"正在下载{file_path.stem}图片:{url} ...")
|
||||
r = RequestUtils().get_res(url=url, raise_exception=True)
|
||||
@@ -361,8 +375,6 @@ class TmdbScraper:
|
||||
"""
|
||||
保存NFO
|
||||
"""
|
||||
if file_path.exists():
|
||||
return
|
||||
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
|
||||
if self._transfer_type in ['rclone_move', 'rclone_copy']:
|
||||
self.__save_remove_file(file_path, xml_str)
|
||||
|
||||
@@ -57,6 +57,8 @@ class Subscribe(BaseModel):
|
||||
best_version: Optional[int] = 0
|
||||
# 当前优先级
|
||||
current_priority: Optional[int] = None
|
||||
# 保存路径
|
||||
save_path: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
30
database/versions/d71e624f0208_1_0_12.py
Normal file
30
database/versions/d71e624f0208_1_0_12.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""1_0_12
|
||||
|
||||
Revision ID: d71e624f0208
|
||||
Revises: 06abf3e7090b
|
||||
Create Date: 2023-12-12 13:26:34.039497
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd71e624f0208'
|
||||
down_revision = '06abf3e7090b'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("subscribe") as batch_op:
|
||||
batch_op.add_column(sa.Column('save_path', sa.String, nullable=True))
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.4.9'
|
||||
APP_VERSION = 'v1.5.5-1'
|
||||
|
||||
Reference in New Issue
Block a user