mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-15 23:16:45 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cd768b3d0 | ||
|
|
abc26b65ed | ||
|
|
dc1a41da90 | ||
|
|
a95dac1b32 | ||
|
|
18d9620687 | ||
|
|
8808dcee52 | ||
|
|
17adc4deab | ||
|
|
9351489166 | ||
|
|
e2148cb77f | ||
|
|
e322204094 | ||
|
|
0fa884157a | ||
|
|
96468213fe | ||
|
|
d044a9db00 | ||
|
|
d5f5e0d526 | ||
|
|
14a3bb8fc2 | ||
|
|
5921d43ae8 | ||
|
|
635061c054 | ||
|
|
3c8c6e5375 | ||
|
|
dd063bb16b | ||
|
|
750711611b | ||
|
|
d3983c51c2 | ||
|
|
b9dec73773 | ||
|
|
b310367d25 | ||
|
|
55beea87fd | ||
|
|
4510382f74 | ||
|
|
9b9ae9401e | ||
|
|
e10464c278 | ||
|
|
542531a1ca | ||
|
|
04c21232e3 | ||
|
|
48a19fd57c | ||
|
|
59cb69a96b | ||
|
|
e7d94f7f70 | ||
|
|
27d2d01a20 | ||
|
|
8b4495c857 | ||
|
|
15bdb694cc | ||
|
|
3ef9c5ea2c | ||
|
|
ab6577f752 | ||
|
|
49a82d7a48 | ||
|
|
bdcbb168a0 | ||
|
|
2e1cb0bd76 | ||
|
|
851864cd49 | ||
|
|
b5d7b6fb53 | ||
|
|
92bab2fc2f | ||
|
|
0dad6860c4 | ||
|
|
de4a7becc2 | ||
|
|
2eeb24e22d | ||
|
|
e4a67ea052 | ||
|
|
a4df2f5213 | ||
|
|
4f89780a0f | ||
|
|
26d6201b30 | ||
|
|
c9a9ff2692 | ||
|
|
0be49953b4 | ||
|
|
0de952f090 | ||
|
|
2b570bf48f | ||
|
|
9476017af5 | ||
|
|
54f808485e | ||
|
|
fa5c82899b | ||
|
|
4a57071809 | ||
|
|
4631db9a45 | ||
|
|
0f09da55b0 | ||
|
|
b14b41c2c1 | ||
|
|
897758d829 | ||
|
|
c450dfc0fa |
34
README.md
34
README.md
@@ -106,6 +106,7 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **❗SUPERUSER:** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面,**注意:启动一次后再次修改该值不会生效,除非删除数据库文件!**
|
||||
- **❗API_TOKEN:** API密钥,默认`moviepilot`,在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
|
||||
- **BIG_MEMORY_MODE:** 大内存模式,默认为`false`,开启后会增加缓存数量,占用更多的内存,但响应速度会更快
|
||||
- **META_CACHE_EXPIRE:** 元数据识别缓存过期时间(小时),数字型,不配置或者配置为0时使用系统默认(大内存模式为7天,否则为3天),调大该值可减少themoviedb的访问次数
|
||||
- **GITHUB_TOKEN:** Github token,提高自动更新、插件安装等请求Github Api的限流阈值,格式:ghp_****
|
||||
- **DEV:** 开发者模式,`true`/`false`,默认`false`,开启后会暂停所有定时任务
|
||||
- **AUTO_UPDATE_RESOURCE**:启动时自动检测和更新资源包(站点索引及认证等),`true`/`false`,默认`true`,需要能正常连接Github,仅支持Docker镜像
|
||||
@@ -185,13 +186,25 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
|
||||
## 使用
|
||||
|
||||
- 通过设置的超级管理员用户登录管理界面(默认用户:admin,默认端口:3000),**注意:初始密码为自动生成,需要在首次运行时的后台日志中查看!**
|
||||
- 通过CookieCloud同步快速添加站点,不需要使用的站点可在WEB管理界面中禁用或删除,无法同步的站点可手动新增。
|
||||
- 通过打开下载器监控实现下载完成后自动整理入库并刮削媒体信息。
|
||||
- 通过`微信`/`Telegram`/`Slack`/`SynologyChat`/`VoceChat`远程管理,其中 微信/Telegram 将会自动添加操作菜单(微信菜单条数有限制,部分菜单不显示);微信需要在官方页面设置回调地址,SynologyChat/VoceChat 需要设置机器人传入地址/Webhook,地址相对路径均为:`/api/v1/message/`。
|
||||
- 设置媒体服务器Webhook,通过MoviePilot发送播放通知等。Webhook回调相对路径为`/api/v1/webhook?token=moviepilot`,其中`moviepilot`为设置的`API_TOKEN`。
|
||||
- 将MoviePilot做为Radarr或Sonarr服务器添加到Overseerr或Jellyseerr,可使用Overseerr/Jellyseerr浏览订阅。
|
||||
- 映射宿主机docker.sock文件到容器`/var/run/docker.sock`,以支持内建重启操作。实例:`-v /var/run/docker.sock:/var/run/docker.sock:ro`。
|
||||
### 1. **WEB后台管理**
|
||||
- 通过设置的超级管理员用户登录后台管理界面(`SUPERUSER`配置项,默认用户:admin,默认端口:3000)
|
||||
> ❗**注意:超级管理员用户初始密码为自动生成,需要在首次运行时的后台日志中查看!** 如首次运行日志丢失,则需要删除配置文件目录下的`user.db`文件,然后重启服务。
|
||||
### 2. **站点维护**
|
||||
- 通过CookieCloud同步快速添加站点,不需要使用的站点可在WEB管理界面中禁用或删除,无法同步的站点也可手动新增。
|
||||
- 需要通过环境变量设置用户认证信息且认证成功后才能使用站点相关功能,未认证通过时站点相关的插件也会无法显示。
|
||||
### 3. **文件整理**
|
||||
- 默认通过监控下载器实现下载完成后自动整理入库并刮削媒体信息,需要后台打开`下载器监控`开关,且仅会处理通过MoviePilot添加下载的任务。
|
||||
- 使用`目录监控`等插件实现更灵活的自动整理。
|
||||
### 4. **通知交互**
|
||||
- 支持通过`微信`/`Telegram`/`Slack`/`SynologyChat`/`VoceChat`等渠道远程管理和订阅下载,其中 微信/Telegram 将会自动添加操作菜单(微信菜单条数有限制,部分菜单不显示)。
|
||||
- `微信`回调地址、`SynologyChat`传入地址地址相对路径均为:`/api/v1/message/`;`VoceChat`的Webhook地址相对路径为:`/api/v1/message/?token=moviepilot`,其中moviepilot为设置的`API_TOKEN`。
|
||||
### 5. **订阅与搜索**
|
||||
- 通过MoviePilot管理后台搜索和订阅。
|
||||
- 将MoviePilot做为`Radarr`或`Sonarr`服务器添加到`Overseerr`或`Jellyseerr`,可使用`Overseerr/Jellyseerr`浏览和添加订阅。
|
||||
- 安装`豆瓣榜单订阅`、`猫眼订阅`等插件,实现自动订阅豆瓣榜单、猫眼榜单等。
|
||||
### 6. **其他**
|
||||
- 通过设置媒体服务器Webhook指向MoviePilot(相对路径为`/api/v1/webhook?token=moviepilot`,其中`moviepilot`为设置的`API_TOKEN`),可实现通过MoviePilot发送播放通知,以及配合各类插件实现播放限速等功能。
|
||||
- 映射宿主机`docker.sock`文件到容器`/var/run/docker.sock`,可支持应用内建重启操作。实例:`-v /var/run/docker.sock:/var/run/docker.sock:ro`。
|
||||
- 将WEB页面添加到手机桌面图标可获得与App一样的使用体验。
|
||||
|
||||
### **注意**
|
||||
@@ -219,6 +232,13 @@ location /cgi-bin/menu/create {
|
||||
}
|
||||
```
|
||||
|
||||
- 部分插件功能基于文件系统监控实现(如`目录监控`等),需在宿主机上(不是docker容器内)执行以下命令并重启:
|
||||
```shell
|
||||
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
|
||||
echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -75,18 +75,16 @@ def downloader(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询下载器信息
|
||||
"""
|
||||
transfer_info = DashboardChain().downloader_info()
|
||||
free_space = SystemUtils.free_space(settings.SAVE_PATH)
|
||||
if transfer_info:
|
||||
return schemas.DownloaderInfo(
|
||||
download_speed=transfer_info.download_speed,
|
||||
upload_speed=transfer_info.upload_speed,
|
||||
download_size=transfer_info.download_size,
|
||||
upload_size=transfer_info.upload_size,
|
||||
free_space=free_space
|
||||
)
|
||||
else:
|
||||
return schemas.DownloaderInfo()
|
||||
downloader_info = schemas.DownloaderInfo()
|
||||
transfer_infos = DashboardChain().downloader_info()
|
||||
if transfer_infos:
|
||||
for transfer_info in transfer_infos:
|
||||
downloader_info.download_speed += transfer_info.download_speed
|
||||
downloader_info.upload_speed += transfer_info.upload_speed
|
||||
downloader_info.download_size += transfer_info.download_size
|
||||
downloader_info.upload_size += transfer_info.upload_size
|
||||
downloader_info.free_space = SystemUtils.free_space(settings.SAVE_PATH)
|
||||
return downloader_info
|
||||
|
||||
|
||||
@router.get("/downloader2", summary="下载器信息(API_TOKEN)", response_model=schemas.DownloaderInfo)
|
||||
|
||||
@@ -45,7 +45,7 @@ def add_downloading(
|
||||
media_info=mediainfo,
|
||||
torrent_info=torrentinfo
|
||||
)
|
||||
did = DownloadChain().download_single(context=context, username=current_user.name)
|
||||
did = DownloadChain().download_single(context=context, userid=current_user.name, username=current_user.name)
|
||||
return schemas.Response(success=True if did else False, data={
|
||||
"download_id": did
|
||||
})
|
||||
|
||||
@@ -36,21 +36,11 @@ async def user_message(background_tasks: BackgroundTasks, request: Request):
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/", summary="VoceChat验证")
|
||||
def vocechat_verify() -> Any:
|
||||
"""
|
||||
VoceChat验证响应
|
||||
"""
|
||||
return {"status": "OK"}
|
||||
|
||||
|
||||
@router.get("/", summary="微信验证")
|
||||
def wechat_verify(echostr: str, msg_signature: str,
|
||||
timestamp: Union[str, int], nonce: str) -> Any:
|
||||
"""
|
||||
微信验证响应
|
||||
"""
|
||||
logger.info(f"收到微信验证请求: {echostr}")
|
||||
try:
|
||||
wxcpt = WXBizMsgCrypt(sToken=settings.WECHAT_TOKEN,
|
||||
sEncodingAESKey=settings.WECHAT_ENCODING_AESKEY,
|
||||
@@ -68,6 +58,28 @@ def wechat_verify(echostr: str, msg_signature: str,
|
||||
return PlainTextResponse(sEchoStr)
|
||||
|
||||
|
||||
def vocechat_verify(token: str) -> Any:
|
||||
"""
|
||||
VoceChat验证响应
|
||||
"""
|
||||
if token == settings.API_TOKEN:
|
||||
return {"status": "OK"}
|
||||
return {"status": "ERROR"}
|
||||
|
||||
|
||||
@router.get("/", summary="回调请求验证")
|
||||
def incoming_verify(token: str = None, echostr: str = None, msg_signature: str = None,
|
||||
timestamp: Union[str, int] = None, nonce: str = None) -> Any:
|
||||
"""
|
||||
微信/VoceChat等验证响应
|
||||
"""
|
||||
logger.info(f"收到验证请求: token={token}, echostr={echostr}, "
|
||||
f"msg_signature={msg_signature}, timestamp={timestamp}, nonce={nonce}")
|
||||
if echostr and msg_signature and timestamp and nonce:
|
||||
return wechat_verify(echostr, msg_signature, timestamp, nonce)
|
||||
return vocechat_verify(token)
|
||||
|
||||
|
||||
@router.get("/switchs", summary="查询通知消息渠道开关", response_model=List[NotificationSwitch])
|
||||
def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
|
||||
@@ -306,8 +306,8 @@ def reload_module(_: schemas.TokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
重新加载模块
|
||||
"""
|
||||
ModuleManager().stop()
|
||||
ModuleManager().load_modules()
|
||||
ModuleManager().reload()
|
||||
Scheduler().init()
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
|
||||
@@ -282,7 +282,8 @@ class ChainBase(metaclass=ABCMeta):
|
||||
mediainfo=mediainfo)
|
||||
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None
|
||||
episodes: Set[int] = None, category: str = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER
|
||||
) -> Optional[Tuple[Optional[str], str]]:
|
||||
"""
|
||||
根据种子文件,选择并添加下载任务
|
||||
@@ -291,10 +292,12 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:param cookie: cookie
|
||||
:param episodes: 需要下载的集数
|
||||
:param category: 种子分类
|
||||
:param downloader: 下载器
|
||||
:return: 种子Hash,错误信息
|
||||
"""
|
||||
return self.run_module("download", content=content, download_dir=download_dir,
|
||||
cookie=cookie, episodes=episodes, category=category)
|
||||
cookie=cookie, episodes=episodes, category=category,
|
||||
downloader=downloader)
|
||||
|
||||
def download_added(self, context: Context, download_dir: Path, torrent_path: Path = None) -> None:
|
||||
"""
|
||||
@@ -308,14 +311,17 @@ class ChainBase(metaclass=ABCMeta):
|
||||
download_dir=download_dir)
|
||||
|
||||
def list_torrents(self, status: TorrentStatus = None,
|
||||
hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
hashs: Union[list, str] = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER
|
||||
) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
"""
|
||||
获取下载器种子列表
|
||||
:param status: 种子状态
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: 下载器中符合状态的种子列表
|
||||
"""
|
||||
return self.run_module("list_torrents", status=status, hashs=hashs)
|
||||
return self.run_module("list_torrents", status=status, hashs=hashs, downloader=downloader)
|
||||
|
||||
def transfer(self, path: Path, meta: MetaBase, mediainfo: MediaInfo,
|
||||
transfer_type: str, target: Path = None,
|
||||
@@ -331,49 +337,56 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:return: {path, target_path, message}
|
||||
"""
|
||||
return self.run_module("transfer", path=path, meta=meta, mediainfo=mediainfo,
|
||||
transfer_type=transfer_type, target=target,
|
||||
episodes_info=episodes_info)
|
||||
transfer_type=transfer_type, target=target, episodes_info=episodes_info)
|
||||
|
||||
def transfer_completed(self, hashs: Union[str, list], path: Path = None) -> None:
|
||||
def transfer_completed(self, hashs: Union[str, list], path: Path = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> None:
|
||||
"""
|
||||
转移完成后的处理
|
||||
:param hashs: 种子Hash
|
||||
:param path: 源目录
|
||||
:param downloader: 下载器
|
||||
"""
|
||||
return self.run_module("transfer_completed", hashs=hashs, path=path)
|
||||
return self.run_module("transfer_completed", hashs=hashs, path=path, downloader=downloader)
|
||||
|
||||
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True) -> bool:
|
||||
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> bool:
|
||||
"""
|
||||
删除下载器种子
|
||||
:param hashs: 种子Hash
|
||||
:param delete_file: 是否删除文件
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
return self.run_module("remove_torrents", hashs=hashs, delete_file=delete_file)
|
||||
return self.run_module("remove_torrents", hashs=hashs, delete_file=delete_file, downloader=downloader)
|
||||
|
||||
def start_torrents(self, hashs: Union[list, str]) -> bool:
|
||||
def start_torrents(self, hashs: Union[list, str], downloader: str = settings.DEFAULT_DOWNLOADER) -> bool:
|
||||
"""
|
||||
开始下载
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
return self.run_module("start_torrents", hashs=hashs)
|
||||
return self.run_module("start_torrents", hashs=hashs, downloader=downloader)
|
||||
|
||||
def stop_torrents(self, hashs: Union[list, str]) -> bool:
|
||||
def stop_torrents(self, hashs: Union[list, str], downloader: str = settings.DEFAULT_DOWNLOADER) -> bool:
|
||||
"""
|
||||
停止下载
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
return self.run_module("stop_torrents", hashs=hashs)
|
||||
return self.run_module("stop_torrents", hashs=hashs, downloader=downloader)
|
||||
|
||||
def torrent_files(self, tid: str) -> Optional[Union[TorrentFilesList, List[File]]]:
|
||||
def torrent_files(self, tid: str,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[Union[TorrentFilesList, List[File]]]:
|
||||
"""
|
||||
获取种子文件
|
||||
:param tid: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: 种子文件
|
||||
"""
|
||||
return self.run_module("torrent_files", tid=tid)
|
||||
return self.run_module("torrent_files", tid=tid, downloader=downloader)
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,7 @@ class DashboardChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
return self.run_module("media_statistic")
|
||||
|
||||
def downloader_info(self) -> schemas.DownloaderInfo:
|
||||
def downloader_info(self) -> Optional[List[schemas.DownloaderInfo]]:
|
||||
"""
|
||||
下载器信息
|
||||
"""
|
||||
|
||||
@@ -212,7 +212,7 @@ class DownloadChain(ChainBase):
|
||||
if _media.genre_ids \
|
||||
and set(_media.genre_ids).intersection(set(settings.ANIME_GENREIDS)):
|
||||
# 动漫
|
||||
download_dir = settings.SAVE_ANIME_PATH
|
||||
download_dir = settings.SAVE_ANIME_PATH / _media.category
|
||||
else:
|
||||
# 电视剧
|
||||
download_dir = settings.SAVE_TV_PATH / _media.category
|
||||
@@ -292,7 +292,7 @@ class DownloadChain(ChainBase):
|
||||
continue
|
||||
files_to_add.append({
|
||||
"download_hash": _hash,
|
||||
"downloader": settings.DOWNLOADER,
|
||||
"downloader": settings.DEFAULT_DOWNLOADER,
|
||||
"fullpath": str(download_dir / _folder_name / file),
|
||||
"savepath": str(download_dir / _folder_name),
|
||||
"filepath": file,
|
||||
@@ -301,8 +301,8 @@ class DownloadChain(ChainBase):
|
||||
if files_to_add:
|
||||
self.downloadhis.add_files(files_to_add)
|
||||
|
||||
# 发送消息
|
||||
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent, channel=channel, userid=userid)
|
||||
# 发送消息(群发,不带channel和userid)
|
||||
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent)
|
||||
# 下载成功后处理
|
||||
self.download_added(context=context, download_dir=download_dir, torrent_path=torrent_file)
|
||||
# 广播事件
|
||||
@@ -408,8 +408,8 @@ class DownloadChain(ChainBase):
|
||||
# 如果是电影,直接下载
|
||||
for context in contexts:
|
||||
if context.media_info.type == MediaType.MOVIE:
|
||||
if self.download_single(context, save_path=save_path,
|
||||
channel=channel, userid=userid, username=username):
|
||||
if self.download_single(context, save_path=save_path, channel=channel,
|
||||
userid=userid, username=username):
|
||||
# 下载成功
|
||||
downloaded_list.append(context)
|
||||
|
||||
@@ -482,8 +482,9 @@ class DownloadChain(ChainBase):
|
||||
)
|
||||
else:
|
||||
# 下载
|
||||
download_id = self.download_single(context, save_path=save_path,
|
||||
channel=channel, userid=userid, username=username)
|
||||
download_id = self.download_single(context,
|
||||
save_path=save_path, channel=channel,
|
||||
userid=userid, username=username)
|
||||
|
||||
if download_id:
|
||||
# 下载成功
|
||||
@@ -545,8 +546,9 @@ class DownloadChain(ChainBase):
|
||||
# 为需要集的子集则下载
|
||||
if torrent_episodes.issubset(set(need_episodes)):
|
||||
# 下载
|
||||
download_id = self.download_single(context, save_path=save_path,
|
||||
channel=channel, userid=userid, username=username)
|
||||
download_id = self.download_single(context,
|
||||
save_path=save_path, channel=channel,
|
||||
userid=userid, username=username)
|
||||
if download_id:
|
||||
# 下载成功
|
||||
downloaded_list.append(context)
|
||||
@@ -821,6 +823,7 @@ class DownloadChain(ChainBase):
|
||||
}
|
||||
# 下载用户
|
||||
torrent.userid = history.userid
|
||||
torrent.username = history.username
|
||||
ret_torrents.append(torrent)
|
||||
return ret_torrents
|
||||
|
||||
|
||||
@@ -275,7 +275,8 @@ class MessageChain(ChainBase):
|
||||
# 下载种子
|
||||
context: Context = cache_list[_choice]
|
||||
# 下载
|
||||
self.downloadchain.download_single(context, userid=userid, channel=channel, username=username)
|
||||
self.downloadchain.download_single(context, channel=channel,
|
||||
userid=userid, username=username)
|
||||
|
||||
elif text.lower() == "p":
|
||||
# 上一页
|
||||
|
||||
@@ -124,7 +124,12 @@ class SearchChain(ChainBase):
|
||||
if keyword:
|
||||
keywords = [keyword]
|
||||
else:
|
||||
keywords = list({mediainfo.title, mediainfo.original_title, mediainfo.en_title} - {None})
|
||||
# 去重去空,但要保持顺序
|
||||
keywords = list(dict.fromkeys([k for k in [mediainfo.title,
|
||||
mediainfo.original_title,
|
||||
mediainfo.en_title,
|
||||
mediainfo.sg_title] if k]))
|
||||
|
||||
# 执行搜索
|
||||
torrents: List[TorrentInfo] = self.__search_all_sites(
|
||||
mediainfo=mediainfo,
|
||||
|
||||
@@ -372,9 +372,9 @@ class SiteChain(ChainBase):
|
||||
else:
|
||||
render_str = ""
|
||||
if site.is_active:
|
||||
messages.append(f"{site.id}. [{site.name}]({site.url}){render_str}")
|
||||
messages.append(f"{site.id}. {site.name} {render_str}")
|
||||
else:
|
||||
messages.append(f"{site.id}. {site.name}")
|
||||
messages.append(f"{site.id}. {site.name} ⚠️")
|
||||
# 发送列表
|
||||
self.post_message(Notification(
|
||||
channel=channel,
|
||||
|
||||
@@ -143,9 +143,8 @@ class SubscribeChain(ChainBase):
|
||||
text = f"评分:{mediainfo.vote_average},来自用户:{username or userid}"
|
||||
else:
|
||||
text = f"评分:{mediainfo.vote_average}"
|
||||
# 广而告之
|
||||
self.post_message(Notification(channel=channel,
|
||||
mtype=NotificationType.Subscribe,
|
||||
# 群发
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f"{mediainfo.title_year} {metainfo.season} 已添加订阅",
|
||||
text=text,
|
||||
image=mediainfo.get_message_image()))
|
||||
@@ -323,6 +322,7 @@ class SubscribeChain(ChainBase):
|
||||
downloads, lefts = self.downloadchain.batch_download(
|
||||
contexts=matched_contexts,
|
||||
no_exists=no_exists,
|
||||
userid=subscribe.username,
|
||||
username=subscribe.username,
|
||||
save_path=subscribe.save_path
|
||||
)
|
||||
@@ -708,6 +708,7 @@ class SubscribeChain(ChainBase):
|
||||
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context,
|
||||
no_exists=no_exists,
|
||||
userid=subscribe.username,
|
||||
username=subscribe.username,
|
||||
save_path=subscribe.save_path)
|
||||
# 判断是否要完成订阅
|
||||
@@ -858,20 +859,12 @@ class SubscribeChain(ChainBase):
|
||||
messages = []
|
||||
for subscribe in subscribes:
|
||||
if subscribe.type == MediaType.MOVIE.value:
|
||||
if subscribe.tmdbid:
|
||||
link = f"https://www.themoviedb.org/movie/{subscribe.tmdbid}"
|
||||
else:
|
||||
link = f"https://movie.douban.com/subject/{subscribe.doubanid}"
|
||||
messages.append(f"{subscribe.id}. [{subscribe.name}({subscribe.year})]({link})")
|
||||
messages.append(f"{subscribe.id}. {subscribe.name}({subscribe.year})")
|
||||
else:
|
||||
if subscribe.tmdbid:
|
||||
link = f"https://www.themoviedb.org/tv/{subscribe.tmdbid}"
|
||||
else:
|
||||
link = f"https://movie.douban.com/subject/{subscribe.doubanid}"
|
||||
messages.append(f"{subscribe.id}. [{subscribe.name}({subscribe.year})]({link}) "
|
||||
messages.append(f"{subscribe.id}. {subscribe.name}({subscribe.year})"
|
||||
f"第{subscribe.season}季 "
|
||||
f"_{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}"
|
||||
f"/{subscribe.total_episode}_")
|
||||
f"[{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}"
|
||||
f"/{subscribe.total_episode}]")
|
||||
# 发送列表
|
||||
self.post_message(Notification(channel=channel,
|
||||
title=title, text='\n'.join(messages), userid=userid))
|
||||
|
||||
@@ -267,7 +267,10 @@ class Command(metaclass=Singleton):
|
||||
停止事件处理线程
|
||||
"""
|
||||
self._event.set()
|
||||
self._thread.join()
|
||||
try:
|
||||
self._thread.join()
|
||||
except Exception as e:
|
||||
logger.error(f"停止事件处理线程出错:{str(e)} - {traceback.format_exc()}")
|
||||
|
||||
def get_commands(self):
|
||||
"""
|
||||
|
||||
@@ -69,6 +69,8 @@ class Settings(BaseSettings):
|
||||
'.tp']
|
||||
# 支持的字幕文件后缀格式
|
||||
RMT_SUBEXT: list = ['.srt', '.ass', '.ssa', '.sup']
|
||||
# 下载器临时文件后缀
|
||||
DOWNLOAD_TMPEXT: list = ['.!qB', '.part']
|
||||
# 支持的音轨文件后缀格式
|
||||
RMT_AUDIO_TRACK_EXT: list = ['.mka']
|
||||
# 索引器
|
||||
@@ -123,7 +125,7 @@ class Settings(BaseSettings):
|
||||
VOCECHAT_API_KEY: str = ""
|
||||
# VoceChat 频道ID
|
||||
VOCECHAT_CHANNEL_ID: str = ""
|
||||
# 下载器 qbittorrent/transmission
|
||||
# 下载器 qbittorrent/transmission,启用多个下载器时使用,分隔,只有第一个会被默认使用
|
||||
DOWNLOADER: str = "qbittorrent"
|
||||
# 下载器监控开关
|
||||
DOWNLOADER_MONITOR: bool = True
|
||||
@@ -228,10 +230,13 @@ class Settings(BaseSettings):
|
||||
GITHUB_TOKEN: Optional[str] = None
|
||||
# 自动检查和更新站点资源包(站点索引、认证等)
|
||||
AUTO_UPDATE_RESOURCE: bool = True
|
||||
# 元数据识别缓存过期时间(小时)
|
||||
META_CACHE_EXPIRE: int = 0
|
||||
|
||||
@validator("SUBSCRIBE_RSS_INTERVAL",
|
||||
"COOKIECLOUD_INTERVAL",
|
||||
"MEDIASERVER_SYNC_INTERVAL",
|
||||
"META_CACHE_EXPIRE",
|
||||
pre=True, always=True)
|
||||
def convert_int(cls, value):
|
||||
if not value:
|
||||
@@ -280,7 +285,7 @@ class Settings(BaseSettings):
|
||||
"torrents": 100,
|
||||
"douban": 512,
|
||||
"fanart": 512,
|
||||
"meta": 15 * 24 * 3600
|
||||
"meta": (self.META_CACHE_EXPIRE or 168) * 3600
|
||||
}
|
||||
return {
|
||||
"tmdb": 256,
|
||||
@@ -288,7 +293,7 @@ class Settings(BaseSettings):
|
||||
"torrents": 50,
|
||||
"douban": 256,
|
||||
"fanart": 128,
|
||||
"meta": 7 * 24 * 3600
|
||||
"meta": (self.META_CACHE_EXPIRE or 72) * 3600
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -360,6 +365,24 @@ class Settings(BaseSettings):
|
||||
}
|
||||
return {}
|
||||
|
||||
@property
|
||||
def DEFAULT_DOWNLOADER(self):
|
||||
"""
|
||||
默认下载器
|
||||
"""
|
||||
if not self.DOWNLOADER:
|
||||
return None
|
||||
return self.DOWNLOADER.split(",")[0]
|
||||
|
||||
@property
|
||||
def DOWNLOADERS(self):
|
||||
"""
|
||||
下载器列表
|
||||
"""
|
||||
if not self.DOWNLOADER:
|
||||
return []
|
||||
return self.DOWNLOADER.split(",")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
with self.CONFIG_PATH as p:
|
||||
|
||||
@@ -139,6 +139,8 @@ class MediaInfo:
|
||||
title: str = None
|
||||
# 英文标题
|
||||
en_title: str = None
|
||||
# 新加坡标题
|
||||
sg_title: str = None
|
||||
# 年份
|
||||
year: str = None
|
||||
# 季
|
||||
@@ -164,7 +166,7 @@ class MediaInfo:
|
||||
# LOGO
|
||||
logo_path: str = None
|
||||
# 评分
|
||||
vote_average: int = 0
|
||||
vote_average: float = 0
|
||||
# 描述
|
||||
overview: str = None
|
||||
# 风格ID
|
||||
@@ -374,6 +376,8 @@ class MediaInfo:
|
||||
self.original_language = info.get('original_language')
|
||||
# 英文标题
|
||||
self.en_title = info.get('en_title')
|
||||
# 新加坡标题
|
||||
self.sg_title = info.get('sg_title')
|
||||
if self.type == MediaType.MOVIE:
|
||||
# 标题
|
||||
self.title = info.get('title')
|
||||
|
||||
@@ -131,6 +131,10 @@ class MetaBase(object):
|
||||
except Exception as err:
|
||||
logger.debug(f'识别季失败:{str(err)} - {traceback.format_exc()}')
|
||||
return
|
||||
if begin_season and begin_season > 100:
|
||||
return
|
||||
if end_season and end_season > 100:
|
||||
return
|
||||
if self.begin_season is None and isinstance(begin_season, int):
|
||||
self.begin_season = begin_season
|
||||
self.total_season = 1
|
||||
@@ -162,6 +166,10 @@ class MetaBase(object):
|
||||
except Exception as err:
|
||||
logger.debug(f'识别集失败:{str(err)} - {traceback.format_exc()}')
|
||||
return
|
||||
if begin_episode and begin_episode >= 10000:
|
||||
return
|
||||
if end_episode and end_episode >= 10000:
|
||||
return
|
||||
if self.begin_episode is None and isinstance(begin_episode, int):
|
||||
self.begin_episode = begin_episode
|
||||
self.total_episode = 1
|
||||
|
||||
@@ -51,6 +51,13 @@ class ModuleManager(metaclass=Singleton):
|
||||
if hasattr(module, "stop"):
|
||||
module.stop()
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
重新加载所有模块
|
||||
"""
|
||||
self.stop()
|
||||
self.load_modules()
|
||||
|
||||
def test(self, modleid: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
测试模块
|
||||
|
||||
@@ -131,3 +131,11 @@ class DownloadHistoryOper(DbOper):
|
||||
type=type,
|
||||
tmdbid=tmdbid,
|
||||
seasons=seasons)
|
||||
|
||||
def list_by_type(self, mtype: str, days: int = 7) -> List[DownloadHistory]:
|
||||
"""
|
||||
获取指定类型的下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_type(db=self._db,
|
||||
mtype=mtype,
|
||||
days=days)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -140,6 +142,16 @@ class DownloadHistory(Base):
|
||||
DownloadHistory.tmdbid == tmdbid).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def list_by_type(db: Session, mtype: str, days: int):
|
||||
result = db.query(DownloadHistory) \
|
||||
.filter(DownloadHistory.type == mtype,
|
||||
DownloadHistory.date >= time.strftime("%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(time.time() - 86400 * int(days)))
|
||||
).all()
|
||||
return list(result)
|
||||
|
||||
|
||||
class DownloadFiles(Base):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -126,3 +128,13 @@ class Subscribe(Base):
|
||||
if subscribe:
|
||||
subscribe.delete(db, subscribe.id)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
@db_query
|
||||
def list_by_type(db: Session, mtype: str, days: int):
|
||||
result = db.query(Subscribe) \
|
||||
.filter(Subscribe.type == mtype,
|
||||
Subscribe.date >= time.strftime("%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(time.time() - 86400 * int(days)))
|
||||
).all()
|
||||
return list(result)
|
||||
|
||||
@@ -83,3 +83,9 @@ class SubscribeOper(DbOper):
|
||||
subscribe = self.get(sid)
|
||||
subscribe.update(self._db, payload)
|
||||
return subscribe
|
||||
|
||||
def list_by_type(self, mtype: str, days: int = 7) -> Subscribe:
|
||||
"""
|
||||
获取指定类型的订阅
|
||||
"""
|
||||
return Subscribe.list_by_type(self._db, mtype=mtype, days=days)
|
||||
|
||||
@@ -324,24 +324,26 @@ class TorrentHelper(metaclass=Singleton):
|
||||
|
||||
if not filter_rule:
|
||||
return True
|
||||
|
||||
# 匹配内容
|
||||
content = f"{torrent_info.title} {torrent_info.description} {' '.join(torrent_info.labels or [])}"
|
||||
|
||||
# 最少做种人数
|
||||
min_seeders = filter_rule.get("min_seeders")
|
||||
if min_seeders and torrent_info.seeders < int(min_seeders):
|
||||
logger.info(f"{torrent_info.title} 做种人数不足 {min_seeders}")
|
||||
return False
|
||||
|
||||
# 包含
|
||||
include = filter_rule.get("include")
|
||||
if include:
|
||||
if not re.search(r"%s" % include,
|
||||
f"{torrent_info.title} {torrent_info.description}", re.I):
|
||||
if not re.search(r"%s" % include, content, re.I):
|
||||
logger.info(f"{torrent_info.title} 不匹配包含规则 {include}")
|
||||
return False
|
||||
# 排除
|
||||
exclude = filter_rule.get("exclude")
|
||||
if exclude:
|
||||
if re.search(r"%s" % exclude,
|
||||
f"{torrent_info.title} {torrent_info.description}", re.I):
|
||||
if re.search(r"%s" % exclude, content, re.I):
|
||||
logger.info(f"{torrent_info.title} 匹配排除规则 {exclude}")
|
||||
return False
|
||||
# 质量
|
||||
|
||||
@@ -436,20 +436,45 @@ class Emby:
|
||||
return None
|
||||
req_url = "%semby/Items/%s/RemoteImages?api_key=%s" % (self._host, item_id, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
res = RequestUtils(timeout=10).get_res(req_url)
|
||||
if res:
|
||||
images = res.json().get("Images")
|
||||
for image in images:
|
||||
if image.get("ProviderName") == "TheMovieDb" and image.get("Type") == image_type:
|
||||
return image.get("Url")
|
||||
else:
|
||||
logger.error(f"Items/RemoteImages 未获取到返回数据")
|
||||
return None
|
||||
if images:
|
||||
for image in images:
|
||||
logger.info(image)
|
||||
if image.get("ProviderName") == "TheMovieDb" and image.get("Type") == image_type:
|
||||
return image.get("Url")
|
||||
# 数据为空
|
||||
logger.info(f"Items/RemoteImages 未获取到返回数据,采用本地图片")
|
||||
return self.generate_external_image_link(item_id, image_type)
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Id/RemoteImages出错:" + str(e))
|
||||
return None
|
||||
return None
|
||||
|
||||
def generate_external_image_link(self, item_id: str, image_type: str) -> Optional[str]:
|
||||
"""
|
||||
根据ItemId和imageType查询本地对应图片
|
||||
:param item_id: 在Emby中的ID
|
||||
:param image_type: 图片类型,如Backdrop、Primary
|
||||
:return: 图片对应在外网播放器中的URL
|
||||
"""
|
||||
if not self._playhost:
|
||||
logger.error("Emby外网播放地址未能获取或为空")
|
||||
return None
|
||||
|
||||
req_url = "%sItems/%s/Images/%s" % (self._playhost, item_id, image_type)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res and res.status_code != 404:
|
||||
logger.info("影片图片链接:{}".format(res.url))
|
||||
return res.url
|
||||
else:
|
||||
logger.error("Items/Id/Images 未获取到返回数据或无该影片{}图片".format(image_type))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Id/Images出错:" + str(e))
|
||||
return None
|
||||
|
||||
def __refresh_emby_library_by_id(self, item_id: str) -> bool:
|
||||
"""
|
||||
通知Emby刷新一个项目的媒体库
|
||||
|
||||
@@ -34,21 +34,31 @@ class FileTransferModule(_ModuleBase):
|
||||
if not settings.DOWNLOAD_PATH:
|
||||
return False, "下载目录未设置"
|
||||
# 检查下载目录
|
||||
download_path = Path(settings.DOWNLOAD_PATH)
|
||||
if not download_path.exists():
|
||||
return False, "下载目录不存在"
|
||||
download_paths: List[str] = []
|
||||
for path in [settings.DOWNLOAD_PATH,
|
||||
settings.DOWNLOAD_MOVIE_PATH,
|
||||
settings.DOWNLOAD_TV_PATH,
|
||||
settings.DOWNLOAD_ANIME_PATH]:
|
||||
if not path:
|
||||
continue
|
||||
download_path = Path(path)
|
||||
if not download_path.exists():
|
||||
return False, f"目录 {download_path} 不存在"
|
||||
download_paths.append(path)
|
||||
# 下载目录的设备ID
|
||||
download_devids = [Path(path).stat().st_dev for path in download_paths]
|
||||
# 检查媒体库目录
|
||||
if not settings.LIBRARY_PATH:
|
||||
return False, "媒体库目录未设置"
|
||||
# 下载目录的设备ID
|
||||
download_devid = download_path.stat().st_dev
|
||||
# 比较媒体库目录的设备ID
|
||||
for path in settings.LIBRARY_PATHS:
|
||||
library_path = Path(path)
|
||||
if not library_path.exists():
|
||||
return False, f"目录不存在:{library_path}"
|
||||
if settings.TRANSFER_TYPE == "link":
|
||||
if library_path.stat().st_dev != download_devid:
|
||||
return False, "下载目录与媒体库目录不在同一设备,将导致硬链接失败"
|
||||
if settings.DOWNLOADER_MONITOR and settings.TRANSFER_TYPE == "link":
|
||||
if library_path.stat().st_dev not in download_devids:
|
||||
return False, f"媒体库目录 {library_path} " \
|
||||
f"与下载目录 {','.join(download_paths)} 不在同一设备,将无法硬链接"
|
||||
return True, ""
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
@@ -195,6 +205,8 @@ class FileTransferModule(_ModuleBase):
|
||||
if (org_path.stem == Path(sub_file_name).stem) or \
|
||||
(sub_metainfo.cn_name and sub_metainfo.cn_name == metainfo.cn_name) \
|
||||
or (sub_metainfo.en_name and sub_metainfo.en_name == metainfo.en_name):
|
||||
if metainfo.part and metainfo.part != sub_metainfo.part:
|
||||
continue
|
||||
if metainfo.season \
|
||||
and metainfo.season != sub_metainfo.season:
|
||||
continue
|
||||
|
||||
@@ -39,12 +39,19 @@ class FilterModule(_ModuleBase):
|
||||
# 中字
|
||||
"CNSUB": {
|
||||
"include": [
|
||||
r'[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]|繁體|简体|[中国國][字配]|国语|國語|中文|中字'],
|
||||
r'[中国國繁简](/|\s|\\|\|)?[繁简英粤]|[英简繁](/|\s|\\|\|)?[中繁简]'
|
||||
r'|繁體|简体|[中国國][字配]|国语|國語|中文|中字|简日|繁日|简繁|繁体'
|
||||
r'|([\s,.-\[])(CHT|CHS|cht|chs)(|[\s,.-\]])'],
|
||||
"exclude": [],
|
||||
"tmdb": {
|
||||
"original_language": "zh,cn"
|
||||
}
|
||||
},
|
||||
# 官种
|
||||
"GZ": {
|
||||
"include": [r'官方', r'官种'],
|
||||
"match": ["labels"]
|
||||
},
|
||||
# 特效字幕
|
||||
"SPECSUB": {
|
||||
"include": [r'特效'],
|
||||
@@ -256,14 +263,30 @@ class FilterModule(_ModuleBase):
|
||||
# 符合TMDB规则的直接返回True,即不过滤
|
||||
if tmdb and self.__match_tmdb(tmdb):
|
||||
return True
|
||||
# 匹配项:标题、副标题、标签
|
||||
content = f"{torrent.title} {torrent.description} {' '.join(torrent.labels or [])}"
|
||||
# 只匹配指定关键字
|
||||
match_content = []
|
||||
matchs = self.rule_set[rule_name].get("match") or []
|
||||
if matchs:
|
||||
for match in matchs:
|
||||
if not hasattr(torrent, match):
|
||||
continue
|
||||
match_value = getattr(torrent, match)
|
||||
if not match_value:
|
||||
continue
|
||||
if isinstance(match_value, list):
|
||||
match_content.extend(match_value)
|
||||
else:
|
||||
match_content.append(match_value)
|
||||
if match_content:
|
||||
content = " ".join(match_content)
|
||||
# 包含规则项
|
||||
includes = self.rule_set[rule_name].get("include") or []
|
||||
# 排除规则项
|
||||
excludes = self.rule_set[rule_name].get("exclude") or []
|
||||
# FREE规则
|
||||
downloadvolumefactor = self.rule_set[rule_name].get("downloadvolumefactor")
|
||||
# 匹配项
|
||||
content = f"{torrent.title} {torrent.description} {' '.join(torrent.labels or [])}"
|
||||
for include in includes:
|
||||
if not re.search(r"%s" % include, content, re.IGNORECASE):
|
||||
# 未发现包含项
|
||||
|
||||
@@ -383,9 +383,19 @@ class TorrentSpider:
|
||||
item = self.__index(items, selector)
|
||||
download_link = self.__filter_text(item, selector.get('filters'))
|
||||
if download_link:
|
||||
if not download_link.startswith("http") and not download_link.startswith("magnet"):
|
||||
self.torrents_info['enclosure'] = self.domain + download_link[1:] if download_link.startswith(
|
||||
"/") else self.domain + download_link
|
||||
if not download_link.startswith("http") \
|
||||
and not download_link.startswith("magnet"):
|
||||
_scheme, _domain = StringUtils.get_url_netloc(self.domain)
|
||||
if _domain in download_link:
|
||||
if download_link.startswith("/"):
|
||||
self.torrents_info['enclosure'] = f"{_scheme}:{download_link}"
|
||||
else:
|
||||
self.torrents_info['enclosure'] = f"{_scheme}://{download_link}"
|
||||
else:
|
||||
if download_link.startswith("/"):
|
||||
self.torrents_info['enclosure'] = f"{self.domain}{download_link[1:]}"
|
||||
else:
|
||||
self.torrents_info['enclosure'] = f"{self.domain}{download_link}"
|
||||
else:
|
||||
self.torrents_info['enclosure'] = download_link
|
||||
|
||||
|
||||
@@ -68,11 +68,11 @@ class Jellyfin:
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
req_url = "%sLibrary/VirtualFolders/Query?api_key=%s" % (self._host, self._apikey)
|
||||
req_url = "%sLibrary/VirtualFolders?api_key=%s" % (self._host, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
library_items = res.json().get("Items")
|
||||
library_items = res.json()
|
||||
librarys = []
|
||||
for library_item in library_items:
|
||||
library_name = library_item.get('Name')
|
||||
@@ -91,10 +91,10 @@ class Jellyfin:
|
||||
})
|
||||
return librarys
|
||||
else:
|
||||
logger.error(f"Library/VirtualFolders/Query 未获取到返回数据")
|
||||
logger.error(f"Library/VirtualFolders 未获取到返回数据")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"连接Library/VirtualFolders/Query 出错:" + str(e))
|
||||
logger.error(f"连接Library/VirtualFolders 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def __get_jellyfin_librarys(self, username: str = None) -> List[dict]:
|
||||
@@ -422,20 +422,73 @@ class Jellyfin:
|
||||
return None
|
||||
req_url = "%sItems/%s/RemoteImages?api_key=%s" % (self._host, item_id, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
res = RequestUtils(timeout=10).get_res(req_url)
|
||||
if res:
|
||||
images = res.json().get("Images")
|
||||
for image in images:
|
||||
if image.get("ProviderName") == "TheMovieDb" and image.get("Type") == image_type:
|
||||
return image.get("Url")
|
||||
# return images[0].get("Url") # 首选无则返回第一张
|
||||
else:
|
||||
logger.error(f"Items/RemoteImages 未获取到返回数据")
|
||||
return None
|
||||
logger.info(f"Items/RemoteImages 未获取到返回数据,采用本地图片")
|
||||
return self.generate_image_link(item_id, image_type, True)
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Id/RemoteImages出错:" + str(e))
|
||||
return None
|
||||
return None
|
||||
|
||||
def generate_image_link(self, item_id: str, image_type: str, host_type: bool) -> Optional[str]:
|
||||
"""
|
||||
根据ItemId和imageType查询本地对应图片
|
||||
:param item_id: 在Jellyfin中的ID
|
||||
:param image_type: 图片类型,如Backdrop、Primary
|
||||
:param host_type: True为外网链接, False为内网链接
|
||||
:return: 图片对应在host_type的播放器中的URL
|
||||
"""
|
||||
if not self._playhost:
|
||||
logger.error("Jellyfin外网播放地址未能获取或为空")
|
||||
return None
|
||||
# 检测是否为TV
|
||||
_parent_id = self.get_itemId_ancestors(item_id, 0, "ParentBackdropItemId")
|
||||
if _parent_id:
|
||||
item_id = _parent_id
|
||||
|
||||
_host = self._host
|
||||
if host_type:
|
||||
_host = self._playhost
|
||||
req_url = "%sItems/%s/Images/%s" % (_host, item_id, image_type)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res and res.status_code != 404:
|
||||
logger.info("影片图片链接:{}".format(res.url))
|
||||
return res.url
|
||||
else:
|
||||
logger.error("Items/Id/Images 未获取到返回数据或无该影片{}图片".format(image_type))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Id/Images出错:" + str(e))
|
||||
return None
|
||||
|
||||
def get_itemId_ancestors(self, item_id: str, index: int, key: str) -> Optional[Union[str, list, int, dict, bool]]:
|
||||
"""
|
||||
获得itemId的父item
|
||||
:param item_id: 在Jellyfin中剧集的ID (S01E02的E02的item_id)
|
||||
:param index: 第几个json对象
|
||||
:param key: 需要得到父item中的键值对
|
||||
:return key对应类型的值
|
||||
"""
|
||||
req_url = "%sItems/%s/Ancestors?api_key=%s" % (self._host, item_id, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
return res.json()[index].get(key)
|
||||
else:
|
||||
logger.error(f"Items/Id/Ancestors 未获取到返回数据")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Id/Ancestors出错:" + str(e))
|
||||
return None
|
||||
|
||||
def refresh_root_library(self) -> bool:
|
||||
"""
|
||||
通知Jellyfin刷新整个媒体库
|
||||
@@ -730,6 +783,10 @@ class Jellyfin:
|
||||
image_tag=item.get("BackdropImageTags")[0])
|
||||
else:
|
||||
image = self.__get_local_image_by_id(item.get("Id"))
|
||||
# 小部分剧集无[xxx-S01E01-thumb.jpg]图片
|
||||
image_res = RequestUtils().get_res(image)
|
||||
if not image_res or image_res.status_code == 404:
|
||||
image = self.generate_image_link(item.get("Id"), "Backdrop", False)
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
title = item.get("Name")
|
||||
subtitle = item.get("ProductionYear")
|
||||
|
||||
@@ -32,8 +32,8 @@ class QbittorrentModule(_ModuleBase):
|
||||
"""
|
||||
if self.qbittorrent.is_inactive():
|
||||
self.qbittorrent.reconnect()
|
||||
if self.qbittorrent.is_inactive():
|
||||
return False, "无法连接Qbittorrent,请检查参数配置"
|
||||
if not self.qbittorrent.transfer_info():
|
||||
return False, "无法获取Qbittorrent状态,请检查参数配置"
|
||||
return True, ""
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
@@ -48,7 +48,8 @@ class QbittorrentModule(_ModuleBase):
|
||||
self.qbittorrent.reconnect()
|
||||
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
||||
episodes: Set[int] = None, category: str = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[Tuple[Optional[str], str]]:
|
||||
"""
|
||||
根据种子文件,选择并添加下载任务
|
||||
:param content: 种子文件地址或者磁力链接
|
||||
@@ -56,6 +57,7 @@ class QbittorrentModule(_ModuleBase):
|
||||
:param cookie: cookie
|
||||
:param episodes: 需要下载的集数
|
||||
:param category: 分类
|
||||
:param downloader: 下载器
|
||||
:return: 种子Hash,错误信息
|
||||
"""
|
||||
|
||||
@@ -73,8 +75,12 @@ class QbittorrentModule(_ModuleBase):
|
||||
logger.error(f"获取种子名称失败:{e}")
|
||||
return "", 0
|
||||
|
||||
# 不是默认下载器不处理
|
||||
if downloader != "qbittorrent":
|
||||
return None
|
||||
|
||||
if not content:
|
||||
return
|
||||
return None
|
||||
if isinstance(content, Path) and not content.exists():
|
||||
return None, f"种子文件不存在:{content}"
|
||||
|
||||
@@ -161,13 +167,18 @@ class QbittorrentModule(_ModuleBase):
|
||||
return torrent_hash, "添加下载成功"
|
||||
|
||||
def list_torrents(self, status: TorrentStatus = None,
|
||||
hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
hashs: Union[list, str] = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER
|
||||
) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
"""
|
||||
获取下载器种子列表
|
||||
:param status: 种子状态
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: 下载器中符合状态的种子列表
|
||||
"""
|
||||
if downloader != "qbittorrent":
|
||||
return None
|
||||
ret_torrents = []
|
||||
if hashs:
|
||||
# 按Hash获取
|
||||
@@ -227,13 +238,16 @@ class QbittorrentModule(_ModuleBase):
|
||||
return None
|
||||
return ret_torrents
|
||||
|
||||
def transfer_completed(self, hashs: Union[str, list],
|
||||
path: Path = None) -> None:
|
||||
def transfer_completed(self, hashs: Union[str, list], path: Path = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> None:
|
||||
"""
|
||||
转移完成后的处理
|
||||
:param hashs: 种子Hash
|
||||
:param path: 源目录
|
||||
:param downloader: 下载器
|
||||
"""
|
||||
if downloader != "qbittorrent":
|
||||
return
|
||||
self.qbittorrent.set_torrents_tag(ids=hashs, tags=['已整理'])
|
||||
# 移动模式删除种子
|
||||
if settings.TRANSFER_TYPE == "move":
|
||||
@@ -246,48 +260,61 @@ class QbittorrentModule(_ModuleBase):
|
||||
logger.warn(f"删除残留文件夹:{path}")
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True) -> bool:
|
||||
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
|
||||
"""
|
||||
删除下载器种子
|
||||
:param hashs: 种子Hash
|
||||
:param delete_file: 是否删除文件
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
if downloader != "qbittorrent":
|
||||
return None
|
||||
return self.qbittorrent.delete_torrents(delete_file=delete_file, ids=hashs)
|
||||
|
||||
def start_torrents(self, hashs: Union[list, str]) -> bool:
|
||||
def start_torrents(self, hashs: Union[list, str],
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
|
||||
"""
|
||||
开始下载
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
if downloader != "qbittorrent":
|
||||
return None
|
||||
return self.qbittorrent.start_torrents(ids=hashs)
|
||||
|
||||
def stop_torrents(self, hashs: Union[list, str]) -> bool:
|
||||
def stop_torrents(self, hashs: Union[list, str], downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
|
||||
"""
|
||||
停止下载
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
if downloader != "qbittorrent":
|
||||
return None
|
||||
return self.qbittorrent.stop_torrents(ids=hashs)
|
||||
|
||||
def torrent_files(self, tid: str) -> Optional[TorrentFilesList]:
|
||||
def torrent_files(self, tid: str, downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[TorrentFilesList]:
|
||||
"""
|
||||
获取种子文件列表
|
||||
"""
|
||||
if downloader != "qbittorrent":
|
||||
return None
|
||||
return self.qbittorrent.get_files(tid=tid)
|
||||
|
||||
def downloader_info(self) -> schemas.DownloaderInfo:
|
||||
def downloader_info(self) -> [schemas.DownloaderInfo]:
|
||||
"""
|
||||
下载器信息
|
||||
"""
|
||||
# 调用Qbittorrent API查询实时信息
|
||||
info = self.qbittorrent.transfer_info()
|
||||
if not info:
|
||||
return schemas.DownloaderInfo()
|
||||
return schemas.DownloaderInfo(
|
||||
return [schemas.DownloaderInfo()]
|
||||
return [schemas.DownloaderInfo(
|
||||
download_speed=info.get("dl_info_speed"),
|
||||
upload_speed=info.get("up_info_speed"),
|
||||
download_size=info.get("dl_info_data"),
|
||||
upload_size=info.get("up_info_data")
|
||||
)
|
||||
)]
|
||||
|
||||
@@ -119,9 +119,9 @@ class Qbittorrent:
|
||||
tags=tags)
|
||||
return None if error else torrents or []
|
||||
|
||||
def remove_torrents_tag(self, ids: Union[str, list], tag: Union[str, list]) -> bool:
|
||||
def delete_torrents_tag(self, ids: Union[str, list], tag: Union[str, list]) -> bool:
|
||||
"""
|
||||
移除种子Tag
|
||||
删除Tag
|
||||
:param ids: 种子Hash列表
|
||||
:param tag: 标签内容
|
||||
"""
|
||||
@@ -130,10 +130,25 @@ class Qbittorrent:
|
||||
try:
|
||||
self.qbc.torrents_delete_tags(torrent_hashes=ids, tags=tag)
|
||||
return True
|
||||
except Exception as err:
|
||||
logger.error(f"删除种子Tag出错:{str(err)}")
|
||||
return False
|
||||
|
||||
def remove_torrents_tag(self, ids: Union[str, list], tag: Union[str, list]) -> bool:
|
||||
"""
|
||||
移除种子Tag
|
||||
:param ids: 种子Hash列表
|
||||
:param tag: 标签内容
|
||||
"""
|
||||
if not self.qbc:
|
||||
return False
|
||||
try:
|
||||
self.qbc.torrents_remove_tags(torrent_hashes=ids, tags=tag)
|
||||
return True
|
||||
except Exception as err:
|
||||
logger.error(f"移除种子Tag出错:{str(err)}")
|
||||
return False
|
||||
|
||||
|
||||
def set_torrents_tag(self, ids: Union[str, list], tags: list):
|
||||
"""
|
||||
设置种子状态为已整理,以及是否强制做种
|
||||
@@ -187,7 +202,7 @@ class Qbittorrent:
|
||||
if torrent_id is None:
|
||||
continue
|
||||
else:
|
||||
self.remove_torrents_tag(torrent_id, tags)
|
||||
self.delete_torrents_tag(torrent_id, tags)
|
||||
break
|
||||
return torrent_id
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ class TheMovieDbModule(_ModuleBase):
|
||||
"""
|
||||
测试模块连接性
|
||||
"""
|
||||
ret = RequestUtils().get_res(f"https://{settings.TMDB_API_DOMAIN}/3/movie/550?api_key={settings.TMDB_API_KEY}")
|
||||
ret = RequestUtils(proxies=settings.PROXY).get_res(
|
||||
f"https://{settings.TMDB_API_DOMAIN}/3/movie/550?api_key={settings.TMDB_API_KEY}")
|
||||
if ret and ret.status_code == 200:
|
||||
return True, ""
|
||||
elif ret:
|
||||
@@ -131,7 +132,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
logger.error("识别媒体信息时未提供元数据或tmdbid")
|
||||
return None
|
||||
# 保存到缓存
|
||||
if meta and cache:
|
||||
if meta:
|
||||
self.cache.update(meta, info)
|
||||
else:
|
||||
# 使用缓存信息
|
||||
|
||||
@@ -15,6 +15,7 @@ class CategoryHelper(metaclass=Singleton):
|
||||
_categorys = {}
|
||||
_movie_categorys = {}
|
||||
_tv_categorys = {}
|
||||
_anime_categorys = {}
|
||||
|
||||
def __init__(self):
|
||||
self._category_path: Path = settings.CONFIG_PATH / "category.yaml"
|
||||
@@ -43,6 +44,7 @@ class CategoryHelper(metaclass=Singleton):
|
||||
if self._categorys:
|
||||
self._movie_categorys = self._categorys.get('movie')
|
||||
self._tv_categorys = self._categorys.get('tv')
|
||||
self._anime_categorys = self._categorys.get('anime')
|
||||
logger.info(f"已加载二级分类策略 category.yaml")
|
||||
|
||||
@property
|
||||
@@ -81,6 +83,15 @@ class CategoryHelper(metaclass=Singleton):
|
||||
return []
|
||||
return self._tv_categorys.keys()
|
||||
|
||||
@property
|
||||
def anime_categorys(self) -> list:
|
||||
"""
|
||||
获取动漫分类清单
|
||||
"""
|
||||
if not self._anime_categorys:
|
||||
return []
|
||||
return self._anime_categorys.keys()
|
||||
|
||||
def get_movie_category(self, tmdb_info) -> str:
|
||||
"""
|
||||
判断电影的分类
|
||||
@@ -91,10 +102,14 @@ class CategoryHelper(metaclass=Singleton):
|
||||
|
||||
def get_tv_category(self, tmdb_info) -> str:
|
||||
"""
|
||||
判断电视剧的分类
|
||||
判断电视剧的分类,包括动漫
|
||||
:param tmdb_info: 识别的TMDB中的信息
|
||||
:return: 二级分类的名称
|
||||
"""
|
||||
genre_ids = tmdb_info.get("genre_ids") or []
|
||||
if self._anime_categorys and genre_ids \
|
||||
and set(genre_ids).intersection(set(settings.ANIME_GENREIDS)):
|
||||
return self.get_category(self._anime_categorys, tmdb_info)
|
||||
return self.get_category(self._tv_categorys, tmdb_info)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -309,7 +309,7 @@ class TmdbScraper:
|
||||
# 添加时间
|
||||
DomUtils.add_node(doc, root, "dateadded", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
|
||||
# TMDBID
|
||||
uniqueid = DomUtils.add_node(doc, root, "uniqueid", str(tmdbid))
|
||||
uniqueid = DomUtils.add_node(doc, root, "uniqueid", str(episodeinfo.get("id")))
|
||||
uniqueid.setAttribute("type", "tmdb")
|
||||
uniqueid.setAttribute("default", "true")
|
||||
# tmdbid
|
||||
|
||||
@@ -553,10 +553,10 @@ class TmdbHelper:
|
||||
tmdb_info['genre_ids'] = __get_genre_ids(tmdb_info.get('genres'))
|
||||
# 别名和译名
|
||||
tmdb_info['names'] = self.__get_names(tmdb_info)
|
||||
# 转换多语种标题
|
||||
self.__update_tmdbinfo_extra_title(tmdb_info)
|
||||
# 转换中文标题
|
||||
self.__update_tmdbinfo_cn_title(tmdb_info)
|
||||
# 转换英文标题
|
||||
self.__update_tmdbinfo_en_title(tmdb_info)
|
||||
|
||||
return tmdb_info
|
||||
|
||||
@@ -585,49 +585,61 @@ class TmdbHelper:
|
||||
return title
|
||||
return tmdbinfo.get("title") if tmdbinfo.get("media_type") == MediaType.MOVIE else tmdbinfo.get("name")
|
||||
|
||||
# 查找中文名
|
||||
# 原标题
|
||||
org_title = tmdb_info.get("title") \
|
||||
if tmdb_info.get("media_type") == MediaType.MOVIE \
|
||||
else tmdb_info.get("name")
|
||||
# 查找中文名
|
||||
if not StringUtils.is_chinese(org_title):
|
||||
cn_title = __get_tmdb_chinese_title(tmdb_info)
|
||||
if cn_title and cn_title != org_title:
|
||||
# 使用中文别名
|
||||
if tmdb_info.get("media_type") == MediaType.MOVIE:
|
||||
tmdb_info['title'] = cn_title
|
||||
else:
|
||||
tmdb_info['name'] = cn_title
|
||||
else:
|
||||
# 使用新加坡名
|
||||
sg_title = tmdb_info.get("sg_title")
|
||||
if sg_title and sg_title != org_title and StringUtils.is_chinese(sg_title):
|
||||
if tmdb_info.get("media_type") == MediaType.MOVIE:
|
||||
tmdb_info['title'] = sg_title
|
||||
else:
|
||||
tmdb_info['name'] = sg_title
|
||||
|
||||
@staticmethod
|
||||
def __update_tmdbinfo_en_title(tmdb_info: dict):
|
||||
def __update_tmdbinfo_extra_title(tmdb_info: dict):
|
||||
"""
|
||||
更新TMDB信息中的英文名称
|
||||
更新TMDB信息中的其它语种名称
|
||||
"""
|
||||
|
||||
def __get_tmdb_english_title(tmdbinfo):
|
||||
def __get_tmdb_lang_title(tmdbinfo: dict, lang: str = "US"):
|
||||
"""
|
||||
从别名中获取英文标题
|
||||
从译名中获取其它语种标题
|
||||
"""
|
||||
if not tmdbinfo:
|
||||
return None
|
||||
translations = tmdb_info.get("translations", {}).get("translations", [])
|
||||
for translation in translations:
|
||||
if translation.get("iso_3166_1") == "US":
|
||||
if translation.get("iso_3166_1") == lang:
|
||||
return translation.get("data", {}).get("title") if tmdbinfo.get("media_type") == MediaType.MOVIE \
|
||||
else translation.get("data", {}).get("name")
|
||||
return None
|
||||
|
||||
# 查找英文名
|
||||
# 原标题
|
||||
org_title = (
|
||||
tmdb_info.get("original_title")
|
||||
if tmdb_info.get("media_type") == MediaType.MOVIE
|
||||
else tmdb_info.get("original_name")
|
||||
)
|
||||
# 查找英文名
|
||||
if tmdb_info.get("original_language") == "en":
|
||||
tmdb_info['en_title'] = org_title
|
||||
# TODO: 对于日文标题,使用罗马字作为英文标题可能更合适?
|
||||
else:
|
||||
en_title = __get_tmdb_english_title(tmdb_info)
|
||||
en_title = __get_tmdb_lang_title(tmdb_info, "US")
|
||||
tmdb_info['en_title'] = en_title or org_title
|
||||
# 查找新加坡名(用于替代中文名)
|
||||
tmdb_info['sg_title'] = __get_tmdb_lang_title(tmdb_info, "SG") or org_title
|
||||
|
||||
def __get_movie_detail(self,
|
||||
tmdbid: int,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
@@ -9,7 +8,6 @@ from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class TheTvDbModule(_ModuleBase):
|
||||
|
||||
tvdb: tvdbapi.Tvdb = None
|
||||
|
||||
def init_module(self) -> None:
|
||||
@@ -25,7 +23,7 @@ class TheTvDbModule(_ModuleBase):
|
||||
"""
|
||||
测试模块连接性
|
||||
"""
|
||||
ret = RequestUtils().get_res("https://api.thetvdb.com/series/81189")
|
||||
ret = RequestUtils(proxies=settings.PROXY).get_res("https://api.thetvdb.com/series/81189")
|
||||
if ret and ret.status_code == 200:
|
||||
return True, ""
|
||||
elif ret:
|
||||
|
||||
@@ -32,8 +32,8 @@ class TransmissionModule(_ModuleBase):
|
||||
"""
|
||||
if self.transmission.is_inactive():
|
||||
self.transmission.reconnect()
|
||||
if self.transmission.is_inactive():
|
||||
return False, "无法连接Transmission,请检查参数配置"
|
||||
if not self.transmission.transfer_info():
|
||||
return False, "无法获取Transmission状态,请检查参数配置"
|
||||
return True, ""
|
||||
|
||||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||||
@@ -48,7 +48,8 @@ class TransmissionModule(_ModuleBase):
|
||||
self.transmission.reconnect()
|
||||
|
||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
||||
episodes: Set[int] = None, category: str = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[Tuple[Optional[str], str]]:
|
||||
"""
|
||||
根据种子文件,选择并添加下载任务
|
||||
:param content: 种子文件地址或者磁力链接
|
||||
@@ -56,6 +57,7 @@ class TransmissionModule(_ModuleBase):
|
||||
:param cookie: cookie
|
||||
:param episodes: 需要下载的集数
|
||||
:param category: 分类,TR中未使用
|
||||
:param downloader: 下载器
|
||||
:return: 种子Hash
|
||||
"""
|
||||
|
||||
@@ -73,8 +75,12 @@ class TransmissionModule(_ModuleBase):
|
||||
logger.error(f"获取种子名称失败:{e}")
|
||||
return "", 0
|
||||
|
||||
# 不是默认下载器不处理
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
|
||||
if not content:
|
||||
return
|
||||
return None
|
||||
if isinstance(content, Path) and not content.exists():
|
||||
return None, f"种子文件不存在:{content}"
|
||||
|
||||
@@ -154,13 +160,18 @@ class TransmissionModule(_ModuleBase):
|
||||
return torrent_hash, "添加下载任务成功"
|
||||
|
||||
def list_torrents(self, status: TorrentStatus = None,
|
||||
hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
hashs: Union[list, str] = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER
|
||||
) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
|
||||
"""
|
||||
获取下载器种子列表
|
||||
:param status: 种子状态
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: 下载器中符合状态的种子列表
|
||||
"""
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
ret_torrents = []
|
||||
if hashs:
|
||||
# 按Hash获取
|
||||
@@ -215,14 +226,17 @@ class TransmissionModule(_ModuleBase):
|
||||
return None
|
||||
return ret_torrents
|
||||
|
||||
def transfer_completed(self, hashs: Union[str, list],
|
||||
path: Path = None) -> None:
|
||||
def transfer_completed(self, hashs: Union[str, list], path: Path = None,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> None:
|
||||
"""
|
||||
转移完成后的处理
|
||||
:param hashs: 种子Hash
|
||||
:param path: 源目录
|
||||
:param downloader: 下载器
|
||||
:return: None
|
||||
"""
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
self.transmission.set_torrent_tag(ids=hashs, tags=['已整理'])
|
||||
# 移动模式删除种子
|
||||
if settings.TRANSFER_TYPE == "move":
|
||||
@@ -235,47 +249,61 @@ class TransmissionModule(_ModuleBase):
|
||||
logger.warn(f"删除残留文件夹:{path}")
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True) -> bool:
|
||||
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True,
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
|
||||
"""
|
||||
删除下载器种子
|
||||
:param hashs: 种子Hash
|
||||
:param delete_file: 是否删除文件
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
return self.transmission.delete_torrents(delete_file=delete_file, ids=hashs)
|
||||
|
||||
def start_torrents(self, hashs: Union[list, str]) -> bool:
|
||||
def start_torrents(self, hashs: Union[list, str],
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
|
||||
"""
|
||||
开始下载
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
return self.transmission.start_torrents(ids=hashs)
|
||||
|
||||
def stop_torrents(self, hashs: Union[list, str]) -> bool:
|
||||
def stop_torrents(self, hashs: Union[list, str],
|
||||
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
|
||||
"""
|
||||
停止下载
|
||||
:param hashs: 种子Hash
|
||||
:param downloader: 下载器
|
||||
:return: bool
|
||||
"""
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
return self.transmission.start_torrents(ids=hashs)
|
||||
|
||||
def torrent_files(self, tid: str) -> Optional[List[File]]:
|
||||
def torrent_files(self, tid: str, downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[List[File]]:
|
||||
"""
|
||||
获取种子文件列表
|
||||
"""
|
||||
if downloader != "transmission":
|
||||
return None
|
||||
return self.transmission.get_files(tid=tid)
|
||||
|
||||
def downloader_info(self) -> schemas.DownloaderInfo:
|
||||
def downloader_info(self) -> [schemas.DownloaderInfo]:
|
||||
"""
|
||||
下载器信息
|
||||
"""
|
||||
info = self.transmission.transfer_info()
|
||||
if not info:
|
||||
return schemas.DownloaderInfo()
|
||||
return schemas.DownloaderInfo(
|
||||
return [schemas.DownloaderInfo()]
|
||||
return [schemas.DownloaderInfo(
|
||||
download_speed=info.download_speed,
|
||||
upload_speed=info.upload_speed,
|
||||
download_size=info.current_stats.downloaded_bytes,
|
||||
upload_size=info.current_stats.uploaded_bytes
|
||||
)
|
||||
)]
|
||||
|
||||
@@ -67,6 +67,11 @@ class VoceChatModule(_ModuleBase):
|
||||
# 非新消息
|
||||
return None
|
||||
logger.debug(f"收到VoceChat请求:{msg_body}")
|
||||
# token校验
|
||||
token = args.get("token")
|
||||
if not token or token != settings.API_TOKEN:
|
||||
logger.warn(f"VoceChat请求token校验失败:{token}")
|
||||
return None
|
||||
# 文本内容
|
||||
content = msg_body.get("detail", {}).get("content")
|
||||
# 用户ID
|
||||
|
||||
@@ -98,10 +98,8 @@ class VoceChat:
|
||||
return None
|
||||
|
||||
try:
|
||||
index, image, caption = 1, "", "**%s**" % title
|
||||
index, caption = 1, "**%s**" % title
|
||||
for media in medias:
|
||||
if not image:
|
||||
image = media.get_message_image()
|
||||
if media.vote_average:
|
||||
caption = "%s\n%s. [%s](%s)\n_%s,%s_" % (caption,
|
||||
index,
|
||||
@@ -141,7 +139,6 @@ class VoceChat:
|
||||
|
||||
try:
|
||||
index, caption = 1, "**%s**" % title
|
||||
mediainfo = torrents[0].media_info
|
||||
for context in torrents:
|
||||
torrent = context.torrent_info
|
||||
site_name = torrent.site_name
|
||||
@@ -163,8 +160,7 @@ class VoceChat:
|
||||
else:
|
||||
chat_id = f"GID#{self._channel_id}"
|
||||
|
||||
return self.__send_request(userid=chat_id, caption=caption,
|
||||
image=mediainfo.get_message_image())
|
||||
return self.__send_request(userid=chat_id, caption=caption)
|
||||
|
||||
except Exception as msg_e:
|
||||
logger.error(f"发送消息失败:{msg_e}")
|
||||
@@ -183,17 +179,13 @@ class VoceChat:
|
||||
else:
|
||||
action = "send_to_user"
|
||||
idstr = userid[4:]
|
||||
|
||||
with lock:
|
||||
try:
|
||||
logger.info(f"VoceChat发送消息:action={action}, userid={idstr}, text={caption}")
|
||||
result = self._client.post_res(f"{self._host}api/bot/{action}/{idstr}", data=caption.encode("utf-8"))
|
||||
if result and result.status_code == 200:
|
||||
return True
|
||||
elif result is not None:
|
||||
logger.error(f"VoceChat发送消息失败,错误码:{result.status_code}")
|
||||
return False
|
||||
else:
|
||||
raise Exception("VoceChat发送消息失败,连接失败")
|
||||
except Exception as msg_e:
|
||||
logger.error(f"VoceChat发送消息错误:{str(msg_e)}")
|
||||
return False
|
||||
result = self._client.post_res(f"{self._host}api/bot/{action}/{idstr}", data=caption.encode("utf-8"))
|
||||
if result and result.status_code == 200:
|
||||
return True
|
||||
elif result is not None:
|
||||
logger.error(f"VoceChat发送消息失败,错误码:{result.status_code}")
|
||||
return False
|
||||
else:
|
||||
raise Exception("VoceChat发送消息失败,连接失败")
|
||||
|
||||
@@ -39,16 +39,21 @@ class Scheduler(metaclass=Singleton):
|
||||
定时任务管理
|
||||
"""
|
||||
# 定时服务
|
||||
_scheduler = BackgroundScheduler(timezone=settings.TZ,
|
||||
executors={
|
||||
'default': ThreadPoolExecutor(100)
|
||||
})
|
||||
_scheduler = None
|
||||
# 退出事件
|
||||
_event = threading.Event()
|
||||
# 锁
|
||||
_lock = threading.Lock()
|
||||
# 各服务的运行状态
|
||||
_jobs = {}
|
||||
|
||||
def __init__(self):
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
初始化定时服务
|
||||
"""
|
||||
|
||||
def clear_cache():
|
||||
"""
|
||||
@@ -92,10 +97,19 @@ class Scheduler(metaclass=Singleton):
|
||||
}
|
||||
}
|
||||
|
||||
# 停止定时服务
|
||||
self.stop()
|
||||
|
||||
# 调试模式不启动定时服务
|
||||
if settings.DEV:
|
||||
return
|
||||
|
||||
# 创建定时服务
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ,
|
||||
executors={
|
||||
'default': ThreadPoolExecutor(100)
|
||||
})
|
||||
|
||||
# CookieCloud定时同步
|
||||
if settings.COOKIECLOUD_INTERVAL \
|
||||
and str(settings.COOKIECLOUD_INTERVAL).isdigit():
|
||||
@@ -264,7 +278,7 @@ class Scheduler(metaclass=Singleton):
|
||||
kwargs = job.get("kwargs") or {}
|
||||
job["func"](*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"定时任务 {job_id} 执行失败:{str(e)}")
|
||||
logger.error(f"定时任务 {job_id} 执行失败:{str(e)} - {traceback.format_exc()}")
|
||||
# 运行结束
|
||||
with self._lock:
|
||||
try:
|
||||
@@ -276,6 +290,8 @@ class Scheduler(metaclass=Singleton):
|
||||
"""
|
||||
更新插件定时服务
|
||||
"""
|
||||
if not self._scheduler:
|
||||
return
|
||||
# 移除该插件的全部服务
|
||||
self.remove_plugin_job(pid)
|
||||
# 获取插件服务列表
|
||||
@@ -318,6 +334,8 @@ class Scheduler(metaclass=Singleton):
|
||||
"""
|
||||
移除插件定时服务
|
||||
"""
|
||||
if not self._scheduler:
|
||||
return
|
||||
with self._lock:
|
||||
# 获取插件名称
|
||||
plugin_name = PluginManager().get_plugin_attr(pid, "plugin_name")
|
||||
@@ -337,6 +355,8 @@ class Scheduler(metaclass=Singleton):
|
||||
"""
|
||||
当前所有任务
|
||||
"""
|
||||
if not self._scheduler:
|
||||
return []
|
||||
with self._lock:
|
||||
# 返回计时任务
|
||||
schedulers = []
|
||||
@@ -385,6 +405,12 @@ class Scheduler(metaclass=Singleton):
|
||||
"""
|
||||
关闭定时服务
|
||||
"""
|
||||
self._event.set()
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._event.set()
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error(f"停止定时任务失败::{str(e)} - {traceback.format_exc()}")
|
||||
|
||||
@@ -94,7 +94,7 @@ class MediaInfo(BaseModel):
|
||||
# 海报图片
|
||||
poster_path: Optional[str] = None
|
||||
# 评分
|
||||
vote_average: Optional[int] = 0
|
||||
vote_average: Optional[float] = 0
|
||||
# 描述
|
||||
overview: Optional[str] = None
|
||||
# 二级分类
|
||||
|
||||
@@ -31,6 +31,7 @@ class DownloadingTorrent(BaseModel):
|
||||
dlspeed: Optional[str] = None
|
||||
media: Optional[dict] = {}
|
||||
userid: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
left_time: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
@@ -389,6 +389,8 @@ class SystemUtils:
|
||||
"""
|
||||
判断是否为蓝光原盘目录
|
||||
"""
|
||||
if not dir_path.is_dir():
|
||||
return False
|
||||
# 蓝光原盘目录必备的文件或文件夹
|
||||
required_files = ['BDMV', 'CERTIFICATE']
|
||||
# 检查目录下是否存在所需文件或文件夹
|
||||
|
||||
@@ -11,6 +11,8 @@ DEV=false
|
||||
SUPERUSER=admin
|
||||
# 大内存模式,开启后会增加缓存数量,但会占用更多内存
|
||||
BIG_MEMORY_MODE=false
|
||||
# 元数据识别缓存过期时间,数字型,单位小时,0为系统默认(大内存模式为7天,滞则为3天),调大该值可减少themoviedb的访问次数
|
||||
META_CACHE_EXPIRE=0
|
||||
# 自动检查和更新站点资源包(索引、认证等)
|
||||
AUTO_UPDATE_RESOURCE=true
|
||||
# 【*】API密钥,建议更换复杂字符串,有Jellyseerr/Overseerr、媒体服务器Webhook等配置以及部分支持API_TOKEN的API中使用
|
||||
|
||||
@@ -47,6 +47,15 @@ tv:
|
||||
# 未匹配以上分类,则命名为未分类
|
||||
未分类:
|
||||
|
||||
# 配置动漫的分类策略
|
||||
anime:
|
||||
国漫:
|
||||
# 匹配 origin_country 国家,CN是中国大陆,TW是中国台湾,HK是中国香港
|
||||
origin_country: 'CN,TW,HK'
|
||||
日番:
|
||||
# 匹配 origin_country 国家,JP是日本
|
||||
origin_country: 'JP'
|
||||
未分类:
|
||||
|
||||
## genre_ids 内容类型 字典,注意部分中英文是不一样的
|
||||
# 28 Action
|
||||
|
||||
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.7.0'
|
||||
APP_VERSION = 'v1.7.2'
|
||||
|
||||
Reference in New Issue
Block a user