Skip to content

Commit 1ae220c

Browse files
committed
集成CookieCloud服务端
1 parent 399d269 commit 1ae220c

File tree

9 files changed

+243
-63
lines changed

9 files changed

+243
-63
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ app/helper/*.pyd
1010
app/helper/*.bin
1111
app/plugins/**
1212
!app/plugins/__init__.py
13+
config/cookies/**
1314
config/user.db
1415
config/sites/**
1516
*.pyc
1617
*.log
18+
.vscode

app/api/servcookie.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import gzip
2+
import json
3+
import os
4+
from typing import Annotated, Any, Callable, Dict
5+
6+
from fastapi import APIRouter, Depends, HTTPException, Path, Request, Response
7+
from fastapi.responses import PlainTextResponse
8+
from fastapi.routing import APIRoute
9+
10+
from app import schemas
11+
from app.core.config import settings
12+
from app.utils.common import get_decrypted_cookie_data
13+
14+
15+
class GzipRequest(Request):
16+
17+
async def body(self) -> bytes:
18+
if not hasattr(self, "_body"):
19+
body = await super().body()
20+
if "gzip" in self.headers.getlist("Content-Encoding"):
21+
body = gzip.decompress(body)
22+
self._body = body
23+
return self._body
24+
25+
26+
class GzipRoute(APIRoute):
27+
28+
def get_route_handler(self) -> Callable:
29+
original_route_handler = super().get_route_handler()
30+
31+
async def custom_route_handler(request: Request) -> Response:
32+
request = GzipRequest(request.scope, request.receive)
33+
return await original_route_handler(request)
34+
35+
return custom_route_handler
36+
37+
38+
async def verify_server_enabled():
39+
"""
40+
校验CookieCloud服务路由是否打开
41+
"""
42+
if not settings.COOKIECLOUD_ENABLE_LOCAL:
43+
raise HTTPException(status_code=400, detail="本地CookieCloud服务器未启用")
44+
return True
45+
46+
47+
cookie_router = APIRouter(route_class=GzipRoute,
48+
tags=['servcookie'],
49+
dependencies=[Depends(verify_server_enabled)])
50+
51+
52+
@cookie_router.get("/", response_class=PlainTextResponse)
53+
def get_root():
54+
return "Hello World! API ROOT = /cookiecloud"
55+
56+
57+
@cookie_router.post("/", response_class=PlainTextResponse)
58+
def post_root():
59+
return "Hello World! API ROOT = /cookiecloud"
60+
61+
62+
@cookie_router.post("/update")
63+
async def update_cookie(req: schemas.CookieData):
64+
file_path = os.path.join(settings.COOKIE_PATH,
65+
os.path.basename(req.uuid) + ".json")
66+
content = json.dumps({"encrypted": req.encrypted})
67+
with open(file_path, encoding="utf-8", mode="w") as file:
68+
file.write(content)
69+
read_content = None
70+
with open(file_path, encoding="utf-8", mode="r") as file:
71+
read_content = file.read()
72+
if (read_content == content):
73+
return {"action": "done"}
74+
else:
75+
return {"action": "error"}
76+
77+
78+
def load_encrypt_data(uuid: str) -> Dict[str, Any]:
79+
file_path = os.path.join(settings.COOKIE_PATH,
80+
os.path.basename(uuid) + ".json")
81+
82+
# 检查文件是否存在
83+
if not os.path.exists(file_path):
84+
raise HTTPException(status_code=404, detail="Item not found")
85+
86+
# 读取文件
87+
with open(file_path, encoding="utf-8", mode="r") as file:
88+
read_content = file.read()
89+
data = json.loads(read_content)
90+
return data
91+
92+
93+
@cookie_router.get("/get/{uuid}")
94+
async def get_cookie(
95+
uuid: Annotated[str, Path(min_length=5, pattern="^[a-zA-Z0-9]+$")]):
96+
return load_encrypt_data(uuid)
97+
98+
99+
@cookie_router.post("/get/{uuid}")
100+
async def post_cookie(
101+
uuid: Annotated[str, Path(min_length=5, pattern="^[a-zA-Z0-9]+$")],
102+
request: schemas.CookiePassword):
103+
data = load_encrypt_data(uuid)
104+
return get_decrypted_cookie_data(uuid, request.password, data["encrypted"])

app/chain/site.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def __init__(self):
4343
self.cookiecloud = CookieCloudHelper(
4444
server=settings.COOKIECLOUD_HOST,
4545
key=settings.COOKIECLOUD_KEY,
46-
password=settings.COOKIECLOUD_PASSWORD
46+
password=settings.COOKIECLOUD_PASSWORD,
47+
enable_local=settings.COOKIECLOUD_ENABLE_LOCAL,
48+
local_path=settings.COOKIE_PATH
4749
)
4850

4951
# 特殊站点登录验证

app/core/config.py

+9
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ class Settings(BaseSettings):
187187
PLEX_TOKEN: Optional[str] = None
188188
# 转移方式 link/copy/move/softlink
189189
TRANSFER_TYPE: str = "copy"
190+
# CookieCloud是否启动本地服务
191+
COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = True
190192
# CookieCloud服务器地址
191193
COOKIECLOUD_HOST: str = "https://movie-pilot.org/cookiecloud"
192194
# CookieCloud用户KEY
@@ -275,6 +277,10 @@ def PLUGIN_DATA_PATH(self):
275277
@property
276278
def LOG_PATH(self):
277279
return self.CONFIG_PATH / "logs"
280+
281+
@property
282+
def COOKIE_PATH(self):
283+
return self.CONFIG_PATH / "cookies"
278284

279285
@property
280286
def CACHE_CONF(self):
@@ -397,6 +403,9 @@ def __init__(self, **kwargs):
397403
with self.LOG_PATH as p:
398404
if not p.exists():
399405
p.mkdir(parents=True, exist_ok=True)
406+
with self.COOKIE_PATH as p:
407+
if not p.exists():
408+
p.mkdir(parents=True, exist_ok=True)
400409

401410
class Config:
402411
case_sensitive = True

app/helper/cookiecloud.py

+85-59
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import os
12
import json
23

3-
from typing import Tuple, Optional
4+
from typing import Any, Dict, Tuple, Optional
45
from hashlib import md5
56

67
from app.utils.http import RequestUtils
@@ -12,83 +13,108 @@ class CookieCloudHelper:
1213

1314
_ignore_cookies: list = ["CookieAutoDeleteBrowsingDataCleanup", "CookieAutoDeleteCleaningDiscarded"]
1415

15-
def __init__(self, server: str, key: str, password: str):
16+
def __init__(self, server: str, key: str, password: str, enable_local: bool, local_path: str):
1617
self._server = server
1718
self._key = key
1819
self._password = password
20+
self._enable_local = enable_local
21+
self._local_path = local_path
1922
self._req = RequestUtils(content_type="application/json")
2023

2124
def download(self) -> Tuple[Optional[dict], str]:
2225
"""
2326
从CookieCloud下载数据
2427
:return: Cookie数据、错误信息
2528
"""
26-
if not self._server or not self._key or not self._password:
29+
if (not self._server and
30+
not self._enable_local) or not self._key or not self._password:
2731
return None, "CookieCloud参数不正确"
28-
req_url = "%s/get/%s" % (self._server, str(self._key).strip())
29-
ret = self._req.get_res(url=req_url)
30-
if ret and ret.status_code == 200:
31-
result = ret.json()
32-
if not result:
33-
return {}, "未下载到数据"
34-
encrypted = result.get("encrypted")
35-
if not encrypted:
36-
return {}, "未获取到cookie密文"
37-
else:
38-
crypt_key = self.get_crypt_key()
39-
try:
40-
decrypted_data = decrypt(encrypted, crypt_key).decode('utf-8')
41-
result = json.loads(decrypted_data)
42-
except Exception as e:
43-
return {}, "cookie解密失败" + str(e)
4432

33+
result = None
34+
if self._enable_local:
35+
# 开启本地服务时,从本地直接读取数据
36+
result = self.load_local_encrypt_data(self._key)
4537
if not result:
46-
return {}, "cookie解密为空"
47-
48-
if result.get("cookie_data"):
49-
contents = result.get("cookie_data")
38+
return {}, "未从本地CookieCloud服务加载到cookie数据"
39+
else:
40+
req_url = "%s/get/%s" % (self._server, str(self._key).strip())
41+
ret = self._req.get_res(url=req_url)
42+
if ret and ret.status_code == 200:
43+
result = ret.json()
44+
if not result:
45+
return {}, "未从" + self._server + "下载到数据"
46+
elif ret:
47+
return None, f"远程同步CookieCloud失败,错误码:{ret.status_code}"
5048
else:
51-
contents = result
52-
# 整理数据,使用domain域名的最后两级作为分组依据
53-
domain_groups = {}
54-
for site, cookies in contents.items():
55-
for cookie in cookies:
56-
domain_key = StringUtils.get_url_domain(cookie.get("domain"))
57-
if not domain_groups.get(domain_key):
58-
domain_groups[domain_key] = [cookie]
59-
else:
60-
domain_groups[domain_key].append(cookie)
61-
# 返回错误
62-
ret_cookies = {}
63-
# 索引器
64-
for domain, content_list in domain_groups.items():
65-
if not content_list:
66-
continue
67-
# 只有cf的cookie过滤掉
68-
cloudflare_cookie = True
69-
for content in content_list:
70-
if content["name"] != "cf_clearance":
71-
cloudflare_cookie = False
72-
break
73-
if cloudflare_cookie:
74-
continue
75-
# 站点Cookie
76-
cookie_str = ";".join(
77-
[f"{content.get('name')}={content.get('value')}"
78-
for content in content_list
79-
if content.get("name") and content.get("name") not in self._ignore_cookies]
80-
)
81-
ret_cookies[domain] = cookie_str
82-
return ret_cookies, ""
83-
elif ret:
84-
return None, f"同步CookieCloud失败,错误码:{ret.status_code}"
49+
return None, "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确"
50+
51+
encrypted = result.get("encrypted")
52+
if not encrypted:
53+
return {}, "未获取到cookie密文"
8554
else:
86-
return None, "CookieCloud请求失败,请检查服务器地址、用户KEY及加密密码是否正确"
87-
55+
crypt_key = self.get_crypt_key()
56+
try:
57+
decrypted_data = decrypt(encrypted, crypt_key).decode('utf-8')
58+
result = json.loads(decrypted_data)
59+
except Exception as e:
60+
return {}, "cookie解密失败" + str(e)
61+
62+
if not result:
63+
return {}, "cookie解密为空"
64+
65+
if result.get("cookie_data"):
66+
contents = result.get("cookie_data")
67+
else:
68+
contents = result
69+
# 整理数据,使用domain域名的最后两级作为分组依据
70+
domain_groups = {}
71+
for site, cookies in contents.items():
72+
for cookie in cookies:
73+
domain_key = StringUtils.get_url_domain(cookie.get("domain"))
74+
if not domain_groups.get(domain_key):
75+
domain_groups[domain_key] = [cookie]
76+
else:
77+
domain_groups[domain_key].append(cookie)
78+
# 返回错误
79+
ret_cookies = {}
80+
# 索引器
81+
for domain, content_list in domain_groups.items():
82+
if not content_list:
83+
continue
84+
# 只有cf的cookie过滤掉
85+
cloudflare_cookie = True
86+
for content in content_list:
87+
if content["name"] != "cf_clearance":
88+
cloudflare_cookie = False
89+
break
90+
if cloudflare_cookie:
91+
continue
92+
# 站点Cookie
93+
cookie_str = ";".join(
94+
[f"{content.get('name')}={content.get('value')}"
95+
for content in content_list
96+
if content.get("name") and content.get("name") not in self._ignore_cookies]
97+
)
98+
ret_cookies[domain] = cookie_str
99+
return ret_cookies, ""
100+
88101
def get_crypt_key(self) -> bytes:
89102
"""
90103
使用UUID和密码生成CookieCloud的加解密密钥
91104
"""
92105
md5_generator = md5()
93106
md5_generator.update((str(self._key).strip() + '-' + str(self._password).strip()).encode('utf-8'))
94107
return (md5_generator.hexdigest()[:16]).encode('utf-8')
108+
109+
def load_local_encrypt_data(self,uuid: str) -> Dict[str, Any]:
110+
file_path = os.path.join(self._local_path, os.path.basename(uuid) + ".json")
111+
112+
# 检查文件是否存在
113+
if not os.path.exists(file_path):
114+
return None
115+
116+
# 读取文件
117+
with open(file_path, encoding="utf-8", mode="r") as file:
118+
read_content = file.read()
119+
data = json.loads(read_content)
120+
return data

app/main.py

+3
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,13 @@ def init_routers():
5353
"""
5454
from app.api.apiv1 import api_router
5555
from app.api.servarr import arr_router
56+
from app.api.servcookie import cookie_router
5657
# API路由
5758
App.include_router(api_router, prefix=settings.API_V1_STR)
5859
# Radarr、Sonarr路由
5960
App.include_router(arr_router, prefix="/api/v3")
61+
# CookieCloud路由
62+
App.include_router(cookie_router, prefix="/cookiecloud")
6063

6164

6265
def start_frontend():

app/schemas/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .subscribe import *
66
from .context import *
77
from .servarr import *
8+
from .servcookie import *
89
from .plugin import *
910
from .history import *
1011
from .dashboard import *

app/schemas/servcookie.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Union
2+
3+
from fastapi import Query
4+
from pydantic import BaseModel
5+
6+
7+
class CookieData(BaseModel):
8+
uuid: str = Query(min_length=5, pattern="^[a-zA-Z0-9]+$")
9+
encrypted: str = Query(min_length=1, max_length=1024 * 1024 * 50)
10+
11+
12+
class CookiePassword(BaseModel):
13+
password: str

0 commit comments

Comments
 (0)