mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-11 18:10:15 +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`
|
- **PGID**:运行程序用户的`gid`,默认`0`
|
||||||
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`
|
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`
|
||||||
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**
|
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**
|
||||||
|
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`
|
||||||
- **NGINX_PORT:** WEB服务端口,默认`3000`,可自行修改,不能与API服务端口冲突
|
- **NGINX_PORT:** WEB服务端口,默认`3000`,可自行修改,不能与API服务端口冲突
|
||||||
- **PORT:** API服务端口,默认`3001`,可自行修改,不能与WEB服务端口冲突
|
- **PORT:** API服务端口,默认`3001`,可自行修改,不能与WEB服务端口冲突
|
||||||
- **SUPERUSER:** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
- **SUPERUSER:** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
||||||
@@ -188,10 +189,11 @@ docker pull jxxghp/moviepilot:latest
|
|||||||
> `original_title`: 原语种标题
|
> `original_title`: 原语种标题
|
||||||
> `name`: 识别名称
|
> `name`: 识别名称
|
||||||
> `year`: 年份
|
> `year`: 年份
|
||||||
> `edition`: 版本
|
> `resourceType`:资源类型
|
||||||
|
> `effect`:特效
|
||||||
|
> `edition`: 版本(资源类型+特效)
|
||||||
> `videoFormat`: 分辨率
|
> `videoFormat`: 分辨率
|
||||||
> `releaseGroup`: 制作组/字幕组
|
> `releaseGroup`: 制作组/字幕组
|
||||||
> `effect`: 特效
|
|
||||||
> `videoCodec`: 视频编码
|
> `videoCodec`: 视频编码
|
||||||
> `audioCodec`: 音频编码
|
> `audioCodec`: 音频编码
|
||||||
> `tmdbid`: TMDBID
|
> `tmdbid`: TMDBID
|
||||||
@@ -212,6 +214,7 @@ docker pull jxxghp/moviepilot:latest
|
|||||||
> `season`: 季号
|
> `season`: 季号
|
||||||
> `episode`: 集号
|
> `episode`: 集号
|
||||||
> `season_episode`: 季集 SxxExx
|
> `season_episode`: 季集 SxxExx
|
||||||
|
> `episode_title`: 集标题
|
||||||
|
|
||||||
`TV_RENAME_FORMAT`默认配置格式:
|
`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 import schemas
|
||||||
from app.chain.transfer import TransferChain
|
from app.chain.transfer import TransferChain
|
||||||
|
from app.core.event import eventmanager
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.db import get_db
|
from app.db import get_db
|
||||||
from app.db.models.downloadhistory import DownloadHistory
|
from app.db.models.downloadhistory import DownloadHistory
|
||||||
from app.db.models.transferhistory import TransferHistory
|
from app.db.models.transferhistory import TransferHistory
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
|
from app.schemas.types import EventType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -78,6 +80,13 @@ def delete_transfer_history(history_in: schemas.TransferHistory,
|
|||||||
# 删除源文件
|
# 删除源文件
|
||||||
if deletesrc and history.src:
|
if deletesrc and history.src:
|
||||||
TransferChain(db).delete_files(Path(history.src))
|
TransferChain(db).delete_files(Path(history.src))
|
||||||
|
# 发送事件
|
||||||
|
eventmanager.send_event(
|
||||||
|
EventType.DownloadFileDeleted,
|
||||||
|
{
|
||||||
|
"src": history.src
|
||||||
|
}
|
||||||
|
)
|
||||||
# 删除记录
|
# 删除记录
|
||||||
TransferHistory.delete(db, history_in.id)
|
TransferHistory.delete(db, history_in.id)
|
||||||
return schemas.Response(success=True)
|
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)
|
@router.post("/transfer", summary="历史记录重新转移", response_model=schemas.Response)
|
||||||
def redo_transfer_history(history_in: schemas.TransferHistory,
|
def redo_transfer_history(history_in: schemas.TransferHistory,
|
||||||
mtype: str,
|
mtype: str = None,
|
||||||
new_tmdbid: int,
|
new_tmdbid: int = None,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
历史记录重新转移
|
历史记录重新转移,不输入 mtype 和 new_tmdbid 时,自动使用文件名重新识别
|
||||||
"""
|
"""
|
||||||
state, errmsg = TransferChain(db).re_transfer(logid=history_in.id,
|
if mtype and new_tmdbid:
|
||||||
mtype=MediaType(mtype), tmdbid=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:
|
if state:
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def read_sites(db: Session = Depends(get_db),
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/", summary="新增站点", response_model=schemas.Response)
|
@router.post("/", summary="新增站点", response_model=schemas.Response)
|
||||||
def update_site(
|
def add_site(
|
||||||
*,
|
*,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
site_in: schemas.Site,
|
site_in: schemas.Site,
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ def update_subscribe(
|
|||||||
subscribe = Subscribe.get(db, subscribe_in.id)
|
subscribe = Subscribe.get(db, subscribe_in.id)
|
||||||
if not subscribe:
|
if not subscribe:
|
||||||
return schemas.Response(success=False, message="订阅不存在")
|
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_in.sites = json.dumps(subscribe_in.sites)
|
||||||
# 避免更新缺失集数
|
# 避免更新缺失集数
|
||||||
subscribe_dict = subscribe_in.dict()
|
subscribe_dict = subscribe_in.dict()
|
||||||
@@ -162,7 +162,9 @@ def search_subscribes(
|
|||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
Scheduler().start,
|
Scheduler().start,
|
||||||
job_id="subscribe_search",
|
job_id="subscribe_search",
|
||||||
sid=None, state='R'
|
sid=None,
|
||||||
|
state='R',
|
||||||
|
manual=True
|
||||||
)
|
)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
@@ -178,7 +180,9 @@ def search_subscribe(
|
|||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
Scheduler().start,
|
Scheduler().start,
|
||||||
job_id="subscribe_search",
|
job_id="subscribe_search",
|
||||||
sid=subscribe_id, state=None
|
sid=subscribe_id,
|
||||||
|
state=None,
|
||||||
|
manual=True
|
||||||
)
|
)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from app.core.meta import MetaBase
|
|||||||
from app.core.module import ModuleManager
|
from app.core.module import ModuleManager
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \
|
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \
|
||||||
WebhookEventInfo
|
WebhookEventInfo, TmdbEpisode
|
||||||
from app.schemas.types import TorrentStatus, MediaType, MediaImageType, EventType
|
from app.schemas.types import TorrentStatus, MediaType, MediaImageType, EventType
|
||||||
from app.utils.object import ObjectUtils
|
from app.utils.object import ObjectUtils
|
||||||
|
|
||||||
@@ -274,7 +274,8 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
return self.run_module("list_torrents", status=status, hashs=hashs)
|
return self.run_module("list_torrents", status=status, hashs=hashs)
|
||||||
|
|
||||||
def transfer(self, path: Path, meta: MetaBase, mediainfo: MediaInfo,
|
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: 文件路径
|
:param path: 文件路径
|
||||||
@@ -282,10 +283,12 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:param transfer_type: 转移模式
|
:param transfer_type: 转移模式
|
||||||
:param target: 转移目标路径
|
:param target: 转移目标路径
|
||||||
|
:param episodes_info: 当前季的全部集信息
|
||||||
:return: {path, target_path, message}
|
:return: {path, target_path, message}
|
||||||
"""
|
"""
|
||||||
return self.run_module("transfer", path=path, meta=meta, mediainfo=mediainfo,
|
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:
|
def transfer_completed(self, hashs: Union[str, list], path: Path = None) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple, Set, Dict, Union
|
from typing import List, Optional, Tuple, Set, Dict, Union
|
||||||
|
|
||||||
@@ -39,8 +40,10 @@ class DownloadChain(ChainBase):
|
|||||||
发送添加下载的消息
|
发送添加下载的消息
|
||||||
"""
|
"""
|
||||||
msg_text = ""
|
msg_text = ""
|
||||||
|
if userid:
|
||||||
|
msg_text = f"用户:{userid}"
|
||||||
if torrent.site_name:
|
if torrent.site_name:
|
||||||
msg_text = f"站点:{torrent.site_name}"
|
msg_text = f"{msg_text}\n站点:{torrent.site_name}"
|
||||||
if meta.resource_term:
|
if meta.resource_term:
|
||||||
msg_text = f"{msg_text}\n质量:{meta.resource_term}"
|
msg_text = f"{msg_text}\n质量:{meta.resource_term}"
|
||||||
if torrent.size:
|
if torrent.size:
|
||||||
@@ -266,7 +269,10 @@ class DownloadChain(ChainBase):
|
|||||||
download_hash=_hash,
|
download_hash=_hash,
|
||||||
torrent_name=_torrent.title,
|
torrent_name=_torrent.title,
|
||||||
torrent_description=_torrent.description,
|
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],
|
contexts: List[Context],
|
||||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||||
save_path: str = None,
|
save_path: str = None,
|
||||||
channel: str = None,
|
channel: MessageChannel = None,
|
||||||
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
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.chain import ChainBase
|
||||||
from app.core.context import Context, MediaInfo
|
from app.core.context import Context, MediaInfo
|
||||||
from app.core.meta import MetaBase
|
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.log import logger
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
@@ -38,12 +38,8 @@ class MediaChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
logger.info(f'开始识别媒体信息,文件:{path} ...')
|
logger.info(f'开始识别媒体信息,文件:{path} ...')
|
||||||
file_path = Path(path)
|
file_path = Path(path)
|
||||||
# 上级目录元数据
|
# 元数据
|
||||||
dir_meta = MetaInfo(title=file_path.parent.name)
|
file_meta = MetaInfoPath(file_path)
|
||||||
# 文件元数据,不包含后缀
|
|
||||||
file_meta = MetaInfo(title=file_path.stem)
|
|
||||||
# 合并元数据
|
|
||||||
file_meta.merge(dir_meta)
|
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
mediainfo = self.recognize_media(meta=file_meta)
|
mediainfo = self.recognize_media(meta=file_meta)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class MessageChain(ChainBase):
|
|||||||
# 下载种子
|
# 下载种子
|
||||||
context: Context = cache_list[int(text) - 1]
|
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":
|
elif text.lower() == "p":
|
||||||
# 上一页
|
# 上一页
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.chain.media import MediaChain
|
from app.chain.media import MediaChain
|
||||||
|
from app.chain.tmdb import TmdbChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.meta import MetaBase
|
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.downloadhistory_oper import DownloadHistoryOper
|
||||||
from app.db.models.downloadhistory import DownloadHistory
|
from app.db.models.downloadhistory import DownloadHistory
|
||||||
from app.db.models.transferhistory import TransferHistory
|
from app.db.models.transferhistory import TransferHistory
|
||||||
@@ -41,6 +42,7 @@ class TransferChain(ChainBase):
|
|||||||
self.transferhis = TransferHistoryOper(self._db)
|
self.transferhis = TransferHistoryOper(self._db)
|
||||||
self.progress = ProgressHelper()
|
self.progress = ProgressHelper()
|
||||||
self.mediachain = MediaChain(self._db)
|
self.mediachain = MediaChain(self._db)
|
||||||
|
self.tmdbchain = TmdbChain(self._db)
|
||||||
self.systemconfig = SystemConfigOper()
|
self.systemconfig = SystemConfigOper()
|
||||||
|
|
||||||
def process(self) -> bool:
|
def process(self) -> bool:
|
||||||
@@ -110,17 +112,6 @@ class TransferChain(ChainBase):
|
|||||||
logger.warn(f"{path.name} 没有找到可转移的媒体文件")
|
logger.warn(f"{path.name} 没有找到可转移的媒体文件")
|
||||||
return False, 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,
|
formaterHandler = FormatParser(eformat=epformat.format,
|
||||||
details=epformat.detail,
|
details=epformat.detail,
|
||||||
@@ -129,17 +120,24 @@ class TransferChain(ChainBase):
|
|||||||
|
|
||||||
# 开始进度
|
# 开始进度
|
||||||
self.progress.start(ProgressKey.FileTransfer)
|
self.progress.start(ProgressKey.FileTransfer)
|
||||||
# 总数
|
# 目录所有文件清单
|
||||||
transfer_files = SystemUtils.list_files(directory=path,
|
transfer_files = SystemUtils.list_files(directory=path,
|
||||||
extensions=settings.RMT_MEDIAEXT,
|
extensions=settings.RMT_MEDIAEXT,
|
||||||
min_filesize=min_filesize)
|
min_filesize=min_filesize)
|
||||||
if formaterHandler:
|
if formaterHandler:
|
||||||
# 有集自定义格式,过滤文件
|
# 有集自定义格式,过滤文件
|
||||||
transfer_files = [f for f in transfer_files if formaterHandler.match(f.name)]
|
transfer_files = [f for f in transfer_files if formaterHandler.match(f.name)]
|
||||||
# 总数
|
|
||||||
|
# 汇总错误信息
|
||||||
|
err_msgs: List[str] = []
|
||||||
|
# 总文件数
|
||||||
total_num = len(transfer_files)
|
total_num = len(transfer_files)
|
||||||
# 已处理数量
|
# 已处理数量
|
||||||
processed_num = 0
|
processed_num = 0
|
||||||
|
# 失败数量
|
||||||
|
fail_num = 0
|
||||||
|
# 跳过数量
|
||||||
|
skip_num = 0
|
||||||
self.progress.update(value=0,
|
self.progress.update(value=0,
|
||||||
text=f"开始转移 {path},共 {total_num} 个文件 ...",
|
text=f"开始转移 {path},共 {total_num} 个文件 ...",
|
||||||
key=ProgressKey.FileTransfer)
|
key=ProgressKey.FileTransfer)
|
||||||
@@ -149,6 +147,15 @@ class TransferChain(ChainBase):
|
|||||||
|
|
||||||
# 处理所有待转移目录或文件,默认一个转移路径或文件只有一个媒体信息
|
# 处理所有待转移目录或文件,默认一个转移路径或文件只有一个媒体信息
|
||||||
for trans_path in trans_paths:
|
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()
|
if (not trans_path.is_file()
|
||||||
and not SystemUtils.is_bluray_dir(trans_path)):
|
and not SystemUtils.is_bluray_dir(trans_path)):
|
||||||
@@ -165,7 +172,6 @@ class TransferChain(ChainBase):
|
|||||||
|
|
||||||
# 转移所有文件
|
# 转移所有文件
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
|
|
||||||
# 回收站及隐藏的文件不处理
|
# 回收站及隐藏的文件不处理
|
||||||
file_path_str = str(file_path)
|
file_path_str = str(file_path)
|
||||||
if file_path_str.find('/@Recycle/') != -1 \
|
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('/.') != -1 \
|
||||||
or file_path_str.find('/@eaDir') != -1:
|
or file_path_str.find('/@eaDir') != -1:
|
||||||
logger.debug(f"{file_path_str} 是回收站或隐藏的文件")
|
logger.debug(f"{file_path_str} 是回收站或隐藏的文件")
|
||||||
|
# 计数
|
||||||
|
processed_num += 1
|
||||||
|
skip_num += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 整理屏蔽词不处理
|
# 整理屏蔽词不处理
|
||||||
@@ -187,6 +196,9 @@ class TransferChain(ChainBase):
|
|||||||
break
|
break
|
||||||
if is_blocked:
|
if is_blocked:
|
||||||
err_msgs.append(f"{file_path.name} 命中整理屏蔽词")
|
err_msgs.append(f"{file_path.name} 命中整理屏蔽词")
|
||||||
|
# 计数
|
||||||
|
processed_num += 1
|
||||||
|
skip_num += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 转移成功的不再处理
|
# 转移成功的不再处理
|
||||||
@@ -194,6 +206,9 @@ class TransferChain(ChainBase):
|
|||||||
transferd = self.transferhis.get_by_src(file_path_str)
|
transferd = self.transferhis.get_by_src(file_path_str)
|
||||||
if transferd and transferd.status:
|
if transferd and transferd.status:
|
||||||
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
|
logger.info(f"{file_path} 已成功转移过,如需重新处理,请删除历史记录。")
|
||||||
|
# 计数
|
||||||
|
processed_num += 1
|
||||||
|
skip_num += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 更新进度
|
# 更新进度
|
||||||
@@ -202,12 +217,8 @@ class TransferChain(ChainBase):
|
|||||||
key=ProgressKey.FileTransfer)
|
key=ProgressKey.FileTransfer)
|
||||||
|
|
||||||
if not meta:
|
if not meta:
|
||||||
# 上级目录元数据
|
# 文件元数据
|
||||||
dir_meta = MetaInfo(title=file_path.parent.name)
|
file_meta = MetaInfoPath(file_path)
|
||||||
# 文件元数据,不包含后缀
|
|
||||||
file_meta = MetaInfo(title=file_path.stem)
|
|
||||||
# 合并元数据
|
|
||||||
file_meta.merge(dir_meta)
|
|
||||||
else:
|
else:
|
||||||
file_meta = meta
|
file_meta = meta
|
||||||
|
|
||||||
@@ -218,6 +229,9 @@ class TransferChain(ChainBase):
|
|||||||
if not file_meta:
|
if not file_meta:
|
||||||
logger.error(f"{file_path} 无法识别有效信息")
|
logger.error(f"{file_path} 无法识别有效信息")
|
||||||
err_msgs.append(f"{file_path} 无法识别有效信息")
|
err_msgs.append(f"{file_path} 无法识别有效信息")
|
||||||
|
# 计数
|
||||||
|
processed_num += 1
|
||||||
|
fail_num += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 自定义识别
|
# 自定义识别
|
||||||
@@ -241,7 +255,7 @@ class TransferChain(ChainBase):
|
|||||||
# 新增转移失败历史记录
|
# 新增转移失败历史记录
|
||||||
his = self.transferhis.add_fail(
|
his = self.transferhis.add_fail(
|
||||||
src_path=file_path,
|
src_path=file_path,
|
||||||
mode=settings.TRANSFER_TYPE,
|
mode=transfer_type,
|
||||||
meta=file_meta,
|
meta=file_meta,
|
||||||
download_hash=download_hash
|
download_hash=download_hash
|
||||||
)
|
)
|
||||||
@@ -250,6 +264,9 @@ class TransferChain(ChainBase):
|
|||||||
title=f"{file_path.name} 未识别到媒体信息,无法入库!\n"
|
title=f"{file_path.name} 未识别到媒体信息,无法入库!\n"
|
||||||
f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。"
|
f"回复:```\n/redo {his.id} [tmdbid]|[类型]\n``` 手动识别转移。"
|
||||||
))
|
))
|
||||||
|
# 计数
|
||||||
|
processed_num += 1
|
||||||
|
fail_num += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
|
||||||
@@ -261,31 +278,17 @@ class TransferChain(ChainBase):
|
|||||||
|
|
||||||
logger.info(f"{file_path.name} 识别为:{file_mediainfo.type.value} {file_mediainfo.title_year}")
|
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)
|
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:
|
if not download_hash:
|
||||||
download_file = self.downloadhis.get_file_by_fullpath(file_path_str)
|
download_file = self.downloadhis.get_file_by_fullpath(file_path_str)
|
||||||
if download_file:
|
if download_file:
|
||||||
@@ -296,7 +299,8 @@ class TransferChain(ChainBase):
|
|||||||
mediainfo=file_mediainfo,
|
mediainfo=file_mediainfo,
|
||||||
path=file_path,
|
path=file_path,
|
||||||
transfer_type=transfer_type,
|
transfer_type=transfer_type,
|
||||||
target=target)
|
target=target,
|
||||||
|
episodes_info=episodes_info)
|
||||||
if not transferinfo:
|
if not transferinfo:
|
||||||
logger.error("文件转移模块运行失败")
|
logger.error("文件转移模块运行失败")
|
||||||
return False, "文件转移模块运行失败"
|
return False, "文件转移模块运行失败"
|
||||||
@@ -307,7 +311,7 @@ class TransferChain(ChainBase):
|
|||||||
# 新增转移失败历史记录
|
# 新增转移失败历史记录
|
||||||
self.transferhis.add_fail(
|
self.transferhis.add_fail(
|
||||||
src_path=file_path,
|
src_path=file_path,
|
||||||
mode=settings.TRANSFER_TYPE,
|
mode=transfer_type,
|
||||||
download_hash=download_hash,
|
download_hash=download_hash,
|
||||||
meta=file_meta,
|
meta=file_meta,
|
||||||
mediainfo=file_mediainfo,
|
mediainfo=file_mediainfo,
|
||||||
@@ -320,6 +324,9 @@ class TransferChain(ChainBase):
|
|||||||
text=f"原因:{transferinfo.message or '未知'}",
|
text=f"原因:{transferinfo.message or '未知'}",
|
||||||
image=file_mediainfo.get_message_image()
|
image=file_mediainfo.get_message_image()
|
||||||
))
|
))
|
||||||
|
# 计数
|
||||||
|
processed_num += 1
|
||||||
|
fail_num += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 汇总信息
|
# 汇总信息
|
||||||
@@ -343,7 +350,7 @@ class TransferChain(ChainBase):
|
|||||||
# 新增转移成功历史记录
|
# 新增转移成功历史记录
|
||||||
self.transferhis.add_success(
|
self.transferhis.add_success(
|
||||||
src_path=file_path,
|
src_path=file_path,
|
||||||
mode=settings.TRANSFER_TYPE,
|
mode=transfer_type,
|
||||||
download_hash=download_hash,
|
download_hash=download_hash,
|
||||||
meta=file_meta,
|
meta=file_meta,
|
||||||
mediainfo=file_mediainfo,
|
mediainfo=file_mediainfo,
|
||||||
@@ -359,8 +366,7 @@ class TransferChain(ChainBase):
|
|||||||
key=ProgressKey.FileTransfer)
|
key=ProgressKey.FileTransfer)
|
||||||
|
|
||||||
# 目录或文件转移完成
|
# 目录或文件转移完成
|
||||||
self.progress.update(value=100,
|
self.progress.update(text=f"{trans_path} 转移完成,正在执行后续处理 ...",
|
||||||
text=f"所有文件转移完成,正在执行后续处理 ...",
|
|
||||||
key=ProgressKey.FileTransfer)
|
key=ProgressKey.FileTransfer)
|
||||||
|
|
||||||
# 执行后续处理
|
# 执行后续处理
|
||||||
@@ -387,10 +393,16 @@ class TransferChain(ChainBase):
|
|||||||
'mediainfo': media,
|
'mediainfo': media,
|
||||||
'transferinfo': transfer_info
|
'transferinfo': transfer_info
|
||||||
})
|
})
|
||||||
# 结束进度
|
|
||||||
logger.info(f"{path} 转移完成,共 {total_num} 个文件,"
|
# 结束进度
|
||||||
f"成功 {total_num - len(err_msgs)} 个,失败 {len(err_msgs)} 个")
|
logger.info(f"{path} 转移完成,共 {total_num} 个文件,"
|
||||||
self.progress.end(ProgressKey.FileTransfer)
|
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)
|
return True, "\n".join(err_msgs)
|
||||||
|
|
||||||
@@ -474,7 +486,8 @@ class TransferChain(ChainBase):
|
|||||||
text=errmsg, userid=userid))
|
text=errmsg, userid=userid))
|
||||||
return
|
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目录
|
根据历史记录,重新识别转移,只处理对应的src目录
|
||||||
:param logid: 历史记录ID
|
:param logid: 历史记录ID
|
||||||
@@ -492,11 +505,15 @@ class TransferChain(ChainBase):
|
|||||||
return False, f"源目录不存在:{src_path}"
|
return False, f"源目录不存在:{src_path}"
|
||||||
dest_path = Path(history.dest) if history.dest else None
|
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:
|
if not mediainfo:
|
||||||
return False, f"未识别到媒体信息,类型:{mtype.value},tmdbid:{tmdbid}"
|
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)
|
self.obtain_images(mediainfo=mediainfo)
|
||||||
|
|
||||||
@@ -599,6 +616,7 @@ class TransferChain(ChainBase):
|
|||||||
def delete_files(path: Path):
|
def delete_files(path: Path):
|
||||||
"""
|
"""
|
||||||
删除转移后的文件以及空目录
|
删除转移后的文件以及空目录
|
||||||
|
:param path: 文件路径
|
||||||
"""
|
"""
|
||||||
logger.info(f"开始删除文件以及空目录:{path} ...")
|
logger.info(f"开始删除文件以及空目录:{path} ...")
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field, asdict
|
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.config import settings
|
||||||
from app.core.meta import MetaBase
|
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元数据
|
:param tmdbinfo: TMDB元数据
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from app.core.meta.words import WordsMatcher
|
|||||||
|
|
||||||
def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
||||||
"""
|
"""
|
||||||
媒体整理入口,根据名称和副标题,判断是哪种类型的识别,返回对应对象
|
根据标题和副标题识别元数据
|
||||||
:param title: 标题、种子名、文件名
|
:param title: 标题、种子名、文件名
|
||||||
:param subtitle: 副标题、描述
|
:param subtitle: 副标题、描述
|
||||||
:return: MetaAnime、MetaVideo
|
:return: MetaAnime、MetaVideo
|
||||||
@@ -33,6 +33,20 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
|
|||||||
return meta
|
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:
|
def is_anime(name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否为动漫
|
判断是否为动漫
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ class DownloadHistoryOper(DbOper):
|
|||||||
"""
|
"""
|
||||||
DownloadFiles.delete_by_fullpath(self._db, fullpath)
|
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]:
|
def list_by_page(self, page: int = 1, count: int = 30) -> List[DownloadHistory]:
|
||||||
"""
|
"""
|
||||||
分页查询下载历史
|
分页查询下载历史
|
||||||
@@ -98,3 +108,11 @@ class DownloadHistoryOper(DbOper):
|
|||||||
season=season,
|
season=season,
|
||||||
episode=episode,
|
episode=episode,
|
||||||
tmdbid=tmdbid)
|
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_description = Column(String)
|
||||||
# 种子站点
|
# 种子站点
|
||||||
torrent_site = Column(String)
|
torrent_site = Column(String)
|
||||||
|
# 下载用户
|
||||||
|
userid = Column(String)
|
||||||
|
# 下载渠道
|
||||||
|
channel = Column(String)
|
||||||
|
# 创建时间
|
||||||
|
date = Column(String)
|
||||||
# 附加信息
|
# 附加信息
|
||||||
note = Column(String)
|
note = Column(String)
|
||||||
|
|
||||||
@@ -90,6 +96,19 @@ class DownloadHistory(Base):
|
|||||||
DownloadHistory.episodes == episode).order_by(
|
DownloadHistory.episodes == episode).order_by(
|
||||||
DownloadHistory.id.desc()).all()
|
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):
|
class DownloadFiles(Base):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ class TransferHistory(Base):
|
|||||||
def get_by_src(db: Session, src: str):
|
def get_by_src(db: Session, src: str):
|
||||||
return db.query(TransferHistory).filter(TransferHistory.src == src).first()
|
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
|
@staticmethod
|
||||||
def statistic(db: Session, days: int = 7):
|
def statistic(db: Session, days: int = 7):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ class TransferHistoryOper(DbOper):
|
|||||||
"""
|
"""
|
||||||
return TransferHistory.get_by_src(self._db, src)
|
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:
|
def add(self, **kwargs) -> TransferHistory:
|
||||||
"""
|
"""
|
||||||
新增转移历史
|
新增转移历史
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
class NfoReader:
|
class NfoReader:
|
||||||
@@ -8,6 +9,9 @@ class NfoReader:
|
|||||||
self.tree = ET.parse(xml_file_path)
|
self.tree = ET.parse(xml_file_path)
|
||||||
self.root = self.tree.getroot()
|
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)
|
element = self.root.find(element_path)
|
||||||
return element.text if element is not None else None
|
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():
|
if not self.emby.is_inactive():
|
||||||
self.emby = Emby()
|
self.emby.reconnect()
|
||||||
|
|
||||||
def user_authenticate(self, name: str, password: str) -> Optional[str]:
|
def user_authenticate(self, name: str, password: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ class Emby(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
return True if not self.user else 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]:
|
def get_emby_folders(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
获取Emby媒体库路径列表
|
获取Emby媒体库路径列表
|
||||||
@@ -481,14 +488,14 @@ class Emby(metaclass=Singleton):
|
|||||||
# 匹配子目录
|
# 匹配子目录
|
||||||
subfolder_path = Path(subfolder.get("Path"))
|
subfolder_path = Path(subfolder.get("Path"))
|
||||||
if item_path.is_relative_to(subfolder_path):
|
if item_path.is_relative_to(subfolder_path):
|
||||||
return subfolder.get("Id")
|
return folder.get("Id")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(str(err))
|
print(str(err))
|
||||||
# 如果找不到,只要路径中有分类目录名就命中
|
# 如果找不到,只要路径中有分类目录名就命中
|
||||||
for subfolder in folder.get("SubFolders"):
|
for subfolder in folder.get("SubFolders"):
|
||||||
if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category,
|
if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category,
|
||||||
subfolder.get("Path")):
|
subfolder.get("Path")):
|
||||||
return folder.get("Id")
|
return folder.get("Id")
|
||||||
# 刷新根目录
|
# 刷新根目录
|
||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
|
|||||||
@@ -329,7 +329,11 @@ class FanartModule(_ModuleBase):
|
|||||||
if mediainfo.type == MediaType.MOVIE:
|
if mediainfo.type == MediaType.MOVIE:
|
||||||
result = self.__request_fanart(mediainfo.type, mediainfo.tmdb_id)
|
result = self.__request_fanart(mediainfo.type, mediainfo.tmdb_id)
|
||||||
else:
|
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':
|
if not result or result.get('status') == 'error':
|
||||||
logger.warn(f"没有获取到 {mediainfo.title_year} 的Fanart图片数据")
|
logger.warn(f"没有获取到 {mediainfo.title_year} 的Fanart图片数据")
|
||||||
return
|
return
|
||||||
@@ -351,6 +355,7 @@ class FanartModule(_ModuleBase):
|
|||||||
# 季图片格式 seasonxx-poster
|
# 季图片格式 seasonxx-poster
|
||||||
image_name = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
image_name = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
||||||
if not mediainfo.get_image(image_name):
|
if not mediainfo.get_image(image_name):
|
||||||
|
# 没有图片才设置
|
||||||
mediainfo.set_image(image_name, image_obj.get('url'))
|
mediainfo.set_image(image_name, image_obj.get('url'))
|
||||||
|
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from app.core.meta import MetaBase
|
|||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
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.schemas.types import MediaType
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
@@ -30,7 +30,8 @@ class FileTransferModule(_ModuleBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def transfer(self, path: Path, meta: MetaBase, mediainfo: MediaInfo,
|
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: 文件路径
|
:param path: 文件路径
|
||||||
@@ -38,6 +39,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:param transfer_type: 转移方式
|
:param transfer_type: 转移方式
|
||||||
:param target: 目标路径
|
:param target: 目标路径
|
||||||
|
:param episodes_info: 当前季的全部集信息
|
||||||
:return: {path, target_path, message}
|
:return: {path, target_path, message}
|
||||||
"""
|
"""
|
||||||
# 获取目标路径
|
# 获取目标路径
|
||||||
@@ -49,13 +51,14 @@ class FileTransferModule(_ModuleBase):
|
|||||||
logger.error("未找到媒体库目录,无法转移文件")
|
logger.error("未找到媒体库目录,无法转移文件")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
path=path,
|
path=path,
|
||||||
message="未找到媒体库目录,无法转移文件")
|
message="未找到媒体库目录")
|
||||||
# 转移
|
# 转移
|
||||||
return self.transfer_media(in_path=path,
|
return self.transfer_media(in_path=path,
|
||||||
in_meta=meta,
|
in_meta=meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
transfer_type=transfer_type,
|
transfer_type=transfer_type,
|
||||||
target_dir=target)
|
target_dir=target,
|
||||||
|
episodes_info=episodes_info)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __transfer_command(file_item: Path, target_file: Path, transfer_type: str) -> int:
|
def __transfer_command(file_item: Path, target_file: Path, transfer_type: str) -> int:
|
||||||
@@ -355,6 +358,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
mediainfo: MediaInfo,
|
mediainfo: MediaInfo,
|
||||||
transfer_type: str,
|
transfer_type: str,
|
||||||
target_dir: Path,
|
target_dir: Path,
|
||||||
|
episodes_info: List[TmdbEpisode] = None
|
||||||
) -> TransferInfo:
|
) -> TransferInfo:
|
||||||
"""
|
"""
|
||||||
识别并转移一个文件或者一个目录下的所有文件
|
识别并转移一个文件或者一个目录下的所有文件
|
||||||
@@ -363,6 +367,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
:param mediainfo: 媒体信息
|
:param mediainfo: 媒体信息
|
||||||
:param target_dir: 媒体库根目录
|
:param target_dir: 媒体库根目录
|
||||||
:param transfer_type: 文件转移方式
|
:param transfer_type: 文件转移方式
|
||||||
|
:param episodes_info: 当前季的全部集信息
|
||||||
:return: TransferInfo、错误信息
|
:return: TransferInfo、错误信息
|
||||||
"""
|
"""
|
||||||
# 检查目录路径
|
# 检查目录路径
|
||||||
@@ -404,7 +409,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
logger.error(f"文件夹 {in_path} 转移失败,错误码:{retcode}")
|
logger.error(f"文件夹 {in_path} 转移失败,错误码:{retcode}")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"文件夹 {in_path} 转移失败,错误码:{retcode}",
|
message=f"错误码:{retcode}",
|
||||||
path=in_path,
|
path=in_path,
|
||||||
target_path=new_path,
|
target_path=new_path,
|
||||||
is_bluray=bluray_flag)
|
is_bluray=bluray_flag)
|
||||||
@@ -418,17 +423,24 @@ class FileTransferModule(_ModuleBase):
|
|||||||
is_bluray=bluray_flag)
|
is_bluray=bluray_flag)
|
||||||
else:
|
else:
|
||||||
# 转移单个文件
|
# 转移单个文件
|
||||||
# 文件结束季为空
|
if mediainfo.type == MediaType.TV:
|
||||||
in_meta.end_season = None
|
# 电视剧
|
||||||
|
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.end_season = None
|
||||||
in_meta.total_season = 1
|
# 文件总季数为1
|
||||||
|
if in_meta.total_season:
|
||||||
# 文件不可能有多集
|
in_meta.total_season = 1
|
||||||
if in_meta.total_episode > 2:
|
# 文件不可能超过2集
|
||||||
in_meta.total_episode = 1
|
if in_meta.total_episode > 2:
|
||||||
in_meta.end_episode = None
|
in_meta.total_episode = 1
|
||||||
|
in_meta.end_episode = None
|
||||||
|
|
||||||
# 目的文件名
|
# 目的文件名
|
||||||
new_file = self.get_rename_path(
|
new_file = self.get_rename_path(
|
||||||
@@ -437,6 +449,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
rename_dict=self.__get_naming_dict(
|
rename_dict=self.__get_naming_dict(
|
||||||
meta=in_meta,
|
meta=in_meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
|
episodes_info=episodes_info,
|
||||||
file_ext=in_path.suffix
|
file_ext=in_path.suffix
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -456,7 +469,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
logger.error(f"文件 {in_path} 转移失败,错误码:{retcode}")
|
logger.error(f"文件 {in_path} 转移失败,错误码:{retcode}")
|
||||||
return TransferInfo(success=False,
|
return TransferInfo(success=False,
|
||||||
message=f"文件 {in_path.name} 转移失败,错误码:{retcode}",
|
message=f"错误码:{retcode}",
|
||||||
path=in_path,
|
path=in_path,
|
||||||
target_path=new_file,
|
target_path=new_file,
|
||||||
fail_list=[str(in_path)])
|
fail_list=[str(in_path)])
|
||||||
@@ -472,13 +485,23 @@ class FileTransferModule(_ModuleBase):
|
|||||||
file_list_new=[str(new_file)])
|
file_list_new=[str(new_file)])
|
||||||
|
|
||||||
@staticmethod
|
@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字典
|
根据媒体信息,返回Format字典
|
||||||
:param meta: 文件元数据
|
:param meta: 文件元数据
|
||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:param file_ext: 文件扩展名
|
: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 {
|
return {
|
||||||
# 标题
|
# 标题
|
||||||
"title": mediainfo.title,
|
"title": mediainfo.title,
|
||||||
@@ -490,14 +513,16 @@ class FileTransferModule(_ModuleBase):
|
|||||||
"name": meta.name,
|
"name": meta.name,
|
||||||
# 年份
|
# 年份
|
||||||
"year": mediainfo.year or meta.year,
|
"year": mediainfo.year or meta.year,
|
||||||
|
# 资源类型
|
||||||
|
"resourceType": meta.resource_type,
|
||||||
|
# 特效
|
||||||
|
"effect": meta.resource_effect,
|
||||||
# 版本
|
# 版本
|
||||||
"edition": meta.edition,
|
"edition": meta.edition,
|
||||||
# 分辨率
|
# 分辨率
|
||||||
"videoFormat": meta.resource_pix,
|
"videoFormat": meta.resource_pix,
|
||||||
# 制作组/字幕组
|
# 制作组/字幕组
|
||||||
"releaseGroup": meta.resource_team,
|
"releaseGroup": meta.resource_team,
|
||||||
# 特效
|
|
||||||
"effect": meta.resource_effect,
|
|
||||||
# 视频编码
|
# 视频编码
|
||||||
"videoCodec": meta.video_encode,
|
"videoCodec": meta.video_encode,
|
||||||
# 音频编码
|
# 音频编码
|
||||||
@@ -514,6 +539,8 @@ class FileTransferModule(_ModuleBase):
|
|||||||
"season_episode": "%s%s" % (meta.season, meta.episodes),
|
"season_episode": "%s%s" % (meta.season, meta.episodes),
|
||||||
# 段/节
|
# 段/节
|
||||||
"part": meta.part,
|
"part": meta.part,
|
||||||
|
# 剧集标题
|
||||||
|
"episode_title": episode_title,
|
||||||
# 文件后缀
|
# 文件后缀
|
||||||
"fileExt": file_ext
|
"fileExt": file_ext
|
||||||
}
|
}
|
||||||
@@ -613,9 +640,10 @@ class FileTransferModule(_ModuleBase):
|
|||||||
rename_format = settings.TV_RENAME_FORMAT \
|
rename_format = settings.TV_RENAME_FORMAT \
|
||||||
if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT
|
if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT
|
||||||
# 相对路径
|
# 相对路径
|
||||||
|
meta = MetaInfo(mediainfo.title)
|
||||||
rel_path = self.get_rename_path(
|
rel_path = self.get_rename_path(
|
||||||
template_string=rename_format,
|
template_string=rename_format,
|
||||||
rename_dict=self.__get_naming_dict(meta=MetaInfo(mediainfo.title),
|
rename_dict=self.__get_naming_dict(meta=meta,
|
||||||
mediainfo=mediainfo)
|
mediainfo=mediainfo)
|
||||||
)
|
)
|
||||||
# 取相对路径的第1层目录
|
# 取相对路径的第1层目录
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class FilterModule(_ModuleBase):
|
|||||||
},
|
},
|
||||||
# 国语配音
|
# 国语配音
|
||||||
"CNVOI": {
|
"CNVOI": {
|
||||||
"include": [r'[国國][语語]配音|[国國]配'],
|
"include": [r'[国國][语語]配音|[国國]配|[国國][语語]'],
|
||||||
"exclude": []
|
"exclude": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ class JellyfinModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
# 定时重连
|
# 定时重连
|
||||||
if not self.jellyfin.is_inactive():
|
if not self.jellyfin.is_inactive():
|
||||||
self.jellyfin = Jellyfin()
|
self.jellyfin.reconnect()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
return True if not self.user else 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]:
|
def __get_jellyfin_librarys(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
获取Jellyfin媒体库的信息
|
获取Jellyfin媒体库的信息
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class PlexModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
# 定时重连
|
# 定时重连
|
||||||
if not self.plex.is_inactive():
|
if not self.plex.is_inactive():
|
||||||
self.plex = Plex()
|
self.plex.reconnect()
|
||||||
|
|
||||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ class Plex(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
return True if not self._plex else 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):
|
def get_librarys(self):
|
||||||
"""
|
"""
|
||||||
获取媒体服务器所有媒体库列表
|
获取媒体服务器所有媒体库列表
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class QbittorrentModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
# 定时重连
|
# 定时重连
|
||||||
if self.qbittorrent.is_inactive():
|
if self.qbittorrent.is_inactive():
|
||||||
self.qbittorrent = Qbittorrent()
|
self.qbittorrent.reconnect()
|
||||||
|
|
||||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||||
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ class Qbittorrent(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
return True if not self.qbc else False
|
return True if not self.qbc else False
|
||||||
|
|
||||||
|
def reconnect(self):
|
||||||
|
"""
|
||||||
|
重连
|
||||||
|
"""
|
||||||
|
self.qbc = self.__login_qbittorrent()
|
||||||
|
|
||||||
def __login_qbittorrent(self) -> Optional[Client]:
|
def __login_qbittorrent(self) -> Optional[Client]:
|
||||||
"""
|
"""
|
||||||
连接qbittorrent
|
连接qbittorrent
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from app.schemas.types import MediaType
|
|||||||
from app.utils.common import retry
|
from app.utils.common import retry
|
||||||
from app.utils.dom import DomUtils
|
from app.utils.dom import DomUtils
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
class TmdbScraper:
|
class TmdbScraper:
|
||||||
@@ -121,8 +122,25 @@ class TmdbScraper:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{file_path} 刮削失败:{e}")
|
logger.error(f"{file_path} 刮削失败:{e}")
|
||||||
|
|
||||||
@staticmethod
|
def __get_chinese_name(self, person: dict):
|
||||||
def __gen_common_nfo(mediainfo: MediaInfo, doc, root):
|
"""
|
||||||
|
获取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
|
生成公共NFO
|
||||||
"""
|
"""
|
||||||
@@ -155,18 +173,19 @@ class TmdbScraper:
|
|||||||
xoutline.appendChild(doc.createCDATASection(mediainfo.overview or ""))
|
xoutline.appendChild(doc.createCDATASection(mediainfo.overview or ""))
|
||||||
# 导演
|
# 导演
|
||||||
for director in mediainfo.directors:
|
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 ""))
|
xdirector.setAttribute("tmdbid", str(director.get("id") or ""))
|
||||||
# 演员
|
# 演员
|
||||||
for actor in mediainfo.actors:
|
for actor in mediainfo.actors:
|
||||||
|
# 获取中文名
|
||||||
|
cn_name = self.__get_chinese_name(actor)
|
||||||
xactor = DomUtils.add_node(doc, root, "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, "type", "Actor")
|
||||||
DomUtils.add_node(doc, xactor, "role", actor.get("character") or actor.get("role") or "")
|
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, "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 []
|
genres = mediainfo.genres or []
|
||||||
for genre in genres:
|
for genre in genres:
|
||||||
@@ -307,14 +326,18 @@ class TmdbScraper:
|
|||||||
directors = episodeinfo.get("crew") or []
|
directors = episodeinfo.get("crew") or []
|
||||||
for director in directors:
|
for director in directors:
|
||||||
if director.get("known_for_department") == "Directing":
|
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 ""))
|
xdirector.setAttribute("tmdbid", str(director.get("id") or ""))
|
||||||
# 演员
|
# 演员
|
||||||
actors = episodeinfo.get("guest_stars") or []
|
actors = episodeinfo.get("guest_stars") or []
|
||||||
for actor in actors:
|
for actor in actors:
|
||||||
if actor.get("known_for_department") == "Acting":
|
if actor.get("known_for_department") == "Acting":
|
||||||
|
# 获取中文名
|
||||||
|
cn_name = self.__get_chinese_name(actor)
|
||||||
xactor = DomUtils.add_node(doc, root, "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, "type", "Actor")
|
||||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||||
# 保存文件
|
# 保存文件
|
||||||
@@ -336,6 +359,8 @@ class TmdbScraper:
|
|||||||
logger.info(f"图片已保存:{file_path}")
|
logger.info(f"图片已保存:{file_path}")
|
||||||
else:
|
else:
|
||||||
logger.info(f"{file_path.stem}图片下载失败,请检查网络连通性")
|
logger.info(f"{file_path.stem}图片下载失败,请检查网络连通性")
|
||||||
|
except RequestException as err:
|
||||||
|
raise err
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"{file_path.stem}图片下载失败:{err}")
|
logger.error(f"{file_path.stem}图片下载失败:{err}")
|
||||||
|
|
||||||
|
|||||||
@@ -1136,6 +1136,26 @@ class TmdbHelper:
|
|||||||
def get_person_detail(self, person_id: int) -> dict:
|
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:
|
if not self.person:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TMDb(object):
|
|||||||
self.__class__._session = requests.Session() if session is None else session
|
self.__class__._session = requests.Session() if session is None else session
|
||||||
self._remaining = 40
|
self._remaining = 40
|
||||||
self._reset = None
|
self._reset = None
|
||||||
|
self._timeout = 15
|
||||||
self.obj_cached = obj_cached
|
self.obj_cached = obj_cached
|
||||||
if os.environ.get(self.TMDB_LANGUAGE) is None:
|
if os.environ.get(self.TMDB_LANGUAGE) is None:
|
||||||
os.environ[self.TMDB_LANGUAGE] = "en-US"
|
os.environ[self.TMDB_LANGUAGE] = "en-US"
|
||||||
@@ -131,12 +132,14 @@ class TMDb(object):
|
|||||||
|
|
||||||
@lru_cache(maxsize=REQUEST_CACHE_MAXSIZE)
|
@lru_cache(maxsize=REQUEST_CACHE_MAXSIZE)
|
||||||
def cached_request(self, method, url, data, json):
|
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):
|
def cache_clear(self):
|
||||||
return self.cached_request.cache_clear()
|
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 == "":
|
if self.api_key is None or self.api_key == "":
|
||||||
raise TMDbException("No API key found.")
|
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":
|
if self.cache and self.obj_cached and call_cached and method != "POST":
|
||||||
req = self.cached_request(method, url, data, json)
|
req = self.cached_request(method, url, data, json)
|
||||||
else:
|
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
|
headers = req.headers
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TransmissionModule(_ModuleBase):
|
|||||||
"""
|
"""
|
||||||
# 定时重连
|
# 定时重连
|
||||||
if not self.transmission.is_inactive():
|
if not self.transmission.is_inactive():
|
||||||
self.transmission = Transmission()
|
self.transmission.reconnect()
|
||||||
|
|
||||||
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
|
||||||
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
episodes: Set[int] = None, category: str = None) -> Optional[Tuple[Optional[str], str]]:
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ class Transmission(metaclass=Singleton):
|
|||||||
return False
|
return False
|
||||||
return True if not self.trc else 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,
|
def get_torrents(self, ids: Union[str, list] = None, status: Union[str, list] = None,
|
||||||
tags: Union[str, list] = None) -> Tuple[List[Torrent], bool]:
|
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
|
plugin_order = 21
|
||||||
# 可使用的用户级别
|
# 可使用的用户级别
|
||||||
auth_level = 3
|
auth_level = 2
|
||||||
|
|
||||||
# 私有属性
|
# 私有属性
|
||||||
siteshelper = None
|
siteshelper = None
|
||||||
@@ -120,6 +120,9 @@ class BrushFlow(_PluginBase):
|
|||||||
self.save_data("statistic", {})
|
self.save_data("statistic", {})
|
||||||
# 清除种子记录
|
# 清除种子记录
|
||||||
self.save_data("torrents", {})
|
self.save_data("torrents", {})
|
||||||
|
# 关闭一次性开关
|
||||||
|
self._clear_task = False
|
||||||
|
self.__update_config()
|
||||||
|
|
||||||
# 停止现有任务
|
# 停止现有任务
|
||||||
self.stop_service()
|
self.stop_service()
|
||||||
@@ -729,6 +732,7 @@ class BrushFlow(_PluginBase):
|
|||||||
"enabled": False,
|
"enabled": False,
|
||||||
"notify": True,
|
"notify": True,
|
||||||
"onlyonce": False,
|
"onlyonce": False,
|
||||||
|
"clear_task": False,
|
||||||
"freeleech": "free"
|
"freeleech": "free"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1218,7 +1222,8 @@ class BrushFlow(_PluginBase):
|
|||||||
"seed_inactivetime": self._seed_inactivetime,
|
"seed_inactivetime": self._seed_inactivetime,
|
||||||
"up_speed": self._up_speed,
|
"up_speed": self._up_speed,
|
||||||
"dl_speed": self._dl_speed,
|
"dl_speed": self._dl_speed,
|
||||||
"save_path": self._save_path
|
"save_path": self._save_path,
|
||||||
|
"clear_task": self._clear_task
|
||||||
})
|
})
|
||||||
|
|
||||||
def brush(self):
|
def brush(self):
|
||||||
@@ -1265,12 +1270,6 @@ class BrushFlow(_PluginBase):
|
|||||||
f"{task.get('site_name')}{task.get('title')}" for task in task_info.values()
|
f"{task.get('site_name')}{task.get('title')}" for task in task_info.values()
|
||||||
]:
|
]:
|
||||||
continue
|
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:
|
if self._freeleech and torrent.downloadvolumefactor != 0:
|
||||||
continue
|
continue
|
||||||
@@ -1349,6 +1348,12 @@ class BrushFlow(_PluginBase):
|
|||||||
logger.warn(f"当前总下载带宽 {StringUtils.str_filesize(current_download_speed)} "
|
logger.warn(f"当前总下载带宽 {StringUtils.str_filesize(current_download_speed)} "
|
||||||
f"已达到最大值 {self._maxdlspeed} KB/s,暂时停止新增任务")
|
f"已达到最大值 {self._maxdlspeed} KB/s,暂时停止新增任务")
|
||||||
break
|
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)
|
hash_string = self.__download(torrent=torrent)
|
||||||
if not hash_string:
|
if not hash_string:
|
||||||
@@ -1767,19 +1772,22 @@ class BrushFlow(_PluginBase):
|
|||||||
"""
|
"""
|
||||||
发送删除种子的消息
|
发送删除种子的消息
|
||||||
"""
|
"""
|
||||||
if self._notify:
|
if not self._notify:
|
||||||
self.chain.post_message(Notification(
|
return
|
||||||
mtype=NotificationType.SiteMessage,
|
self.chain.post_message(Notification(
|
||||||
title=f"【刷流任务删种】",
|
mtype=NotificationType.SiteMessage,
|
||||||
text=f"站点:{site_name}\n"
|
title=f"【刷流任务删种】",
|
||||||
f"标题:{torrent_title}\n"
|
text=f"站点:{site_name}\n"
|
||||||
f"原因:{reason}"
|
f"标题:{torrent_title}\n"
|
||||||
))
|
f"原因:{reason}"
|
||||||
|
))
|
||||||
|
|
||||||
def __send_add_message(self, torrent: TorrentInfo):
|
def __send_add_message(self, torrent: TorrentInfo):
|
||||||
"""
|
"""
|
||||||
发送添加下载的消息
|
发送添加下载的消息
|
||||||
"""
|
"""
|
||||||
|
if not self._notify:
|
||||||
|
return
|
||||||
msg_text = ""
|
msg_text = ""
|
||||||
if torrent.site_name:
|
if torrent.site_name:
|
||||||
msg_text = f"站点:{torrent.site_name}"
|
msg_text = f"站点:{torrent.site_name}"
|
||||||
@@ -1819,25 +1827,29 @@ class BrushFlow(_PluginBase):
|
|||||||
|
|
||||||
def __get_downloader_info(self) -> schemas.DownloaderInfo:
|
def __get_downloader_info(self) -> schemas.DownloaderInfo:
|
||||||
"""
|
"""
|
||||||
获取下载器实时信息
|
获取下载器实时信息(所有下载器)
|
||||||
"""
|
"""
|
||||||
if self._downloader == "qbittorrent":
|
ret_info = schemas.DownloaderInfo()
|
||||||
# 调用Qbittorrent API查询实时信息
|
|
||||||
|
# Qbittorrent
|
||||||
|
if self.qb:
|
||||||
info = self.qb.transfer_info()
|
info = self.qb.transfer_info()
|
||||||
return schemas.DownloaderInfo(
|
if info:
|
||||||
download_speed=info.get("dl_info_speed"),
|
ret_info.download_speed += info.get("dl_info_speed")
|
||||||
upload_speed=info.get("up_info_speed"),
|
ret_info.upload_speed += info.get("up_info_speed")
|
||||||
download_size=info.get("dl_info_data"),
|
ret_info.download_size += info.get("dl_info_data")
|
||||||
upload_size=info.get("up_info_data")
|
ret_info.upload_size += info.get("up_info_data")
|
||||||
)
|
|
||||||
else:
|
# Transmission
|
||||||
|
if self.tr:
|
||||||
info = self.tr.transfer_info()
|
info = self.tr.transfer_info()
|
||||||
return schemas.DownloaderInfo(
|
if info:
|
||||||
download_speed=info.download_speed,
|
ret_info.download_speed += info.download_speed
|
||||||
upload_speed=info.upload_speed,
|
ret_info.upload_speed += info.upload_speed
|
||||||
download_size=info.current_stats.downloaded_bytes,
|
ret_info.download_size += info.current_stats.downloaded_bytes
|
||||||
upload_size=info.current_stats.uploaded_bytes
|
ret_info.upload_size += info.current_stats.uploaded_bytes
|
||||||
)
|
|
||||||
|
return ret_info
|
||||||
|
|
||||||
def __get_downloading_count(self) -> int:
|
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.config import settings
|
||||||
from app.core.context import MediaInfo
|
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.log import logger
|
||||||
from app.plugins import _PluginBase
|
from app.plugins import _PluginBase
|
||||||
from app.schemas import TransferInfo
|
from app.schemas import TransferInfo
|
||||||
@@ -183,7 +183,7 @@ class ChineseSubFinder(_PluginBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@eventmanager.register(EventType.TransferComplete)
|
@eventmanager.register(EventType.TransferComplete)
|
||||||
def download(self, event):
|
def download(self, event: Event):
|
||||||
"""
|
"""
|
||||||
调用ChineseSubFinder下载字幕
|
调用ChineseSubFinder下载字幕
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ from watchdog.events import FileSystemEventHandler
|
|||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.observers.polling import PollingObserver
|
from watchdog.observers.polling import PollingObserver
|
||||||
|
|
||||||
|
from app.chain.tmdb import TmdbChain
|
||||||
from app.chain.transfer import TransferChain
|
from app.chain.transfer import TransferChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.context import MediaInfo
|
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.downloadhistory_oper import DownloadHistoryOper
|
||||||
from app.db.transferhistory_oper import TransferHistoryOper
|
from app.db.transferhistory_oper import TransferHistoryOper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
@@ -74,6 +75,7 @@ class DirMonitor(_PluginBase):
|
|||||||
transferhis = None
|
transferhis = None
|
||||||
downloadhis = None
|
downloadhis = None
|
||||||
transferchian = None
|
transferchian = None
|
||||||
|
tmdbchain = None
|
||||||
_observer = []
|
_observer = []
|
||||||
_enabled = False
|
_enabled = False
|
||||||
_notify = False
|
_notify = False
|
||||||
@@ -93,7 +95,7 @@ class DirMonitor(_PluginBase):
|
|||||||
self.transferhis = TransferHistoryOper(self.db)
|
self.transferhis = TransferHistoryOper(self.db)
|
||||||
self.downloadhis = DownloadHistoryOper(self.db)
|
self.downloadhis = DownloadHistoryOper(self.db)
|
||||||
self.transferchian = TransferChain(self.db)
|
self.transferchian = TransferChain(self.db)
|
||||||
|
self.tmdbchain = TmdbChain(self.db)
|
||||||
# 清空配置
|
# 清空配置
|
||||||
self._dirconf = {}
|
self._dirconf = {}
|
||||||
|
|
||||||
@@ -237,13 +239,8 @@ class DirMonitor(_PluginBase):
|
|||||||
logger.info(f"{event_path} 已整理过")
|
logger.info(f"{event_path} 已整理过")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 上级目录元数据
|
# 元数据
|
||||||
meta = MetaInfo(title=file_path.parent.name)
|
file_meta = MetaInfoPath(file_path)
|
||||||
# 文件元数据,不包含后缀
|
|
||||||
file_meta = MetaInfo(title=file_path.stem)
|
|
||||||
# 合并元数据
|
|
||||||
file_meta.merge(meta)
|
|
||||||
|
|
||||||
if not file_meta.name:
|
if not file_meta.name:
|
||||||
logger.error(f"{file_path.name} 无法识别有效信息")
|
logger.error(f"{file_path.name} 无法识别有效信息")
|
||||||
return
|
return
|
||||||
@@ -279,6 +276,13 @@ class DirMonitor(_PluginBase):
|
|||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
self.chain.obtain_images(mediainfo=mediainfo)
|
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
|
# 获取downloadhash
|
||||||
download_hash = self.get_download_hash(src=str(file_path))
|
download_hash = self.get_download_hash(src=str(file_path))
|
||||||
|
|
||||||
@@ -287,7 +291,8 @@ class DirMonitor(_PluginBase):
|
|||||||
path=file_path,
|
path=file_path,
|
||||||
transfer_type=self._transfer_type,
|
transfer_type=self._transfer_type,
|
||||||
target=target,
|
target=target,
|
||||||
meta=file_meta)
|
meta=file_meta,
|
||||||
|
episodes_info=episodes_info)
|
||||||
|
|
||||||
if not transferinfo:
|
if not transferinfo:
|
||||||
logger.error("文件转移模块运行失败")
|
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:
|
if media_list:
|
||||||
media_files = media_list.get("files") or []
|
media_files = media_list.get("files") or []
|
||||||
if media_files:
|
if media_files:
|
||||||
@@ -384,7 +389,7 @@ class DirMonitor(_PluginBase):
|
|||||||
],
|
],
|
||||||
"time": datetime.now()
|
"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:
|
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):
|
class MediaSyncDel(_PluginBase):
|
||||||
# 插件名称
|
# 插件名称
|
||||||
plugin_name = "媒体库同步删除"
|
plugin_name = "媒体文件同步删除"
|
||||||
# 插件描述
|
# 插件描述
|
||||||
plugin_desc = "媒体库删除媒体后同步删除历史记录、源文件和下载任务。"
|
plugin_desc = "同步删除历史记录、源文件和下载任务。"
|
||||||
# 插件图标
|
# 插件图标
|
||||||
plugin_icon = "mediasyncdel.png"
|
plugin_icon = "mediasyncdel.png"
|
||||||
# 主题色
|
# 主题色
|
||||||
@@ -187,9 +187,9 @@ class MediaSyncDel(_PluginBase):
|
|||||||
'component': 'VSelect',
|
'component': 'VSelect',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'sync_type',
|
'model': 'sync_type',
|
||||||
'label': '同步方式',
|
'label': '媒体库同步方式',
|
||||||
'items': [
|
'items': [
|
||||||
{'title': 'webhook', 'value': 'webhook'},
|
{'title': 'Webhook', 'value': 'webhook'},
|
||||||
{'title': '日志', 'value': 'log'},
|
{'title': '日志', 'value': 'log'},
|
||||||
{'title': 'Scripter X', 'value': 'plugin'}
|
{'title': 'Scripter X', 'value': 'plugin'}
|
||||||
]
|
]
|
||||||
@@ -208,7 +208,7 @@ class MediaSyncDel(_PluginBase):
|
|||||||
'component': 'VTextField',
|
'component': 'VTextField',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'cron',
|
'model': 'cron',
|
||||||
'label': '执行周期',
|
'label': '日志检查周期',
|
||||||
'placeholder': '5位cron表达式,留空自动'
|
'placeholder': '5位cron表达式,留空自动'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,7 @@ class MediaSyncDel(_PluginBase):
|
|||||||
'props': {
|
'props': {
|
||||||
'model': 'library_path',
|
'model': 'library_path',
|
||||||
'rows': '2',
|
'rows': '2',
|
||||||
'label': '媒体库路径',
|
'label': '媒体库路径映射',
|
||||||
'placeholder': '媒体服务器路径:MoviePilot路径(一行一个)'
|
'placeholder': '媒体服务器路径:MoviePilot路径(一行一个)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,11 +266,11 @@ class MediaSyncDel(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VAlert',
|
'component': 'VAlert',
|
||||||
'props': {
|
'props': {
|
||||||
'text': '同步方式分为webhook、日志同步和Scripter X。'
|
'text': '媒体库同步方式分为Webhook、日志同步和Scripter X:'
|
||||||
'webhook需要Emby4.8.0.45及以上开启媒体删除的webhook'
|
'1、Webhook需要Emby4.8.0.45及以上开启媒体删除的Webhook。'
|
||||||
'(建议使用媒体库刮削插件覆盖元数据重新刮削剧集路径)。'
|
'2、日志同步需要配置检查周期,默认30分钟执行一次。'
|
||||||
'日志同步需要配置执行周期,默认30分钟执行一次。'
|
'3、Scripter X方式需要emby安装并配置Scripter X插件,无需配置执行周期。'
|
||||||
'Scripter X方式需要emby安装并配置Scripter X插件,无需配置执行周期。'
|
'4、启用该插件后,非媒体服务器触发的源文件删除,也会同步处理下载器中的下载任务。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1202,21 +1202,25 @@ class MediaSyncDel(_PluginBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("退出插件失败:%s" % str(e))
|
logger.error("退出插件失败:%s" % str(e))
|
||||||
|
|
||||||
@eventmanager.register(EventType.MediaDeleted)
|
@eventmanager.register(EventType.DownloadFileDeleted)
|
||||||
def remote_sync_del(self, event: Event):
|
def downloadfile_del_sync(self, event: Event):
|
||||||
"""
|
"""
|
||||||
媒体库同步删除
|
下载文件删除处理事件
|
||||||
"""
|
"""
|
||||||
if event:
|
if not self._enabled:
|
||||||
logger.info("收到命令,开始执行媒体库同步删除 ...")
|
return
|
||||||
self.post_message(channel=event.event_data.get("channel"),
|
if not event:
|
||||||
title="开始媒体库同步删除 ...",
|
return
|
||||||
userid=event.event_data.get("user"))
|
event_data = event.event_data
|
||||||
self.sync_del_by_log()
|
src = event_data.get("src")
|
||||||
|
if not src:
|
||||||
if event:
|
return
|
||||||
self.post_message(channel=event.event_data.get("channel"),
|
# 查询下载hash
|
||||||
title="媒体库同步删除完成!", userid=event.event_data.get("user"))
|
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
|
@staticmethod
|
||||||
def get_tmdbimage_url(path: str, prefix="w500"):
|
def get_tmdbimage_url(path: str, prefix="w500"):
|
||||||
|
|||||||
@@ -325,6 +325,9 @@ class MessageForward(_PluginBase):
|
|||||||
logger.info(f"转发消息 {title} 成功")
|
logger.info(f"转发消息 {title} 成功")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
if ret_json.get('errcode') == 81013:
|
||||||
|
return False
|
||||||
|
|
||||||
logger.error(f"转发消息 {title} 失败,错误信息:{ret_json}")
|
logger.error(f"转发消息 {title} 失败,错误信息:{ret_json}")
|
||||||
if ret_json.get('errcode') == 42001 or ret_json.get('errcode') == 40014:
|
if ret_json.get('errcode') == 42001 or ret_json.get('errcode') == 40014:
|
||||||
logger.info("token已过期,正在重新刷新token重试")
|
logger.info("token已过期,正在重新刷新token重试")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||||
from app.db.plugindata_oper import PluginDataOper
|
from app.db.plugindata_oper import PluginDataOper
|
||||||
from app.db.transferhistory_oper import TransferHistoryOper
|
from app.db.transferhistory_oper import TransferHistoryOper
|
||||||
@@ -157,15 +158,16 @@ class NAStoolSync(_PluginBase):
|
|||||||
|
|
||||||
# 替换value
|
# 替换value
|
||||||
if isinstance(plugin_value, str):
|
if isinstance(plugin_value, str):
|
||||||
plugin_value = json.loads(plugin_value)
|
_value: dict = json.loads(plugin_value)
|
||||||
if str(plugin_value.get("to_download")).isdigit() and int(
|
elif isinstance(plugin_value, dict):
|
||||||
plugin_value.get("to_download")) == int(sub_downloaders[0]):
|
if str(plugin_value.get("to_download")).isdigit() and int(
|
||||||
plugin_value["to_download"] = sub_downloaders[1]
|
plugin_value.get("to_download")) == int(sub_downloaders[0]):
|
||||||
|
plugin_value["to_download"] = sub_downloaders[1]
|
||||||
|
|
||||||
# 替换辅种记录
|
# 替换辅种记录
|
||||||
if str(plugin_id) == "IYUUAutoSeed":
|
if str(plugin_id) == "IYUUAutoSeed":
|
||||||
if isinstance(plugin_value, str):
|
if isinstance(plugin_value, str):
|
||||||
plugin_value = json.loads(plugin_value)
|
plugin_value: list = json.loads(plugin_value)
|
||||||
if not isinstance(plugin_value, list):
|
if not isinstance(plugin_value, list):
|
||||||
plugin_value = [plugin_value]
|
plugin_value = [plugin_value]
|
||||||
for value in plugin_value:
|
for value in plugin_value:
|
||||||
@@ -213,6 +215,7 @@ class NAStoolSync(_PluginBase):
|
|||||||
mtorrent = history[9]
|
mtorrent = history[9]
|
||||||
mdesc = history[10]
|
mdesc = history[10]
|
||||||
msite = history[11]
|
msite = history[11]
|
||||||
|
mdate = history[12]
|
||||||
|
|
||||||
# 处理站点映射
|
# 处理站点映射
|
||||||
if self._site:
|
if self._site:
|
||||||
@@ -234,7 +237,9 @@ class NAStoolSync(_PluginBase):
|
|||||||
download_hash=mdownload_hash,
|
download_hash=mdownload_hash,
|
||||||
torrent_name=mtorrent,
|
torrent_name=mtorrent,
|
||||||
torrent_description=mdesc,
|
torrent_description=mdesc,
|
||||||
torrent_site=msite
|
torrent_site=msite,
|
||||||
|
userid=settings.SUPERUSER,
|
||||||
|
date=mdate
|
||||||
)
|
)
|
||||||
cnt += 1
|
cnt += 1
|
||||||
if cnt % 100 == 0:
|
if cnt % 100 == 0:
|
||||||
@@ -358,7 +363,8 @@ class NAStoolSync(_PluginBase):
|
|||||||
DOWNLOAD_ID,
|
DOWNLOAD_ID,
|
||||||
TORRENT,
|
TORRENT,
|
||||||
DESC,
|
DESC,
|
||||||
SITE
|
SITE,
|
||||||
|
DATE
|
||||||
FROM
|
FROM
|
||||||
DOWNLOAD_HISTORY
|
DOWNLOAD_HISTORY
|
||||||
WHERE
|
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:
|
try:
|
||||||
# 总带宽
|
# 总带宽
|
||||||
self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000
|
self._bandwidth = int(float(config.get("bandwidth") or 0)) * 1000000
|
||||||
|
# 自动限速开关
|
||||||
if self._bandwidth > 0:
|
if self._bandwidth > 0:
|
||||||
# 自动限速开关
|
|
||||||
self._auto_limit = True
|
self._auto_limit = True
|
||||||
|
else:
|
||||||
|
self._auto_limit = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"智能限速上行带宽设置错误:{str(e)}")
|
logger.error(f"智能限速上行带宽设置错误:{str(e)}")
|
||||||
self._bandwidth = 0
|
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._allocation_ratio = config.get("allocation_ratio") or ""
|
||||||
# 不限速地址
|
# 不限速地址
|
||||||
self._unlimited_ips["ipv4"] = config.get("ipv4") 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,
|
self.__set_limiter(limit_type="未播放", upload_limit=self._noplay_up_speed,
|
||||||
download_limit=self._noplay_down_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 not self._bandwidth:
|
||||||
if residual_bandwidth < 0:
|
return 10
|
||||||
play_up_speed = 10
|
return round((self._bandwidth - total_bit_rate) / 8 / 1024, 2)
|
||||||
else:
|
|
||||||
play_up_speed = round(residual_bandwidth / 8 / 1024, 2)
|
|
||||||
|
|
||||||
return play_up_speed
|
|
||||||
|
|
||||||
def __set_limiter(self, limit_type: str, upload_limit: float, download_limit: float):
|
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)}")
|
logger.error(f"设置限速失败:{str(e)}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __allow_access(allow_ips, ip):
|
def __allow_access(allow_ips: dict, ip: str) -> bool:
|
||||||
"""
|
"""
|
||||||
判断IP是否合法
|
判断IP是否合法
|
||||||
:param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":}
|
:param allow_ips: 充许的IP范围 {"ipv4":, "ipv6":}
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ class DownloadHistory(BaseModel):
|
|||||||
torrent_description: Optional[str] = None
|
torrent_description: Optional[str] = None
|
||||||
# 站点
|
# 站点
|
||||||
torrent_site: Optional[str] = None
|
torrent_site: Optional[str] = None
|
||||||
|
# 下载用户
|
||||||
|
userid: Optional[str] = None
|
||||||
|
# 下载渠道
|
||||||
|
channel: Optional[str] = None
|
||||||
|
# 创建时间
|
||||||
|
date: Optional[str] = None
|
||||||
# 备注
|
# 备注
|
||||||
note: Optional[str] = None
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class EventType(Enum):
|
|||||||
DownloadAdded = "download.added"
|
DownloadAdded = "download.added"
|
||||||
# 删除历史记录
|
# 删除历史记录
|
||||||
HistoryDeleted = "history.deleted"
|
HistoryDeleted = "history.deleted"
|
||||||
# 删除媒体库文件
|
# 删除下载源文件
|
||||||
MediaDeleted = "media.deleted"
|
DownloadFileDeleted = "downloadfile.deleted"
|
||||||
# 用户外来消息
|
# 用户外来消息
|
||||||
UserMessage = "user.message"
|
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:
|
try:
|
||||||
|
# 当前目录改名
|
||||||
temp = src.replace(src.parent / dest.name)
|
temp = src.replace(src.parent / dest.name)
|
||||||
|
# 移动到目标目录
|
||||||
shutil.move(temp, dest)
|
shutil.move(temp, dest)
|
||||||
return 0, ""
|
return 0, ""
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -74,7 +76,11 @@ class SystemUtils:
|
|||||||
硬链接
|
硬链接
|
||||||
"""
|
"""
|
||||||
try:
|
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, ""
|
return 0, ""
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(str(err))
|
print(str(err))
|
||||||
@@ -341,13 +347,20 @@ class SystemUtils:
|
|||||||
# 创建 Docker 客户端
|
# 创建 Docker 客户端
|
||||||
client = docker.DockerClient(base_url='tcp://127.0.0.1:38379')
|
client = docker.DockerClient(base_url='tcp://127.0.0.1:38379')
|
||||||
# 获取当前容器的 ID
|
# 获取当前容器的 ID
|
||||||
|
container_id = None
|
||||||
with open('/proc/self/mountinfo', 'r') as f:
|
with open('/proc/self/mountinfo', 'r') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
index_resolv_conf = data.find("/sys/fs/cgroup/devices")
|
index_resolv_conf = data.find("resolv.conf")
|
||||||
if index_resolv_conf != -1:
|
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
|
index_first_slash = data.rfind("/", 0, index_second_slash) + 1
|
||||||
container_id = data[index_first_slash:index_second_slash]
|
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:
|
if not container_id:
|
||||||
return False, "获取容器ID失败!"
|
return False, "获取容器ID失败!"
|
||||||
# 重启当前容器
|
# 重启当前容器
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ from app.utils.http import RequestUtils
|
|||||||
|
|
||||||
|
|
||||||
class WebUtils:
|
class WebUtils:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_location(ip: str):
|
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
|
https://api.mir6.com/api/ip
|
||||||
{
|
{
|
||||||
@@ -36,7 +44,33 @@ class WebUtils:
|
|||||||
if r:
|
if r:
|
||||||
return r.json().get("data", {}).get("location") or ''
|
return r.json().get("data", {}).get("location") or ''
|
||||||
except Exception as err:
|
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
|
@staticmethod
|
||||||
def get_bing_wallpaper() -> Optional[str]:
|
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