mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-09 22:13:00 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfe113f6c3 | ||
|
|
83500128c9 | ||
|
|
2bff3a80da | ||
|
|
3dd7b33f3e | ||
|
|
8de487b0bf | ||
|
|
ce88a6818f | ||
|
|
6172832f41 | ||
|
|
a0ed228f4b | ||
|
|
01fd56a019 | ||
|
|
087fcd340a | ||
|
|
b3b09f3c03 | ||
|
|
11d17bf21a | ||
|
|
b1ee80edee | ||
|
|
107d496adb | ||
|
|
9f1112b58d | ||
|
|
989d6e3fe7 | ||
|
|
3999c64853 | ||
|
|
760e3d6de0 | ||
|
|
02111a3b9f | ||
|
|
e6af2c0f34 | ||
|
|
bd4c639761 | ||
|
|
d39b7ec021 | ||
|
|
63ca5f5017 | ||
|
|
2202cf457b | ||
|
|
5d04b7abd6 | ||
|
|
0588d5d5f3 | ||
|
|
5a59e443d7 | ||
|
|
470f4df979 | ||
|
|
84bda71330 | ||
|
|
ea883255cb | ||
|
|
e9abb69fb5 | ||
|
|
ff63390794 | ||
|
|
78b3135276 | ||
|
|
15bd2c09ed | ||
|
|
34d44857e4 | ||
|
|
dccded2d3e | ||
|
|
295cafc060 | ||
|
|
c792e97f67 | ||
|
|
d30a02987d | ||
|
|
84d4c9cf73 | ||
|
|
21ecd1f708 | ||
|
|
248b9a8e8c | ||
|
|
3c7abfada6 | ||
|
|
f363656e0a | ||
|
|
e9ee9dbce1 | ||
|
|
ab0b8653ab | ||
|
|
20711e17fb | ||
|
|
a89bd8b816 | ||
|
|
3692cfea64 | ||
|
|
81d9d39029 | ||
|
|
f5a61ceff1 | ||
|
|
404a7b8337 | ||
|
|
71ce3a2920 | ||
|
|
3a27656769 | ||
|
|
27b1e0ffd5 | ||
|
|
1401ea74dd | ||
|
|
cb93a63970 |
@@ -51,6 +51,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
- **PGID**:运行程序用户的`gid`,默认`0`
|
||||
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`
|
||||
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**
|
||||
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`
|
||||
- **NGINX_PORT:** WEB服务端口,默认`3000`,可自行修改,不能与API服务端口冲突
|
||||
- **PORT:** API服务端口,默认`3001`,可自行修改,不能与WEB服务端口冲突
|
||||
- **SUPERUSER:** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
||||
@@ -188,10 +189,11 @@ docker pull jxxghp/moviepilot:latest
|
||||
> `original_title`: 原语种标题
|
||||
> `name`: 识别名称
|
||||
> `year`: 年份
|
||||
> `edition`: 版本
|
||||
> `resourceType`:资源类型
|
||||
> `effect`:特效
|
||||
> `edition`: 版本(资源类型+特效)
|
||||
> `videoFormat`: 分辨率
|
||||
> `releaseGroup`: 制作组/字幕组
|
||||
> `effect`: 特效
|
||||
> `videoCodec`: 视频编码
|
||||
> `audioCodec`: 音频编码
|
||||
> `tmdbid`: TMDBID
|
||||
@@ -212,6 +214,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
> `season`: 季号
|
||||
> `episode`: 集号
|
||||
> `season_episode`: 季集 SxxExx
|
||||
> `episode_title`: 集标题
|
||||
|
||||
`TV_RENAME_FORMAT`默认配置格式:
|
||||
|
||||
|
||||
30
alembic/versions/a521fbc28b18_1_0_9.py
Normal file
30
alembic/versions/a521fbc28b18_1_0_9.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""1_0_9
|
||||
|
||||
Revision ID: a521fbc28b18
|
||||
Revises: b2f011d3a8b7
|
||||
Create Date: 2023-09-28 13:37:16.479360
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a521fbc28b18'
|
||||
down_revision = 'b2f011d3a8b7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("downloadhistory") as batch_op:
|
||||
batch_op.add_column(sa.Column('date', sa.String, nullable=True))
|
||||
batch_op.add_column(sa.Column('channel', sa.String, nullable=True))
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
29
alembic/versions/b2f011d3a8b7_1_0_8.py
Normal file
29
alembic/versions/b2f011d3a8b7_1_0_8.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""1_0_8
|
||||
|
||||
Revision ID: b2f011d3a8b7
|
||||
Revises: 30329639c12b
|
||||
Create Date: 2023-09-28 10:15:58.410003
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b2f011d3a8b7'
|
||||
down_revision = '30329639c12b'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("downloadhistory") as batch_op:
|
||||
batch_op.add_column(sa.Column('userid', sa.String, nullable=True))
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -6,11 +6,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app import schemas
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.event import eventmanager
|
||||
from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
from app.db.models.downloadhistory import DownloadHistory
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.schemas import MediaType
|
||||
from app.schemas.types import EventType
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -78,6 +80,13 @@ def delete_transfer_history(history_in: schemas.TransferHistory,
|
||||
# 删除源文件
|
||||
if deletesrc and history.src:
|
||||
TransferChain(db).delete_files(Path(history.src))
|
||||
# 发送事件
|
||||
eventmanager.send_event(
|
||||
EventType.DownloadFileDeleted,
|
||||
{
|
||||
"src": history.src
|
||||
}
|
||||
)
|
||||
# 删除记录
|
||||
TransferHistory.delete(db, history_in.id)
|
||||
return schemas.Response(success=True)
|
||||
@@ -85,15 +94,18 @@ def delete_transfer_history(history_in: schemas.TransferHistory,
|
||||
|
||||
@router.post("/transfer", summary="历史记录重新转移", response_model=schemas.Response)
|
||||
def redo_transfer_history(history_in: schemas.TransferHistory,
|
||||
mtype: str,
|
||||
new_tmdbid: int,
|
||||
mtype: str = None,
|
||||
new_tmdbid: int = None,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
历史记录重新转移
|
||||
历史记录重新转移,不输入 mtype 和 new_tmdbid 时,自动使用文件名重新识别
|
||||
"""
|
||||
state, errmsg = TransferChain(db).re_transfer(logid=history_in.id,
|
||||
mtype=MediaType(mtype), tmdbid=new_tmdbid)
|
||||
if mtype and new_tmdbid:
|
||||
state, errmsg = TransferChain(db).re_transfer(logid=history_in.id,
|
||||
mtype=MediaType(mtype), tmdbid=new_tmdbid)
|
||||
else:
|
||||
state, errmsg = TransferChain(db).re_transfer(logid=history_in.id)
|
||||
if state:
|
||||
return schemas.Response(success=True)
|
||||
else:
|
||||
|
||||
@@ -31,7 +31,7 @@ def read_sites(db: Session = Depends(get_db),
|
||||
|
||||
|
||||
@router.post("/", summary="新增站点", response_model=schemas.Response)
|
||||
def update_site(
|
||||
def add_site(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
site_in: schemas.Site,
|
||||
|
||||
@@ -88,7 +88,7 @@ def update_subscribe(
|
||||
subscribe = Subscribe.get(db, subscribe_in.id)
|
||||
if not subscribe:
|
||||
return schemas.Response(success=False, message="订阅不存在")
|
||||
if subscribe_in.sites:
|
||||
if subscribe_in.sites is not None:
|
||||
subscribe_in.sites = json.dumps(subscribe_in.sites)
|
||||
# 避免更新缺失集数
|
||||
subscribe_dict = subscribe_in.dict()
|
||||
@@ -162,7 +162,9 @@ def search_subscribes(
|
||||
background_tasks.add_task(
|
||||
Scheduler().start,
|
||||
job_id="subscribe_search",
|
||||
sid=None, state='R'
|
||||
sid=None,
|
||||
state='R',
|
||||
manual=True
|
||||
)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -178,7 +180,9 @@ def search_subscribe(
|
||||
background_tasks.add_task(
|
||||
Scheduler().start,
|
||||
job_id="subscribe_search",
|
||||
sid=subscribe_id, state=None
|
||||
sid=subscribe_id,
|
||||
state=None,
|
||||
manual=True
|
||||
)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from app.core.meta import MetaBase
|
||||
from app.core.module import ModuleManager
|
||||
from app.log import logger
|
||||
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \
|
||||
WebhookEventInfo
|
||||
WebhookEventInfo, TmdbEpisode
|
||||
from app.schemas.types import TorrentStatus, MediaType, MediaImageType, EventType
|
||||
from app.utils.object import ObjectUtils
|
||||
|
||||
@@ -274,7 +274,8 @@ class ChainBase(metaclass=ABCMeta):
|
||||
return self.run_module("list_torrents", status=status, hashs=hashs)
|
||||
|
||||
def transfer(self, path: Path, meta: MetaBase, mediainfo: MediaInfo,
|
||||
transfer_type: str, target: Path = None) -> Optional[TransferInfo]:
|
||||
transfer_type: str, target: Path = None,
|
||||
episodes_info: List[TmdbEpisode] = None) -> Optional[TransferInfo]:
|
||||
"""
|
||||
文件转移
|
||||
:param path: 文件路径
|
||||
@@ -282,10 +283,12 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param transfer_type: 转移模式
|
||||
:param target: 转移目标路径
|
||||
:param episodes_info: 当前季的全部集信息
|
||||
:return: {path, target_path, message}
|
||||
"""
|
||||
return self.run_module("transfer", path=path, meta=meta, mediainfo=mediainfo,
|
||||
transfer_type=transfer_type, target=target)
|
||||
transfer_type=transfer_type, target=target,
|
||||
episodes_info=episodes_info)
|
||||
|
||||
def transfer_completed(self, hashs: Union[str, list], path: Path = None) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple, Set, Dict, Union
|
||||
|
||||
@@ -39,8 +40,10 @@ class DownloadChain(ChainBase):
|
||||
发送添加下载的消息
|
||||
"""
|
||||
msg_text = ""
|
||||
if userid:
|
||||
msg_text = f"用户:{userid}"
|
||||
if torrent.site_name:
|
||||
msg_text = f"站点:{torrent.site_name}"
|
||||
msg_text = f"{msg_text}\n站点:{torrent.site_name}"
|
||||
if meta.resource_term:
|
||||
msg_text = f"{msg_text}\n质量:{meta.resource_term}"
|
||||
if torrent.size:
|
||||
@@ -266,7 +269,10 @@ class DownloadChain(ChainBase):
|
||||
download_hash=_hash,
|
||||
torrent_name=_torrent.title,
|
||||
torrent_description=_torrent.description,
|
||||
torrent_site=_torrent.site_name
|
||||
torrent_site=_torrent.site_name,
|
||||
userid=userid,
|
||||
channel=channel.value if channel else None,
|
||||
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
)
|
||||
|
||||
# 登记下载文件
|
||||
@@ -318,7 +324,7 @@ class DownloadChain(ChainBase):
|
||||
contexts: List[Context],
|
||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||
save_path: str = None,
|
||||
channel: str = None,
|
||||
channel: MessageChannel = None,
|
||||
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||
"""
|
||||
根据缺失数据,自动种子列表中组合择优下载
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, List, Tuple
|
||||
from app.chain import ChainBase
|
||||
from app.core.context import Context, MediaInfo
|
||||
from app.core.meta import MetaBase
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.core.metainfo import MetaInfo, MetaInfoPath
|
||||
from app.log import logger
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
@@ -38,12 +38,8 @@ class MediaChain(ChainBase):
|
||||
"""
|
||||
logger.info(f'开始识别媒体信息,文件:{path} ...')
|
||||
file_path = Path(path)
|
||||
# 上级目录元数据
|
||||
dir_meta = MetaInfo(title=file_path.parent.name)
|
||||
# 文件元数据,不包含后缀
|
||||
file_meta = MetaInfo(title=file_path.stem)
|
||||
# 合并元数据
|
||||
file_meta.merge(dir_meta)
|
||||
# 元数据
|
||||
file_meta = MetaInfoPath(file_path)
|
||||
# 识别媒体信息
|
||||
mediainfo = self.recognize_media(meta=file_meta)
|
||||
if not mediainfo:
|
||||
|
||||
@@ -187,7 +187,7 @@ class MessageChain(ChainBase):
|
||||
# 下载种子
|
||||
context: Context = cache_list[int(text) - 1]
|
||||
# 下载
|
||||
self.downloadchain.download_single(context, userid=userid)
|
||||
self.downloadchain.download_single(context, userid=userid, channel=channel)
|
||||
|
||||
elif text.lower() == "p":
|
||||
# 上一页
|
||||
|
||||
@@ -9,10 +9,11 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.meta import MetaBase
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.core.metainfo import MetaInfoPath
|
||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||
from app.db.models.downloadhistory import DownloadHistory
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
@@ -41,6 +42,7 @@ class TransferChain(ChainBase):
|
||||
self.transferhis = TransferHistoryOper(self._db)
|
||||
self.progress = ProgressHelper()
|
||||
self.mediachain = MediaChain(self._db)
|
||||
self.tmdbchain = TmdbChain(self._db)
|
||||
self.systemconfig = SystemConfigOper()
|
||||
|
||||
def process(self) -> bool:
|
||||
@@ -110,17 +112,6 @@ class TransferChain(ChainBase):
|
||||
logger.warn(f"{path.name} 没有找到可转移的媒体文件")
|
||||
return False, f"{path.name} 没有找到可转移的媒体文件"
|
||||
|
||||
# 汇总错误信息
|
||||
err_msgs: List[str] = []
|
||||
# 汇总季集清单
|
||||
season_episodes: Dict[Tuple, List[int]] = {}
|
||||
# 汇总元数据
|
||||
metas: Dict[Tuple, MetaBase] = {}
|
||||
# 汇总媒体信息
|
||||
medias: Dict[Tuple, MediaInfo] = {}
|
||||
# 汇总转移信息
|
||||
transfers: Dict[Tuple, TransferInfo] = {}
|
||||
|
||||
# 有集自定义格式
|
||||
formaterHandler = FormatParser(eformat=epformat.format,
|
||||
details=epformat.detail,
|
||||
@@ -129,17 +120,24 @@ class TransferChain(ChainBase):
|
||||
|
||||
# 开始进度
|
||||
self.progress.start(ProgressKey.FileTransfer)
|
||||
# 总数
|
||||
# 目录所有文件清单
|
||||
transfer_files = SystemUtils.list_files(directory=path,
|
||||
extensions=settings.RMT_MEDIAEXT,
|
||||
min_filesize=min_filesize)
|
||||
if formaterHandler:
|
||||
# 有集自定义格式,过滤文件
|
||||
transfer_files = [f for f in transfer_files if formaterHandler.match(f.name)]
|
||||
# 总数
|
||||
|
||||
# 汇总错误信息
|
||||
err_msgs: List[str] = []
|
||||
# 总文件数
|
||||
total_num = len(transfer_files)
|
||||
# 已处理数量
|
||||
processed_num = 0
|
||||
# 失败数量
|
||||
fail_num = 0
|
||||
# 跳过数量
|
||||
skip_num = 0
|
||||
self.progress.update(value=0,
|
||||
text=f"开始转移 {path},共 {total_num} 个文件 ...",
|
||||
key=ProgressKey.FileTransfer)
|
||||
@@ -149,6 +147,15 @@ class TransferChain(ChainBase):
|
||||
|
||||
# 处理所有待转移目录或文件,默认一个转移路径或文件只有一个媒体信息
|
||||
for trans_path in trans_paths:
|
||||
# 汇总季集清单
|
||||
season_episodes: Dict[Tuple, List[int]] = {}
|
||||
# 汇总元数据
|
||||
metas: Dict[Tuple, MetaBase] = {}
|
||||
# 汇总媒体信息
|
||||
medias: Dict[Tuple, MediaInfo] = {}
|
||||
# 汇总转移信息
|
||||
transfers: Dict[Tuple, TransferInfo] = {}
|
||||
|
||||
# 如果是目录且不是⼀蓝光原盘,获取所有文件并转移
|
||||
if (not trans_path.is_file()
|
||||
and not SystemUtils.is_bluray_dir(trans_path)):
|
||||
@@ -165,7 +172,6 @@ class TransferChain(ChainBase):
|
||||
|
||||
# 转移所有文件
|
||||
for file_path in file_paths:
|
||||
|
||||
# 回收站及隐藏的文件不处理
|
||||
file_path_str = str(file_path)
|
||||
if file_path_str.find('/@Recycle/') != -1 \
|
||||
@@ -173,6 +179,9 @@ class TransferChain(ChainBase):
|
||||
or file_path_str.find('/.') != -1 \
|
||||
or file_path_str.find('/@eaDir') != -1:
|
||||
logger.debug(f"{file_path_str} 是回收站或隐藏的文件")
|
||||
# 计数
|
||||
processed_num += 1
|
||||
skip_num += 1
|
||||
continue
|
||||
|
||||
# 整理屏蔽词不处理
|
||||
@@ -187,6 +196,9 @@ class TransferChain(ChainBase):
|
||||
break
|
||||
if is_blocked:
|
||||
err_msgs.append(f"{file_path.name} 命中整理屏蔽词")
|
||||
# 计数
|
||||
processed_num += 1
|
||||
skip_num += 1
|
||||
continue
|
||||
|
||||
# 转移成功的不再处理
|
||||
@@ -194,6 +206,9 @@ class TransferChain(ChainBase):
|
||||
transferd = self.transferhis.get_by_src(file_path_str)
|
||||
if transferd and transferd.status:
|
||||
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
|
||||
# 计数
|
||||
processed_num += 1
|
||||
skip_num += 1
|
||||
continue
|
||||
|
||||
# 更新进度
|
||||
@@ -202,12 +217,8 @@ class TransferChain(ChainBase):
|
||||
key=ProgressKey.FileTransfer)
|
||||
|
||||
if not meta:
|
||||
# 上级目录元数据
|
||||
dir_meta = MetaInfo(title=file_path.parent.name)
|
||||
# 文件元数据,不包含后缀
|
||||
file_meta = MetaInfo(title=file_path.stem)
|
||||
# 合并元数据
|
||||
file_meta.merge(dir_meta)
|
||||
# 文件元数据
|
||||
file_meta = MetaInfoPath(file_path)
|
||||
else:
|
||||
file_meta = meta
|
||||
|
||||
@@ -218,6 +229,9 @@ class TransferChain(ChainBase):
|
||||
if not file_meta:
|
||||
logger.error(f"{file_path} 无法识别有效信息")
|
||||
err_msgs.append(f"{file_path} 无法识别有效信息")
|
||||
# 计数
|
||||
processed_num += 1
|
||||
fail_num += 1
|
||||
continue
|
||||
|
||||
# 自定义识别
|
||||
@@ -241,7 +255,7 @@ class TransferChain(ChainBase):
|
||||
# 新增转移失败历史记录
|
||||
his = self.transferhis.add_fail(
|
||||
src_path=file_path,
|
||||
mode=settings.TRANSFER_TYPE,
|
||||
mode=transfer_type,
|
||||
meta=file_meta,
|
||||
download_hash=download_hash
|
||||
)
|
||||
@@ -250,6 +264,9 @@ class TransferChain(ChainBase):
|
||||
title=f"{file_path.name} 未识别到媒体信息,无法入库!\n"
|
||||
f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。"
|
||||
))
|
||||
# 计数
|
||||
processed_num += 1
|
||||
fail_num += 1
|
||||
continue
|
||||
|
||||
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
||||
@@ -261,31 +278,17 @@ class TransferChain(ChainBase):
|
||||
|
||||
logger.info(f"{file_path.name} 识别为:{file_mediainfo.type.value} {file_mediainfo.title_year}")
|
||||
|
||||
# 电视剧没有集无法转移
|
||||
if file_mediainfo.type == MediaType.TV and not file_meta.episode:
|
||||
# 转移失败
|
||||
logger.warn(f"{file_path.name} 入库失败:未识别到集数")
|
||||
err_msgs.append(f"{file_path.name} 未识别到集数")
|
||||
# 新增转移失败历史记录
|
||||
self.transferhis.add_fail(
|
||||
src_path=file_path,
|
||||
mode=settings.TRANSFER_TYPE,
|
||||
download_hash=download_hash,
|
||||
meta=file_meta,
|
||||
mediainfo=file_mediainfo
|
||||
)
|
||||
# 发送消息
|
||||
self.post_message(Notification(
|
||||
mtype=NotificationType.Manual,
|
||||
title=f"{file_path.name} 入库失败!",
|
||||
text=f"原因:未识别到集数",
|
||||
image=file_mediainfo.get_message_image()
|
||||
))
|
||||
continue
|
||||
|
||||
# 更新媒体图片
|
||||
self.obtain_images(mediainfo=file_mediainfo)
|
||||
|
||||
# 获取集数据
|
||||
if file_mediainfo.type == MediaType.TV:
|
||||
episodes_info = self.tmdbchain.tmdb_episodes(tmdbid=file_mediainfo.tmdb_id,
|
||||
season=file_meta.begin_season or 1)
|
||||
else:
|
||||
episodes_info = None
|
||||
|
||||
# 获取下载hash
|
||||
if not download_hash:
|
||||
download_file = self.downloadhis.get_file_by_fullpath(file_path_str)
|
||||
if download_file:
|
||||
@@ -296,7 +299,8 @@ class TransferChain(ChainBase):
|
||||
mediainfo=file_mediainfo,
|
||||
path=file_path,
|
||||
transfer_type=transfer_type,
|
||||
target=target)
|
||||
target=target,
|
||||
episodes_info=episodes_info)
|
||||
if not transferinfo:
|
||||
logger.error("文件转移模块运行失败")
|
||||
return False, "文件转移模块运行失败"
|
||||
@@ -307,7 +311,7 @@ class TransferChain(ChainBase):
|
||||
# 新增转移失败历史记录
|
||||
self.transferhis.add_fail(
|
||||
src_path=file_path,
|
||||
mode=settings.TRANSFER_TYPE,
|
||||
mode=transfer_type,
|
||||
download_hash=download_hash,
|
||||
meta=file_meta,
|
||||
mediainfo=file_mediainfo,
|
||||
@@ -320,6 +324,9 @@ class TransferChain(ChainBase):
|
||||
text=f"原因:{transferinfo.message or '未知'}",
|
||||
image=file_mediainfo.get_message_image()
|
||||
))
|
||||
# 计数
|
||||
processed_num += 1
|
||||
fail_num += 1
|
||||
continue
|
||||
|
||||
# 汇总信息
|
||||
@@ -343,7 +350,7 @@ class TransferChain(ChainBase):
|
||||
# 新增转移成功历史记录
|
||||
self.transferhis.add_success(
|
||||
src_path=file_path,
|
||||
mode=settings.TRANSFER_TYPE,
|
||||
mode=transfer_type,
|
||||
download_hash=download_hash,
|
||||
meta=file_meta,
|
||||
mediainfo=file_mediainfo,
|
||||
@@ -359,8 +366,7 @@ class TransferChain(ChainBase):
|
||||
key=ProgressKey.FileTransfer)
|
||||
|
||||
# 目录或文件转移完成
|
||||
self.progress.update(value=100,
|
||||
text=f"所有文件转移完成,正在执行后续处理 ...",
|
||||
self.progress.update(text=f"{trans_path} 转移完成,正在执行后续处理 ...",
|
||||
key=ProgressKey.FileTransfer)
|
||||
|
||||
# 执行后续处理
|
||||
@@ -387,10 +393,16 @@ class TransferChain(ChainBase):
|
||||
'mediainfo': media,
|
||||
'transferinfo': transfer_info
|
||||
})
|
||||
# 结束进度
|
||||
logger.info(f"{path} 转移完成,共 {total_num} 个文件,"
|
||||
f"成功 {total_num - len(err_msgs)} 个,失败 {len(err_msgs)} 个")
|
||||
self.progress.end(ProgressKey.FileTransfer)
|
||||
|
||||
# 结束进度
|
||||
logger.info(f"{path} 转移完成,共 {total_num} 个文件,"
|
||||
f"失败 {fail_num} 个,跳过 {skip_num} 个")
|
||||
|
||||
self.progress.update(value=100,
|
||||
text=f"{path} 转移完成,共 {total_num} 个文件,"
|
||||
f"失败 {fail_num} 个,跳过 {skip_num} 个",
|
||||
key=ProgressKey.FileTransfer)
|
||||
self.progress.end(ProgressKey.FileTransfer)
|
||||
|
||||
return True, "\n".join(err_msgs)
|
||||
|
||||
@@ -474,7 +486,8 @@ class TransferChain(ChainBase):
|
||||
text=errmsg, userid=userid))
|
||||
return
|
||||
|
||||
def re_transfer(self, logid: int, mtype: MediaType, tmdbid: int) -> Tuple[bool, str]:
|
||||
def re_transfer(self, logid: int,
|
||||
mtype: MediaType = None, tmdbid: int = None) -> Tuple[bool, str]:
|
||||
"""
|
||||
根据历史记录,重新识别转移,只处理对应的src目录
|
||||
:param logid: 历史记录ID
|
||||
@@ -492,11 +505,15 @@ class TransferChain(ChainBase):
|
||||
return False, f"源目录不存在:{src_path}"
|
||||
dest_path = Path(history.dest) if history.dest else None
|
||||
# 查询媒体信息
|
||||
mediainfo = self.recognize_media(mtype=mtype, tmdbid=tmdbid)
|
||||
if mtype and tmdbid:
|
||||
mediainfo = self.recognize_media(mtype=mtype, tmdbid=tmdbid)
|
||||
else:
|
||||
meta = MetaInfoPath(src_path)
|
||||
mediainfo = self.recognize_media(meta=meta)
|
||||
if not mediainfo:
|
||||
return False, f"未识别到媒体信息,类型:{mtype.value},tmdbid:{tmdbid}"
|
||||
# 重新执行转移
|
||||
logger.info(f"{mtype.value} {tmdbid} 识别为:{mediainfo.title_year}")
|
||||
logger.info(f"{src_path.name} 识别为:{mediainfo.title_year}")
|
||||
# 更新媒体图片
|
||||
self.obtain_images(mediainfo=mediainfo)
|
||||
|
||||
@@ -599,6 +616,7 @@ class TransferChain(ChainBase):
|
||||
def delete_files(path: Path):
|
||||
"""
|
||||
删除转移后的文件以及空目录
|
||||
:param path: 文件路径
|
||||
"""
|
||||
logger.info(f"开始删除文件以及空目录:{path} ...")
|
||||
if not path.exists():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Tuple
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta import MetaBase
|
||||
@@ -272,7 +272,7 @@ class MediaInfo:
|
||||
初始化媒信息
|
||||
"""
|
||||
|
||||
def __directors_actors(tmdbinfo: dict):
|
||||
def __directors_actors(tmdbinfo: dict) -> Tuple[List[dict], List[dict]]:
|
||||
"""
|
||||
查询导演和演员
|
||||
:param tmdbinfo: TMDB元数据
|
||||
|
||||
@@ -9,7 +9,7 @@ from app.core.meta.words import WordsMatcher
|
||||
|
||||
def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
||||
"""
|
||||
媒体整理入口,根据名称和副标题,判断是哪种类型的识别,返回对应对象
|
||||
根据标题和副标题识别元数据
|
||||
:param title: 标题、种子名、文件名
|
||||
:param subtitle: 副标题、描述
|
||||
:return: MetaAnime、MetaVideo
|
||||
@@ -33,6 +33,20 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
||||
return meta
|
||||
|
||||
|
||||
def MetaInfoPath(path: Path) -> MetaBase:
|
||||
"""
|
||||
根据路径识别元数据
|
||||
:param path: 路径
|
||||
"""
|
||||
# 上级目录元数据
|
||||
dir_meta = MetaInfo(title=path.parent.name)
|
||||
# 文件元数据,不包含后缀
|
||||
file_meta = MetaInfo(title=path.stem)
|
||||
# 合并元数据
|
||||
file_meta.merge(dir_meta)
|
||||
return file_meta
|
||||
|
||||
|
||||
def is_anime(name: str) -> bool:
|
||||
"""
|
||||
判断是否为动漫
|
||||
|
||||
@@ -74,6 +74,16 @@ class DownloadHistoryOper(DbOper):
|
||||
"""
|
||||
DownloadFiles.delete_by_fullpath(self._db, fullpath)
|
||||
|
||||
def get_hash_by_fullpath(self, fullpath: str) -> str:
|
||||
"""
|
||||
按fullpath查询下载文件记录hash
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
fileinfo: DownloadFiles = DownloadFiles.get_by_fullpath(self._db, fullpath)
|
||||
if fileinfo:
|
||||
return fileinfo.download_hash
|
||||
return ""
|
||||
|
||||
def list_by_page(self, page: int = 1, count: int = 30) -> List[DownloadHistory]:
|
||||
"""
|
||||
分页查询下载历史
|
||||
@@ -98,3 +108,11 @@ class DownloadHistoryOper(DbOper):
|
||||
season=season,
|
||||
episode=episode,
|
||||
tmdbid=tmdbid)
|
||||
|
||||
def list_by_user_date(self, date: str, userid: str = None) -> List[DownloadHistory]:
|
||||
"""
|
||||
查询某用户某时间之后的下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_user_date(db=self._db,
|
||||
date=date,
|
||||
userid=userid)
|
||||
|
||||
@@ -35,6 +35,12 @@ class DownloadHistory(Base):
|
||||
torrent_description = Column(String)
|
||||
# 种子站点
|
||||
torrent_site = Column(String)
|
||||
# 下载用户
|
||||
userid = Column(String)
|
||||
# 下载渠道
|
||||
channel = Column(String)
|
||||
# 创建时间
|
||||
date = Column(String)
|
||||
# 附加信息
|
||||
note = Column(String)
|
||||
|
||||
@@ -90,6 +96,19 @@ class DownloadHistory(Base):
|
||||
DownloadHistory.episodes == episode).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def list_by_user_date(db: Session, date: str, userid: str = None):
|
||||
"""
|
||||
查询某用户某时间之后的下载历史
|
||||
"""
|
||||
if userid:
|
||||
return db.query(DownloadHistory).filter(DownloadHistory.date < date,
|
||||
DownloadHistory.userid == userid).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
else:
|
||||
return db.query(DownloadHistory).filter(DownloadHistory.date < date).order_by(
|
||||
DownloadHistory.id.desc()).all()
|
||||
|
||||
|
||||
class DownloadFiles(Base):
|
||||
"""
|
||||
|
||||
@@ -65,6 +65,10 @@ class TransferHistory(Base):
|
||||
def get_by_src(db: Session, src: str):
|
||||
return db.query(TransferHistory).filter(TransferHistory.src == src).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_hash(db: Session, download_hash: str):
|
||||
return db.query(TransferHistory).filter(TransferHistory.download_hash == download_hash).all()
|
||||
|
||||
@staticmethod
|
||||
def statistic(db: Session, days: int = 7):
|
||||
"""
|
||||
|
||||
@@ -36,6 +36,13 @@ class TransferHistoryOper(DbOper):
|
||||
"""
|
||||
return TransferHistory.get_by_src(self._db, src)
|
||||
|
||||
def list_by_hash(self, download_hash: str) -> List[TransferHistory]:
|
||||
"""
|
||||
按种子hash查询转移记录
|
||||
:param download_hash: 种子hash
|
||||
"""
|
||||
return TransferHistory.list_by_hash(self._db, download_hash)
|
||||
|
||||
def add(self, **kwargs) -> TransferHistory:
|
||||
"""
|
||||
新增转移历史
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class NfoReader:
|
||||
@@ -8,6 +9,9 @@ class NfoReader:
|
||||
self.tree = ET.parse(xml_file_path)
|
||||
self.root = self.tree.getroot()
|
||||
|
||||
def get_element_value(self, element_path):
|
||||
def get_element_value(self, element_path) -> Optional[str]:
|
||||
element = self.root.find(element_path)
|
||||
return element.text if element is not None else None
|
||||
|
||||
def get_elements(self, element_path) -> List[ET.Element]:
|
||||
return self.root.findall(element_path)
|
||||
|
||||
@@ -28,7 +28,7 @@ class EmbyModule(_ModuleBase):
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.emby.is_inactive():
|
||||
self.emby = Emby()
|
||||
self.emby.reconnect()
|
||||
|
||||
def user_authenticate(self, name: str, password: str) -> Optional[str]:
|
||||
"""
|
||||
|
||||
@@ -35,6 +35,13 @@ class Emby(metaclass=Singleton):
|
||||
return False
|
||||
return True if not self.user else False
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
重连
|
||||
"""
|
||||
self.user = self.get_user()
|
||||
self.folders = self.get_emby_folders()
|
||||
|
||||
def get_emby_folders(self) -> List[dict]:
|
||||
"""
|
||||
获取Emby媒体库路径列表
|
||||
@@ -481,14 +488,14 @@ class Emby(metaclass=Singleton):
|
||||
# 匹配子目录
|
||||
subfolder_path = Path(subfolder.get("Path"))
|
||||
if item_path.is_relative_to(subfolder_path):
|
||||
return subfolder.get("Id")
|
||||
return folder.get("Id")
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
# 如果找不到,只要路径中有分类目录名就命中
|
||||
for subfolder in folder.get("SubFolders"):
|
||||
if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category,
|
||||
subfolder.get("Path")):
|
||||
return folder.get("Id")
|
||||
# 如果找不到,只要路径中有分类目录名就命中
|
||||
for subfolder in folder.get("SubFolders"):
|
||||
if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category,
|
||||
subfolder.get("Path")):
|
||||
return folder.get("Id")
|
||||
# 刷新根目录
|
||||
return "/"
|
||||
|
||||
|
||||
@@ -329,7 +329,11 @@ class FanartModule(_ModuleBase):
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
result = self.__request_fanart(mediainfo.type, mediainfo.tmdb_id)
|
||||
else:
|
||||
result = self.__request_fanart(mediainfo.type, mediainfo.tvdb_id)
|
||||
if mediainfo.tvdb_id:
|
||||
result = self.__request_fanart(mediainfo.type, mediainfo.tvdb_id)
|
||||
else:
|
||||
logger.info(f"{mediainfo.title_year} 没有tvdbid,无法获取Fanart图片")
|
||||
return
|
||||
if not result or result.get('status') == 'error':
|
||||
logger.warn(f"没有获取到 {mediainfo.title_year} 的Fanart图片数据")
|
||||
return
|
||||
@@ -351,6 +355,7 @@ class FanartModule(_ModuleBase):
|
||||
# 季图片格式 seasonxx-poster
|
||||
image_name = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
||||
if not mediainfo.get_image(image_name):
|
||||
# 没有图片才设置
|
||||
mediainfo.set_image(image_name, image_obj.get('url'))
|
||||
|
||||
return mediainfo
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.core.meta import MetaBase
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.schemas import TransferInfo, ExistMediaInfo
|
||||
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
@@ -30,7 +30,8 @@ class FileTransferModule(_ModuleBase):
|
||||
pass
|
||||
|
||||
def transfer(self, path: Path, meta: MetaBase, mediainfo: MediaInfo,
|
||||
transfer_type: str, target: Path = None) -> TransferInfo:
|
||||
transfer_type: str, target: Path = None,
|
||||
episodes_info: List[TmdbEpisode] = None) -> TransferInfo:
|
||||
"""
|
||||
文件转移
|
||||
:param path: 文件路径
|
||||
@@ -38,6 +39,7 @@ class FileTransferModule(_ModuleBase):
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param transfer_type: 转移方式
|
||||
:param target: 目标路径
|
||||
:param episodes_info: 当前季的全部集信息
|
||||
:return: {path, target_path, message}
|
||||
"""
|
||||
# 获取目标路径
|
||||
@@ -49,13 +51,14 @@ class FileTransferModule(_ModuleBase):
|
||||
logger.error("未找到媒体库目录,无法转移文件")
|
||||
return TransferInfo(success=False,
|
||||
path=path,
|
||||
message="未找到媒体库目录,无法转移文件")
|
||||
message="未找到媒体库目录")
|
||||
# 转移
|
||||
return self.transfer_media(in_path=path,
|
||||
in_meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
transfer_type=transfer_type,
|
||||
target_dir=target)
|
||||
target_dir=target,
|
||||
episodes_info=episodes_info)
|
||||
|
||||
@staticmethod
|
||||
def __transfer_command(file_item: Path, target_file: Path, transfer_type: str) -> int:
|
||||
@@ -355,6 +358,7 @@ class FileTransferModule(_ModuleBase):
|
||||
mediainfo: MediaInfo,
|
||||
transfer_type: str,
|
||||
target_dir: Path,
|
||||
episodes_info: List[TmdbEpisode] = None
|
||||
) -> TransferInfo:
|
||||
"""
|
||||
识别并转移一个文件或者一个目录下的所有文件
|
||||
@@ -363,6 +367,7 @@ class FileTransferModule(_ModuleBase):
|
||||
:param mediainfo: 媒体信息
|
||||
:param target_dir: 媒体库根目录
|
||||
:param transfer_type: 文件转移方式
|
||||
:param episodes_info: 当前季的全部集信息
|
||||
:return: TransferInfo、错误信息
|
||||
"""
|
||||
# 检查目录路径
|
||||
@@ -404,7 +409,7 @@ class FileTransferModule(_ModuleBase):
|
||||
if retcode != 0:
|
||||
logger.error(f"文件夹 {in_path} 转移失败,错误码:{retcode}")
|
||||
return TransferInfo(success=False,
|
||||
message=f"文件夹 {in_path} 转移失败,错误码:{retcode}",
|
||||
message=f"错误码:{retcode}",
|
||||
path=in_path,
|
||||
target_path=new_path,
|
||||
is_bluray=bluray_flag)
|
||||
@@ -418,17 +423,24 @@ class FileTransferModule(_ModuleBase):
|
||||
is_bluray=bluray_flag)
|
||||
else:
|
||||
# 转移单个文件
|
||||
# 文件结束季为空
|
||||
in_meta.end_season = None
|
||||
if mediainfo.type == MediaType.TV:
|
||||
# 电视剧
|
||||
if in_meta.begin_episode is None:
|
||||
logger.warn(f"文件 {in_path} 转移失败:未识别到文件集数")
|
||||
return TransferInfo(success=False,
|
||||
message=f"未识别到文件集数",
|
||||
path=in_path,
|
||||
fail_list=[str(in_path)])
|
||||
|
||||
# 文件总季数为1
|
||||
if in_meta.total_season:
|
||||
in_meta.total_season = 1
|
||||
|
||||
# 文件不可能有多集
|
||||
if in_meta.total_episode > 2:
|
||||
in_meta.total_episode = 1
|
||||
in_meta.end_episode = None
|
||||
# 文件结束季为空
|
||||
in_meta.end_season = None
|
||||
# 文件总季数为1
|
||||
if in_meta.total_season:
|
||||
in_meta.total_season = 1
|
||||
# 文件不可能超过2集
|
||||
if in_meta.total_episode > 2:
|
||||
in_meta.total_episode = 1
|
||||
in_meta.end_episode = None
|
||||
|
||||
# 目的文件名
|
||||
new_file = self.get_rename_path(
|
||||
@@ -437,6 +449,7 @@ class FileTransferModule(_ModuleBase):
|
||||
rename_dict=self.__get_naming_dict(
|
||||
meta=in_meta,
|
||||
mediainfo=mediainfo,
|
||||
episodes_info=episodes_info,
|
||||
file_ext=in_path.suffix
|
||||
)
|
||||
)
|
||||
@@ -456,7 +469,7 @@ class FileTransferModule(_ModuleBase):
|
||||
if retcode != 0:
|
||||
logger.error(f"文件 {in_path} 转移失败,错误码:{retcode}")
|
||||
return TransferInfo(success=False,
|
||||
message=f"文件 {in_path.name} 转移失败,错误码:{retcode}",
|
||||
message=f"错误码:{retcode}",
|
||||
path=in_path,
|
||||
target_path=new_file,
|
||||
fail_list=[str(in_path)])
|
||||
@@ -472,13 +485,23 @@ class FileTransferModule(_ModuleBase):
|
||||
file_list_new=[str(new_file)])
|
||||
|
||||
@staticmethod
|
||||
def __get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: str = None) -> dict:
|
||||
def __get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: str = None,
|
||||
episodes_info: List[TmdbEpisode] = None) -> dict:
|
||||
"""
|
||||
根据媒体信息,返回Format字典
|
||||
:param meta: 文件元数据
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:param file_ext: 文件扩展名
|
||||
:param episodes_info: 当前季的全部集信息
|
||||
"""
|
||||
# 获取集标题
|
||||
episode_title = None
|
||||
if meta.begin_episode and episodes_info:
|
||||
for episode in episodes_info:
|
||||
if episode.episode_number == meta.begin_episode:
|
||||
episode_title = episode.name
|
||||
break
|
||||
|
||||
return {
|
||||
# 标题
|
||||
"title": mediainfo.title,
|
||||
@@ -490,14 +513,16 @@ class FileTransferModule(_ModuleBase):
|
||||
"name": meta.name,
|
||||
# 年份
|
||||
"year": mediainfo.year or meta.year,
|
||||
# 资源类型
|
||||
"resourceType": meta.resource_type,
|
||||
# 特效
|
||||
"effect": meta.resource_effect,
|
||||
# 版本
|
||||
"edition": meta.edition,
|
||||
# 分辨率
|
||||
"videoFormat": meta.resource_pix,
|
||||
# 制作组/字幕组
|
||||
"releaseGroup": meta.resource_team,
|
||||
# 特效
|
||||
"effect": meta.resource_effect,
|
||||
# 视频编码
|
||||
"videoCodec": meta.video_encode,
|
||||
# 音频编码
|
||||
@@ -514,6 +539,8 @@ class FileTransferModule(_ModuleBase):
|
||||
"season_episode": "%s%s" % (meta.season, meta.episodes),
|
||||
# 段/节
|
||||
"part": meta.part,
|
||||
# 剧集标题
|
||||
"episode_title": episode_title,
|
||||
# 文件后缀
|
||||
"fileExt": file_ext
|
||||
}
|
||||
@@ -613,9 +640,10 @@ class FileTransferModule(_ModuleBase):
|
||||
rename_format = settings.TV_RENAME_FORMAT \
|
||||
if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT
|
||||
# 相对路径
|
||||
meta = MetaInfo(mediainfo.title)
|
||||
rel_path = self.get_rename_path(
|
||||
template_string=rename_format,
|
||||
rename_dict=self.__get_naming_dict(meta=MetaInfo(mediainfo.title),
|
||||
rename_dict=self.__get_naming_dict(meta=meta,
|
||||
mediainfo=mediainfo)
|
||||
)
|
||||
# 取相对路径的第1层目录
|
||||
|
||||
@@ -96,7 +96,7 @@ class FilterModule(_ModuleBase):
|
||||
},
|
||||
# 国语配音
|
||||
"CNVOI": {
|
||||
"include": [r'[国國][语語]配音|[国國]配'],
|
||||
"include": [r'[国國][语語]配音|[国國]配|[国國][语語]'],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||
|
||||
@@ -26,7 +25,7 @@ class JellyfinModule(_ModuleBase):
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.jellyfin.is_inactive():
|
||||
self.jellyfin = Jellyfin()
|
||||
self.jellyfin.reconnect()
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
@@ -33,6 +33,13 @@ class Jellyfin(metaclass=Singleton):
|
||||
return False
|
||||
return True if not self.user else False
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
重连
|
||||
"""
|
||||
self.user = self.get_user()
|
||||
self.serverid = self.get_server_id()
|
||||
|
||||
def __get_jellyfin_librarys(self) -> List[dict]:
|
||||
"""
|
||||
获取Jellyfin媒体库的信息
|
||||
|
||||
@@ -29,7 +29,7 @@ class PlexModule(_ModuleBase):
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.plex.is_inactive():
|
||||
self.plex = Plex()
|
||||
self.plex.reconnect()
|
||||
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||
"""
|
||||
|
||||
@@ -38,6 +38,17 @@ class Plex(metaclass=Singleton):
|
||||
return False
|
||||
return True if not self._plex else False
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
重连
|
||||
"""
|
||||
try:
|
||||
self._plex = PlexServer(self._host, self._token)
|
||||
self._libraries = self._plex.library.sections()
|
||||
except Exception as e:
|
||||
self._plex = None
|
||||
logger.error(f"Plex服务器连接失败:{str(e)}")
|
||||
|
||||
def get_librarys(self):
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
|
||||
@@ -34,7 +34,7 @@ class QbittorrentModule(_ModuleBase):
|
||||
"""
|
||||
# 定时重连
|
||||
if self.qbittorrent.is_inactive():
|
||||
self.qbittorrent = Qbittorrent()
|
||||
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]]:
|
||||
|
||||
@@ -35,6 +35,12 @@ class Qbittorrent(metaclass=Singleton):
|
||||
return False
|
||||
return True if not self.qbc else False
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
重连
|
||||
"""
|
||||
self.qbc = self.__login_qbittorrent()
|
||||
|
||||
def __login_qbittorrent(self) -> Optional[Client]:
|
||||
"""
|
||||
连接qbittorrent
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.schemas.types import MediaType
|
||||
from app.utils.common import retry
|
||||
from app.utils.dom import DomUtils
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class TmdbScraper:
|
||||
@@ -121,8 +122,25 @@ class TmdbScraper:
|
||||
except Exception as e:
|
||||
logger.error(f"{file_path} 刮削失败:{e}")
|
||||
|
||||
@staticmethod
|
||||
def __gen_common_nfo(mediainfo: MediaInfo, doc, root):
|
||||
def __get_chinese_name(self, person: dict):
|
||||
"""
|
||||
获取TMDB别名中的中文名
|
||||
"""
|
||||
if not person.get("id"):
|
||||
return ""
|
||||
try:
|
||||
personinfo = self.tmdb.get_person_detail(person.get("id"))
|
||||
if personinfo:
|
||||
also_known_as = personinfo.get("also_known_as") or []
|
||||
if also_known_as:
|
||||
for name in also_known_as:
|
||||
if name and StringUtils.is_chinese(name):
|
||||
return name
|
||||
except Exception as err:
|
||||
logger.error(f"获取人物中文名失败:{err}")
|
||||
return person.get("name") or ""
|
||||
|
||||
def __gen_common_nfo(self, mediainfo: MediaInfo, doc, root):
|
||||
"""
|
||||
生成公共NFO
|
||||
"""
|
||||
@@ -155,18 +173,19 @@ class TmdbScraper:
|
||||
xoutline.appendChild(doc.createCDATASection(mediainfo.overview or ""))
|
||||
# 导演
|
||||
for director in mediainfo.directors:
|
||||
xdirector = DomUtils.add_node(doc, root, "director", director.get("name") or "")
|
||||
# 获取中文名
|
||||
cn_name = self.__get_chinese_name(director)
|
||||
xdirector = DomUtils.add_node(doc, root, "director", cn_name)
|
||||
xdirector.setAttribute("tmdbid", str(director.get("id") or ""))
|
||||
# 演员
|
||||
for actor in mediainfo.actors:
|
||||
# 获取中文名
|
||||
cn_name = self.__get_chinese_name(actor)
|
||||
xactor = DomUtils.add_node(doc, root, "actor")
|
||||
DomUtils.add_node(doc, xactor, "name", actor.get("name") or "")
|
||||
DomUtils.add_node(doc, xactor, "name", cn_name)
|
||||
DomUtils.add_node(doc, xactor, "type", "Actor")
|
||||
DomUtils.add_node(doc, xactor, "role", actor.get("character") or actor.get("role") or "")
|
||||
DomUtils.add_node(doc, xactor, "order", actor.get("order") if actor.get("order") is not None else "")
|
||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||
DomUtils.add_node(doc, xactor, "thumb", actor.get('image'))
|
||||
DomUtils.add_node(doc, xactor, "profile", actor.get('profile'))
|
||||
# 风格
|
||||
genres = mediainfo.genres or []
|
||||
for genre in genres:
|
||||
@@ -307,14 +326,18 @@ class TmdbScraper:
|
||||
directors = episodeinfo.get("crew") or []
|
||||
for director in directors:
|
||||
if director.get("known_for_department") == "Directing":
|
||||
xdirector = DomUtils.add_node(doc, root, "director", director.get("name") or "")
|
||||
# 获取中文名
|
||||
cn_name = self.__get_chinese_name(director)
|
||||
xdirector = DomUtils.add_node(doc, root, "director", cn_name)
|
||||
xdirector.setAttribute("tmdbid", str(director.get("id") or ""))
|
||||
# 演员
|
||||
actors = episodeinfo.get("guest_stars") or []
|
||||
for actor in actors:
|
||||
if actor.get("known_for_department") == "Acting":
|
||||
# 获取中文名
|
||||
cn_name = self.__get_chinese_name(actor)
|
||||
xactor = DomUtils.add_node(doc, root, "actor")
|
||||
DomUtils.add_node(doc, xactor, "name", actor.get("name") or "")
|
||||
DomUtils.add_node(doc, xactor, "name", cn_name)
|
||||
DomUtils.add_node(doc, xactor, "type", "Actor")
|
||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||
# 保存文件
|
||||
@@ -336,6 +359,8 @@ class TmdbScraper:
|
||||
logger.info(f"图片已保存:{file_path}")
|
||||
else:
|
||||
logger.info(f"{file_path.stem}图片下载失败,请检查网络连通性")
|
||||
except RequestException as err:
|
||||
raise err
|
||||
except Exception as err:
|
||||
logger.error(f"{file_path.stem}图片下载失败:{err}")
|
||||
|
||||
|
||||
@@ -1136,6 +1136,26 @@ class TmdbHelper:
|
||||
def get_person_detail(self, person_id: int) -> dict:
|
||||
"""
|
||||
获取人物详情
|
||||
{
|
||||
"adult": false,
|
||||
"also_known_as": [
|
||||
"Michael Chen",
|
||||
"Chen He",
|
||||
"陈赫"
|
||||
],
|
||||
"biography": "陈赫,xxx",
|
||||
"birthday": "1985-11-09",
|
||||
"deathday": null,
|
||||
"gender": 2,
|
||||
"homepage": "https://movie.douban.com/celebrity/1313841/",
|
||||
"id": 1397016,
|
||||
"imdb_id": "nm4369305",
|
||||
"known_for_department": "Acting",
|
||||
"name": "Chen He",
|
||||
"place_of_birth": "Fuzhou,Fujian Province,China",
|
||||
"popularity": 9.228,
|
||||
"profile_path": "/2Bk39zVuoHUNHtpZ7LVg7OgkDd4.jpg"
|
||||
}
|
||||
"""
|
||||
if not self.person:
|
||||
return {}
|
||||
|
||||
@@ -30,6 +30,7 @@ class TMDb(object):
|
||||
self.__class__._session = requests.Session() if session is None else session
|
||||
self._remaining = 40
|
||||
self._reset = None
|
||||
self._timeout = 15
|
||||
self.obj_cached = obj_cached
|
||||
if os.environ.get(self.TMDB_LANGUAGE) is None:
|
||||
os.environ[self.TMDB_LANGUAGE] = "en-US"
|
||||
@@ -131,12 +132,14 @@ class TMDb(object):
|
||||
|
||||
@lru_cache(maxsize=REQUEST_CACHE_MAXSIZE)
|
||||
def cached_request(self, method, url, data, json):
|
||||
return requests.request(method, url, data=data, json=json, proxies=self.proxies)
|
||||
return requests.request(method, url, data=data, json=json,
|
||||
timeout=self._timeout, proxies=self.proxies)
|
||||
|
||||
def cache_clear(self):
|
||||
return self.cached_request.cache_clear()
|
||||
|
||||
def _request_obj(self, action, params="", call_cached=True, method="GET", data=None, json=None, key=None):
|
||||
def _request_obj(self, action, params="", call_cached=True,
|
||||
method="GET", data=None, json=None, key=None):
|
||||
if self.api_key is None or self.api_key == "":
|
||||
raise TMDbException("No API key found.")
|
||||
|
||||
@@ -151,7 +154,8 @@ class TMDb(object):
|
||||
if self.cache and self.obj_cached and call_cached and method != "POST":
|
||||
req = self.cached_request(method, url, data, json)
|
||||
else:
|
||||
req = self.__class__._session.request(method, url, data=data, json=json, proxies=self.proxies)
|
||||
req = self.__class__._session.request(method, url, data=data, json=json,
|
||||
timeout=self._timeout, proxies=self.proxies)
|
||||
|
||||
headers = req.headers
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class TransmissionModule(_ModuleBase):
|
||||
"""
|
||||
# 定时重连
|
||||
if not self.transmission.is_inactive():
|
||||
self.transmission = Transmission()
|
||||
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]]:
|
||||
|
||||
@@ -56,6 +56,12 @@ class Transmission(metaclass=Singleton):
|
||||
return False
|
||||
return True if not self.trc else False
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
重连
|
||||
"""
|
||||
self.trc = self.__login_transmission()
|
||||
|
||||
def get_torrents(self, ids: Union[str, list] = None, status: Union[str, list] = None,
|
||||
tags: Union[str, list] = None) -> Tuple[List[Torrent], bool]:
|
||||
"""
|
||||
|
||||
551
app/plugins/autoclean/__init__.py
Normal file
551
app/plugins/autoclean/__init__.py
Normal file
@@ -0,0 +1,551 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager
|
||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.plugins import _PluginBase
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
from app.log import logger
|
||||
from app.schemas import NotificationType, DownloadHistory
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
class AutoClean(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "定时清理媒体库"
|
||||
# 插件描述
|
||||
plugin_desc = "定时清理用户下载的种子、源文件、媒体库文件。"
|
||||
# 插件图标
|
||||
plugin_icon = "clean.png"
|
||||
# 主题色
|
||||
plugin_color = "#3377ed"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/thsrite"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "autoclean_"
|
||||
# 加载顺序
|
||||
plugin_order = 23
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
_enabled = False
|
||||
# 任务执行间隔
|
||||
_cron = None
|
||||
_type = None
|
||||
_onlyonce = False
|
||||
_notify = False
|
||||
_cleantype = None
|
||||
_cleanuser = None
|
||||
_cleandate = None
|
||||
_downloadhis = None
|
||||
_transferhis = None
|
||||
|
||||
# 定时器
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._cron = config.get("cron")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._notify = config.get("notify")
|
||||
self._cleantype = config.get("cleantype")
|
||||
self._cleanuser = config.get("cleanuser")
|
||||
self._cleandate = config.get("cleandate")
|
||||
|
||||
# 加载模块
|
||||
if self._enabled:
|
||||
self._downloadhis = DownloadHistoryOper(self.db)
|
||||
self._transferhis = TransferHistoryOper(self.db)
|
||||
# 定时服务
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.__clean,
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="定时清理媒体库")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
|
||||
if self._onlyonce:
|
||||
logger.info(f"定时清理媒体库服务启动,立即运行一次")
|
||||
self._scheduler.add_job(func=self.__clean, trigger='date',
|
||||
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3),
|
||||
name="定时清理媒体库")
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
self.update_config({
|
||||
"onlyonce": False,
|
||||
"cron": self._cron,
|
||||
"cleantype": self._cleantype,
|
||||
"enabled": self._enabled,
|
||||
"cleanuser": self._cleanuser,
|
||||
"cleandate": self._cleandate,
|
||||
"notify": self._notify,
|
||||
})
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def __clean(self):
|
||||
"""
|
||||
定时清理媒体库
|
||||
"""
|
||||
if not self._cleandate:
|
||||
logger.error("未配置清理媒体库时间,停止运行")
|
||||
return
|
||||
|
||||
# 清理日期
|
||||
current_time = datetime.now()
|
||||
days_ago = current_time - timedelta(days=int(self._cleandate))
|
||||
clean_date = days_ago.strftime("%Y-%m-%d")
|
||||
|
||||
# 查询用户清理日期之后的下载历史
|
||||
if not self._cleanuser:
|
||||
downloadhis_list = self._downloadhis.list_by_user_date(date=clean_date)
|
||||
logger.info(f'获取到日期 {clean_date} 之后的下载历史 {len(downloadhis_list)} 条')
|
||||
|
||||
self.__clean_history(date=clean_date, downloadhis_list=downloadhis_list)
|
||||
else:
|
||||
for userid in str(self._cleanuser).split(","):
|
||||
downloadhis_list = self._downloadhis.list_by_user_date(date=clean_date,
|
||||
userid=userid)
|
||||
logger.info(
|
||||
f'获取到用户 {userid} 日期 {clean_date} 之后的下载历史 {len(downloadhis_list)} 条')
|
||||
self.__clean_history(date=clean_date, downloadhis_list=downloadhis_list, userid=userid)
|
||||
|
||||
def __clean_history(self, date: str, downloadhis_list: List[DownloadHistory], userid: str = None):
|
||||
"""
|
||||
清理下载历史、转移记录
|
||||
"""
|
||||
if not downloadhis_list:
|
||||
logger.warn(f"未获取到日期 {date} 之后的下载记录,停止运行")
|
||||
return
|
||||
|
||||
# 读取历史记录
|
||||
history = self.get_data('history') or []
|
||||
|
||||
# 创建一个字典来保存分组结果
|
||||
downloadhis_grouped_dict: Dict[tuple, List[DownloadHistory]] = defaultdict(list)
|
||||
# 遍历DownloadHistory对象列表
|
||||
for downloadhis in downloadhis_list:
|
||||
# 获取type和tmdbid的值
|
||||
dtype = downloadhis.type
|
||||
tmdbid = downloadhis.tmdbid
|
||||
|
||||
# 将DownloadHistory对象添加到对应分组的列表中
|
||||
downloadhis_grouped_dict[(dtype, tmdbid)].append(downloadhis)
|
||||
|
||||
# 输出分组结果
|
||||
for key, downloadhis_list in downloadhis_grouped_dict.items():
|
||||
logger.info(f"开始清理 {key}")
|
||||
|
||||
del_transferhis_cnt = 0
|
||||
del_media_name = downloadhis_list[0].title
|
||||
del_media_user = downloadhis_list[0].userid
|
||||
del_media_type = downloadhis_list[0].type
|
||||
del_media_year = downloadhis_list[0].year
|
||||
del_media_season = downloadhis_list[0].seasons
|
||||
del_media_episode = downloadhis_list[0].episodes
|
||||
del_image = downloadhis_list[0].image
|
||||
for downloadhis in downloadhis_list:
|
||||
if not downloadhis.download_hash:
|
||||
logger.debug(f'下载历史 {downloadhis.id} {downloadhis.title} 未获取到download_hash,跳过处理')
|
||||
continue
|
||||
# 根据hash获取转移记录
|
||||
transferhis_list = self._transferhis.list_by_hash(download_hash=downloadhis.download_hash)
|
||||
if not transferhis_list:
|
||||
logger.warn(f"下载历史 {downloadhis.download_hash} 未查询到转移记录,跳过处理")
|
||||
continue
|
||||
|
||||
for history in transferhis_list:
|
||||
# 册除媒体库文件
|
||||
if str(self._cleantype == "dest") or str(self._cleantype == "all"):
|
||||
TransferChain(self.db).delete_files(Path(history.dest))
|
||||
# 删除记录
|
||||
self._transferhis.delete(history.id)
|
||||
# 删除源文件
|
||||
if str(self._cleantype == "src") or str(self._cleantype == "all"):
|
||||
TransferChain(self.db).delete_files(Path(history.src))
|
||||
# 发送事件
|
||||
eventmanager.send_event(
|
||||
EventType.DownloadFileDeleted,
|
||||
{
|
||||
"src": history.src
|
||||
}
|
||||
)
|
||||
|
||||
# 累加删除数量
|
||||
del_transferhis_cnt += len(transferhis_list)
|
||||
|
||||
# 发送消息
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.MediaServer,
|
||||
title="【定时清理媒体库任务完成】",
|
||||
text=f"清理媒体名称 {del_media_name}\n"
|
||||
f"下载媒体用户 {del_media_user}\n"
|
||||
f"删除历史记录 {del_transferhis_cnt}",
|
||||
userid=userid)
|
||||
|
||||
history.append({
|
||||
"type": del_media_type,
|
||||
"title": del_media_name,
|
||||
"year": del_media_year,
|
||||
"season": del_media_season,
|
||||
"episode": del_media_episode,
|
||||
"image": del_image,
|
||||
"del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
|
||||
})
|
||||
|
||||
# 保存历史
|
||||
self.save_data("history", history)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '开启通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '0 0 ? ? ?'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'cleantype',
|
||||
'label': '清理方式',
|
||||
'items': [
|
||||
{'title': '媒体库文件', 'value': 'dest'},
|
||||
{'title': '源文件', 'value': 'src'},
|
||||
{'title': '所有文件', 'value': 'all'},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cleandate',
|
||||
'label': '清理媒体日期',
|
||||
'placeholder': '清理多少天之前的下载记录(天)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cleanuser',
|
||||
'label': '清理下载用户',
|
||||
'placeholder': '多个用户,分割'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"onlyonce": False,
|
||||
"notify": False,
|
||||
"cleantype": "dest",
|
||||
"cron": "",
|
||||
"cleanuser": "",
|
||||
"cleandate": 30
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
# 查询同步详情
|
||||
historys = self.get_data('history')
|
||||
if not historys:
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'text': '暂无数据',
|
||||
'props': {
|
||||
'class': 'text-center',
|
||||
}
|
||||
}
|
||||
]
|
||||
# 数据按时间降序排序
|
||||
historys = sorted(historys, key=lambda x: x.get('del_time'), reverse=True)
|
||||
# 拼装页面
|
||||
contents = []
|
||||
for history in historys:
|
||||
htype = history.get("type")
|
||||
title = history.get("title")
|
||||
year = history.get("year")
|
||||
season = history.get("season")
|
||||
episode = history.get("episode")
|
||||
image = history.get("image")
|
||||
del_time = history.get("del_time")
|
||||
|
||||
if season:
|
||||
sub_contents = [
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'类型:{htype}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'标题:{title}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'年份:{year}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'季:{season}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'集:{episode}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'时间:{del_time}'
|
||||
}
|
||||
]
|
||||
else:
|
||||
sub_contents = [
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'类型:{htype}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'标题:{title}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'年份:{year}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'时间:{del_time}'
|
||||
}
|
||||
]
|
||||
|
||||
contents.append(
|
||||
{
|
||||
'component': 'VCard',
|
||||
'content': [
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'd-flex justify-space-start flex-nowrap flex-row',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'div',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VImg',
|
||||
'props': {
|
||||
'src': image,
|
||||
'height': 120,
|
||||
'width': 80,
|
||||
'aspect-ratio': '2/3',
|
||||
'class': 'object-cover shadow ring-gray-500',
|
||||
'cover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'div',
|
||||
'content': sub_contents
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'grid gap-3 grid-info-card',
|
||||
},
|
||||
'content': contents
|
||||
}
|
||||
]
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error("退出插件失败:%s" % str(e))
|
||||
@@ -43,7 +43,7 @@ class BrushFlow(_PluginBase):
|
||||
# 加载顺序
|
||||
plugin_order = 21
|
||||
# 可使用的用户级别
|
||||
auth_level = 3
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
siteshelper = None
|
||||
@@ -120,6 +120,9 @@ class BrushFlow(_PluginBase):
|
||||
self.save_data("statistic", {})
|
||||
# 清除种子记录
|
||||
self.save_data("torrents", {})
|
||||
# 关闭一次性开关
|
||||
self._clear_task = False
|
||||
self.__update_config()
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
@@ -729,6 +732,7 @@ class BrushFlow(_PluginBase):
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"onlyonce": False,
|
||||
"clear_task": False,
|
||||
"freeleech": "free"
|
||||
}
|
||||
|
||||
@@ -1218,7 +1222,8 @@ class BrushFlow(_PluginBase):
|
||||
"seed_inactivetime": self._seed_inactivetime,
|
||||
"up_speed": self._up_speed,
|
||||
"dl_speed": self._dl_speed,
|
||||
"save_path": self._save_path
|
||||
"save_path": self._save_path,
|
||||
"clear_task": self._clear_task
|
||||
})
|
||||
|
||||
def brush(self):
|
||||
@@ -1265,12 +1270,6 @@ class BrushFlow(_PluginBase):
|
||||
f"{task.get('site_name')}{task.get('title')}" for task in task_info.values()
|
||||
]:
|
||||
continue
|
||||
# 保种体积(GB) 促销
|
||||
if self._disksize \
|
||||
and (torrents_size + torrent.size) > float(self._disksize) * 1024**3:
|
||||
logger.warn(f"当前做种体积 {StringUtils.str_filesize(torrents_size)} "
|
||||
f"已超过保种体积 {self._disksize},停止新增任务")
|
||||
break
|
||||
# 促销
|
||||
if self._freeleech and torrent.downloadvolumefactor != 0:
|
||||
continue
|
||||
@@ -1349,6 +1348,12 @@ class BrushFlow(_PluginBase):
|
||||
logger.warn(f"当前总下载带宽 {StringUtils.str_filesize(current_download_speed)} "
|
||||
f"已达到最大值 {self._maxdlspeed} KB/s,暂时停止新增任务")
|
||||
break
|
||||
# 保种体积(GB)
|
||||
if self._disksize \
|
||||
and (torrents_size + torrent.size) > float(self._disksize) * 1024**3:
|
||||
logger.warn(f"当前做种体积 {StringUtils.str_filesize(torrents_size)} "
|
||||
f"已超过保种体积 {self._disksize},停止新增任务")
|
||||
break
|
||||
# 添加下载任务
|
||||
hash_string = self.__download(torrent=torrent)
|
||||
if not hash_string:
|
||||
@@ -1767,19 +1772,22 @@ class BrushFlow(_PluginBase):
|
||||
"""
|
||||
发送删除种子的消息
|
||||
"""
|
||||
if self._notify:
|
||||
self.chain.post_message(Notification(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title=f"【刷流任务删种】",
|
||||
text=f"站点:{site_name}\n"
|
||||
f"标题:{torrent_title}\n"
|
||||
f"原因:{reason}"
|
||||
))
|
||||
if not self._notify:
|
||||
return
|
||||
self.chain.post_message(Notification(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title=f"【刷流任务删种】",
|
||||
text=f"站点:{site_name}\n"
|
||||
f"标题:{torrent_title}\n"
|
||||
f"原因:{reason}"
|
||||
))
|
||||
|
||||
def __send_add_message(self, torrent: TorrentInfo):
|
||||
"""
|
||||
发送添加下载的消息
|
||||
"""
|
||||
if not self._notify:
|
||||
return
|
||||
msg_text = ""
|
||||
if torrent.site_name:
|
||||
msg_text = f"站点:{torrent.site_name}"
|
||||
@@ -1819,25 +1827,29 @@ class BrushFlow(_PluginBase):
|
||||
|
||||
def __get_downloader_info(self) -> schemas.DownloaderInfo:
|
||||
"""
|
||||
获取下载器实时信息
|
||||
获取下载器实时信息(所有下载器)
|
||||
"""
|
||||
if self._downloader == "qbittorrent":
|
||||
# 调用Qbittorrent API查询实时信息
|
||||
ret_info = schemas.DownloaderInfo()
|
||||
|
||||
# Qbittorrent
|
||||
if self.qb:
|
||||
info = self.qb.transfer_info()
|
||||
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")
|
||||
)
|
||||
else:
|
||||
if info:
|
||||
ret_info.download_speed += info.get("dl_info_speed")
|
||||
ret_info.upload_speed += info.get("up_info_speed")
|
||||
ret_info.download_size += info.get("dl_info_data")
|
||||
ret_info.upload_size += info.get("up_info_data")
|
||||
|
||||
# Transmission
|
||||
if self.tr:
|
||||
info = self.tr.transfer_info()
|
||||
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
|
||||
)
|
||||
if info:
|
||||
ret_info.download_speed += info.download_speed
|
||||
ret_info.upload_speed += info.upload_speed
|
||||
ret_info.download_size += info.current_stats.downloaded_bytes
|
||||
ret_info.upload_size += info.current_stats.uploaded_bytes
|
||||
|
||||
return ret_info
|
||||
|
||||
def __get_downloading_count(self) -> int:
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List, Tuple, Dict, Any
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import TransferInfo
|
||||
@@ -183,7 +183,7 @@ class ChineseSubFinder(_PluginBase):
|
||||
pass
|
||||
|
||||
@eventmanager.register(EventType.TransferComplete)
|
||||
def download(self, event):
|
||||
def download(self, event: Event):
|
||||
"""
|
||||
调用ChineseSubFinder下载字幕
|
||||
"""
|
||||
|
||||
@@ -12,10 +12,11 @@ from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.core.metainfo import MetaInfoPath
|
||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.log import logger
|
||||
@@ -74,6 +75,7 @@ class DirMonitor(_PluginBase):
|
||||
transferhis = None
|
||||
downloadhis = None
|
||||
transferchian = None
|
||||
tmdbchain = None
|
||||
_observer = []
|
||||
_enabled = False
|
||||
_notify = False
|
||||
@@ -93,7 +95,7 @@ class DirMonitor(_PluginBase):
|
||||
self.transferhis = TransferHistoryOper(self.db)
|
||||
self.downloadhis = DownloadHistoryOper(self.db)
|
||||
self.transferchian = TransferChain(self.db)
|
||||
|
||||
self.tmdbchain = TmdbChain(self.db)
|
||||
# 清空配置
|
||||
self._dirconf = {}
|
||||
|
||||
@@ -237,13 +239,8 @@ class DirMonitor(_PluginBase):
|
||||
logger.info(f"{event_path} 已整理过")
|
||||
return
|
||||
|
||||
# 上级目录元数据
|
||||
meta = MetaInfo(title=file_path.parent.name)
|
||||
# 文件元数据,不包含后缀
|
||||
file_meta = MetaInfo(title=file_path.stem)
|
||||
# 合并元数据
|
||||
file_meta.merge(meta)
|
||||
|
||||
# 元数据
|
||||
file_meta = MetaInfoPath(file_path)
|
||||
if not file_meta.name:
|
||||
logger.error(f"{file_path.name} 无法识别有效信息")
|
||||
return
|
||||
@@ -279,6 +276,13 @@ class DirMonitor(_PluginBase):
|
||||
# 更新媒体图片
|
||||
self.chain.obtain_images(mediainfo=mediainfo)
|
||||
|
||||
# 获取集数据
|
||||
if mediainfo.type == MediaType.TV:
|
||||
episodes_info = self.tmdbchain.tmdb_episodes(tmdbid=mediainfo.tmdb_id,
|
||||
season=file_meta.begin_season or 1)
|
||||
else:
|
||||
episodes_info = None
|
||||
|
||||
# 获取downloadhash
|
||||
download_hash = self.get_download_hash(src=str(file_path))
|
||||
|
||||
@@ -287,7 +291,8 @@ class DirMonitor(_PluginBase):
|
||||
path=file_path,
|
||||
transfer_type=self._transfer_type,
|
||||
target=target,
|
||||
meta=file_meta)
|
||||
meta=file_meta,
|
||||
episodes_info=episodes_info)
|
||||
|
||||
if not transferinfo:
|
||||
logger.error("文件转移模块运行失败")
|
||||
@@ -343,7 +348,7 @@ class DirMonitor(_PluginBase):
|
||||
}
|
||||
"""
|
||||
# 发送消息汇总
|
||||
media_list = self._medias.get(mediainfo.title_year + " " + meta.season) or {}
|
||||
media_list = self._medias.get(mediainfo.title_year + " " + file_meta.season) or {}
|
||||
if media_list:
|
||||
media_files = media_list.get("files") or []
|
||||
if media_files:
|
||||
@@ -384,7 +389,7 @@ class DirMonitor(_PluginBase):
|
||||
],
|
||||
"time": datetime.now()
|
||||
}
|
||||
self._medias[mediainfo.title_year + " " + meta.season] = media_list
|
||||
self._medias[mediainfo.title_year + " " + file_meta.season] = media_list
|
||||
|
||||
# 汇总刷新媒体库
|
||||
if settings.REFRESH_MEDIASERVER:
|
||||
|
||||
320
app/plugins/downloadingmsg/__init__.py
Normal file
320
app/plugins/downloadingmsg/__init__.py
Normal file
@@ -0,0 +1,320 @@
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.media import MediaChain
|
||||
from app.core.config import settings
|
||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||
from app.plugins import _PluginBase
|
||||
from typing import Any, List, Dict, Tuple, Optional, Union
|
||||
from app.log import logger
|
||||
from app.schemas import NotificationType, TransferTorrent, DownloadingTorrent
|
||||
from app.schemas.types import TorrentStatus, MessageChannel
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class DownloadingMsg(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "下载进度推送"
|
||||
# 插件描述
|
||||
plugin_desc = "定时推送正在下载进度。"
|
||||
# 插件图标
|
||||
plugin_icon = "downloadmsg.png"
|
||||
# 主题色
|
||||
plugin_color = "#3DE75D"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/thsrite"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "downloading_"
|
||||
# 加载顺序
|
||||
plugin_order = 22
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
_enabled = False
|
||||
# 任务执行间隔
|
||||
_seconds = None
|
||||
_type = None
|
||||
_adminuser = None
|
||||
_downloadhis = None
|
||||
|
||||
# 定时器
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._seconds = config.get("seconds") or 300
|
||||
self._type = config.get("type") or 'admin'
|
||||
self._adminuser = config.get("adminuser")
|
||||
|
||||
# 加载模块
|
||||
if self._enabled:
|
||||
self._downloadhis = DownloadHistoryOper(self.db)
|
||||
# 定时服务
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
|
||||
if self._seconds:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.__downloading,
|
||||
trigger='interval',
|
||||
seconds=int(self._seconds),
|
||||
name="下载进度推送")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def __downloading(self):
|
||||
"""
|
||||
定时推送正在下载进度
|
||||
"""
|
||||
# 正在下载种子
|
||||
torrents = DownloadChain(self.db).list_torrents(status=TorrentStatus.DOWNLOADING)
|
||||
if not torrents:
|
||||
logger.info("当前没有正在下载的任务!")
|
||||
return
|
||||
# 推送用户
|
||||
if self._type == "admin" or self._type == "both":
|
||||
if not self._adminuser:
|
||||
logger.error("未配置管理员用户")
|
||||
return
|
||||
|
||||
for userid in str(self._adminuser).split(","):
|
||||
self.__send_msg(torrents=torrents, userid=userid)
|
||||
|
||||
if self._type == "user" or self._type == "both":
|
||||
user_torrents = {}
|
||||
# 根据正在下载种子hash获取下载历史
|
||||
for torrent in torrents:
|
||||
downloadhis = self._downloadhis.get_by_hash(download_hash=torrent.hash)
|
||||
if not downloadhis:
|
||||
logger.warn(f"种子 {torrent.hash} 未获取到MoviePilot下载历史,无法推送下载进度")
|
||||
continue
|
||||
if not downloadhis.userid:
|
||||
logger.debug(f"种子 {torrent.hash} 未获取到下载用户记录,无法推送下载进度")
|
||||
continue
|
||||
user_torrent = user_torrents.get(downloadhis.userid) or []
|
||||
user_torrent.append(torrent)
|
||||
user_torrents[downloadhis.userid] = user_torrent
|
||||
|
||||
if not user_torrents or not user_torrents.keys():
|
||||
logger.warn("未获取到用户下载记录,无法推送下载进度")
|
||||
return
|
||||
|
||||
# 推送用户下载任务进度
|
||||
for userid in list(user_torrents.keys()):
|
||||
if not userid:
|
||||
continue
|
||||
# 如果用户是管理员,无需重复推送
|
||||
if self._type == "admin" or self._type == "both" and self._adminuser and userid in str(
|
||||
self._adminuser).split(","):
|
||||
logger.debug("管理员已推送")
|
||||
continue
|
||||
|
||||
user_torrent = user_torrents.get(userid)
|
||||
if not user_torrent:
|
||||
logger.warn(f"未获取到用户 {userid} 下载任务")
|
||||
continue
|
||||
self.__send_msg(torrents=user_torrent,
|
||||
userid=userid)
|
||||
|
||||
if self._type == "all":
|
||||
self.__send_msg(torrents=torrents)
|
||||
|
||||
def __send_msg(self, torrents: Optional[List[Union[TransferTorrent, DownloadingTorrent]]], userid: str = None):
|
||||
"""
|
||||
发送消息
|
||||
"""
|
||||
title = f"共 {len(torrents)} 个任务正在下载:"
|
||||
messages = []
|
||||
index = 1
|
||||
channel_value = None
|
||||
for torrent in torrents:
|
||||
year = None
|
||||
name = None
|
||||
se = None
|
||||
ep = None
|
||||
# 先查询下载记录,没有再识别
|
||||
downloadhis = self._downloadhis.get_by_hash(download_hash=torrent.hash)
|
||||
if downloadhis:
|
||||
name = downloadhis.title
|
||||
year = downloadhis.year
|
||||
se = downloadhis.seasons
|
||||
ep = downloadhis.episodes
|
||||
if not channel_value:
|
||||
channel_value = downloadhis.channel
|
||||
else:
|
||||
try:
|
||||
context = MediaChain(self.db).recognize_by_title(title=torrent.title)
|
||||
if not context or not context.media_info:
|
||||
continue
|
||||
media_info = context.media_info
|
||||
year = media_info.year
|
||||
name = media_info.title
|
||||
if media_info.number_of_seasons:
|
||||
se = f"S{str(media_info.number_of_seasons).rjust(2, '0')}"
|
||||
if media_info.number_of_episodes:
|
||||
ep = f"E{str(media_info.number_of_episodes).rjust(2, '0')}"
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
# 拼装标题
|
||||
if year:
|
||||
media_name = "%s (%s) %s%s" % (name, year, se, ep)
|
||||
elif name:
|
||||
media_name = "%s %s%s" % (name, se, ep)
|
||||
else:
|
||||
media_name = torrent.title
|
||||
|
||||
messages.append(f"{index}. {media_name}\n"
|
||||
f"{torrent.title} "
|
||||
f"{StringUtils.str_filesize(torrent.size)} "
|
||||
f"{round(torrent.progress, 1)}%")
|
||||
index += 1
|
||||
|
||||
# 用户消息渠道
|
||||
if channel_value:
|
||||
channel = next(
|
||||
(channel for channel in MessageChannel.__members__.values() if channel.value == channel_value), None)
|
||||
else:
|
||||
channel = None
|
||||
self.post_message(mtype=NotificationType.Download,
|
||||
channel=channel,
|
||||
title=title,
|
||||
text="\n".join(messages),
|
||||
userid=userid)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'seconds',
|
||||
'label': '执行间隔',
|
||||
'placeholder': '单位(秒)'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'adminuser',
|
||||
'label': '管理员用户',
|
||||
'placeholder': '多个用户,分割'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'type',
|
||||
'label': '推送类型',
|
||||
'items': [
|
||||
{'title': '管理员', 'value': 'admin'},
|
||||
{'title': '下载用户', 'value': 'user'},
|
||||
{'title': '管理员和下载用户', 'value': 'both'},
|
||||
{'title': '所有用户', 'value': 'all'}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"seconds": 300,
|
||||
"adminuser": "",
|
||||
"type": "admin"
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error("退出插件失败:%s" % str(e))
|
||||
@@ -25,9 +25,9 @@ from app.schemas.types import NotificationType, EventType, MediaType
|
||||
|
||||
class MediaSyncDel(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "媒体库同步删除"
|
||||
plugin_name = "媒体文件同步删除"
|
||||
# 插件描述
|
||||
plugin_desc = "媒体库删除媒体后同步删除历史记录、源文件和下载任务。"
|
||||
plugin_desc = "同步删除历史记录、源文件和下载任务。"
|
||||
# 插件图标
|
||||
plugin_icon = "mediasyncdel.png"
|
||||
# 主题色
|
||||
@@ -187,9 +187,9 @@ class MediaSyncDel(_PluginBase):
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'sync_type',
|
||||
'label': '同步方式',
|
||||
'label': '媒体库同步方式',
|
||||
'items': [
|
||||
{'title': 'webhook', 'value': 'webhook'},
|
||||
{'title': 'Webhook', 'value': 'webhook'},
|
||||
{'title': '日志', 'value': 'log'},
|
||||
{'title': 'Scripter X', 'value': 'plugin'}
|
||||
]
|
||||
@@ -208,7 +208,7 @@ class MediaSyncDel(_PluginBase):
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'label': '日志检查周期',
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class MediaSyncDel(_PluginBase):
|
||||
'props': {
|
||||
'model': 'library_path',
|
||||
'rows': '2',
|
||||
'label': '媒体库路径',
|
||||
'label': '媒体库路径映射',
|
||||
'placeholder': '媒体服务器路径:MoviePilot路径(一行一个)'
|
||||
}
|
||||
}
|
||||
@@ -266,11 +266,11 @@ class MediaSyncDel(_PluginBase):
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'text': '同步方式分为webhook、日志同步和Scripter X。'
|
||||
'webhook需要Emby4.8.0.45及以上开启媒体删除的webhook'
|
||||
'(建议使用媒体库刮削插件覆盖元数据重新刮削剧集路径)。'
|
||||
'日志同步需要配置执行周期,默认30分钟执行一次。'
|
||||
'Scripter X方式需要emby安装并配置Scripter X插件,无需配置执行周期。'
|
||||
'text': '媒体库同步方式分为Webhook、日志同步和Scripter X:'
|
||||
'1、Webhook需要Emby4.8.0.45及以上开启媒体删除的Webhook。'
|
||||
'2、日志同步需要配置检查周期,默认30分钟执行一次。'
|
||||
'3、Scripter X方式需要emby安装并配置Scripter X插件,无需配置执行周期。'
|
||||
'4、启用该插件后,非媒体服务器触发的源文件删除,也会同步处理下载器中的下载任务。'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1202,21 +1202,25 @@ class MediaSyncDel(_PluginBase):
|
||||
except Exception as e:
|
||||
logger.error("退出插件失败:%s" % str(e))
|
||||
|
||||
@eventmanager.register(EventType.MediaDeleted)
|
||||
def remote_sync_del(self, event: Event):
|
||||
@eventmanager.register(EventType.DownloadFileDeleted)
|
||||
def downloadfile_del_sync(self, event: Event):
|
||||
"""
|
||||
媒体库同步删除
|
||||
下载文件删除处理事件
|
||||
"""
|
||||
if event:
|
||||
logger.info("收到命令,开始执行媒体库同步删除 ...")
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="开始媒体库同步删除 ...",
|
||||
userid=event.event_data.get("user"))
|
||||
self.sync_del_by_log()
|
||||
|
||||
if event:
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="媒体库同步删除完成!", userid=event.event_data.get("user"))
|
||||
if not self._enabled:
|
||||
return
|
||||
if not event:
|
||||
return
|
||||
event_data = event.event_data
|
||||
src = event_data.get("src")
|
||||
if not src:
|
||||
return
|
||||
# 查询下载hash
|
||||
download_hash = self._downloadhis.get_hash_by_fullpath(src)
|
||||
if download_hash:
|
||||
self.handle_torrent(src=src, torrent_hash=download_hash)
|
||||
else:
|
||||
logger.warn(f"未查询到文件 {src} 对应的下载记录")
|
||||
|
||||
@staticmethod
|
||||
def get_tmdbimage_url(path: str, prefix="w500"):
|
||||
|
||||
@@ -325,6 +325,9 @@ class MessageForward(_PluginBase):
|
||||
logger.info(f"转发消息 {title} 成功")
|
||||
return True
|
||||
else:
|
||||
if ret_json.get('errcode') == 81013:
|
||||
return False
|
||||
|
||||
logger.error(f"转发消息 {title} 失败,错误信息:{ret_json}")
|
||||
if ret_json.get('errcode') == 42001 or ret_json.get('errcode') == 40014:
|
||||
logger.info("token已过期,正在重新刷新token重试")
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.config import settings
|
||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
@@ -157,15 +158,16 @@ class NAStoolSync(_PluginBase):
|
||||
|
||||
# 替换value
|
||||
if isinstance(plugin_value, str):
|
||||
plugin_value = json.loads(plugin_value)
|
||||
if str(plugin_value.get("to_download")).isdigit() and int(
|
||||
plugin_value.get("to_download")) == int(sub_downloaders[0]):
|
||||
plugin_value["to_download"] = sub_downloaders[1]
|
||||
_value: dict = json.loads(plugin_value)
|
||||
elif isinstance(plugin_value, dict):
|
||||
if str(plugin_value.get("to_download")).isdigit() and int(
|
||||
plugin_value.get("to_download")) == int(sub_downloaders[0]):
|
||||
plugin_value["to_download"] = sub_downloaders[1]
|
||||
|
||||
# 替换辅种记录
|
||||
if str(plugin_id) == "IYUUAutoSeed":
|
||||
if isinstance(plugin_value, str):
|
||||
plugin_value = json.loads(plugin_value)
|
||||
plugin_value: list = json.loads(plugin_value)
|
||||
if not isinstance(plugin_value, list):
|
||||
plugin_value = [plugin_value]
|
||||
for value in plugin_value:
|
||||
@@ -213,6 +215,7 @@ class NAStoolSync(_PluginBase):
|
||||
mtorrent = history[9]
|
||||
mdesc = history[10]
|
||||
msite = history[11]
|
||||
mdate = history[12]
|
||||
|
||||
# 处理站点映射
|
||||
if self._site:
|
||||
@@ -234,7 +237,9 @@ class NAStoolSync(_PluginBase):
|
||||
download_hash=mdownload_hash,
|
||||
torrent_name=mtorrent,
|
||||
torrent_description=mdesc,
|
||||
torrent_site=msite
|
||||
torrent_site=msite,
|
||||
userid=settings.SUPERUSER,
|
||||
date=mdate
|
||||
)
|
||||
cnt += 1
|
||||
if cnt % 100 == 0:
|
||||
@@ -358,7 +363,8 @@ class NAStoolSync(_PluginBase):
|
||||
DOWNLOAD_ID,
|
||||
TORRENT,
|
||||
DESC,
|
||||
SITE
|
||||
SITE,
|
||||
DATE
|
||||
FROM
|
||||
DOWNLOAD_HISTORY
|
||||
WHERE
|
||||
|
||||
204
app/plugins/personmeta/__init__.py
Normal file
204
app/plugins/personmeta/__init__.py
Normal file
@@ -0,0 +1,204 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Dict, Tuple
|
||||
|
||||
from requests import RequestException
|
||||
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.helper.nfo import NfoReader
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import TransferInfo, MediaInfo
|
||||
from app.schemas.types import EventType, MediaType
|
||||
from app.utils.common import retry
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class PersonMeta(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "演职人员刮削"
|
||||
# 插件描述
|
||||
plugin_desc = "刮削演职人员图片以及中文名称。"
|
||||
# 插件图标
|
||||
plugin_icon = "actor.png"
|
||||
# 主题色
|
||||
plugin_color = "#E66E72"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/jxxghp"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "personmeta_"
|
||||
# 加载顺序
|
||||
plugin_order = 24
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 私有属性
|
||||
tmdbchain = None
|
||||
_enabled = False
|
||||
_metadir = ""
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self.tmdbchain = TmdbChain(self.db)
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._metadir = config.get("metadir")
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'metadir',
|
||||
'label': '人物元数据目录',
|
||||
'placeholder': '/metadata/people'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"metadir": ""
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
@eventmanager.register(EventType.TransferComplete)
|
||||
def scrap_rt(self, event: Event):
|
||||
"""
|
||||
根据事件实时刮削演员信息
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
# 下载人物头像
|
||||
if not self._metadir:
|
||||
logger.warning("人物元数据目录未配置,无法下载人物头像")
|
||||
return
|
||||
# 事件数据
|
||||
mediainfo: MediaInfo = event.event_data.get("mediainfo")
|
||||
transferinfo: TransferInfo = event.event_data.get("transferinfo")
|
||||
if not mediainfo or not transferinfo:
|
||||
return
|
||||
# 文件路径
|
||||
filepath = transferinfo.target_path
|
||||
if not filepath:
|
||||
return
|
||||
# 电影
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# nfo文件
|
||||
nfofile = filepath.with_name("movie.nfo")
|
||||
if not nfofile.exists():
|
||||
nfofile = filepath.parent / f"{filepath.stem}.nfo"
|
||||
if not nfofile.exists():
|
||||
logger.warning(f"电影nfo文件不存在:{nfofile}")
|
||||
return
|
||||
else:
|
||||
# nfo文件
|
||||
nfofile = filepath.parent.with_name("tvshow.nfo")
|
||||
if not nfofile.exists():
|
||||
logger.warning(f"剧集nfo文件不存在:{nfofile}")
|
||||
return
|
||||
# 读取nfo文件
|
||||
nfo = NfoReader(nfofile)
|
||||
# 读取演员信息
|
||||
actors = nfo.get_elements("actor") or []
|
||||
for actor in actors:
|
||||
# 演员ID
|
||||
actor_id = actor.find("id").text
|
||||
if not actor_id:
|
||||
continue
|
||||
# 演员名称
|
||||
actor_name = actor.find("name").text
|
||||
# 查询演员详情
|
||||
actor_info = self.tmdbchain.person_detail(actor_id)
|
||||
if not actor_info:
|
||||
continue
|
||||
# 演员头像
|
||||
actor_image = actor_info.get("profile_path")
|
||||
if not actor_image:
|
||||
continue
|
||||
# 计算保存路径
|
||||
image_path = Path(self._metadir) / f"{actor_name}-tmdb-{actor_id}" / f"folder{Path(actor_image).suffix}"
|
||||
if image_path.exists():
|
||||
continue
|
||||
# 下载图片
|
||||
self.download_image(f"https://image.tmdb.org/t/p/original{actor_image}", image_path)
|
||||
|
||||
@staticmethod
|
||||
@retry(RequestException, logger=logger)
|
||||
def download_image(image_url: str, path: Path):
|
||||
"""
|
||||
下载图片,保存到指定路径
|
||||
"""
|
||||
try:
|
||||
logger.info(f"正在下载演职人员图片:{image_url} ...")
|
||||
r = RequestUtils().get_res(url=image_url, raise_exception=True)
|
||||
if r:
|
||||
path.write_bytes(r.content)
|
||||
logger.info(f"图片已保存:{path}")
|
||||
else:
|
||||
logger.info(f"图片下载失败,请检查网络连通性:{image_url}")
|
||||
except RequestException as err:
|
||||
raise err
|
||||
except Exception as err:
|
||||
logger.error(f"图片下载失败:{err}")
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
pass
|
||||
@@ -73,15 +73,19 @@ class SpeedLimiter(_PluginBase):
|
||||
try:
|
||||
# 总带宽
|
||||
self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000
|
||||
# 自动限速开关
|
||||
if self._bandwidth > 0:
|
||||
# 自动限速开关
|
||||
self._auto_limit = True
|
||||
else:
|
||||
self._auto_limit = False
|
||||
except Exception as e:
|
||||
logger.error(f"智能限速上行带宽设置错误:{str(e)}")
|
||||
self._bandwidth = 0
|
||||
|
||||
# 限速服务开关
|
||||
self._limit_enabled = True if self._play_up_speed or self._play_down_speed or self._auto_limit else False
|
||||
self._limit_enabled = True if (self._play_up_speed
|
||||
or self._play_down_speed
|
||||
or self._auto_limit) else False
|
||||
self._allocation_ratio = config.get("allocation_ratio") or ""
|
||||
# 不限速地址
|
||||
self._unlimited_ips["ipv4"] = config.get("ipv4") or ""
|
||||
@@ -479,17 +483,13 @@ class SpeedLimiter(_PluginBase):
|
||||
self.__set_limiter(limit_type="未播放", upload_limit=self._noplay_up_speed,
|
||||
download_limit=self._noplay_down_speed)
|
||||
|
||||
def __calc_limit(self, total_bit_rate):
|
||||
def __calc_limit(self, total_bit_rate: float) -> float:
|
||||
"""
|
||||
计算智能上传限速
|
||||
"""
|
||||
residual_bandwidth = (self._bandwidth - total_bit_rate)
|
||||
if residual_bandwidth < 0:
|
||||
play_up_speed = 10
|
||||
else:
|
||||
play_up_speed = round(residual_bandwidth / 8 / 1024, 2)
|
||||
|
||||
return play_up_speed
|
||||
if not self._bandwidth:
|
||||
return 10
|
||||
return round((self._bandwidth - total_bit_rate) / 8 / 1024, 2)
|
||||
|
||||
def __set_limiter(self, limit_type: str, upload_limit: float, download_limit: float):
|
||||
"""
|
||||
@@ -572,7 +572,7 @@ class SpeedLimiter(_PluginBase):
|
||||
logger.error(f"设置限速失败:{str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def __allow_access(allow_ips, ip):
|
||||
def __allow_access(allow_ips: dict, ip: str) -> bool:
|
||||
"""
|
||||
判断IP是否合法
|
||||
:param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":}
|
||||
|
||||
@@ -36,6 +36,12 @@ class DownloadHistory(BaseModel):
|
||||
torrent_description: Optional[str] = None
|
||||
# 站点
|
||||
torrent_site: Optional[str] = None
|
||||
# 下载用户
|
||||
userid: Optional[str] = None
|
||||
# 下载渠道
|
||||
channel: Optional[str] = None
|
||||
# 创建时间
|
||||
date: Optional[str] = None
|
||||
# 备注
|
||||
note: Optional[str] = None
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ class EventType(Enum):
|
||||
DownloadAdded = "download.added"
|
||||
# 删除历史记录
|
||||
HistoryDeleted = "history.deleted"
|
||||
# 删除媒体库文件
|
||||
MediaDeleted = "media.deleted"
|
||||
# 删除下载源文件
|
||||
DownloadFileDeleted = "downloadfile.deleted"
|
||||
# 用户外来消息
|
||||
UserMessage = "user.message"
|
||||
# 通知消息
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import os
|
||||
|
||||
|
||||
class PathUtils:
|
||||
|
||||
@staticmethod
|
||||
def get_dir_files(in_path, exts="", filesize=0, episode_format=None):
|
||||
"""
|
||||
获得目录下的媒体文件列表List ,按后缀、大小、格式过滤
|
||||
"""
|
||||
if not in_path:
|
||||
return []
|
||||
if not os.path.exists(in_path):
|
||||
return []
|
||||
ret_list = []
|
||||
if os.path.isdir(in_path):
|
||||
for root, dirs, files in os.walk(in_path):
|
||||
for file in files:
|
||||
cur_path = os.path.join(root, file)
|
||||
# 检查路径是否合法
|
||||
if PathUtils.is_invalid_path(cur_path):
|
||||
continue
|
||||
# 检查格式匹配
|
||||
if episode_format and not episode_format.match(file):
|
||||
continue
|
||||
# 检查后缀
|
||||
if exts and os.path.splitext(file)[-1].lower() not in exts:
|
||||
continue
|
||||
# 检查文件大小
|
||||
if filesize and os.path.getsize(cur_path) < filesize:
|
||||
continue
|
||||
# 命中
|
||||
if cur_path not in ret_list:
|
||||
ret_list.append(cur_path)
|
||||
else:
|
||||
# 检查路径是否合法
|
||||
if PathUtils.is_invalid_path(in_path):
|
||||
return []
|
||||
# 检查后缀
|
||||
if exts and os.path.splitext(in_path)[-1].lower() not in exts:
|
||||
return []
|
||||
# 检查格式
|
||||
if episode_format and not episode_format.match(os.path.basename(in_path)):
|
||||
return []
|
||||
# 检查文件大小
|
||||
if filesize and os.path.getsize(in_path) < filesize:
|
||||
return []
|
||||
ret_list.append(in_path)
|
||||
return ret_list
|
||||
|
||||
@staticmethod
|
||||
def get_dir_level1_files(in_path, exts=""):
|
||||
"""
|
||||
查询目录下的文件(只查询一级)
|
||||
"""
|
||||
ret_list = []
|
||||
if not os.path.exists(in_path):
|
||||
return []
|
||||
for file in os.listdir(in_path):
|
||||
path = os.path.join(in_path, file)
|
||||
if os.path.isfile(path):
|
||||
if not exts or os.path.splitext(file)[-1].lower() in exts:
|
||||
ret_list.append(path)
|
||||
return ret_list
|
||||
|
||||
@staticmethod
|
||||
def get_dir_level1_medias(in_path, exts=""):
|
||||
"""
|
||||
根据后缀,返回目录下所有的文件及文件夹列表(只查询一级)
|
||||
"""
|
||||
ret_list = []
|
||||
if not os.path.exists(in_path):
|
||||
return []
|
||||
if os.path.isdir(in_path):
|
||||
for file in os.listdir(in_path):
|
||||
path = os.path.join(in_path, file)
|
||||
if os.path.isfile(path):
|
||||
if not exts or os.path.splitext(file)[-1].lower() in exts:
|
||||
ret_list.append(path)
|
||||
else:
|
||||
ret_list.append(path)
|
||||
else:
|
||||
ret_list.append(in_path)
|
||||
return ret_list
|
||||
|
||||
@staticmethod
|
||||
def is_invalid_path(path):
|
||||
"""
|
||||
判断是否不能处理的路径
|
||||
"""
|
||||
if not path:
|
||||
return True
|
||||
if path.find('/@Recycle/') != -1 or path.find('/#recycle/') != -1 or path.find('/.') != -1 or path.find(
|
||||
'/@eaDir') != -1:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_path_in_path(path1, path2):
|
||||
"""
|
||||
判断两个路径是否包含关系 path1 in path2
|
||||
"""
|
||||
if not path1 or not path2:
|
||||
return False
|
||||
path1 = os.path.normpath(path1).replace("\\", "/")
|
||||
path2 = os.path.normpath(path2).replace("\\", "/")
|
||||
if path1 == path2:
|
||||
return True
|
||||
path = os.path.dirname(path2)
|
||||
while True:
|
||||
if path == path1:
|
||||
return True
|
||||
path = os.path.dirname(path)
|
||||
if path == os.path.dirname(path):
|
||||
break
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_bluray_dir(path):
|
||||
"""
|
||||
判断是否蓝光原盘目录,是则返回原盘的根目录,否则返回空
|
||||
"""
|
||||
if not path or not os.path.exists(path):
|
||||
return None
|
||||
if os.path.isdir(path):
|
||||
if os.path.exists(os.path.join(path, "BDMV", "index.bdmv")):
|
||||
return path
|
||||
elif os.path.normpath(path).endswith("BDMV") \
|
||||
and os.path.exists(os.path.join(path, "index.bdmv")):
|
||||
return os.path.dirname(path)
|
||||
elif os.path.normpath(path).endswith("STREAM") \
|
||||
and os.path.exists(os.path.join(os.path.dirname(path), "index.bdmv")):
|
||||
return PathUtils.get_parent_paths(path, 2)
|
||||
else:
|
||||
# 电视剧原盘下会存在多个目录形如:Spider Man 2021/DIsc1, Spider Man 2021/Disc2
|
||||
for level1 in PathUtils.get_dir_level1_medias(path):
|
||||
if os.path.exists(os.path.join(level1, "BDMV", "index.bdmv")):
|
||||
return path
|
||||
return None
|
||||
else:
|
||||
if str(os.path.splitext(path)[-1]).lower() in [".m2ts", ".ts"] \
|
||||
and os.path.normpath(os.path.dirname(path)).endswith("STREAM") \
|
||||
and os.path.exists(os.path.join(PathUtils.get_parent_paths(path, 2), "index.bdmv")):
|
||||
return PathUtils.get_parent_paths(path, 3)
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_parent_paths(path, level: int = 1):
|
||||
"""
|
||||
获取父目录路径,level为向上查找的层数
|
||||
"""
|
||||
for lv in range(0, level):
|
||||
path = os.path.dirname(path)
|
||||
return path
|
||||
@@ -61,7 +61,9 @@ class SystemUtils:
|
||||
移动
|
||||
"""
|
||||
try:
|
||||
# 当前目录改名
|
||||
temp = src.replace(src.parent / dest.name)
|
||||
# 移动到目标目录
|
||||
shutil.move(temp, dest)
|
||||
return 0, ""
|
||||
except Exception as err:
|
||||
@@ -74,7 +76,11 @@ class SystemUtils:
|
||||
硬链接
|
||||
"""
|
||||
try:
|
||||
dest.hardlink_to(src)
|
||||
# link到当前目录并改名
|
||||
tmp_path = (src.parent / dest.name).with_suffix(".mp")
|
||||
tmp_path.hardlink_to(src)
|
||||
# 移动到目标目录
|
||||
shutil.move(tmp_path, dest)
|
||||
return 0, ""
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
@@ -341,13 +347,20 @@ class SystemUtils:
|
||||
# 创建 Docker 客户端
|
||||
client = docker.DockerClient(base_url='tcp://127.0.0.1:38379')
|
||||
# 获取当前容器的 ID
|
||||
container_id = None
|
||||
with open('/proc/self/mountinfo', 'r') as f:
|
||||
data = f.read()
|
||||
index_resolv_conf = data.find("/sys/fs/cgroup/devices")
|
||||
index_resolv_conf = data.find("resolv.conf")
|
||||
if index_resolv_conf != -1:
|
||||
index_second_slash = data.rfind(" ", 0, index_resolv_conf)
|
||||
index_second_slash = data.rfind("/", 0, index_resolv_conf)
|
||||
index_first_slash = data.rfind("/", 0, index_second_slash) + 1
|
||||
container_id = data[index_first_slash:index_second_slash]
|
||||
if len(container_id) < 20:
|
||||
index_resolv_conf = data.find("/sys/fs/cgroup/devices")
|
||||
if index_resolv_conf != -1:
|
||||
index_second_slash = data.rfind(" ", 0, index_resolv_conf)
|
||||
index_first_slash = data.rfind("/", 0, index_second_slash) + 1
|
||||
container_id = data[index_first_slash:index_second_slash]
|
||||
if not container_id:
|
||||
return False, "获取容器ID失败!"
|
||||
# 重启当前容器
|
||||
|
||||
@@ -4,8 +4,16 @@ from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class WebUtils:
|
||||
|
||||
@staticmethod
|
||||
def get_location(ip: str):
|
||||
"""
|
||||
查询IP所属地
|
||||
"""
|
||||
return WebUtils.get_location1(ip) or WebUtils.get_location2(ip)
|
||||
|
||||
@staticmethod
|
||||
def get_location1(ip: str):
|
||||
"""
|
||||
https://api.mir6.com/api/ip
|
||||
{
|
||||
@@ -36,7 +44,33 @@ class WebUtils:
|
||||
if r:
|
||||
return r.json().get("data", {}).get("location") or ''
|
||||
except Exception as err:
|
||||
return str(err)
|
||||
print(str(err))
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_location2(ip: str):
|
||||
"""
|
||||
https://whois.pconline.com.cn/ipJson.jsp?json=true&ip=
|
||||
{
|
||||
"ip": "122.8.12.22",
|
||||
"pro": "上海市",
|
||||
"proCode": "310000",
|
||||
"city": "上海市",
|
||||
"cityCode": "310000",
|
||||
"region": "",
|
||||
"regionCode": "0",
|
||||
"addr": "上海市 铁通",
|
||||
"regionNames": "",
|
||||
"err": ""
|
||||
}
|
||||
"""
|
||||
try:
|
||||
r = RequestUtils().get_res(f"https://whois.pconline.com.cn/ipJson.jsp?json=true&ip={ip}")
|
||||
if r:
|
||||
return r.json().get("addr") or ''
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_bing_wallpaper() -> Optional[str]:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.2.5'
|
||||
APP_VERSION = 'v1.2.7'
|
||||
|
||||
Reference in New Issue
Block a user