mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
refactor: reorganize backend module boundaries
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
"""插件市场、CookieCloud、OCR 和远程 MoviePilot 服务集成。"""
|
||||
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
import json
|
||||
from typing import Any, Dict, Tuple, Optional
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.log import logger
|
||||
from app.foundation.crypto import CryptoJsUtils, HashUtils
|
||||
from app.adapters.network.http import RequestUtils
|
||||
from app.domain.string import StringUtils
|
||||
from app.foundation.url import UrlUtils
|
||||
|
||||
|
||||
class CookieCloudHelper:
|
||||
"""负责 CookieCloud 配置同步、下载和本地数据解析。"""
|
||||
|
||||
_ignore_cookies: list = ["CookieAutoDeleteBrowsingDataCleanup", "CookieAutoDeleteCleaningDiscarded"]
|
||||
|
||||
def __init__(self):
|
||||
"""加载当前 CookieCloud 配置。"""
|
||||
self.__sync_setting()
|
||||
|
||||
def __sync_setting(self):
|
||||
"""
|
||||
同步CookieCloud配置项
|
||||
"""
|
||||
self._server = UrlUtils.standardize_base_url(settings.COOKIECLOUD_HOST)
|
||||
self._key = StringUtils.safe_strip(settings.COOKIECLOUD_KEY)
|
||||
self._password = StringUtils.safe_strip(settings.COOKIECLOUD_PASSWORD)
|
||||
self._enable_local = settings.COOKIECLOUD_ENABLE_LOCAL
|
||||
self._local_path = settings.COOKIE_PATH
|
||||
|
||||
def download(self) -> Tuple[Optional[dict], str]:
|
||||
"""
|
||||
从CookieCloud下载数据
|
||||
:return: Cookie数据、错误信息
|
||||
"""
|
||||
# 更新为最新设置
|
||||
self.__sync_setting()
|
||||
|
||||
if ((not self._server and not self._enable_local)
|
||||
or not self._key
|
||||
or not self._password):
|
||||
return None, "CookieCloud参数不正确"
|
||||
|
||||
if self._enable_local:
|
||||
# 开启本地服务时,从本地直接读取数据
|
||||
result = self.__load_local_encrypt_data(self._key)
|
||||
if not result:
|
||||
return {}, "未从本地CookieCloud服务加载到cookie数据,请检查服务器设置、用户KEY及加密密码是否正确"
|
||||
else:
|
||||
req_url = UrlUtils.combine_url(host=self._server, path=f"get/{self._key}")
|
||||
ret = RequestUtils(content_type="application/json").get_res(url=req_url)
|
||||
if ret and ret.status_code == 200:
|
||||
try:
|
||||
result = ret.json()
|
||||
if not result:
|
||||
return {}, f"未从{self._server}下载到cookie数据"
|
||||
except Exception as err:
|
||||
return {}, f"从{self._server}下载cookie数据错误:{str(err)}"
|
||||
elif ret:
|
||||
return None, f"远程同步CookieCloud失败,错误码:{ret.status_code}"
|
||||
else:
|
||||
return None, "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确"
|
||||
|
||||
encrypted = result.get("encrypted")
|
||||
if not encrypted:
|
||||
return {}, "未获取到cookie密文"
|
||||
else:
|
||||
crypt_key = self.__get_crypt_key()
|
||||
try:
|
||||
decrypted_data = CryptoJsUtils.decrypt(encrypted, crypt_key).decode("utf-8")
|
||||
result = json.loads(decrypted_data)
|
||||
except Exception as e:
|
||||
return {}, "cookie解密失败:" + str(e)
|
||||
|
||||
if not result:
|
||||
return {}, "cookie解密为空"
|
||||
|
||||
if result.get("cookie_data"):
|
||||
contents = result.get("cookie_data")
|
||||
else:
|
||||
contents = result
|
||||
# 整理数据,使用domain域名的最后两级作为分组依据
|
||||
domain_groups = {}
|
||||
for site, cookies in contents.items():
|
||||
for cookie in cookies:
|
||||
domain_key = StringUtils.get_url_domain(cookie.get("domain"))
|
||||
if not domain_groups.get(domain_key):
|
||||
domain_groups[domain_key] = [cookie]
|
||||
else:
|
||||
domain_groups[domain_key].append(cookie)
|
||||
# 返回错误
|
||||
ret_cookies = {}
|
||||
# 索引器
|
||||
for domain, content_list in domain_groups.items():
|
||||
if not content_list:
|
||||
continue
|
||||
# 只有cf的cookie过滤掉
|
||||
cloudflare_cookie = True
|
||||
for content in content_list:
|
||||
if content["name"] != "cf_clearance":
|
||||
cloudflare_cookie = False
|
||||
break
|
||||
if cloudflare_cookie:
|
||||
continue
|
||||
# 站点Cookie
|
||||
cookie_str = ";".join(
|
||||
[f"{content.get('name')}={content.get('value')}"
|
||||
for content in content_list
|
||||
if content.get("name") and content.get("name") not in self._ignore_cookies]
|
||||
)
|
||||
ret_cookies[domain] = cookie_str
|
||||
return ret_cookies, ""
|
||||
|
||||
def __get_crypt_key(self) -> bytes:
|
||||
"""
|
||||
使用UUID和密码生成CookieCloud的加解密密钥
|
||||
"""
|
||||
combined_string = f"{self._key}-{self._password}"
|
||||
return HashUtils.md5(combined_string)[:16].encode("utf-8")
|
||||
|
||||
def __load_local_encrypt_data(self, uuid: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取本地CookieCloud数据
|
||||
"""
|
||||
file_path = self._local_path / f"{uuid}.json"
|
||||
# 检查文件是否存在
|
||||
if not file_path.exists():
|
||||
logger.warn(f"本地CookieCloud文件不存在:{file_path}")
|
||||
return {}
|
||||
|
||||
# 读取文件
|
||||
with open(file_path, encoding="utf-8", errors="replace", mode="r") as file:
|
||||
read_content = file.read()
|
||||
data = json.loads(read_content.encode("utf-8"))
|
||||
return data
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
from app.adapters.network.http import RequestUtils
|
||||
|
||||
|
||||
class WebUtils:
|
||||
"""通过外部网络服务查询 IP 归属信息。"""
|
||||
|
||||
@staticmethod
|
||||
def get_location(ip: str):
|
||||
"""
|
||||
查询IP所属地
|
||||
"""
|
||||
return WebUtils.get_location1(ip) or WebUtils.get_location2(ip)
|
||||
|
||||
@staticmethod
|
||||
def get_location1(ip: str):
|
||||
"""
|
||||
https://api.mir6.com/api/ip
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"ip": "240e:97c:2f:1::5c",
|
||||
"dec": "47925092370311863177116789888333643868",
|
||||
"country": "中国",
|
||||
"countryCode": "CN",
|
||||
"province": "广东省",
|
||||
"city": "广州市",
|
||||
"districts": "",
|
||||
"idc": "",
|
||||
"isp": "中国电信",
|
||||
"net": "数据中心",
|
||||
"zipcode": "510000",
|
||||
"areacode": "020",
|
||||
"protocol": "IPv6",
|
||||
"location": "中国[CN] 广东省 广州市",
|
||||
"myip": "125.89.7.89",
|
||||
"time": "2023-09-01 17:28:23"
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
r = RequestUtils().get_res(f"https://api.mir6.com/api/ip?ip={ip}&type=json")
|
||||
if r:
|
||||
return r.json().get("data", {}).get("location") or ''
|
||||
except Exception as 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 ""
|
||||
Vendored
+3144
File diff suppressed because it is too large
Load Diff
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
import base64
|
||||
from typing import Optional
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.adapters.network.http import RequestUtils
|
||||
|
||||
|
||||
class OcrHelper:
|
||||
"""
|
||||
OCR 辅助类,负责获取验证码图片并调用 OCR 服务识别文本。
|
||||
"""
|
||||
|
||||
_ocr_b64_url = f"{settings.OCR_HOST}/captcha/base64"
|
||||
|
||||
def get_captcha_text(
|
||||
self,
|
||||
image_url: Optional[str] = None,
|
||||
image_b64: Optional[str] = None,
|
||||
cookie: Optional[str] = None,
|
||||
ua: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
根据图片地址,获取验证码图片,并识别内容
|
||||
:param image_url: 图片地址
|
||||
:param image_b64: 图片base64,跳过图片地址下载
|
||||
:param cookie: 下载图片使用的cookie
|
||||
:param ua: 下载图片使用的ua
|
||||
:return: 验证码识别结果,失败时返回空字符串
|
||||
"""
|
||||
image_b64 = self._normalize_image_base64(image_b64)
|
||||
if image_url:
|
||||
data_url_b64 = self._extract_data_url_base64(image_url)
|
||||
if data_url_b64:
|
||||
image_b64 = self._normalize_image_base64(data_url_b64)
|
||||
else:
|
||||
ret = RequestUtils(ua=ua,
|
||||
cookies=cookie).get_res(image_url)
|
||||
if ret is not None:
|
||||
image_bin = ret.content
|
||||
if not image_bin:
|
||||
return ""
|
||||
image_b64 = base64.b64encode(image_bin).decode()
|
||||
if not image_b64:
|
||||
return ""
|
||||
ret = RequestUtils(content_type="application/json").post_res(
|
||||
url=self._ocr_b64_url,
|
||||
json={"base64_img": image_b64})
|
||||
if ret:
|
||||
return ret.json().get("result") or ""
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _normalize_image_base64(image_b64: Optional[str]) -> str:
|
||||
"""规范化外部传入的图片 base64 内容。"""
|
||||
if not image_b64:
|
||||
return ""
|
||||
clean_image_b64 = OcrHelper._extract_data_url_base64(image_b64) or image_b64
|
||||
clean_image_b64 = "".join(clean_image_b64.split())
|
||||
if not clean_image_b64:
|
||||
return ""
|
||||
padding_size = len(clean_image_b64) % 4
|
||||
if padding_size:
|
||||
clean_image_b64 = f"{clean_image_b64}{'=' * (4 - padding_size)}"
|
||||
return clean_image_b64
|
||||
|
||||
@staticmethod
|
||||
def _extract_data_url_base64(image_url: Optional[str]) -> str:
|
||||
"""从 data:image/...;base64,... 地址中提取纯 base64 内容。"""
|
||||
image_url = (image_url or "").strip()
|
||||
if not image_url.lower().startswith("data:image/"):
|
||||
return ""
|
||||
metadata, separator, data = image_url.partition(",")
|
||||
if not separator or ";base64" not in metadata.lower():
|
||||
return ""
|
||||
return data.strip()
|
||||
Vendored
+1921
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user