refactor: reorganize backend module boundaries

This commit is contained in:
jxxghp
2026-08-14 19:53:33 +08:00
parent fe0b444ceb
commit 369d7d6448
584 changed files with 2423 additions and 2275 deletions
+1
View File
@@ -0,0 +1 @@
"""插件市场、CookieCloud、OCR 和远程 MoviePilot 服务集成。"""
+135
View File
@@ -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
+72
View File
@@ -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 ""
+3144
View File
File diff suppressed because it is too large Load Diff
+75
View File
@@ -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()
+1921
View File
File diff suppressed because it is too large Load Diff