mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-10 06:22:48 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb12a052ac | ||
|
|
995c359f20 | ||
|
|
690066ad32 | ||
|
|
73942e315a | ||
|
|
48badb3243 | ||
|
|
d5eb12cc4e | ||
|
|
7d7539df4c | ||
|
|
14a8f44f8c | ||
|
|
a7be470f33 | ||
|
|
a677169f60 | ||
|
|
b72ef4f2aa | ||
|
|
403054751b | ||
|
|
b3e5c734d4 | ||
|
|
5732125ff6 | ||
|
|
eb66cf7aad | ||
|
|
a317c35eab | ||
|
|
ab138560c1 | ||
|
|
f0fbad889d | ||
|
|
1323cd5dc6 | ||
|
|
2c43d8e145 | ||
|
|
0214beb679 | ||
|
|
7d73cdef33 | ||
|
|
fcfab2c750 | ||
|
|
e048be17a5 | ||
|
|
024f1de4f1 | ||
|
|
d2c9f7a778 |
@@ -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; \
|
||||
|
||||
18
README.md
18
README.md
@@ -7,7 +7,7 @@
|
||||
发布频道:https://t.me/moviepilot_channel
|
||||
|
||||
## 主要特性
|
||||
- 前后端分离,基于FastApi + Vue3,前端项目地址:[MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)
|
||||
- 前后端分离,基于FastApi + Vue3,前端项目地址:[MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend),API:http://localhost:3001/docs
|
||||
- 聚焦核心需求,简化功能和设置,部分设置项可直接使用默认值。
|
||||
- 重新设计了用户界面,更加美观易用。
|
||||
|
||||
@@ -213,10 +213,11 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
|
||||
`MOVIE_RENAME_FORMAT`支持的配置项:
|
||||
|
||||
> `title`: 标题
|
||||
> `original_name`: 原文件名
|
||||
> `original_title`: 原语种标题
|
||||
> `name`: 识别名称
|
||||
> `title`: TMDB/豆瓣中的标题
|
||||
> `original_title`: TMDB/豆瓣中的原语种标题
|
||||
> `name`: 从文件名中识别的名称(同时存在中英文时,优先使用中文)
|
||||
> `en_name`:从文件名中识别的英文名称(可能为空)
|
||||
> `original_name`: 原文件名(包括文件外缀)
|
||||
> `year`: 年份
|
||||
> `resourceType`:资源类型
|
||||
> `effect`:特效
|
||||
@@ -226,12 +227,11 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
> `customization`: 自定义占位符
|
||||
> `videoCodec`: 视频编码
|
||||
> `audioCodec`: 音频编码
|
||||
> `tmdbid`: TMDBID
|
||||
> `imdbid`: IMDBID
|
||||
> `tmdbid`: TMDB ID(非TMDB识别源时为空)
|
||||
> `imdbid`: IMDB ID(可能为空)
|
||||
> `doubanid`:豆瓣ID(非豆瓣识别源时为空)
|
||||
> `part`:段/节
|
||||
> `fileExt`:文件扩展名
|
||||
> `tmdbid`:TMDB ID
|
||||
> `imdbid`:IMDB ID
|
||||
> `customization`:自定义占位符
|
||||
|
||||
`MOVIE_RENAME_FORMAT`默认配置格式:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,7 +52,7 @@ class MediaServerChain(ChainBase):
|
||||
# 汇总统计
|
||||
total_count = 0
|
||||
# 清空登记薄
|
||||
self.dboper.empty(server=settings.MEDIASERVER)
|
||||
self.dboper.empty()
|
||||
# 同步黑名单
|
||||
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
|
||||
",") if settings.MEDIASERVER_SYNC_BLACKLIST else []
|
||||
|
||||
@@ -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
|
||||
|
||||
# 非洗版状态
|
||||
@@ -240,7 +241,7 @@ class SubscribeChain(ChainBase):
|
||||
|
||||
# 电视剧订阅处理缺失集
|
||||
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,67 @@ 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:
|
||||
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} 已洗版完成',
|
||||
image=mediainfo.get_message_image()))
|
||||
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):
|
||||
"""
|
||||
判断是否应完成订阅
|
||||
"""
|
||||
if not subscribe.best_version:
|
||||
# 全部下载完成
|
||||
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} 已完成订阅',
|
||||
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} 洗版完成,删除订阅')
|
||||
# 非洗板
|
||||
if (not lefts and meta.type == MediaType.TV) or (downloads and meta.type == MediaType.MOVIE):
|
||||
# 全部下载完成
|
||||
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 +515,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:
|
||||
@@ -537,7 +559,7 @@ class SubscribeChain(ChainBase):
|
||||
|
||||
# 电视剧订阅
|
||||
if meta.type == MediaType.TV:
|
||||
# 使用订阅的总集数和开始集数替换no_exists
|
||||
# 整合实际缺失集与订阅开始集结束集
|
||||
no_exists = self.__get_subscribe_no_exits(
|
||||
no_exists=no_exists,
|
||||
mediakey=mediakey,
|
||||
@@ -546,7 +568,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 +655,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 +704,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 +777,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 +808,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 +869,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
|
||||
|
||||
@@ -120,7 +120,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":
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from typing import Any, Self, List
|
||||
from typing import Tuple, Optional, Generator
|
||||
|
||||
from sqlalchemy import create_engine, QueuePool
|
||||
from sqlalchemy.orm import sessionmaker, Session, scoped_session
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.orm import declared_attr
|
||||
from sqlalchemy.orm import sessionmaker, Session, scoped_session, as_declarative
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -135,6 +138,52 @@ def db_query(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
@as_declarative()
|
||||
class Base:
|
||||
id: Any
|
||||
__name__: str
|
||||
|
||||
@db_update
|
||||
def create(self, db: Session):
|
||||
db.add(self)
|
||||
|
||||
@classmethod
|
||||
@db_query
|
||||
def get(cls, db: Session, rid: int) -> Self:
|
||||
return db.query(cls).filter(cls.id == rid).first()
|
||||
|
||||
@db_update
|
||||
def update(self, db: Session, payload: dict):
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
for key, value in payload.items():
|
||||
setattr(self, key, value)
|
||||
if inspect(self).detached:
|
||||
db.add(self)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete(cls, db: Session, rid):
|
||||
db.query(cls).filter(cls.id == rid).delete()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def truncate(cls, db: Session):
|
||||
db.query(cls).delete()
|
||||
|
||||
@classmethod
|
||||
@db_query
|
||||
def list(cls, db: Session) -> List[Self]:
|
||||
result = db.query(cls).all()
|
||||
return list(result)
|
||||
|
||||
def to_dict(self):
|
||||
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns}
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(self) -> str:
|
||||
return self.__name__.lower()
|
||||
|
||||
|
||||
class DbOper:
|
||||
"""
|
||||
数据库操作基类
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
from alembic.command import upgrade
|
||||
from alembic.config import Config
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import get_password_hash
|
||||
from app.db import Engine, SessionFactory
|
||||
from app.db.models import Base
|
||||
from app.db.models.user import User
|
||||
from app.db import Engine, SessionFactory, Base
|
||||
from app.db.models import *
|
||||
from app.log import logger
|
||||
|
||||
|
||||
@@ -16,21 +12,18 @@ def init_db():
|
||||
"""
|
||||
初始化数据库
|
||||
"""
|
||||
# 导入模块,避免建表缺失
|
||||
for module in Path(__file__).with_name("models").glob("*.py"):
|
||||
importlib.import_module(f"app.db.models.{module.stem}")
|
||||
# 全量建表
|
||||
Base.metadata.create_all(bind=Engine)
|
||||
# 初始化超级管理员
|
||||
with SessionFactory() as db:
|
||||
user = User.get_by_name(db=db, name=settings.SUPERUSER)
|
||||
if not user:
|
||||
user = User(
|
||||
_user = User.get_by_name(db=db, name=settings.SUPERUSER)
|
||||
if not _user:
|
||||
_user = User(
|
||||
name=settings.SUPERUSER,
|
||||
hashed_password=get_password_hash(settings.SUPERUSER_PASSWORD),
|
||||
is_superuser=True,
|
||||
)
|
||||
user.create(db)
|
||||
_user.create(db)
|
||||
|
||||
|
||||
def update_db():
|
||||
|
||||
@@ -25,7 +25,7 @@ class MediaServerOper(DbOper):
|
||||
return True
|
||||
return False
|
||||
|
||||
def empty(self, server: str):
|
||||
def empty(self, server: Optional[str] = None):
|
||||
"""
|
||||
清空媒体服务器数据
|
||||
"""
|
||||
|
||||
@@ -1,52 +1,9 @@
|
||||
from typing import Any, Self, List
|
||||
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.orm import as_declarative, declared_attr, Session
|
||||
|
||||
from app.db import db_update, db_query
|
||||
|
||||
|
||||
@as_declarative()
|
||||
class Base:
|
||||
id: Any
|
||||
__name__: str
|
||||
|
||||
@db_update
|
||||
def create(self, db: Session):
|
||||
db.add(self)
|
||||
|
||||
@classmethod
|
||||
@db_query
|
||||
def get(cls, db: Session, rid: int) -> Self:
|
||||
return db.query(cls).filter(cls.id == rid).first()
|
||||
|
||||
@db_update
|
||||
def update(self, db: Session, payload: dict):
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
for key, value in payload.items():
|
||||
setattr(self, key, value)
|
||||
if inspect(self).detached:
|
||||
db.add(self)
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def delete(cls, db: Session, rid):
|
||||
db.query(cls).filter(cls.id == rid).delete()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def truncate(cls, db: Session):
|
||||
db.query(cls).delete()
|
||||
|
||||
@classmethod
|
||||
@db_query
|
||||
def list(cls, db: Session) -> List[Self]:
|
||||
result = db.query(cls).all()
|
||||
return list(result)
|
||||
|
||||
def to_dict(self):
|
||||
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns}
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(self) -> str:
|
||||
return self.__name__.lower()
|
||||
from .downloadhistory import DownloadHistory, DownloadFiles
|
||||
from .mediaserver import MediaServerItem
|
||||
from .plugindata import PluginData
|
||||
from .site import Site
|
||||
from .siteicon import SiteIcon
|
||||
from .subscribe import Subscribe
|
||||
from .systemconfig import SystemConfig
|
||||
from .transferhistory import TransferHistory
|
||||
from .user import User
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query
|
||||
from app.db.models import Base, db_update
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class DownloadHistory(Base):
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query
|
||||
from app.db.models import Base, db_update
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class MediaServerItem(Base):
|
||||
"""
|
||||
站点表
|
||||
媒体服务器媒体条目表
|
||||
"""
|
||||
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
|
||||
# 服务器类型
|
||||
@@ -48,8 +48,11 @@ class MediaServerItem(Base):
|
||||
|
||||
@staticmethod
|
||||
@db_update
|
||||
def empty(db: Session, server: str):
|
||||
db.query(MediaServerItem).filter(MediaServerItem.server == server).delete()
|
||||
def empty(db: Session, server: Optional[str] = None):
|
||||
if server is None:
|
||||
db.query(MediaServerItem).delete()
|
||||
else:
|
||||
db.query(MediaServerItem).filter(MediaServerItem.server == server).delete()
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query
|
||||
from app.db.models import Base, db_update
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class PluginData(Base):
|
||||
@@ -3,8 +3,7 @@ from datetime import datetime
|
||||
from sqlalchemy import Boolean, Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query
|
||||
from app.db.models import Base, db_update
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class Site(Base):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query
|
||||
from app.db.models import Base
|
||||
from app.db import db_query, Base
|
||||
|
||||
|
||||
class SiteIcon(Base):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_update, db_query
|
||||
from app.db.models import Base
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class Subscribe(Base):
|
||||
@@ -66,6 +65,8 @@ class Subscribe(Base):
|
||||
best_version = Column(Integer, default=0)
|
||||
# 当前优先级
|
||||
current_priority = Column(Integer)
|
||||
# 保存路径
|
||||
save_path = Column(String)
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_update, db_query
|
||||
from app.db.models import Base
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
|
||||
@@ -3,8 +3,7 @@ import time
|
||||
from sqlalchemy import Column, Integer, String, Sequence, Boolean, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import db_query
|
||||
from app.db.models import Base, db_update
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class TransferHistory(Base):
|
||||
|
||||
@@ -2,8 +2,7 @@ from sqlalchemy import Boolean, Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.security import verify_password
|
||||
from app.db import db_update, db_query
|
||||
from app.db.models import Base
|
||||
from app.db import db_query, db_update, Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.plugin import PluginData
|
||||
from app.db.models.plugindata import PluginData
|
||||
from app.utils.object import ObjectUtils
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import importlib
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ModuleHelper:
|
||||
@@ -35,3 +36,17 @@ class ModuleHelper:
|
||||
print(f'加载模块 {package_name} 失败:{err}')
|
||||
|
||||
return submodules
|
||||
|
||||
@staticmethod
|
||||
def dynamic_import_all_modules(base_path: Path, package_name: str):
|
||||
"""
|
||||
动态导入目录下所有模块
|
||||
"""
|
||||
modules = []
|
||||
# 遍历文件夹,找到所有模块文件
|
||||
for file in base_path.glob("*.py"):
|
||||
file_name = file.stem
|
||||
if file_name != "__init__":
|
||||
modules.append(file_name)
|
||||
full_module_name = f"{package_name}.{file_name}"
|
||||
importlib.import_module(full_module_name)
|
||||
|
||||
@@ -107,7 +107,7 @@ class PluginHelper(metaclass=Singleton):
|
||||
l, m = __get_filelist(p)
|
||||
if not l:
|
||||
return False, m
|
||||
return __download_files(p, l)
|
||||
__download_files(p, l)
|
||||
return True, ""
|
||||
|
||||
if not pid or not repo_url:
|
||||
|
||||
@@ -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 \
|
||||
@@ -547,12 +563,14 @@ class FileTransferModule(_ModuleBase):
|
||||
return {
|
||||
# 标题
|
||||
"title": mediainfo.title,
|
||||
# 原文件名
|
||||
"original_name": f"{meta.org_string}{file_ext}",
|
||||
# 原语种标题
|
||||
"original_title": mediainfo.original_title,
|
||||
# 识别名称
|
||||
# 原文件名
|
||||
"original_name": f"{meta.org_string}{file_ext}",
|
||||
# 识别名称(优先使用中文)
|
||||
"name": meta.name,
|
||||
# 识别的英文名称(可能为空)
|
||||
"en_name": meta.en_name,
|
||||
# 年份
|
||||
"year": mediainfo.year or meta.year,
|
||||
# 资源类型
|
||||
@@ -573,6 +591,8 @@ class FileTransferModule(_ModuleBase):
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
# IMDBID
|
||||
"imdbid": mediainfo.imdb_id,
|
||||
# 豆瓣ID
|
||||
"doubanid": mediainfo.douban_id,
|
||||
# 季号
|
||||
"season": meta.season_seq,
|
||||
# 集号
|
||||
@@ -653,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:
|
||||
|
||||
@@ -120,10 +120,11 @@ class TmdbScraper:
|
||||
file_path=file_path)
|
||||
# 集的图片
|
||||
episode_image = episodeinfo.get("still_path")
|
||||
image_path = file_path.with_name(file_path.stem + "-thumb").with_suffix(Path(episode_image).suffix)
|
||||
if episode_image:
|
||||
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)}")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from app.db.models import Base
|
||||
from app.db import Base
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
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.8'
|
||||
APP_VERSION = 'v1.5.2'
|
||||
|
||||
Reference in New Issue
Block a user