|
|
|
|
|
""" |
|
|
Crypto Resources API - Hugging Face Space |
|
|
سرور API با رابط کاربری وب و WebSocket |
|
|
""" |
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from fastapi.responses import JSONResponse, HTMLResponse |
|
|
from fastapi.staticfiles import StaticFiles |
|
|
from datetime import datetime |
|
|
from pathlib import Path |
|
|
import json |
|
|
import asyncio |
|
|
from typing import List, Dict, Any, Set |
|
|
import logging |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
def load_resources(): |
|
|
"""بارگذاری منابع از فایل JSON""" |
|
|
resources_file = Path("api-resources/crypto_resources_unified_2025-11-11.json") |
|
|
|
|
|
if not resources_file.exists(): |
|
|
logger.warning(f"Resources file not found: {resources_file}") |
|
|
return {} |
|
|
|
|
|
try: |
|
|
with open(resources_file, 'r', encoding='utf-8') as f: |
|
|
data = json.load(f) |
|
|
logger.info(f"✅ Loaded resources from {resources_file}") |
|
|
return data.get('registry', {}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error loading resources: {e}") |
|
|
return {} |
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="Crypto Resources API", |
|
|
description="API جامع برای دسترسی به منابع داده کریپتوکارنسی", |
|
|
version="2.0.0", |
|
|
docs_url="/docs", |
|
|
redoc_url="/redoc" |
|
|
) |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
RESOURCES = load_resources() |
|
|
|
|
|
|
|
|
class ConnectionManager: |
|
|
def __init__(self): |
|
|
self.active_connections: Set[WebSocket] = set() |
|
|
|
|
|
async def connect(self, websocket: WebSocket): |
|
|
await websocket.accept() |
|
|
self.active_connections.add(websocket) |
|
|
logger.info(f"WebSocket connected. Total: {len(self.active_connections)}") |
|
|
|
|
|
def disconnect(self, websocket: WebSocket): |
|
|
self.active_connections.discard(websocket) |
|
|
logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}") |
|
|
|
|
|
async def broadcast(self, message: dict): |
|
|
"""ارسال پیام به همه کلاینتها""" |
|
|
disconnected = set() |
|
|
for connection in self.active_connections: |
|
|
try: |
|
|
await connection.send_json(message) |
|
|
except Exception as e: |
|
|
logger.error(f"Error sending to client: {e}") |
|
|
disconnected.add(connection) |
|
|
|
|
|
|
|
|
for conn in disconnected: |
|
|
self.active_connections.discard(conn) |
|
|
|
|
|
manager = ConnectionManager() |
|
|
|
|
|
|
|
|
async def broadcast_stats(): |
|
|
"""ارسال دورهای آمار به کلاینتها""" |
|
|
while True: |
|
|
try: |
|
|
if manager.active_connections: |
|
|
stats = get_stats_data() |
|
|
await manager.broadcast({ |
|
|
"type": "stats_update", |
|
|
"data": stats, |
|
|
"timestamp": datetime.now().isoformat() |
|
|
}) |
|
|
await asyncio.sleep(10) |
|
|
except Exception as e: |
|
|
logger.error(f"Error in broadcast_stats: {e}") |
|
|
await asyncio.sleep(5) |
|
|
|
|
|
|
|
|
@app.on_event("startup") |
|
|
async def startup_event(): |
|
|
"""راهاندازی سرویسهای پسزمینه""" |
|
|
logger.info("🚀 Starting Crypto Resources API...") |
|
|
logger.info(f"📦 Loaded {len([k for k,v in RESOURCES.items() if isinstance(v, list)])} categories") |
|
|
|
|
|
|
|
|
asyncio.create_task(broadcast_stats()) |
|
|
logger.info("✅ Background tasks started") |
|
|
|
|
|
|
|
|
def get_stats_data(): |
|
|
"""دریافت آمار کلی""" |
|
|
categories_count = {} |
|
|
total_resources = 0 |
|
|
|
|
|
for key, value in RESOURCES.items(): |
|
|
if isinstance(value, list): |
|
|
count = len(value) |
|
|
categories_count[key] = count |
|
|
total_resources += count |
|
|
|
|
|
return { |
|
|
"total_resources": total_resources, |
|
|
"total_categories": len(categories_count), |
|
|
"categories": categories_count |
|
|
} |
|
|
|
|
|
|
|
|
HTML_TEMPLATE = """ |
|
|
<!DOCTYPE html> |
|
|
<html lang="fa" dir="rtl"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Crypto Resources API</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
padding: 20px; |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.header { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 30px; |
|
|
margin-bottom: 20px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.header h1 { |
|
|
color: #667eea; |
|
|
margin-bottom: 10px; |
|
|
font-size: 2.5em; |
|
|
} |
|
|
|
|
|
.header p { |
|
|
color: #666; |
|
|
font-size: 1.1em; |
|
|
} |
|
|
|
|
|
.status-badge { |
|
|
display: inline-block; |
|
|
padding: 5px 15px; |
|
|
border-radius: 20px; |
|
|
font-size: 0.9em; |
|
|
font-weight: bold; |
|
|
margin-top: 10px; |
|
|
} |
|
|
|
|
|
.status-online { |
|
|
background: #4CAF50; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.status-offline { |
|
|
background: #f44336; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.stats-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
|
|
gap: 20px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.stat-card { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 25px; |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
|
|
transition: transform 0.3s; |
|
|
} |
|
|
|
|
|
.stat-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 10px 25px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.stat-number { |
|
|
font-size: 2.5em; |
|
|
font-weight: bold; |
|
|
color: #667eea; |
|
|
margin: 10px 0; |
|
|
} |
|
|
|
|
|
.stat-label { |
|
|
color: #666; |
|
|
font-size: 1.1em; |
|
|
} |
|
|
|
|
|
.categories-section { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 30px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.categories-section h2 { |
|
|
color: #667eea; |
|
|
margin-bottom: 20px; |
|
|
font-size: 1.8em; |
|
|
} |
|
|
|
|
|
.category-list { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.category-item { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 20px; |
|
|
border-radius: 10px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
.category-item:hover { |
|
|
transform: scale(1.05); |
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.category-name { |
|
|
font-size: 1.2em; |
|
|
font-weight: bold; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.category-count { |
|
|
font-size: 0.9em; |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
.api-endpoints { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 30px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.api-endpoints h2 { |
|
|
color: #667eea; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.endpoint-item { |
|
|
background: #f5f5f5; |
|
|
padding: 15px; |
|
|
border-radius: 8px; |
|
|
margin-bottom: 10px; |
|
|
border-left: 4px solid #667eea; |
|
|
} |
|
|
|
|
|
.endpoint-method { |
|
|
display: inline-block; |
|
|
background: #667eea; |
|
|
color: white; |
|
|
padding: 3px 10px; |
|
|
border-radius: 5px; |
|
|
font-size: 0.85em; |
|
|
font-weight: bold; |
|
|
margin-left: 10px; |
|
|
} |
|
|
|
|
|
.endpoint-path { |
|
|
font-family: monospace; |
|
|
color: #333; |
|
|
font-weight: bold; |
|
|
} |
|
|
|
|
|
.websocket-status { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 20px; |
|
|
margin-top: 20px; |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.websocket-status h3 { |
|
|
color: #667eea; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.ws-messages { |
|
|
background: #f9f9f9; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
max-height: 200px; |
|
|
overflow-y: auto; |
|
|
font-family: monospace; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
|
|
|
.ws-message { |
|
|
padding: 5px 0; |
|
|
border-bottom: 1px solid #eee; |
|
|
} |
|
|
|
|
|
.footer { |
|
|
text-align: center; |
|
|
color: white; |
|
|
margin-top: 30px; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0.5; } |
|
|
} |
|
|
|
|
|
.loading { |
|
|
animation: pulse 1.5s infinite; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<h1>🚀 Crypto Resources API</h1> |
|
|
<p>API جامع برای دسترسی به منابع داده کریپتوکارنسی</p> |
|
|
<span id="statusBadge" class="status-badge status-offline">در حال اتصال...</span> |
|
|
</div> |
|
|
|
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-label">مجموع منابع</div> |
|
|
<div class="stat-number" id="totalResources">0</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-label">دستهبندیها</div> |
|
|
<div class="stat-number" id="totalCategories">0</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-label">وضعیت سرور</div> |
|
|
<div class="stat-number" id="serverStatus">⏳</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="categories-section"> |
|
|
<h2>📂 دستهبندی منابع</h2> |
|
|
<div class="category-list" id="categoryList"> |
|
|
<div class="loading">در حال بارگذاری...</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="api-endpoints"> |
|
|
<h2>📡 API Endpoints</h2> |
|
|
<div class="endpoint-item"> |
|
|
<span class="endpoint-method">GET</span> |
|
|
<span class="endpoint-path">/health</span> |
|
|
<span> - Health check</span> |
|
|
</div> |
|
|
<div class="endpoint-item"> |
|
|
<span class="endpoint-method">GET</span> |
|
|
<span class="endpoint-path">/api/resources/stats</span> |
|
|
<span> - آمار کلی منابع</span> |
|
|
</div> |
|
|
<div class="endpoint-item"> |
|
|
<span class="endpoint-method">GET</span> |
|
|
<span class="endpoint-path">/api/resources/list</span> |
|
|
<span> - لیست تمام منابع</span> |
|
|
</div> |
|
|
<div class="endpoint-item"> |
|
|
<span class="endpoint-method">GET</span> |
|
|
<span class="endpoint-path">/api/categories</span> |
|
|
<span> - لیست دستهبندیها</span> |
|
|
</div> |
|
|
<div class="endpoint-item"> |
|
|
<span class="endpoint-method">GET</span> |
|
|
<span class="endpoint-path">/api/resources/category/{category}</span> |
|
|
<span> - منابع یک دسته خاص</span> |
|
|
</div> |
|
|
<div class="endpoint-item"> |
|
|
<span class="endpoint-method">WS</span> |
|
|
<span class="endpoint-path">/ws</span> |
|
|
<span> - WebSocket برای بروزرسانی لحظهای</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="websocket-status"> |
|
|
<h3>🔌 WebSocket Status: <span id="wsStatus">Disconnected</span></h3> |
|
|
<div class="ws-messages" id="wsMessages"> |
|
|
<div class="ws-message">در انتظار اتصال...</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="footer"> |
|
|
<p>💜 ساخته شده با عشق برای جامعه کریپتو</p> |
|
|
<p>📚 مستندات کامل: <a href="/docs" style="color: white; text-decoration: underline;">/docs</a></p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// WebSocket connection |
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
|
|
const wsUrl = `${protocol}//${window.location.host}/ws`; |
|
|
let ws = null; |
|
|
let reconnectInterval = null; |
|
|
|
|
|
function connectWebSocket() { |
|
|
try { |
|
|
ws = new WebSocket(wsUrl); |
|
|
|
|
|
ws.onopen = () => { |
|
|
console.log('✅ WebSocket connected'); |
|
|
document.getElementById('wsStatus').textContent = 'Connected ✅'; |
|
|
document.getElementById('statusBadge').className = 'status-badge status-online'; |
|
|
document.getElementById('statusBadge').textContent = 'آنلاین ✅'; |
|
|
addWsMessage('اتصال WebSocket برقرار شد ✅'); |
|
|
|
|
|
if (reconnectInterval) { |
|
|
clearInterval(reconnectInterval); |
|
|
reconnectInterval = null; |
|
|
} |
|
|
}; |
|
|
|
|
|
ws.onmessage = (event) => { |
|
|
try { |
|
|
const data = JSON.parse(event.data); |
|
|
console.log('📨 Received:', data); |
|
|
|
|
|
if (data.type === 'stats_update') { |
|
|
updateStats(data.data); |
|
|
addWsMessage(`بروزرسانی آمار: ${data.data.total_resources} منبع`); |
|
|
} |
|
|
} catch (e) { |
|
|
console.error('Error parsing message:', e); |
|
|
} |
|
|
}; |
|
|
|
|
|
ws.onerror = (error) => { |
|
|
console.error('❌ WebSocket error:', error); |
|
|
document.getElementById('wsStatus').textContent = 'Error ❌'; |
|
|
addWsMessage('خطا در اتصال WebSocket ❌'); |
|
|
}; |
|
|
|
|
|
ws.onclose = () => { |
|
|
console.log('🔌 WebSocket disconnected'); |
|
|
document.getElementById('wsStatus').textContent = 'Disconnected'; |
|
|
document.getElementById('statusBadge').className = 'status-badge status-offline'; |
|
|
document.getElementById('statusBadge').textContent = 'آفلاین'; |
|
|
addWsMessage('اتصال WebSocket قطع شد. در حال تلاش مجدد...'); |
|
|
|
|
|
// تلاش مجدد برای اتصال |
|
|
if (!reconnectInterval) { |
|
|
reconnectInterval = setInterval(() => { |
|
|
console.log('🔄 Reconnecting...'); |
|
|
connectWebSocket(); |
|
|
}, 5000); |
|
|
} |
|
|
}; |
|
|
} catch (e) { |
|
|
console.error('Error creating WebSocket:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
function addWsMessage(message) { |
|
|
const container = document.getElementById('wsMessages'); |
|
|
const msgDiv = document.createElement('div'); |
|
|
msgDiv.className = 'ws-message'; |
|
|
msgDiv.textContent = `[${new Date().toLocaleTimeString('fa-IR')}] ${message}`; |
|
|
container.appendChild(msgDiv); |
|
|
container.scrollTop = container.scrollHeight; |
|
|
|
|
|
// نگه داشتن فقط 10 پیام آخر |
|
|
while (container.children.length > 10) { |
|
|
container.removeChild(container.firstChild); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateStats(stats) { |
|
|
document.getElementById('totalResources').textContent = stats.total_resources; |
|
|
document.getElementById('totalCategories').textContent = stats.total_categories; |
|
|
document.getElementById('serverStatus').textContent = '✅'; |
|
|
|
|
|
// بروزرسانی لیست دستهها |
|
|
const categoryList = document.getElementById('categoryList'); |
|
|
categoryList.innerHTML = ''; |
|
|
|
|
|
for (const [name, count] of Object.entries(stats.categories)) { |
|
|
const item = document.createElement('div'); |
|
|
item.className = 'category-item'; |
|
|
item.innerHTML = ` |
|
|
<div class="category-name">${name}</div> |
|
|
<div class="category-count">${count} منبع</div> |
|
|
`; |
|
|
item.onclick = () => { |
|
|
window.open(`/api/resources/category/${name}`, '_blank'); |
|
|
}; |
|
|
categoryList.appendChild(item); |
|
|
} |
|
|
} |
|
|
|
|
|
// بارگذاری اولیه آمار |
|
|
async function loadInitialStats() { |
|
|
try { |
|
|
const response = await fetch('/api/resources/stats'); |
|
|
const stats = await response.json(); |
|
|
updateStats(stats); |
|
|
} catch (e) { |
|
|
console.error('Error loading initial stats:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
// شروع اتصال |
|
|
connectWebSocket(); |
|
|
loadInitialStats(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse) |
|
|
async def root(): |
|
|
"""صفحه اصلی با UI""" |
|
|
return HTMLResponse(content=HTML_TEMPLATE) |
|
|
|
|
|
@app.get("/health") |
|
|
async def health(): |
|
|
"""Health check""" |
|
|
return { |
|
|
"status": "healthy", |
|
|
"timestamp": datetime.now().isoformat(), |
|
|
"resources_loaded": len(RESOURCES) > 0, |
|
|
"total_categories": len([k for k, v in RESOURCES.items() if isinstance(v, list)]), |
|
|
"websocket_connections": len(manager.active_connections) |
|
|
} |
|
|
|
|
|
@app.get("/api/resources/stats") |
|
|
async def resources_stats(): |
|
|
"""آمار منابع""" |
|
|
stats = get_stats_data() |
|
|
metadata = RESOURCES.get('metadata', {}) |
|
|
|
|
|
return { |
|
|
**stats, |
|
|
"metadata": metadata, |
|
|
"timestamp": datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
@app.get("/api/resources/list") |
|
|
async def resources_list(): |
|
|
"""لیست همه منابع""" |
|
|
all_resources = [] |
|
|
|
|
|
for category, resources in RESOURCES.items(): |
|
|
if isinstance(resources, list): |
|
|
for resource in resources: |
|
|
if isinstance(resource, dict): |
|
|
all_resources.append({ |
|
|
"category": category, |
|
|
"id": resource.get('id', 'unknown'), |
|
|
"name": resource.get('name', 'Unknown'), |
|
|
"base_url": resource.get('base_url', ''), |
|
|
"auth_type": resource.get('auth', {}).get('type', 'none') |
|
|
}) |
|
|
|
|
|
return { |
|
|
"total": len(all_resources), |
|
|
"resources": all_resources[:100], |
|
|
"note": f"Showing first 100 of {len(all_resources)} resources", |
|
|
"timestamp": datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
@app.get("/api/resources/category/{category}") |
|
|
async def resources_by_category(category: str): |
|
|
"""منابع یک دسته خاص""" |
|
|
if category not in RESOURCES: |
|
|
return JSONResponse( |
|
|
status_code=404, |
|
|
content={"error": f"Category '{category}' not found"} |
|
|
) |
|
|
|
|
|
resources = RESOURCES.get(category, []) |
|
|
|
|
|
if not isinstance(resources, list): |
|
|
return JSONResponse( |
|
|
status_code=400, |
|
|
content={"error": f"Category '{category}' is not a resource list"} |
|
|
) |
|
|
|
|
|
return { |
|
|
"category": category, |
|
|
"total": len(resources), |
|
|
"resources": resources, |
|
|
"timestamp": datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
@app.get("/api/categories") |
|
|
async def list_categories(): |
|
|
"""لیست دستهبندیها""" |
|
|
categories = [] |
|
|
|
|
|
for key, value in RESOURCES.items(): |
|
|
if isinstance(value, list): |
|
|
categories.append({ |
|
|
"name": key, |
|
|
"count": len(value), |
|
|
"endpoint": f"/api/resources/category/{key}" |
|
|
}) |
|
|
|
|
|
return { |
|
|
"total": len(categories), |
|
|
"categories": categories, |
|
|
"timestamp": datetime.now().isoformat() |
|
|
} |
|
|
|
|
|
@app.websocket("/ws") |
|
|
async def websocket_endpoint(websocket: WebSocket): |
|
|
"""WebSocket endpoint برای بروزرسانی لحظهای""" |
|
|
await manager.connect(websocket) |
|
|
|
|
|
try: |
|
|
|
|
|
stats = get_stats_data() |
|
|
await websocket.send_json({ |
|
|
"type": "initial_stats", |
|
|
"data": stats, |
|
|
"timestamp": datetime.now().isoformat() |
|
|
}) |
|
|
|
|
|
|
|
|
while True: |
|
|
try: |
|
|
|
|
|
data = await websocket.receive_text() |
|
|
logger.info(f"Received from client: {data}") |
|
|
|
|
|
|
|
|
await websocket.send_json({ |
|
|
"type": "pong", |
|
|
"message": "Server is alive", |
|
|
"timestamp": datetime.now().isoformat() |
|
|
}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error in websocket loop: {e}") |
|
|
break |
|
|
|
|
|
except WebSocketDisconnect: |
|
|
manager.disconnect(websocket) |
|
|
logger.info("Client disconnected normally") |
|
|
except Exception as e: |
|
|
logger.error(f"WebSocket error: {e}") |
|
|
manager.disconnect(websocket) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
|
|
|
print("=" * 80) |
|
|
print("🚀 راهاندازی Crypto Resources API Server") |
|
|
print("=" * 80) |
|
|
print(f"\nبارگذاری منابع...") |
|
|
print(f"✅ {len([k for k,v in RESOURCES.items() if isinstance(v, list)])} دسته بارگذاری شد") |
|
|
print(f"\n🌐 Server: http://0.0.0.0:7860") |
|
|
print(f"📚 Docs: http://0.0.0.0:7860/docs") |
|
|
print(f"🔌 WebSocket: ws://0.0.0.0:7860/ws") |
|
|
print(f"\nبرای توقف سرور: Ctrl+C") |
|
|
print("=" * 80 + "\n") |
|
|
|
|
|
uvicorn.run( |
|
|
app, |
|
|
host="0.0.0.0", |
|
|
port=7860, |
|
|
log_level="info", |
|
|
access_log=True |
|
|
) |
|
|
|