Really-amin commited on
Commit
13fcf3a
·
verified ·
1 Parent(s): b63daf8

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +713 -0
app.py ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Crypto Resources API - Hugging Face Space
4
+ سرور API با رابط کاربری وب و WebSocket
5
+ """
6
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse, HTMLResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ import json
13
+ import asyncio
14
+ from typing import List, Dict, Any, Set
15
+ import logging
16
+
17
+ # Setup logging
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Load resources
22
+ def load_resources():
23
+ """بارگذاری منابع از فایل JSON"""
24
+ resources_file = Path("api-resources/crypto_resources_unified_2025-11-11.json")
25
+
26
+ if not resources_file.exists():
27
+ logger.warning(f"Resources file not found: {resources_file}")
28
+ return {}
29
+
30
+ try:
31
+ with open(resources_file, 'r', encoding='utf-8') as f:
32
+ data = json.load(f)
33
+ logger.info(f"✅ Loaded resources from {resources_file}")
34
+ return data.get('registry', {})
35
+ except Exception as e:
36
+ logger.error(f"Error loading resources: {e}")
37
+ return {}
38
+
39
+ # Create FastAPI app
40
+ app = FastAPI(
41
+ title="Crypto Resources API",
42
+ description="API جامع برای دسترسی به منابع داده کریپتوکارنسی",
43
+ version="2.0.0",
44
+ docs_url="/docs",
45
+ redoc_url="/redoc"
46
+ )
47
+
48
+ # CORS middleware
49
+ app.add_middleware(
50
+ CORSMiddleware,
51
+ allow_origins=["*"],
52
+ allow_credentials=True,
53
+ allow_methods=["*"],
54
+ allow_headers=["*"],
55
+ )
56
+
57
+ # Load resources
58
+ RESOURCES = load_resources()
59
+
60
+ # WebSocket connection manager
61
+ class ConnectionManager:
62
+ def __init__(self):
63
+ self.active_connections: Set[WebSocket] = set()
64
+
65
+ async def connect(self, websocket: WebSocket):
66
+ await websocket.accept()
67
+ self.active_connections.add(websocket)
68
+ logger.info(f"WebSocket connected. Total: {len(self.active_connections)}")
69
+
70
+ def disconnect(self, websocket: WebSocket):
71
+ self.active_connections.discard(websocket)
72
+ logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}")
73
+
74
+ async def broadcast(self, message: dict):
75
+ """ارسال پیام به همه کلاینت‌ها"""
76
+ disconnected = set()
77
+ for connection in self.active_connections:
78
+ try:
79
+ await connection.send_json(message)
80
+ except Exception as e:
81
+ logger.error(f"Error sending to client: {e}")
82
+ disconnected.add(connection)
83
+
84
+ # حذف اتصالات قطع شده
85
+ for conn in disconnected:
86
+ self.active_connections.discard(conn)
87
+
88
+ manager = ConnectionManager()
89
+
90
+ # Background task for broadcasting stats
91
+ async def broadcast_stats():
92
+ """ارسال دوره‌ای آمار به کلاینت‌ها"""
93
+ while True:
94
+ try:
95
+ if manager.active_connections:
96
+ stats = get_stats_data()
97
+ await manager.broadcast({
98
+ "type": "stats_update",
99
+ "data": stats,
100
+ "timestamp": datetime.now().isoformat()
101
+ })
102
+ await asyncio.sleep(10) # هر 10 ثانیه
103
+ except Exception as e:
104
+ logger.error(f"Error in broadcast_stats: {e}")
105
+ await asyncio.sleep(5)
106
+
107
+ # Startup event
108
+ @app.on_event("startup")
109
+ async def startup_event():
110
+ """راه‌اندازی سرویس‌های پس‌زمینه"""
111
+ logger.info("🚀 Starting Crypto Resources API...")
112
+ logger.info(f"📦 Loaded {len([k for k,v in RESOURCES.items() if isinstance(v, list)])} categories")
113
+
114
+ # شروع broadcast task
115
+ asyncio.create_task(broadcast_stats())
116
+ logger.info("✅ Background tasks started")
117
+
118
+ # Helper functions
119
+ def get_stats_data():
120
+ """دریافت آمار کلی"""
121
+ categories_count = {}
122
+ total_resources = 0
123
+
124
+ for key, value in RESOURCES.items():
125
+ if isinstance(value, list):
126
+ count = len(value)
127
+ categories_count[key] = count
128
+ total_resources += count
129
+
130
+ return {
131
+ "total_resources": total_resources,
132
+ "total_categories": len(categories_count),
133
+ "categories": categories_count
134
+ }
135
+
136
+ # HTML UI
137
+ HTML_TEMPLATE = """
138
+ <!DOCTYPE html>
139
+ <html lang="fa" dir="rtl">
140
+ <head>
141
+ <meta charset="UTF-8">
142
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
+ <title>Crypto Resources API</title>
144
+ <style>
145
+ * {
146
+ margin: 0;
147
+ padding: 0;
148
+ box-sizing: border-box;
149
+ }
150
+
151
+ body {
152
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
153
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
154
+ min-height: 100vh;
155
+ padding: 20px;
156
+ color: #333;
157
+ }
158
+
159
+ .container {
160
+ max-width: 1200px;
161
+ margin: 0 auto;
162
+ }
163
+
164
+ .header {
165
+ background: white;
166
+ border-radius: 15px;
167
+ padding: 30px;
168
+ margin-bottom: 20px;
169
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
170
+ }
171
+
172
+ .header h1 {
173
+ color: #667eea;
174
+ margin-bottom: 10px;
175
+ font-size: 2.5em;
176
+ }
177
+
178
+ .header p {
179
+ color: #666;
180
+ font-size: 1.1em;
181
+ }
182
+
183
+ .status-badge {
184
+ display: inline-block;
185
+ padding: 5px 15px;
186
+ border-radius: 20px;
187
+ font-size: 0.9em;
188
+ font-weight: bold;
189
+ margin-top: 10px;
190
+ }
191
+
192
+ .status-online {
193
+ background: #4CAF50;
194
+ color: white;
195
+ }
196
+
197
+ .status-offline {
198
+ background: #f44336;
199
+ color: white;
200
+ }
201
+
202
+ .stats-grid {
203
+ display: grid;
204
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
205
+ gap: 20px;
206
+ margin-bottom: 20px;
207
+ }
208
+
209
+ .stat-card {
210
+ background: white;
211
+ border-radius: 15px;
212
+ padding: 25px;
213
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
214
+ transition: transform 0.3s;
215
+ }
216
+
217
+ .stat-card:hover {
218
+ transform: translateY(-5px);
219
+ box-shadow: 0 10px 25px rgba(0,0,0,0.2);
220
+ }
221
+
222
+ .stat-number {
223
+ font-size: 2.5em;
224
+ font-weight: bold;
225
+ color: #667eea;
226
+ margin: 10px 0;
227
+ }
228
+
229
+ .stat-label {
230
+ color: #666;
231
+ font-size: 1.1em;
232
+ }
233
+
234
+ .categories-section {
235
+ background: white;
236
+ border-radius: 15px;
237
+ padding: 30px;
238
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
239
+ margin-bottom: 20px;
240
+ }
241
+
242
+ .categories-section h2 {
243
+ color: #667eea;
244
+ margin-bottom: 20px;
245
+ font-size: 1.8em;
246
+ }
247
+
248
+ .category-list {
249
+ display: grid;
250
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
251
+ gap: 15px;
252
+ }
253
+
254
+ .category-item {
255
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
256
+ color: white;
257
+ padding: 20px;
258
+ border-radius: 10px;
259
+ cursor: pointer;
260
+ transition: all 0.3s;
261
+ }
262
+
263
+ .category-item:hover {
264
+ transform: scale(1.05);
265
+ box-shadow: 0 5px 20px rgba(0,0,0,0.3);
266
+ }
267
+
268
+ .category-name {
269
+ font-size: 1.2em;
270
+ font-weight: bold;
271
+ margin-bottom: 5px;
272
+ }
273
+
274
+ .category-count {
275
+ font-size: 0.9em;
276
+ opacity: 0.9;
277
+ }
278
+
279
+ .api-endpoints {
280
+ background: white;
281
+ border-radius: 15px;
282
+ padding: 30px;
283
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
284
+ }
285
+
286
+ .api-endpoints h2 {
287
+ color: #667eea;
288
+ margin-bottom: 20px;
289
+ }
290
+
291
+ .endpoint-item {
292
+ background: #f5f5f5;
293
+ padding: 15px;
294
+ border-radius: 8px;
295
+ margin-bottom: 10px;
296
+ border-left: 4px solid #667eea;
297
+ }
298
+
299
+ .endpoint-method {
300
+ display: inline-block;
301
+ background: #667eea;
302
+ color: white;
303
+ padding: 3px 10px;
304
+ border-radius: 5px;
305
+ font-size: 0.85em;
306
+ font-weight: bold;
307
+ margin-left: 10px;
308
+ }
309
+
310
+ .endpoint-path {
311
+ font-family: monospace;
312
+ color: #333;
313
+ font-weight: bold;
314
+ }
315
+
316
+ .websocket-status {
317
+ background: white;
318
+ border-radius: 15px;
319
+ padding: 20px;
320
+ margin-top: 20px;
321
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
322
+ }
323
+
324
+ .websocket-status h3 {
325
+ color: #667eea;
326
+ margin-bottom: 10px;
327
+ }
328
+
329
+ .ws-messages {
330
+ background: #f9f9f9;
331
+ border-radius: 8px;
332
+ padding: 15px;
333
+ max-height: 200px;
334
+ overflow-y: auto;
335
+ font-family: monospace;
336
+ font-size: 0.9em;
337
+ }
338
+
339
+ .ws-message {
340
+ padding: 5px 0;
341
+ border-bottom: 1px solid #eee;
342
+ }
343
+
344
+ .footer {
345
+ text-align: center;
346
+ color: white;
347
+ margin-top: 30px;
348
+ padding: 20px;
349
+ }
350
+
351
+ @keyframes pulse {
352
+ 0%, 100% { opacity: 1; }
353
+ 50% { opacity: 0.5; }
354
+ }
355
+
356
+ .loading {
357
+ animation: pulse 1.5s infinite;
358
+ }
359
+ </style>
360
+ </head>
361
+ <body>
362
+ <div class="container">
363
+ <div class="header">
364
+ <h1>🚀 Crypto Resources API</h1>
365
+ <p>API جامع برای دسترسی به منابع داده کریپتوکارنسی</p>
366
+ <span id="statusBadge" class="status-badge status-offline">در حال اتصال...</span>
367
+ </div>
368
+
369
+ <div class="stats-grid">
370
+ <div class="stat-card">
371
+ <div class="stat-label">مجموع منابع</div>
372
+ <div class="stat-number" id="totalResources">0</div>
373
+ </div>
374
+ <div class="stat-card">
375
+ <div class="stat-label">دسته‌بندی‌ها</div>
376
+ <div class="stat-number" id="totalCategories">0</div>
377
+ </div>
378
+ <div class="stat-card">
379
+ <div class="stat-label">وضعیت سرور</div>
380
+ <div class="stat-number" id="serverStatus">⏳</div>
381
+ </div>
382
+ </div>
383
+
384
+ <div class="categories-section">
385
+ <h2>📂 دسته‌بندی منابع</h2>
386
+ <div class="category-list" id="categoryList">
387
+ <div class="loading">در حال بارگذاری...</div>
388
+ </div>
389
+ </div>
390
+
391
+ <div class="api-endpoints">
392
+ <h2>📡 API Endpoints</h2>
393
+ <div class="endpoint-item">
394
+ <span class="endpoint-method">GET</span>
395
+ <span class="endpoint-path">/health</span>
396
+ <span> - Health check</span>
397
+ </div>
398
+ <div class="endpoint-item">
399
+ <span class="endpoint-method">GET</span>
400
+ <span class="endpoint-path">/api/resources/stats</span>
401
+ <span> - آمار کلی منابع</span>
402
+ </div>
403
+ <div class="endpoint-item">
404
+ <span class="endpoint-method">GET</span>
405
+ <span class="endpoint-path">/api/resources/list</span>
406
+ <span> - لیست تمام منابع</span>
407
+ </div>
408
+ <div class="endpoint-item">
409
+ <span class="endpoint-method">GET</span>
410
+ <span class="endpoint-path">/api/categories</span>
411
+ <span> - لیست دسته‌بندی‌ها</span>
412
+ </div>
413
+ <div class="endpoint-item">
414
+ <span class="endpoint-method">GET</span>
415
+ <span class="endpoint-path">/api/resources/category/{category}</span>
416
+ <span> - منابع یک دسته خاص</span>
417
+ </div>
418
+ <div class="endpoint-item">
419
+ <span class="endpoint-method">WS</span>
420
+ <span class="endpoint-path">/ws</span>
421
+ <span> - WebSocket برای بروزرسانی لحظه‌ای</span>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="websocket-status">
426
+ <h3>🔌 WebSocket Status: <span id="wsStatus">Disconnected</span></h3>
427
+ <div class="ws-messages" id="wsMessages">
428
+ <div class="ws-message">در انتظار اتصال...</div>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="footer">
433
+ <p>💜 ساخته شده با عشق برای جامعه کریپتو</p>
434
+ <p>📚 مستندات کامل: <a href="/docs" style="color: white; text-decoration: underline;">/docs</a></p>
435
+ </div>
436
+ </div>
437
+
438
+ <script>
439
+ // WebSocket connection
440
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
441
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
442
+ let ws = null;
443
+ let reconnectInterval = null;
444
+
445
+ function connectWebSocket() {
446
+ try {
447
+ ws = new WebSocket(wsUrl);
448
+
449
+ ws.onopen = () => {
450
+ console.log('✅ WebSocket connected');
451
+ document.getElementById('wsStatus').textContent = 'Connected ✅';
452
+ document.getElementById('statusBadge').className = 'status-badge status-online';
453
+ document.getElementById('statusBadge').textContent = 'آنلاین ✅';
454
+ addWsMessage('اتصال WebSocket برقرار شد ✅');
455
+
456
+ if (reconnectInterval) {
457
+ clearInterval(reconnectInterval);
458
+ reconnectInterval = null;
459
+ }
460
+ };
461
+
462
+ ws.onmessage = (event) => {
463
+ try {
464
+ const data = JSON.parse(event.data);
465
+ console.log('📨 Received:', data);
466
+
467
+ if (data.type === 'stats_update') {
468
+ updateStats(data.data);
469
+ addWsMessage(`بروزرسانی آمار: ${data.data.total_resources} منبع`);
470
+ }
471
+ } catch (e) {
472
+ console.error('Error parsing message:', e);
473
+ }
474
+ };
475
+
476
+ ws.onerror = (error) => {
477
+ console.error('❌ WebSocket error:', error);
478
+ document.getElementById('wsStatus').textContent = 'Error ❌';
479
+ addWsMessage('خطا در اتصال WebSocket ❌');
480
+ };
481
+
482
+ ws.onclose = () => {
483
+ console.log('🔌 WebSocket disconnected');
484
+ document.getElementById('wsStatus').textContent = 'Disconnected';
485
+ document.getElementById('statusBadge').className = 'status-badge status-offline';
486
+ document.getElementById('statusBadge').textContent = 'آفلاین';
487
+ addWsMessage('اتصال WebSocket قطع شد. در حال تلاش مجدد...');
488
+
489
+ // تلاش مجدد برای اتصال
490
+ if (!reconnectInterval) {
491
+ reconnectInterval = setInterval(() => {
492
+ console.log('🔄 Reconnecting...');
493
+ connectWebSocket();
494
+ }, 5000);
495
+ }
496
+ };
497
+ } catch (e) {
498
+ console.error('Error creating WebSocket:', e);
499
+ }
500
+ }
501
+
502
+ function addWsMessage(message) {
503
+ const container = document.getElementById('wsMessages');
504
+ const msgDiv = document.createElement('div');
505
+ msgDiv.className = 'ws-message';
506
+ msgDiv.textContent = `[${new Date().toLocaleTimeString('fa-IR')}] ${message}`;
507
+ container.appendChild(msgDiv);
508
+ container.scrollTop = container.scrollHeight;
509
+
510
+ // نگه داشتن فقط 10 پیام آخر
511
+ while (container.children.length > 10) {
512
+ container.removeChild(container.firstChild);
513
+ }
514
+ }
515
+
516
+ function updateStats(stats) {
517
+ document.getElementById('totalResources').textContent = stats.total_resources;
518
+ document.getElementById('totalCategories').textContent = stats.total_categories;
519
+ document.getElementById('serverStatus').textContent = '✅';
520
+
521
+ // بروزرسانی لیست دسته‌ها
522
+ const categoryList = document.getElementById('categoryList');
523
+ categoryList.innerHTML = '';
524
+
525
+ for (const [name, count] of Object.entries(stats.categories)) {
526
+ const item = document.createElement('div');
527
+ item.className = 'category-item';
528
+ item.innerHTML = `
529
+ <div class="category-name">${name}</div>
530
+ <div class="category-count">${count} منبع</div>
531
+ `;
532
+ item.onclick = () => {
533
+ window.open(`/api/resources/category/${name}`, '_blank');
534
+ };
535
+ categoryList.appendChild(item);
536
+ }
537
+ }
538
+
539
+ // بارگذاری اولیه آمار
540
+ async function loadInitialStats() {
541
+ try {
542
+ const response = await fetch('/api/resources/stats');
543
+ const stats = await response.json();
544
+ updateStats(stats);
545
+ } catch (e) {
546
+ console.error('Error loading initial stats:', e);
547
+ }
548
+ }
549
+
550
+ // شروع اتصال
551
+ connectWebSocket();
552
+ loadInitialStats();
553
+ </script>
554
+ </body>
555
+ </html>
556
+ """
557
+
558
+ # Routes
559
+ @app.get("/", response_class=HTMLResponse)
560
+ async def root():
561
+ """صفحه اصلی با UI"""
562
+ return HTMLResponse(content=HTML_TEMPLATE)
563
+
564
+ @app.get("/health")
565
+ async def health():
566
+ """Health check"""
567
+ return {
568
+ "status": "healthy",
569
+ "timestamp": datetime.now().isoformat(),
570
+ "resources_loaded": len(RESOURCES) > 0,
571
+ "total_categories": len([k for k, v in RESOURCES.items() if isinstance(v, list)]),
572
+ "websocket_connections": len(manager.active_connections)
573
+ }
574
+
575
+ @app.get("/api/resources/stats")
576
+ async def resources_stats():
577
+ """آمار منابع"""
578
+ stats = get_stats_data()
579
+ metadata = RESOURCES.get('metadata', {})
580
+
581
+ return {
582
+ **stats,
583
+ "metadata": metadata,
584
+ "timestamp": datetime.now().isoformat()
585
+ }
586
+
587
+ @app.get("/api/resources/list")
588
+ async def resources_list():
589
+ """لیست همه منابع"""
590
+ all_resources = []
591
+
592
+ for category, resources in RESOURCES.items():
593
+ if isinstance(resources, list):
594
+ for resource in resources:
595
+ if isinstance(resource, dict):
596
+ all_resources.append({
597
+ "category": category,
598
+ "id": resource.get('id', 'unknown'),
599
+ "name": resource.get('name', 'Unknown'),
600
+ "base_url": resource.get('base_url', ''),
601
+ "auth_type": resource.get('auth', {}).get('type', 'none')
602
+ })
603
+
604
+ return {
605
+ "total": len(all_resources),
606
+ "resources": all_resources[:100], # اولین 100 مورد
607
+ "note": f"Showing first 100 of {len(all_resources)} resources",
608
+ "timestamp": datetime.now().isoformat()
609
+ }
610
+
611
+ @app.get("/api/resources/category/{category}")
612
+ async def resources_by_category(category: str):
613
+ """منابع یک دسته خاص"""
614
+ if category not in RESOURCES:
615
+ return JSONResponse(
616
+ status_code=404,
617
+ content={"error": f"Category '{category}' not found"}
618
+ )
619
+
620
+ resources = RESOURCES.get(category, [])
621
+
622
+ if not isinstance(resources, list):
623
+ return JSONResponse(
624
+ status_code=400,
625
+ content={"error": f"Category '{category}' is not a resource list"}
626
+ )
627
+
628
+ return {
629
+ "category": category,
630
+ "total": len(resources),
631
+ "resources": resources,
632
+ "timestamp": datetime.now().isoformat()
633
+ }
634
+
635
+ @app.get("/api/categories")
636
+ async def list_categories():
637
+ """لیست دسته‌بندی‌ها"""
638
+ categories = []
639
+
640
+ for key, value in RESOURCES.items():
641
+ if isinstance(value, list):
642
+ categories.append({
643
+ "name": key,
644
+ "count": len(value),
645
+ "endpoint": f"/api/resources/category/{key}"
646
+ })
647
+
648
+ return {
649
+ "total": len(categories),
650
+ "categories": categories,
651
+ "timestamp": datetime.now().isoformat()
652
+ }
653
+
654
+ @app.websocket("/ws")
655
+ async def websocket_endpoint(websocket: WebSocket):
656
+ """WebSocket endpoint برای بروزرسانی لحظه‌ای"""
657
+ await manager.connect(websocket)
658
+
659
+ try:
660
+ # ارسال آمار اولیه
661
+ stats = get_stats_data()
662
+ await websocket.send_json({
663
+ "type": "initial_stats",
664
+ "data": stats,
665
+ "timestamp": datetime.now().isoformat()
666
+ })
667
+
668
+ # نگه داشتن اتصال
669
+ while True:
670
+ try:
671
+ # دریافت پیام از کلاینت (اگر بفرستد)
672
+ data = await websocket.receive_text()
673
+ logger.info(f"Received from client: {data}")
674
+
675
+ # پاسخ به کلاینت
676
+ await websocket.send_json({
677
+ "type": "pong",
678
+ "message": "Server is alive",
679
+ "timestamp": datetime.now().isoformat()
680
+ })
681
+ except Exception as e:
682
+ logger.error(f"Error in websocket loop: {e}")
683
+ break
684
+
685
+ except WebSocketDisconnect:
686
+ manager.disconnect(websocket)
687
+ logger.info("Client disconnected normally")
688
+ except Exception as e:
689
+ logger.error(f"WebSocket error: {e}")
690
+ manager.disconnect(websocket)
691
+
692
+ # Run with uvicorn
693
+ if __name__ == "__main__":
694
+ import uvicorn
695
+
696
+ print("=" * 80)
697
+ print("🚀 راه‌اندازی Crypto Resources API Server")
698
+ print("=" * 80)
699
+ print(f"\nبارگذاری منابع...")
700
+ print(f"✅ {len([k for k,v in RESOURCES.items() if isinstance(v, list)])} دسته بارگذاری شد")
701
+ print(f"\n🌐 Server: http://0.0.0.0:7860")
702
+ print(f"📚 Docs: http://0.0.0.0:7860/docs")
703
+ print(f"🔌 WebSocket: ws://0.0.0.0:7860/ws")
704
+ print(f"\nبرای توقف سرور: Ctrl+C")
705
+ print("=" * 80 + "\n")
706
+
707
+ uvicorn.run(
708
+ app,
709
+ host="0.0.0.0",
710
+ port=7860,
711
+ log_level="info",
712
+ access_log=True
713
+ )