quark / app.py
wd23's picture
Update app.py
afab10c verified
Raw
History Blame Contribute Delete
16 kB
import os
import requests
from flask import Flask, request, jsonify, render_template_string
from datetime import datetime
import time
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "quark-offline-secret-key")
# ========== 夸克配置 ==========
DEFAULT_COOKIE = os.environ.get("QUARK_COOKIE")
if not DEFAULT_COOKIE:
raise RuntimeError("请在 Hugging Face Secrets 中配置 QUARK_COOKIE")
current_cookie = DEFAULT_COOKIE
rate_limit_records = {}
def get_client_ip():
if request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0]
if request.headers.get('X-Real-IP'):
return request.headers.get('X-Real-IP')
return request.remote_addr or "unknown"
def rate_limit(min_interval=10, max_calls_per_minute=2):
def decorator(f):
def decorated_function(*args, **kwargs):
identifier = get_client_ip() + "_" + f.__name__
now = time.time()
record = rate_limit_records.get(identifier, {"last_call": 0, "calls": []})
record["calls"] = [t for t in record["calls"] if now - t < 60]
if len(record["calls"]) >= max_calls_per_minute:
oldest = record["calls"][0]
wait_time = 60 - (now - oldest)
return jsonify({"success": False, "error": f"操作过于频繁,请等待 {int(wait_time)} 秒"}), 429
if now - record["last_call"] < min_interval:
wait_time = min_interval - (now - record["last_call"])
return jsonify({"success": False, "error": f"请等待 {int(wait_time)} 秒后再试"}), 429
record["last_call"] = now
record["calls"].append(now)
rate_limit_records[identifier] = record
return f(*args, **kwargs)
decorated_function.__name__ = f.__name__
return decorated_function
return decorator
def get_headers():
return {
"Cookie": current_cookie,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Referer": "https://pan.quark.cn",
"Origin": "https://pan.quark.cn",
"Content-Type": "application/json"
}
def mask_cookie(cookie):
if not cookie:
return "未设置"
if len(cookie) <= 20:
return cookie[:8] + "***"
return cookie[:15] + "..." + cookie[-10:]
def safe_format_timestamp(timestamp):
if not timestamp:
return "暂无"
try:
ts = int(timestamp)
if ts <= 0 or ts > 4102444800:
return "暂无"
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
except:
return "暂无"
def get_space_info():
try:
url = "https://drive.quark.cn/1/clouddrive/member"
params = {"pr": "ucpro", "fr": "pc", "fetch_subscribe": "false", "_ch": "home", "fetch_identity": "false"}
resp = requests.get(url, headers=get_headers(), params=params, timeout=10)
data = resp.json()
if data.get("status") == 200 or data.get("code") == 0:
info = data.get("data", {})
total_bytes = info.get("total_capacity", 1)
used_bytes = info.get("use_capacity", 0)
total_gb = total_bytes / (1024**3)
used_gb = used_bytes / (1024**3)
free_gb = max(total_gb - used_gb, 0)
percent = min(used_gb / total_gb * 100, 100) if total_gb > 0 else 0
member_status = info.get("member_status", {})
vip_level = "普通用户"
is_88vip = False
if member_status.get("SUPER_VIP"):
vip_level = "超级会员(SVIP)"
is_88vip = True # SVIP 通常来自 88VIP
elif member_status.get("VIP"):
vip_level = "VIP会员"
elif member_status.get("Z_VIP"):
vip_level = "Z会员"
elif member_status.get("MINI_VIP"):
vip_level = "迷你会员"
expire_ts = info.get("expired_at", 0) or info.get("created_at", 0)
return {
"success": True,
"total": round(total_gb, 2),
"used": round(used_gb, 2),
"free": round(free_gb, 2),
"percent": round(percent, 1),
"vip": vip_level,
"is_88vip": is_88vip,
"expire_date": safe_format_timestamp(expire_ts)
}
return {"success": False, "error": data.get("message", "获取失败")}
except Exception as e:
return {"success": False, "error": str(e)}
HTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>夸克网盘信息查看器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #f0f2f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.card-header h2 {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.badge {
background: #e8f4ff;
color: #0066cc;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 16px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
button:hover { background: #0052a3; }
button:disabled { background: #ccc; cursor: not-allowed; }
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 14px;
resize: vertical;
margin-bottom: 12px;
font-family: monospace;
}
textarea:focus {
outline: none;
border-color: #0066cc;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-label {
color: #666;
font-size: 14px;
}
.info-value {
font-weight: 500;
color: #1a1a2e;
display: flex;
align-items: center;
gap: 8px;
}
.progress-bar {
background: #e0e0e0;
border-radius: 10px;
height: 8px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
background: #28a745;
width: 0%;
height: 100%;
border-radius: 10px;
}
.alert {
padding: 12px;
border-radius: 10px;
margin-bottom: 16px;
font-size: 13px;
}
.alert-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
color: #856404;
}
.alert-info {
background: #e7f3ff;
border-left: 4px solid #17a2b8;
color: #0c5460;
}
.result {
margin-top: 12px;
padding: 10px;
border-radius: 10px;
display: none;
}
.result.success { background: #d4edda; color: #155724; display: block; }
.result.error { background: #f8d7da; color: #721c24; display: block; }
.vip-tag {
background: #e8a020;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
}
.vip-88 {
background: #ff6b6b;
}
.empty-text {
text-align: center;
padding: 30px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<div class="alert alert-warning">
⚠️ 重要提示:手动点击刷新,建议每小时1-2次,避免Cookie失效
</div>
<!-- 会员信息 -->
<div class="card">
<div class="card-header">
<h2>📊 会员信息 & 💾 空间情况</h2>
<button class="btn-sm" onclick="loadSpaceInfo()">🔄 刷新</button>
</div>
<div id="spaceInfo">
<div class="empty-text">点击刷新按钮获取会员信息</div>
</div>
</div>
<!-- Cookie 管理 -->
<div class="card">
<div class="card-header">
<h2>🔧 Cookie 管理</h2>
<span class="badge" id="cookieDisplay">加载中...</span>
</div>
<textarea id="newCookie" rows="3" placeholder="粘贴完整的 Cookie 字符串&#10;格式如: __quark_di=xxx; __quark_uid=xxx; ..."></textarea>
<button onclick="updateCookie()">🔄 更换 Cookie</button>
<div id="cookieResult" class="result"></div>
<div class="alert-info" style="margin-top: 12px; padding: 10px; font-size: 12px;">
💡 如何获取Cookie?浏览器登录 pan.quark.cn → F12 → Application → Cookies → 复制所有键值对
</div>
</div>
</div>
<script>
let lastSpaceCall = 0;
const SPACE_MIN_INTERVAL = 10000;
async function loadSpaceInfo() {
const now = Date.now();
if (now - lastSpaceCall < SPACE_MIN_INTERVAL) {
const waitSeconds = Math.ceil((SPACE_MIN_INTERVAL - (now - lastSpaceCall)) / 1000);
const div = document.getElementById("spaceInfo");
div.innerHTML = `<div class="empty-text" style="color: #856404;">⏰ 请等待 ${waitSeconds} 秒后再试</div>`;
setTimeout(() => {
const currentDiv = document.getElementById("spaceInfo");
if (currentDiv && currentDiv.innerHTML.includes("请等待")) {
currentDiv.innerHTML = '<div class="empty-text">点击刷新按钮获取会员信息</div>';
}
}, 3000);
return;
}
const div = document.getElementById("spaceInfo");
div.innerHTML = '<div class="empty-text">加载中...</div>';
try {
const resp = await fetch("/api/space_info");
const data = await resp.json();
if (data.success) {
lastSpaceCall = now;
let vipHtml = data.vip;
if (data.is_88vip) {
vipHtml = `${data.vip} <span class="vip-tag vip-88">88VIP</span>`;
} else if (data.vip !== '普通用户') {
vipHtml = `${data.vip} <span class="vip-tag">VIP</span>`;
}
const percent = data.percent;
div.innerHTML = `
<div class="info-row"><span class="info-label">会员类型</span><span class="info-value">${vipHtml}</span></div>
<div class="info-row"><span class="info-label">到期时间</span><span class="info-value">${data.expire_date}</span></div>
<div class="info-row"><span class="info-label">总空间</span><span class="info-value">${data.total} GB</span></div>
<div class="info-row"><span class="info-label">已用空间</span><span class="info-value">${data.used} GB</span></div>
<div class="info-row"><span class="info-label">剩余空间</span><span class="info-value">${data.free} GB</span></div>
<div class="info-row"><span class="info-label">使用率</span><span class="info-value">${percent}%</span></div>
<div class="progress-bar"><div class="progress-fill" style="width: ${percent}%;"></div></div>
<div class="alert-info" style="margin-top: 12px; padding: 8px; font-size: 12px;">✅ 获取成功,建议1小时内不要超过2次</div>
`;
} else {
div.innerHTML = `<div class="empty-text" style="color: #dc3545;">❌ ${data.error}</div>`;
}
} catch(e) {
div.innerHTML = `<div class="empty-text" style="color: #dc3545;">❌ 获取失败</div>`;
}
}
async function updateCookie() {
const newCookie = document.getElementById("newCookie").value.trim();
if (!newCookie) {
showResult("cookieResult", "请输入新 Cookie", "error");
return;
}
const btn = event.target;
btn.disabled = true;
btn.textContent = "更换中...";
try {
const resp = await fetch("/api/update_cookie", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cookie: newCookie })
});
const data = await resp.json();
if (resp.ok) {
showResult("cookieResult", "✅ Cookie已更新", "success");
document.getElementById("newCookie").value = "";
loadCurrentCookie();
} else {
showResult("cookieResult", "❌ " + (data.error || "更换失败"), "error");
}
} catch(e) {
showResult("cookieResult", "❌ 网络错误", "error");
} finally {
btn.disabled = false;
btn.textContent = "🔄 更换 Cookie";
}
}
async function loadCurrentCookie() {
try {
const resp = await fetch("/api/current_cookie");
const data = await resp.json();
document.getElementById("cookieDisplay").textContent = data.masked_cookie;
} catch(e) {
document.getElementById("cookieDisplay").textContent = "获取失败";
}
}
function showResult(elementId, msg, type) {
const div = document.getElementById(elementId);
if (!div) return;
div.className = "result " + type;
div.textContent = msg;
setTimeout(() => { if (div) div.className = "result"; }, 5000);
}
loadCurrentCookie();
</script>
</body>
</html>
"""
@app.route("/", methods=["GET"])
def index():
return render_template_string(HTML)
@app.route("/api/space_info", methods=["GET"])
@rate_limit(min_interval=10, max_calls_per_minute=2)
def api_space_info():
return jsonify(get_space_info()), 200
@app.route("/api/update_cookie", methods=["POST"])
def api_update_cookie():
global current_cookie
data = request.get_json()
if not data or "cookie" not in data:
return jsonify({"error": "缺少 cookie"}), 400
new_cookie = data["cookie"].strip()
if not new_cookie:
return jsonify({"error": "Cookie 不能为空"}), 400
current_cookie = new_cookie
return jsonify({"status": "success", "message": "Cookie已更新"}), 200
@app.route("/api/current_cookie", methods=["GET"])
def api_current_cookie():
return jsonify({"masked_cookie": mask_cookie(current_cookie)}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860)