mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-05-24 07:26:52 +00:00
819 lines
32 KiB
Python
819 lines
32 KiB
Python
import importlib
|
||
import re
|
||
import sys
|
||
from base64 import b64encode
|
||
from dataclasses import asdict, is_dataclass
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Any, Dict, Optional, Tuple
|
||
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
||
from zoneinfo import ZoneInfo
|
||
|
||
try:
|
||
from app.core.config import settings
|
||
except Exception:
|
||
settings = None
|
||
try:
|
||
from app.core.plugin import PluginManager
|
||
except Exception:
|
||
PluginManager = None
|
||
|
||
|
||
class P115TransferService:
|
||
"""Reusable 115 share transfer execution layer for Agent影视助手."""
|
||
|
||
CLIENT_COOKIE_REQUIRED_KEYS = {"UID", "CID", "SEID"}
|
||
QR_CLIENT_TYPES = {
|
||
"web",
|
||
"android",
|
||
"115android",
|
||
"ios",
|
||
"115ios",
|
||
"alipaymini",
|
||
"wechatmini",
|
||
"115ipad",
|
||
"tv",
|
||
"qandroid",
|
||
}
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
default_target_path: str = "/待整理",
|
||
cookie: str = "",
|
||
prefer_direct: bool = True,
|
||
) -> None:
|
||
self.default_target_path = self.normalize_pan_path(default_target_path) or "/待整理"
|
||
self.cookie = self.normalize_text(cookie)
|
||
self.prefer_direct = bool(prefer_direct)
|
||
|
||
def set_cookie(self, cookie: str = "") -> None:
|
||
self.cookie = self.normalize_text(cookie)
|
||
|
||
@staticmethod
|
||
def normalize_text(value: Any) -> str:
|
||
if value is None:
|
||
return ""
|
||
return str(value).strip()
|
||
|
||
@staticmethod
|
||
def normalize_pan_path(value: Any) -> str:
|
||
text = str(value or "").strip()
|
||
if not text:
|
||
return ""
|
||
if not text.startswith("/"):
|
||
text = f"/{text}"
|
||
return text.rstrip("/") or "/"
|
||
|
||
@staticmethod
|
||
def _ensure_helper_import_paths() -> None:
|
||
candidate_dirs = [
|
||
"/app/app/plugins",
|
||
"/config/plugins",
|
||
]
|
||
for base in candidate_dirs:
|
||
path = Path(base)
|
||
if path.exists():
|
||
text = str(path)
|
||
if text not in sys.path:
|
||
sys.path.append(text)
|
||
|
||
@staticmethod
|
||
def is_115_share_url(url: str) -> bool:
|
||
host = urlparse(url).netloc.lower()
|
||
return host == "115.com" or host.endswith(".115.com") or "115cdn.com" in host
|
||
|
||
def ensure_115_share_url(self, url: str, access_code: str = "") -> str:
|
||
clean_url = self.normalize_text(url)
|
||
if not clean_url:
|
||
return ""
|
||
access_code = self.normalize_text(access_code)
|
||
parsed = urlparse(clean_url)
|
||
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
||
if access_code and "password" not in query:
|
||
query["password"] = access_code
|
||
clean_url = urlunparse(parsed._replace(query=urlencode(query)))
|
||
return clean_url
|
||
|
||
@staticmethod
|
||
def _extract_115_payload(url: str) -> Tuple[str, str]:
|
||
clean_url = str(url or "").strip()
|
||
if not clean_url:
|
||
return "", ""
|
||
try:
|
||
from p115client.util import share_extract_payload
|
||
|
||
payload = share_extract_payload(clean_url) or {}
|
||
return str(payload.get("share_code") or "").strip(), str(payload.get("receive_code") or "").strip()
|
||
except Exception:
|
||
parsed = urlparse(clean_url)
|
||
share_code = ""
|
||
match = re.search(r"/s/([^/?#]+)", parsed.path or "")
|
||
if match:
|
||
share_code = match.group(1).strip()
|
||
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
||
receive_code = str(query.get("password") or query.get("receive_code") or query.get("pwd") or "").strip()
|
||
return share_code, receive_code
|
||
|
||
@classmethod
|
||
def parse_cookie_pairs(cls, cookie: str) -> Dict[str, str]:
|
||
pairs: Dict[str, str] = {}
|
||
for part in cls.normalize_text(cookie).strip(";").split(";"):
|
||
if "=" not in part:
|
||
continue
|
||
key, value = part.split("=", 1)
|
||
key = key.strip()
|
||
value = value.strip()
|
||
if key and value:
|
||
pairs[key] = value
|
||
return pairs
|
||
|
||
@classmethod
|
||
def validate_client_cookie(cls, cookie: str) -> Tuple[bool, str]:
|
||
if not cls.normalize_text(cookie):
|
||
return False, "未配置独立 115 Cookie"
|
||
pairs = cls.parse_cookie_pairs(cookie)
|
||
missing = sorted(cls.CLIENT_COOKIE_REQUIRED_KEYS - set(pairs))
|
||
if missing:
|
||
return False, f"当前 115 Cookie 缺少 {'/'.join(missing)},看起来不是扫码客户端 Cookie;不建议使用网页版 Cookie"
|
||
return True, ""
|
||
|
||
def cookie_state(self) -> Dict[str, Any]:
|
||
configured = bool(self.normalize_text(self.cookie))
|
||
pairs = self.parse_cookie_pairs(self.cookie)
|
||
cookie_keys = sorted(pairs.keys())
|
||
if not configured:
|
||
return {
|
||
"configured": False,
|
||
"valid": False,
|
||
"mode": "none",
|
||
"cookie_keys": [],
|
||
"message": "未配置独立 115 会话。新环境请先发“115登录”扫码;P115StrmHelper 仅作为旧环境兼容 fallback。",
|
||
}
|
||
cookie_ok, cookie_message = self.validate_client_cookie(self.cookie)
|
||
return {
|
||
"configured": True,
|
||
"valid": cookie_ok,
|
||
"mode": "client_cookie" if cookie_ok else "invalid_cookie",
|
||
"cookie_keys": cookie_keys,
|
||
"message": "" if cookie_ok else cookie_message,
|
||
}
|
||
|
||
@classmethod
|
||
def normalize_qrcode_client_type(cls, client_type: Any) -> str:
|
||
text = cls.normalize_text(client_type).lower()
|
||
return text if text in cls.QR_CLIENT_TYPES else "alipaymini"
|
||
|
||
@staticmethod
|
||
def jsonable(value: Any) -> Any:
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, (str, int, float, bool, list, dict)):
|
||
return value
|
||
if is_dataclass(value):
|
||
return asdict(value)
|
||
if hasattr(value, "model_dump"):
|
||
try:
|
||
return value.model_dump()
|
||
except Exception:
|
||
pass
|
||
if hasattr(value, "__dict__"):
|
||
return {k: v for k, v in vars(value).items() if not k.startswith("_")}
|
||
return str(value)
|
||
|
||
def tz_now(self) -> datetime:
|
||
if settings is not None:
|
||
try:
|
||
return datetime.now(ZoneInfo(getattr(settings, "TZ", "Asia/Shanghai")))
|
||
except Exception:
|
||
pass
|
||
return datetime.now()
|
||
|
||
@staticmethod
|
||
def _safe_int(value: Any, default: int = -1) -> int:
|
||
try:
|
||
return int(value)
|
||
except Exception:
|
||
return default
|
||
|
||
@staticmethod
|
||
def _response_error(resp: Any) -> str:
|
||
if not isinstance(resp, dict):
|
||
return str(resp or "")
|
||
for key in ("error", "message", "msg", "errno"):
|
||
value = resp.get(key)
|
||
if value not in (None, ""):
|
||
return str(value)
|
||
return str(resp)
|
||
|
||
@classmethod
|
||
def _is_already_saved_message(cls, value: Any) -> bool:
|
||
text = cls.normalize_text(value)
|
||
return any(
|
||
marker in text
|
||
for marker in (
|
||
"已经转存",
|
||
"已转存",
|
||
"已经保存",
|
||
"已保存",
|
||
"already",
|
||
"exist",
|
||
)
|
||
)
|
||
|
||
@staticmethod
|
||
def _response_ok(resp: Any) -> bool:
|
||
if not isinstance(resp, dict):
|
||
return False
|
||
if resp.get("state") is True:
|
||
return True
|
||
if resp.get("code") in (0, "0") and resp.get("state") not in (False, 0):
|
||
return True
|
||
if resp.get("errno") in (0, "0") and resp.get("state") not in (False, 0):
|
||
return True
|
||
return False
|
||
|
||
@staticmethod
|
||
def _p115_request_kwargs(*, app: bool = False) -> Dict[str, Any]:
|
||
try:
|
||
P115TransferService._ensure_helper_import_paths()
|
||
from app.plugins.p115strmhelper.core.config import configer
|
||
|
||
return configer.get_ios_ua_app(app=app) or {}
|
||
except Exception:
|
||
try:
|
||
P115TransferService._ensure_helper_import_paths()
|
||
from p115strmhelper.core.config import configer
|
||
|
||
return configer.get_ios_ua_app(app=app) or {}
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
@staticmethod
|
||
def _resolve_servicer_from_loaded_plugin() -> Tuple[Optional[Any], Optional[str]]:
|
||
if PluginManager is None:
|
||
return None, "PluginManager 不可用"
|
||
try:
|
||
plugin = PluginManager().running_plugins.get("P115StrmHelper")
|
||
except Exception as exc:
|
||
return None, f"读取 P115StrmHelper 运行态失败: {exc}"
|
||
if not plugin:
|
||
return None, "P115StrmHelper 未加载"
|
||
|
||
module_names = []
|
||
plugin_module = getattr(plugin.__class__, "__module__", "") or ""
|
||
if plugin_module:
|
||
module_names.append(f"{plugin_module}.service")
|
||
module_names.extend(
|
||
[
|
||
"app.plugins.p115strmhelper.service",
|
||
"p115strmhelper.service",
|
||
]
|
||
)
|
||
|
||
for module_name in module_names:
|
||
try:
|
||
self._ensure_helper_import_paths()
|
||
module = sys.modules.get(module_name) or importlib.import_module(module_name)
|
||
servicer = getattr(module, "servicer", None)
|
||
if servicer is not None:
|
||
return servicer, None
|
||
except Exception:
|
||
continue
|
||
return None, "P115StrmHelper 运行态已加载,但未找到 service.servicer"
|
||
|
||
def _get_loaded_p115_client(self) -> Tuple[Optional[Any], str]:
|
||
servicer, helper_error = self._resolve_servicer_from_loaded_plugin()
|
||
if not servicer:
|
||
return None, helper_error or "P115StrmHelper 未加载"
|
||
client = getattr(servicer, "client", None)
|
||
if not client:
|
||
return None, "P115StrmHelper 未登录 115 或客户端不可用"
|
||
return client, "p115strmhelper_client"
|
||
|
||
def _get_cookie_p115_client(self) -> Tuple[Optional[Any], str]:
|
||
if not self.cookie:
|
||
return None, "未配置独立 115 Cookie"
|
||
cookie_ok, cookie_message = self.validate_client_cookie(self.cookie)
|
||
if not cookie_ok:
|
||
return None, cookie_message
|
||
try:
|
||
from p115client import P115Client
|
||
|
||
return P115Client(
|
||
self.cookie,
|
||
check_for_relogin=False,
|
||
ensure_cookies=False,
|
||
console_qrcode=False,
|
||
), "direct_cookie"
|
||
except Exception as exc:
|
||
return None, f"独立 115 Cookie 初始化失败: {exc}"
|
||
|
||
@classmethod
|
||
def create_qrcode_login(cls, client_type: str = "alipaymini") -> Tuple[bool, Dict[str, Any], str]:
|
||
final_client_type = cls.normalize_qrcode_client_type(client_type)
|
||
try:
|
||
from p115client import P115Client, check_response
|
||
|
||
resp = P115Client.login_qrcode_token()
|
||
check_response(resp)
|
||
resp_info = resp.get("data", {}) if isinstance(resp, dict) else {}
|
||
uid = str(resp_info.get("uid") or "")
|
||
qrcode_time = str(resp_info.get("time") or "")
|
||
sign = str(resp_info.get("sign") or "")
|
||
qrcode = P115Client.login_qrcode(uid)
|
||
if not isinstance(qrcode, (bytes, bytearray)):
|
||
return False, {}, "获取二维码失败:返回内容类型异常"
|
||
return True, {
|
||
"uid": uid,
|
||
"time": qrcode_time,
|
||
"sign": sign,
|
||
"client_type": final_client_type,
|
||
"tips": "请使用 115 App 扫码登录",
|
||
"qrcode": f"data:image/png;base64,{b64encode(qrcode).decode('utf-8')}",
|
||
}, "success"
|
||
except Exception as exc:
|
||
return False, {}, f"获取 115 登录二维码失败: {exc}"
|
||
|
||
@classmethod
|
||
def check_qrcode_login(
|
||
cls,
|
||
*,
|
||
uid: str,
|
||
time_value: str,
|
||
sign: str,
|
||
client_type: str = "alipaymini",
|
||
) -> Tuple[bool, Dict[str, Any], str]:
|
||
final_client_type = cls.normalize_qrcode_client_type(client_type)
|
||
try:
|
||
from p115client import P115Client, check_response
|
||
|
||
payload = {"uid": uid, "time": time_value, "sign": sign}
|
||
resp = P115Client.login_qrcode_scan_status(payload)
|
||
if not isinstance(resp, dict):
|
||
return False, {}, "检查二维码状态失败:返回内容类型异常"
|
||
check_response(resp)
|
||
status_code = (resp.get("data") or {}).get("status")
|
||
except Exception as exc:
|
||
return False, {}, f"检查二维码状态失败: {exc}"
|
||
|
||
if status_code == 0:
|
||
return True, {"status": "waiting", "client_type": final_client_type}, "等待扫码"
|
||
if status_code == 1:
|
||
return True, {"status": "scanned", "client_type": final_client_type}, "已扫码,等待确认"
|
||
if status_code == -1 or status_code is None:
|
||
return False, {"status": "expired", "client_type": final_client_type}, "二维码已过期"
|
||
if status_code == -2:
|
||
return False, {"status": "cancelled", "client_type": final_client_type}, "用户取消登录"
|
||
if status_code != 2:
|
||
return False, {"status": "unknown", "client_type": final_client_type}, f"未知二维码状态: {status_code}"
|
||
|
||
try:
|
||
from p115client import P115Client, check_response
|
||
|
||
resp = P115Client.login_qrcode_scan_result(uid, app=final_client_type)
|
||
if not isinstance(resp, dict):
|
||
return False, {}, "获取登录结果失败:返回内容类型异常"
|
||
check_response(resp)
|
||
except Exception as exc:
|
||
return False, {}, f"获取登录结果失败: {exc}"
|
||
|
||
cookie_data = (resp.get("data") or {}).get("cookie") if isinstance(resp, dict) else None
|
||
if not isinstance(cookie_data, dict):
|
||
return False, {}, "登录成功但未返回 Cookie"
|
||
cookie = "; ".join(f"{name}={value}" for name, value in cookie_data.items() if name and value).strip()
|
||
cookie_ok, cookie_message = cls.validate_client_cookie(cookie)
|
||
if not cookie_ok:
|
||
return False, {}, cookie_message
|
||
return True, {
|
||
"status": "success",
|
||
"client_type": final_client_type,
|
||
"cookie": cookie,
|
||
"cookie_keys": sorted(cls.parse_cookie_pairs(cookie).keys()),
|
||
}, "登录成功"
|
||
|
||
def get_direct_client(self) -> Tuple[Optional[Any], str, str]:
|
||
client, source = self._get_cookie_p115_client()
|
||
if client:
|
||
return client, source, ""
|
||
cookie_error = source
|
||
client, source = self._get_loaded_p115_client()
|
||
if client:
|
||
return client, source, ""
|
||
return None, "none", source or cookie_error
|
||
|
||
@classmethod
|
||
def _import_servicer_fallback(cls) -> Tuple[Optional[Any], Optional[str]]:
|
||
last_error = ""
|
||
for module_name in [
|
||
"app.plugins.p115strmhelper.service",
|
||
"p115strmhelper.service",
|
||
]:
|
||
try:
|
||
cls._ensure_helper_import_paths()
|
||
service_module = importlib.import_module(module_name)
|
||
servicer = getattr(service_module, "servicer", None)
|
||
if servicer is not None:
|
||
return servicer, None
|
||
last_error = f"{module_name} 未暴露 servicer"
|
||
except Exception as exc:
|
||
last_error = f"{module_name} 导入失败: {exc}"
|
||
return None, last_error or "P115StrmHelper 未安装或无法导入"
|
||
|
||
def get_share_helper(self) -> Tuple[Optional[Any], Optional[str]]:
|
||
servicer, helper_error = self._resolve_servicer_from_loaded_plugin()
|
||
if not servicer:
|
||
servicer, helper_error = self._import_servicer_fallback()
|
||
if not servicer:
|
||
return None, f"P115StrmHelper 未安装或无法导入: {helper_error}"
|
||
if not servicer:
|
||
return None, "P115StrmHelper 未初始化"
|
||
if not getattr(servicer, "client", None):
|
||
return None, "P115StrmHelper 未登录 115 或客户端不可用"
|
||
helper = getattr(servicer, "sharetransferhelper", None)
|
||
if not helper:
|
||
return None, "P115StrmHelper 分享转存模块不可用"
|
||
return helper, None
|
||
|
||
def health(self) -> Tuple[bool, Dict[str, Any], str]:
|
||
cookie_state = self.cookie_state()
|
||
direct_client, direct_source, direct_error = self.get_direct_client()
|
||
direct_ready = direct_client is not None
|
||
helper, helper_error = self.get_share_helper()
|
||
helper_ready = bool(helper and not helper_error)
|
||
ready = direct_ready or helper_ready
|
||
message = "" if ready else direct_error or helper_error or "115 转存不可用"
|
||
return ready, {
|
||
"ready": ready,
|
||
"direct_ready": direct_ready,
|
||
"direct_source": direct_source if direct_ready else "",
|
||
"direct_message": "" if direct_ready else direct_error,
|
||
"helper_ready": helper_ready,
|
||
"helper_message": "" if helper_ready else helper_error,
|
||
"cookie_state": cookie_state,
|
||
"message": message or "success",
|
||
}, message
|
||
|
||
def _get_or_create_path_cid(self, client: Any, path: str) -> int:
|
||
return self._get_path_cid(client, path, create=True)
|
||
|
||
def _get_path_cid(self, client: Any, path: str, *, create: bool = True) -> int:
|
||
target_path = self.normalize_pan_path(path) or "/"
|
||
if target_path == "/":
|
||
return 0
|
||
get_kwargs = self._p115_request_kwargs(app=False)
|
||
mkdir_kwargs = self._p115_request_kwargs(app=True)
|
||
try:
|
||
resp = client.fs_dir_getid(target_path, **get_kwargs)
|
||
pid = self._safe_int(resp.get("id") if isinstance(resp, dict) else None, -1)
|
||
if pid > 0:
|
||
return pid
|
||
except Exception:
|
||
pass
|
||
|
||
if not create:
|
||
return -1
|
||
|
||
try:
|
||
resp = client.fs_makedirs_app(target_path, pid=0, **mkdir_kwargs)
|
||
cid = self._safe_int(resp.get("cid") if isinstance(resp, dict) else None, -1)
|
||
if cid >= 0:
|
||
return cid
|
||
if self._response_ok(resp):
|
||
cid = self._safe_int((resp.get("data") or {}).get("cid") if isinstance(resp.get("data"), dict) else None, -1)
|
||
if cid >= 0:
|
||
return cid
|
||
raise RuntimeError(self._response_error(resp))
|
||
except Exception as exc:
|
||
raise RuntimeError(f"无法创建或定位 115 目录 {target_path}: {exc}") from exc
|
||
|
||
def list_directory_current_layer(self, path: str = "") -> Tuple[bool, Dict[str, Any], str]:
|
||
target_path = self.normalize_pan_path(path) or self.default_target_path or "/待整理"
|
||
result = {
|
||
"time": self.tz_now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"ok": False,
|
||
"path": target_path,
|
||
"items": [],
|
||
"file_count": 0,
|
||
"folder_count": 0,
|
||
"removed_count": 0,
|
||
"message": "",
|
||
}
|
||
client, source, client_error = self.get_direct_client()
|
||
if not client:
|
||
result["message"] = client_error or "没有可用的 115 客户端"
|
||
result["direct_source"] = source
|
||
return False, result, result["message"]
|
||
|
||
cid = self._get_path_cid(client, target_path, create=False)
|
||
if cid < 0:
|
||
result["ok"] = True
|
||
result["direct_source"] = source
|
||
result["message"] = "115 默认目录不存在,视为空目录"
|
||
return True, result, result["message"]
|
||
|
||
payload = {
|
||
"cid": int(cid),
|
||
"limit": 1150,
|
||
"offset": 0,
|
||
"show_dir": 1,
|
||
"cur": 1,
|
||
"count_folders": 1,
|
||
}
|
||
items: list[dict[str, Any]] = []
|
||
total = 0
|
||
try:
|
||
while True:
|
||
resp = client.fs_files(payload, **self._p115_request_kwargs(app=False))
|
||
if not isinstance(resp, dict):
|
||
result["message"] = "读取 115 目录失败:返回内容异常"
|
||
result["direct_source"] = source
|
||
return False, result, result["message"]
|
||
batch = resp.get("data") or []
|
||
total = self._safe_int(resp.get("count"), total)
|
||
for entry in batch:
|
||
if not isinstance(entry, dict):
|
||
continue
|
||
fid = self._safe_int(entry.get("fid"), -1)
|
||
item_cid = self._safe_int(entry.get("cid"), -1)
|
||
is_dir = fid < 0
|
||
item_id = item_cid if is_dir else fid
|
||
if item_id < 0:
|
||
continue
|
||
items.append(
|
||
{
|
||
"id": item_id,
|
||
"name": self.normalize_text(entry.get("n") or entry.get("fn") or entry.get("file_name")),
|
||
"is_dir": is_dir,
|
||
"type": "folder" if is_dir else "file",
|
||
"raw": entry,
|
||
}
|
||
)
|
||
payload["offset"] = int(payload["offset"]) + len(batch)
|
||
if not batch or len(batch) < int(payload["limit"]) or int(payload["offset"]) >= total:
|
||
break
|
||
except Exception as exc:
|
||
result["message"] = f"读取 115 目录失败: {exc}"
|
||
result["direct_source"] = source
|
||
return False, result, result["message"]
|
||
|
||
file_count = len([item for item in items if not item.get("is_dir")])
|
||
folder_count = len([item for item in items if item.get("is_dir")])
|
||
result.update(
|
||
{
|
||
"ok": True,
|
||
"direct_source": source,
|
||
"cid": cid,
|
||
"items": items,
|
||
"file_count": file_count,
|
||
"folder_count": folder_count,
|
||
"message": "success",
|
||
}
|
||
)
|
||
return True, result, "success"
|
||
|
||
def delete_items(self, items: list[dict[str, Any]]) -> Tuple[bool, Dict[str, Any], str]:
|
||
client, source, client_error = self.get_direct_client()
|
||
result = {
|
||
"ok": False,
|
||
"direct_source": source,
|
||
"removed_count": 0,
|
||
"message": "",
|
||
}
|
||
if not client:
|
||
result["message"] = client_error or "没有可用的 115 客户端"
|
||
return False, result, result["message"]
|
||
|
||
ids = [str(self._safe_int(item.get("id"), -1)) for item in items or [] if self._safe_int(item.get("id"), -1) >= 0]
|
||
if not ids:
|
||
result.update({"ok": True, "message": "115 默认目录当前层已是空目录"})
|
||
return True, result, result["message"]
|
||
|
||
try:
|
||
resp = client.fs_delete(ids, **self._p115_request_kwargs(app=False))
|
||
except Exception as exc:
|
||
result["message"] = f"删除 115 目录内容失败: {exc}"
|
||
return False, result, result["message"]
|
||
|
||
if not self._response_ok(resp):
|
||
result["message"] = self._response_error(resp) or "删除 115 目录内容失败"
|
||
result["raw"] = self.jsonable(resp)
|
||
return False, result, result["message"]
|
||
|
||
result.update(
|
||
{
|
||
"ok": True,
|
||
"removed_count": len(ids),
|
||
"message": "115 默认目录已清空当前层",
|
||
"raw": self.jsonable(resp),
|
||
}
|
||
)
|
||
return True, result, result["message"]
|
||
|
||
def clear_directory(self, path: str = "") -> Tuple[bool, Dict[str, Any], str]:
|
||
target_path = self.normalize_pan_path(path) or self.default_target_path or "/待整理"
|
||
listed_ok, listed_result, listed_message = self.list_directory_current_layer(target_path)
|
||
if not listed_ok:
|
||
return False, listed_result, listed_message
|
||
|
||
items = listed_result.get("items") or []
|
||
if not items:
|
||
listed_result["message"] = "115 默认目录当前层已是空目录"
|
||
return True, listed_result, listed_result["message"]
|
||
|
||
delete_ok, delete_result, delete_message = self.delete_items(items)
|
||
merged = dict(listed_result)
|
||
merged.update(
|
||
{
|
||
"ok": delete_ok,
|
||
"removed_count": delete_result.get("removed_count", 0),
|
||
"direct_source": delete_result.get("direct_source", listed_result.get("direct_source")),
|
||
"delete_raw": delete_result.get("raw"),
|
||
"message": delete_message,
|
||
}
|
||
)
|
||
return delete_ok, merged, delete_message
|
||
|
||
def transfer_share_direct(
|
||
self,
|
||
*,
|
||
url: str = "",
|
||
access_code: str = "",
|
||
path: str = "",
|
||
trigger: str = "Agent影视助手",
|
||
) -> Tuple[bool, Dict[str, Any], str]:
|
||
transfer_path = self.normalize_pan_path(path) or self.default_target_path or "/待整理"
|
||
share_url = self.ensure_115_share_url(url or "", access_code or "")
|
||
result = {
|
||
"time": self.tz_now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"ok": False,
|
||
"trigger": trigger,
|
||
"strategy": "direct",
|
||
"path": transfer_path,
|
||
"url": share_url,
|
||
"message": "",
|
||
"data": {},
|
||
}
|
||
if not share_url:
|
||
result["message"] = "没有可用于 115 转存的分享链接"
|
||
return False, result, result["message"]
|
||
if not self.is_115_share_url(share_url):
|
||
result["message"] = "当前链接不是 115 分享链接,无法直接转存到 115"
|
||
return False, result, result["message"]
|
||
|
||
share_code, receive_code = self._extract_115_payload(share_url)
|
||
if not share_code or not receive_code:
|
||
result["message"] = "解析 115 分享链接失败,缺少分享码或提取码"
|
||
return False, result, result["message"]
|
||
|
||
client, source, client_error = self.get_direct_client()
|
||
if not client:
|
||
result["message"] = client_error or "没有可用的 115 直转客户端"
|
||
result["data"] = {"direct_source": source}
|
||
return False, result, result["message"]
|
||
|
||
try:
|
||
parent_id = self._get_or_create_path_cid(client, transfer_path)
|
||
except Exception as exc:
|
||
result["message"] = str(exc)
|
||
result["data"] = {"direct_source": source}
|
||
return False, result, result["message"]
|
||
|
||
payload = {
|
||
"share_code": share_code,
|
||
"receive_code": receive_code,
|
||
"file_id": 0,
|
||
"cid": int(parent_id),
|
||
"is_check": 0,
|
||
}
|
||
try:
|
||
resp = client.share_receive(payload, **self._p115_request_kwargs(app=False))
|
||
except Exception as exc:
|
||
result["message"] = f"调用 115 直转接口失败: {exc}"
|
||
result["data"] = {"direct_source": source, "parent_id": parent_id}
|
||
return False, result, result["message"]
|
||
|
||
if not self._response_ok(resp):
|
||
result["message"] = self._response_error(resp) or "115 直转失败"
|
||
result["data"] = {
|
||
"direct_source": source,
|
||
"parent_id": parent_id,
|
||
"raw": self.jsonable(resp),
|
||
}
|
||
if self._is_already_saved_message(result["message"]):
|
||
result["ok"] = True
|
||
result["message"] = "115 直转已存在"
|
||
return True, result, result["message"]
|
||
return False, result, result["message"]
|
||
|
||
result.update(
|
||
{
|
||
"ok": True,
|
||
"message": "115 直转成功",
|
||
"data": {
|
||
"direct_source": source,
|
||
"share_code": share_code,
|
||
"receive_code": receive_code,
|
||
"save_parent": transfer_path,
|
||
"parent_id": parent_id,
|
||
"raw": self.jsonable(resp),
|
||
},
|
||
}
|
||
)
|
||
return True, result, result["message"]
|
||
|
||
def transfer_share(
|
||
self,
|
||
*,
|
||
url: str = "",
|
||
access_code: str = "",
|
||
path: str = "",
|
||
trigger: str = "Agent影视助手",
|
||
) -> Tuple[bool, Dict[str, Any], str]:
|
||
transfer_path = self.normalize_pan_path(path) or self.default_target_path or "/待整理"
|
||
share_url = self.ensure_115_share_url(url or "", access_code or "")
|
||
result = {
|
||
"time": self.tz_now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"ok": False,
|
||
"trigger": trigger,
|
||
"path": transfer_path,
|
||
"url": share_url,
|
||
"message": "",
|
||
"data": {},
|
||
}
|
||
if not share_url:
|
||
result["message"] = "没有可用于 115 转存的分享链接"
|
||
return False, result, result["message"]
|
||
if not self.is_115_share_url(share_url):
|
||
result["message"] = "当前链接不是 115 分享链接,无法直接转存到 115"
|
||
return False, result, result["message"]
|
||
|
||
if self.prefer_direct:
|
||
direct_ok, direct_result, direct_message = self.transfer_share_direct(
|
||
url=share_url,
|
||
access_code=access_code,
|
||
path=transfer_path,
|
||
trigger=trigger,
|
||
)
|
||
if direct_ok:
|
||
return True, direct_result, direct_message
|
||
result["data"]["direct_fallback"] = direct_result
|
||
|
||
helper, helper_error = self.get_share_helper()
|
||
if helper_error or not helper:
|
||
direct_error = ((result.get("data") or {}).get("direct_fallback") or {}).get("message")
|
||
result["message"] = (
|
||
"115 转存不可用:请先发“115登录”完成扫码,或检查 115 直转依赖。"
|
||
f" 直转状态:{direct_error or '未知'};兼容 fallback:{helper_error or '不可用'}"
|
||
)
|
||
return False, result, result["message"]
|
||
|
||
try:
|
||
transfer_result = helper.add_share_115(
|
||
share_url,
|
||
notify=False,
|
||
pan_path=transfer_path,
|
||
)
|
||
except Exception as exc:
|
||
result["message"] = f"调用 P115StrmHelper 转存失败: {exc}"
|
||
return False, result, result["message"]
|
||
|
||
if not transfer_result or not transfer_result[0]:
|
||
error_message = ""
|
||
if isinstance(transfer_result, tuple):
|
||
if len(transfer_result) > 2:
|
||
error_message = self.normalize_text(transfer_result[2])
|
||
elif len(transfer_result) > 1:
|
||
error_message = self.normalize_text(transfer_result[1])
|
||
if self._is_already_saved_message(error_message):
|
||
result.update(
|
||
{
|
||
"ok": True,
|
||
"strategy": "p115strmhelper",
|
||
"message": "115 转存已存在",
|
||
"data": {"raw": self.jsonable(transfer_result)},
|
||
}
|
||
)
|
||
return True, result, result["message"]
|
||
result["message"] = error_message or "115 转存失败"
|
||
result["data"] = {"raw": self.jsonable(transfer_result)}
|
||
return False, result, result["message"]
|
||
|
||
media_info = transfer_result[1] if len(transfer_result) > 1 else None
|
||
save_parent = transfer_result[2] if len(transfer_result) > 2 else transfer_path
|
||
parent_id = transfer_result[3] if len(transfer_result) > 3 else None
|
||
result.update(
|
||
{
|
||
"ok": True,
|
||
"strategy": "p115strmhelper",
|
||
"message": "115 转存成功",
|
||
"data": {
|
||
"media_info": self.jsonable(media_info),
|
||
"save_parent": save_parent,
|
||
"parent_id": parent_id,
|
||
},
|
||
}
|
||
)
|
||
return True, result, result["message"]
|