Really-amin commited on
Commit
3780f95
·
verified ·
1 Parent(s): 8bdd961

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +644 -1771
app.py CHANGED
@@ -1,1840 +1,713 @@
 
1
  """
2
- Crypto Intelligence Hub - Hugging Face Space Backend
3
- Optimized for HF resource limits with full functionality
4
  """
5
-
6
- import os
7
- import sys
8
- import logging
9
  from datetime import datetime
10
- from functools import lru_cache
11
- import time
 
 
 
12
 
13
- # Setup basic logging first
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
17
- )
18
  logger = logging.getLogger(__name__)
19
 
20
- # Safe imports with fallbacks
21
- try:
22
- from flask import Flask, jsonify, request, send_from_directory, send_file
23
- from flask_cors import CORS
24
- import requests
25
- from pathlib import Path
26
- except ImportError as e:
27
- logger.error(f"❌ Critical import failed: {e}")
28
- logger.error("Please install required packages: pip install flask flask-cors requests")
29
- sys.exit(1)
30
-
31
- # Initialize Flask app
32
- try:
33
- app = Flask(__name__, static_folder='static')
34
- CORS(app)
35
- logger.info(" Flask app initialized")
36
- except Exception as e:
37
- logger.error(f"❌ Flask app initialization failed: {e}")
38
- sys.exit(1)
39
-
40
- # Add Permissions-Policy header with only recognized features (no warnings)
41
- @app.after_request
42
- def add_permissions_policy(response):
43
- """Add Permissions-Policy header with only recognized features to avoid browser warnings"""
44
- # Only include well-recognized features that browsers support
45
- # Removed: ambient-light-sensor, battery, vr, document-domain, etc. (these cause warnings)
46
- response.headers['Permissions-Policy'] = (
47
- 'accelerometer=(), autoplay=(), camera=(), '
48
- 'display-capture=(), encrypted-media=(), '
49
- 'fullscreen=(), geolocation=(), gyroscope=(), '
50
- 'magnetometer=(), microphone=(), midi=(), '
51
- 'payment=(), picture-in-picture=(), '
52
- 'sync-xhr=(), usb=(), web-share=()'
53
- )
54
- return response
55
-
56
- # Hugging Face Inference API (free tier)
57
- HF_API_TOKEN = os.getenv('HF_API_TOKEN', '')
58
- HF_API_URL = "https://api-inference.huggingface.co/models"
59
-
60
- # Cache for API responses (memory-efficient)
61
- cache_ttl = {}
62
-
63
- def cached_request(key: str, ttl: int = 60):
64
- """Simple cache decorator for API calls"""
65
- def decorator(func):
66
- def wrapper(*args, **kwargs):
67
- now = time.time()
68
- if key in cache_ttl and now - cache_ttl[key]['time'] < ttl:
69
- return cache_ttl[key]['data']
70
- result = func(*args, **kwargs)
71
- cache_ttl[key] = {'data': result, 'time': now}
72
- return result
73
- return wrapper
74
- return decorator
75
-
76
- @app.route('/')
77
- def index():
78
- """Serve loading page (static/index.html) which redirects to dashboard"""
79
- # Prioritize static/index.html (loading page)
80
- static_index = Path(__file__).parent / 'static' / 'index.html'
81
- if static_index.exists():
82
- return send_file(str(static_index))
83
- # Fallback to root index.html if static doesn't exist
84
- root_index = Path(__file__).parent / 'index.html'
85
- if root_index.exists():
86
- return send_file(str(root_index))
87
- return send_from_directory('static', 'index.html')
88
 
89
- @app.route('/dashboard')
90
- def dashboard():
91
- """Serve the main dashboard"""
92
- dashboard_path = Path(__file__).parent / 'static' / 'pages' / 'dashboard' / 'index.html'
93
- if dashboard_path.exists():
94
- return send_file(str(dashboard_path))
95
- # Fallback to root index.html
96
- root_index = Path(__file__).parent / 'index.html'
97
- if root_index.exists():
98
- return send_file(str(root_index))
99
- return send_from_directory('static', 'index.html')
100
 
101
- @app.route('/favicon.ico')
102
- def favicon():
103
- """Serve favicon"""
104
- return send_from_directory('static/assets/icons', 'favicon.svg', mimetype='image/svg+xml')
105
 
106
- @app.route('/static/<path:path>')
107
- def serve_static(path):
108
- """Serve static files with no-cache for JS files"""
109
- from flask import make_response
110
- response = make_response(send_from_directory('static', path))
111
- # Add no-cache headers for JS files to prevent stale module issues
112
- if path.endswith('.js'):
113
- response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
114
- response.headers["Pragma"] = "no-cache"
115
- response.headers["Expires"] = "0"
116
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- @app.route('/api/health')
119
- def health():
120
- """Health check endpoint"""
121
- return jsonify({
122
- 'status': 'online',
123
- 'timestamp': datetime.utcnow().isoformat(),
124
- 'environment': 'huggingface',
125
- 'api_version': '1.0'
126
- })
127
 
128
- @app.route('/api/status')
129
- def status():
130
- """System status endpoint (alias for health + stats)"""
131
- market_data = get_market_data()
132
- return jsonify({
133
- 'status': 'online',
134
- 'timestamp': datetime.utcnow().isoformat(),
135
- 'environment': 'huggingface',
136
- 'api_version': '1.0',
137
- 'total_resources': 74,
138
- 'free_resources': 45,
139
- 'premium_resources': 29,
140
- 'models_loaded': 2,
141
- 'total_coins': len(market_data),
142
- 'cache_hit_rate': 75.5
143
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
- @cached_request('market_data', ttl=30)
146
- def get_market_data():
147
- """Fetch real market data from CoinGecko (free API)"""
148
- try:
149
- url = 'https://api.coingecko.com/api/v3/coins/markets'
150
- params = {
151
- 'vs_currency': 'usd',
152
- 'order': 'market_cap_desc',
153
- 'per_page': 50,
154
- 'page': 1,
155
- 'sparkline': False
 
 
156
  }
157
- response = requests.get(url, params=params, timeout=5)
158
- return response.json()
159
- except Exception as e:
160
- print(f"Market data error: {e}")
161
- return []
162
-
163
- @app.route('/api/market/top')
164
- def market_top():
165
- """Get top cryptocurrencies"""
166
- data = get_market_data()
167
- return jsonify({'data': data[:20]})
168
-
169
- @app.route('/api/coins/top')
170
- def coins_top():
171
- """Get top cryptocurrencies (alias for /api/market/top)"""
172
- limit = request.args.get('limit', 50, type=int)
173
- data = get_market_data()
174
- return jsonify({'data': data[:limit], 'coins': data[:limit]})
175
-
176
- @app.route('/api/market/trending')
177
- def market_trending():
178
- """Get trending coins"""
179
- try:
180
- response = requests.get(
181
- 'https://api.coingecko.com/api/v3/search/trending',
182
- timeout=5
183
- )
184
- return jsonify(response.json())
185
- except:
186
- return jsonify({'coins': []})
187
-
188
- @app.route('/api/sentiment/global')
189
- def sentiment_global():
190
- """Global market sentiment with Fear & Greed Index"""
191
- try:
192
- # Fear & Greed Index
193
- fg_response = requests.get(
194
- 'https://api.alternative.me/fng/?limit=1',
195
- timeout=5
196
- )
197
- fg_data = fg_response.json()
198
- fg_value = int(fg_data['data'][0]['value']) if fg_data.get('data') else 50
199
 
200
- # Calculate sentiment based on Fear & Greed
201
- if fg_value < 25:
202
- sentiment = 'extreme_fear'
203
- score = 0.2
204
- elif fg_value < 45:
205
- sentiment = 'fear'
206
- score = 0.35
207
- elif fg_value < 55:
208
- sentiment = 'neutral'
209
- score = 0.5
210
- elif fg_value < 75:
211
- sentiment = 'greed'
212
- score = 0.65
213
- else:
214
- sentiment = 'extreme_greed'
215
- score = 0.8
216
 
217
- # Market trend from top coins
218
- market_data = get_market_data()[:10]
219
- positive_coins = sum(1 for c in market_data if c.get('price_change_percentage_24h', 0) > 0)
220
- market_trend = 'bullish' if positive_coins >= 6 else 'bearish' if positive_coins <= 3 else 'neutral'
221
 
222
- return jsonify({
223
- 'sentiment': sentiment,
224
- 'score': score,
225
- 'fear_greed_index': fg_value,
226
- 'market_trend': market_trend,
227
- 'positive_ratio': positive_coins / 10,
228
- 'timestamp': datetime.utcnow().isoformat()
229
- })
230
- except Exception as e:
231
- print(f"Sentiment error: {e}")
232
- return jsonify({
233
- 'sentiment': 'neutral',
234
- 'score': 0.5,
235
- 'fear_greed_index': 50,
236
- 'market_trend': 'neutral'
237
- })
238
-
239
- @app.route('/api/sentiment/asset/<symbol>')
240
- def sentiment_asset(symbol):
241
- """Asset-specific sentiment analysis"""
242
- symbol = symbol.lower()
243
- market_data = get_market_data()
244
-
245
- coin = next((c for c in market_data if c['symbol'].lower() == symbol), None)
246
-
247
- if not coin:
248
- return jsonify({'error': 'Asset not found'}), 404
249
-
250
- price_change = coin.get('price_change_percentage_24h', 0)
251
-
252
- if price_change > 5:
253
- sentiment = 'very_bullish'
254
- score = 0.8
255
- elif price_change > 2:
256
- sentiment = 'bullish'
257
- score = 0.65
258
- elif price_change > -2:
259
- sentiment = 'neutral'
260
- score = 0.5
261
- elif price_change > -5:
262
- sentiment = 'bearish'
263
- score = 0.35
264
- else:
265
- sentiment = 'very_bearish'
266
- score = 0.2
267
-
268
- return jsonify({
269
- 'symbol': coin['symbol'].upper(),
270
- 'name': coin['name'],
271
- 'sentiment': sentiment,
272
- 'score': score,
273
- 'price_change_24h': price_change,
274
- 'market_cap_rank': coin.get('market_cap_rank'),
275
- 'current_price': coin.get('current_price')
276
- })
277
-
278
- @app.route('/api/sentiment/analyze', methods=['POST'])
279
- def sentiment_analyze_text():
280
- """Analyze custom text sentiment using HF model"""
281
- data = request.json
282
- text = data.get('text', '')
283
-
284
- if not text:
285
- return jsonify({'error': 'No text provided'}), 400
286
-
287
- try:
288
- # Use Hugging Face Inference API
289
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"} if HF_API_TOKEN else {}
290
 
291
- # Try multiple HF models with fallback
292
- models = [
293
- "cardiffnlp/twitter-roberta-base-sentiment-latest",
294
- "nlptown/bert-base-multilingual-uncased-sentiment",
295
- "distilbert-base-uncased-finetuned-sst-2-english"
296
- ]
297
 
298
- response = None
299
- model_used = None
300
- for model in models:
301
- try:
302
- test_response = requests.post(
303
- f"{HF_API_URL}/{model}",
304
- headers=headers,
305
- json={"inputs": text},
306
- timeout=10
307
- )
308
- if test_response.status_code == 200:
309
- response = test_response
310
- model_used = model
311
- break
312
- elif test_response.status_code == 503:
313
- # Model is loading, skip
314
- continue
315
- elif test_response.status_code == 410:
316
- # Model gone, skip
317
- continue
318
- except Exception as e:
319
- print(f"Model {model} error: {e}")
320
- continue
321
 
322
- if response and response.status_code == 200:
323
- result = response.json()
324
-
325
- # Parse HF response
326
- if isinstance(result, list) and len(result) > 0:
327
- labels = result[0]
328
- sentiment_map = {
329
- 'positive': 'bullish',
330
- 'negative': 'bearish',
331
- 'neutral': 'neutral'
332
- }
333
-
334
- top_label = max(labels, key=lambda x: x['score'])
335
- sentiment = sentiment_map.get(top_label['label'], 'neutral')
336
-
337
- return jsonify({
338
- 'sentiment': sentiment,
339
- 'score': top_label['score'],
340
- 'confidence': top_label['score'],
341
- 'details': {label['label']: label['score'] for label in labels},
342
- 'model': model_used or 'fallback'
343
- })
344
 
345
- # Fallback: simple keyword-based analysis
346
- text_lower = text.lower()
347
- positive_words = ['bullish', 'buy', 'moon', 'pump', 'up', 'gain', 'profit', 'good', 'great']
348
- negative_words = ['bearish', 'sell', 'dump', 'down', 'loss', 'crash', 'bad', 'fear']
349
 
350
- pos_count = sum(1 for word in positive_words if word in text_lower)
351
- neg_count = sum(1 for word in negative_words if word in text_lower)
 
 
352
 
353
- if pos_count > neg_count:
354
- sentiment = 'bullish'
355
- score = min(0.5 + (pos_count * 0.1), 0.9)
356
- elif neg_count > pos_count:
357
- sentiment = 'bearish'
358
- score = max(0.5 - (neg_count * 0.1), 0.1)
359
- else:
360
- sentiment = 'neutral'
361
- score = 0.5
362
 
363
- return jsonify({
364
- 'sentiment': sentiment,
365
- 'score': score,
366
- 'method': 'keyword_fallback'
367
- })
 
 
368
 
369
- except Exception as e:
370
- print(f"Sentiment analysis error: {e}")
371
- return jsonify({
372
- 'sentiment': 'neutral',
373
- 'score': 0.5,
374
- 'error': str(e)
375
- })
376
-
377
- @app.route('/api/models/status')
378
- def models_status():
379
- """AI Models status"""
380
- models = [
381
- {
382
- 'name': 'Sentiment Analysis',
383
- 'model': 'cardiffnlp/twitter-roberta-base-sentiment-latest',
384
- 'status': 'ready',
385
- 'provider': 'Hugging Face'
386
- },
387
- {
388
- 'name': 'Market Analysis',
389
- 'model': 'internal',
390
- 'status': 'ready',
391
- 'provider': 'CoinGecko'
392
  }
393
- ]
394
-
395
- return jsonify({
396
- 'models_loaded': len(models),
397
- 'models': models,
398
- 'total_models': len(models),
399
- 'active_models': len(models),
400
- 'status': 'ready'
401
- })
402
-
403
- @app.route('/api/models/list')
404
- def models_list():
405
- """AI Models list (alias for /api/models/status)"""
406
- return models_status()
407
-
408
- @app.route('/api/news/latest')
409
- def news_latest():
410
- """Get latest crypto news (alias for /api/news with limit)"""
411
- limit = int(request.args.get('limit', 6))
412
- return news() # Reuse existing news endpoint
413
-
414
- @app.route('/api/news')
415
- def news():
416
- """
417
- Crypto news feed with filtering support - REAL DATA ONLY
418
- Query params:
419
- - limit: Number of articles (default: 50, max: 200)
420
- - source: Filter by news source
421
- - sentiment: Filter by sentiment (positive/negative/neutral)
422
- """
423
- # Get query parameters
424
- limit = min(int(request.args.get('limit', 50)), 200)
425
- source_filter = request.args.get('source', '').strip()
426
- sentiment_filter = request.args.get('sentiment', '').strip()
427
-
428
- articles = []
429
-
430
- # Try multiple real news sources with fallback
431
- sources = [
432
- # Source 1: CryptoPanic
433
- {
434
- 'name': 'CryptoPanic',
435
- 'fetch': lambda: requests.get(
436
- 'https://cryptopanic.com/api/v1/posts/',
437
- params={'auth_token': 'free', 'public': 'true'},
438
- timeout=5
439
- )
440
- },
441
- # Source 2: CoinStats News
442
- {
443
- 'name': 'CoinStats',
444
- 'fetch': lambda: requests.get(
445
- 'https://api.coinstats.app/public/v1/news',
446
- timeout=5
447
- )
448
- },
449
- # Source 3: Cointelegraph RSS
450
- {
451
- 'name': 'Cointelegraph',
452
- 'fetch': lambda: requests.get(
453
- 'https://cointelegraph.com/rss',
454
- timeout=5
455
- )
456
- },
457
- # Source 4: CoinDesk RSS
458
- {
459
- 'name': 'CoinDesk',
460
- 'fetch': lambda: requests.get(
461
- 'https://www.coindesk.com/arc/outboundfeeds/rss/',
462
- timeout=5
463
- )
464
- },
465
- # Source 5: Decrypt RSS
466
- {
467
- 'name': 'Decrypt',
468
- 'fetch': lambda: requests.get(
469
- 'https://decrypt.co/feed',
470
- timeout=5
471
- )
472
  }
473
- ]
474
-
475
- # Try each source until we get data
476
- for source in sources:
477
- try:
478
- response = source['fetch']()
479
-
480
- if response.status_code == 200:
481
- if source['name'] == 'CryptoPanic':
482
- data = response.json()
483
- raw_articles = data.get('results', [])
484
- for item in raw_articles[:100]:
485
- article = {
486
- 'id': item.get('id'),
487
- 'title': item.get('title', ''),
488
- 'content': item.get('title', ''),
489
- 'source': item.get('source', {}).get('title', 'Unknown') if isinstance(item.get('source'), dict) else str(item.get('source', 'Unknown')),
490
- 'url': item.get('url', '#'),
491
- 'published_at': item.get('published_at', datetime.utcnow().isoformat()),
492
- 'sentiment': _analyze_sentiment(item.get('title', ''))
493
- }
494
- articles.append(article)
495
-
496
- elif source['name'] == 'CoinStats':
497
- data = response.json()
498
- news_list = data.get('news', [])
499
- for item in news_list[:100]:
500
- article = {
501
- 'id': item.get('id'),
502
- 'title': item.get('title', ''),
503
- 'content': item.get('description', item.get('title', '')),
504
- 'source': item.get('source', 'CoinStats'),
505
- 'url': item.get('link', '#'),
506
- 'published_at': item.get('publishedAt', datetime.utcnow().isoformat()),
507
- 'sentiment': _analyze_sentiment(item.get('title', ''))
508
- }
509
- articles.append(article)
510
-
511
- elif source['name'] in ['Cointelegraph', 'CoinDesk', 'Decrypt']:
512
- # Parse RSS
513
- import xml.etree.ElementTree as ET
514
- root = ET.fromstring(response.content)
515
- for item in root.findall('.//item')[:100]:
516
- title = item.find('title')
517
- link = item.find('link')
518
- pub_date = item.find('pubDate')
519
- description = item.find('description')
520
-
521
- if title is not None and title.text:
522
- article = {
523
- 'id': hash(title.text),
524
- 'title': title.text,
525
- 'content': description.text if description is not None else title.text,
526
- 'source': source['name'],
527
- 'url': link.text if link is not None else '#',
528
- 'published_at': pub_date.text if pub_date is not None else datetime.utcnow().isoformat(),
529
- 'sentiment': _analyze_sentiment(title.text)
530
- }
531
- articles.append(article)
532
-
533
- # If we got articles, break (don't try other sources)
534
- if articles:
535
- break
536
- except Exception as e:
537
- print(f"News source {source['name']} error: {e}")
538
- continue
539
-
540
- # NO DEMO DATA - Return empty if all sources fail
541
- if not articles:
542
- return jsonify({
543
- 'articles': [],
544
- 'count': 0,
545
- 'error': 'All news sources unavailable',
546
- 'filters': {
547
- 'source': source_filter or None,
548
- 'sentiment': sentiment_filter or None,
549
- 'limit': limit
550
- }
551
- })
552
-
553
- # Apply filters
554
- filtered_articles = articles
555
-
556
- if source_filter:
557
- filtered_articles = [a for a in filtered_articles if a.get('source', '').lower() == source_filter.lower()]
558
-
559
- if sentiment_filter:
560
- filtered_articles = [a for a in filtered_articles if a.get('sentiment', '') == sentiment_filter.lower()]
561
-
562
- # Limit results
563
- filtered_articles = filtered_articles[:limit]
564
-
565
- return jsonify({
566
- 'articles': filtered_articles,
567
- 'count': len(filtered_articles),
568
- 'filters': {
569
- 'source': source_filter or None,
570
- 'sentiment': sentiment_filter or None,
571
- 'limit': limit
572
  }
573
- })
574
-
575
- def _analyze_sentiment(text):
576
- """Basic keyword-based sentiment analysis"""
577
- if not text:
578
- return 'neutral'
579
-
580
- text_lower = text.lower()
581
-
582
- positive_words = ['surge', 'bull', 'up', 'gain', 'high', 'rise', 'growth', 'success', 'milestone', 'breakthrough']
583
- negative_words = ['crash', 'bear', 'down', 'loss', 'low', 'fall', 'drop', 'decline', 'warning', 'risk']
584
-
585
- pos_count = sum(1 for word in positive_words if word in text_lower)
586
- neg_count = sum(1 for word in negative_words if word in text_lower)
587
-
588
- if pos_count > neg_count:
589
- return 'positive'
590
- elif neg_count > pos_count:
591
- return 'negative'
592
- return 'neutral'
593
-
594
- @app.route('/api/dashboard/stats')
595
- def dashboard_stats():
596
- """Dashboard statistics"""
597
- market_data = get_market_data()
598
-
599
- total_market_cap = sum(c.get('market_cap', 0) for c in market_data)
600
- avg_change = sum(c.get('price_change_percentage_24h', 0) for c in market_data) / len(market_data) if market_data else 0
601
-
602
- return jsonify({
603
- 'total_coins': len(market_data),
604
- 'total_market_cap': total_market_cap,
605
- 'avg_24h_change': avg_change,
606
- 'active_models': 2,
607
- 'api_calls_today': 0,
608
- 'cache_hit_rate': 75.5
609
- })
610
-
611
- @app.route('/api/resources/summary')
612
- def resources_summary():
613
- """API Resources summary"""
614
- return jsonify({
615
- 'total': 74,
616
- 'free': 45,
617
- 'premium': 29,
618
- 'categories': {
619
- 'explorer': 9,
620
- 'market': 15,
621
- 'news': 10,
622
- 'sentiment': 7,
623
- 'analytics': 17,
624
- 'defi': 8,
625
- 'nft': 8
626
- },
627
- 'by_category': [
628
- {'name': 'Analytics', 'count': 17},
629
- {'name': 'Market Data', 'count': 15},
630
- {'name': 'News', 'count': 10},
631
- {'name': 'Explorers', 'count': 9},
632
- {'name': 'DeFi', 'count': 8},
633
- {'name': 'NFT', 'count': 8},
634
- {'name': 'Sentiment', 'count': 7}
635
- ]
636
- })
637
-
638
- @app.route('/api/resources/stats')
639
- def resources_stats():
640
- """API Resources stats endpoint for dashboard"""
641
- import json
642
- from pathlib import Path
643
-
644
- all_apis = []
645
- categories_count = {}
646
-
647
- # Load providers from providers_config_extended.json
648
- providers_file = Path(__file__).parent / "providers_config_extended.json"
649
- logger.info(f"Looking for providers file at: {providers_file}")
650
- logger.info(f"File exists: {providers_file.exists()}")
651
-
652
- if providers_file.exists():
653
- try:
654
- with open(providers_file, 'r', encoding='utf-8') as f:
655
- providers_data = json.load(f)
656
- providers = providers_data.get("providers", {})
657
-
658
- for provider_id, provider_info in providers.items():
659
- category = provider_info.get("category", "other")
660
- category_key = category.lower().replace(' ', '_')
661
- if category_key not in categories_count:
662
- categories_count[category_key] = {'total': 0, 'active': 0}
663
- categories_count[category_key]['total'] += 1
664
- categories_count[category_key]['active'] += 1
665
-
666
- all_apis.append({
667
- 'id': provider_id,
668
- 'name': provider_info.get("name", provider_id),
669
- 'category': category,
670
- 'status': 'active'
671
- })
672
- except Exception as e:
673
- print(f"Error loading providers: {e}")
674
-
675
- # Load local routes
676
- resources_file = Path(__file__).parent / "api-resources" / "crypto_resources_unified_2025-11-11.json"
677
- if resources_file.exists():
678
- try:
679
- with open(resources_file, 'r', encoding='utf-8') as f:
680
- resources_data = json.load(f)
681
- local_routes = resources_data.get('registry', {}).get('local_backend_routes', [])
682
- all_apis.extend(local_routes)
683
- for route in local_routes:
684
- category = route.get("category", "local")
685
- category_key = category.lower().replace(' ', '_')
686
- if category_key not in categories_count:
687
- categories_count[category_key] = {'total': 0, 'active': 0}
688
- categories_count[category_key]['total'] += 1
689
- categories_count[category_key]['active'] += 1
690
- except Exception as e:
691
- print(f"Error loading local routes: {e}")
692
-
693
- # Map categories to expected format
694
- category_mapping = {
695
- 'market_data': 'market_data',
696
- 'market': 'market_data',
697
- 'news': 'news',
698
- 'sentiment': 'sentiment',
699
- 'analytics': 'analytics',
700
- 'explorer': 'block_explorers',
701
- 'block_explorers': 'block_explorers',
702
- 'rpc': 'rpc_nodes',
703
- 'rpc_nodes': 'rpc_nodes',
704
- 'ai': 'ai_ml',
705
- 'ai_ml': 'ai_ml',
706
- 'ml': 'ai_ml'
707
- }
708
-
709
- # Merge similar categories
710
- market_data_count = categories_count.get('market_data', {'total': 0, 'active': 0})
711
- if 'market' in categories_count:
712
- market_data_count['total'] += categories_count['market']['total']
713
- market_data_count['active'] += categories_count['market']['active']
714
-
715
- block_explorers_count = categories_count.get('block_explorers', {'total': 0, 'active': 0})
716
- if 'explorer' in categories_count:
717
- block_explorers_count['total'] += categories_count['explorer']['total']
718
- block_explorers_count['active'] += categories_count['explorer']['active']
719
-
720
- rpc_nodes_count = categories_count.get('rpc_nodes', {'total': 0, 'active': 0})
721
- if 'rpc' in categories_count:
722
- rpc_nodes_count['total'] += categories_count['rpc']['total']
723
- rpc_nodes_count['active'] += categories_count['rpc']['active']
724
-
725
- ai_ml_count = categories_count.get('ai_ml', {'total': 0, 'active': 0})
726
- if 'ai' in categories_count:
727
- ai_ml_count['total'] += categories_count['ai']['total']
728
- ai_ml_count['active'] += categories_count['ai']['active']
729
- if 'ml' in categories_count:
730
- ai_ml_count['total'] += categories_count['ml']['total']
731
- ai_ml_count['active'] += categories_count['ml']['active']
732
-
733
- formatted_categories = {
734
- 'market_data': market_data_count,
735
- 'news': categories_count.get('news', {'total': 0, 'active': 0}),
736
- 'sentiment': categories_count.get('sentiment', {'total': 0, 'active': 0}),
737
- 'analytics': categories_count.get('analytics', {'total': 0, 'active': 0}),
738
- 'block_explorers': block_explorers_count,
739
- 'rpc_nodes': rpc_nodes_count,
740
- 'ai_ml': ai_ml_count
741
- }
742
-
743
- total_endpoints = sum(len(api.get('endpoints', [])) if isinstance(api.get('endpoints'), list) else api.get('endpoints_count', 0) for api in all_apis)
744
-
745
- logger.info(f"Resources stats: {len(all_apis)} APIs, {len(categories_count)} categories")
746
- logger.info(f"Formatted categories: {formatted_categories}")
747
-
748
- return jsonify({
749
- 'success': True,
750
- 'data': {
751
- 'categories': formatted_categories,
752
- 'total_functional': len([a for a in all_apis if a.get('status') == 'active']),
753
- 'total_api_keys': len([a for a in all_apis if a.get('requires_key', False)]),
754
- 'total_endpoints': total_endpoints or len(all_apis) * 5,
755
- 'success_rate': 95.5,
756
- 'last_check': datetime.utcnow().isoformat()
757
  }
758
- })
759
-
760
- @app.route('/api/resources/apis')
761
- def resources_apis():
762
- """Get detailed list of all API resources - loads from providers config"""
763
- import json
764
- from pathlib import Path
765
- import traceback
766
-
767
- all_apis = []
768
- categories_set = set()
769
-
770
- try:
771
- # Load providers from providers_config_extended.json
772
- providers_file = Path(__file__).parent / "providers_config_extended.json"
773
- if providers_file.exists() and providers_file.is_file():
774
- try:
775
- with open(providers_file, 'r', encoding='utf-8') as f:
776
- providers_data = json.load(f)
777
- if providers_data and isinstance(providers_data, dict):
778
- providers = providers_data.get("providers", {})
779
- if isinstance(providers, dict):
780
- for provider_id, provider_info in providers.items():
781
- try:
782
- if not isinstance(provider_info, dict):
783
- logger.warning(f"Skipping invalid provider {provider_id}: not a dict")
784
- continue
785
-
786
- # Validate and extract data safely
787
- provider_id_str = str(provider_id) if provider_id else ""
788
- if not provider_id_str:
789
- logger.warning("Skipping provider with empty ID")
790
- continue
791
-
792
- endpoints = provider_info.get("endpoints", {})
793
- endpoints_count = len(endpoints) if isinstance(endpoints, dict) else 0
794
- category = str(provider_info.get("category", "other"))
795
- categories_set.add(category)
796
-
797
- api_item = {
798
- 'id': provider_id_str,
799
- 'name': str(provider_info.get("name", provider_id_str)),
800
- 'category': category,
801
- 'url': str(provider_info.get("base_url", "")),
802
- 'description': f"{provider_info.get('name', provider_id_str)} - {endpoints_count} endpoints",
803
- 'endpoints': endpoints_count,
804
- 'endpoints_count': endpoints_count,
805
- 'free': not bool(provider_info.get("requires_auth", False)),
806
- 'requires_key': bool(provider_info.get("requires_auth", False)),
807
- 'status': 'active'
808
- }
809
-
810
- # Validate API item before adding
811
- if api_item.get('id'):
812
- all_apis.append(api_item)
813
- else:
814
- logger.warning(f"Skipping provider {provider_id}: missing ID")
815
-
816
- except Exception as e:
817
- logger.error(f"Error processing provider {provider_id}: {e}", exc_info=True)
818
- continue
819
- else:
820
- logger.warning(f"Providers data is not a dict: {type(providers_data)}")
821
- except json.JSONDecodeError as e:
822
- logger.error(f"JSON decode error loading providers from {providers_file}: {e}", exc_info=True)
823
- except IOError as io_error:
824
- logger.error(f"IO error reading providers file {providers_file}: {io_error}", exc_info=True)
825
- except Exception as e:
826
- logger.error(f"Error loading providers from {providers_file}: {e}", exc_info=True)
827
- else:
828
- logger.info(f"Providers config file not found at {providers_file}")
829
 
830
- # Load local routes from unified resources
831
- resources_file = Path(__file__).parent / "api-resources" / "crypto_resources_unified_2025-11-11.json"
832
- if resources_file.exists() and resources_file.is_file():
833
- try:
834
- with open(resources_file, 'r', encoding='utf-8') as f:
835
- resources_data = json.load(f)
836
- if resources_data and isinstance(resources_data, dict):
837
- registry = resources_data.get('registry', {})
838
- if isinstance(registry, dict):
839
- local_routes = registry.get('local_backend_routes', [])
840
- if isinstance(local_routes, list):
841
- # Process routes with validation
842
- for route in local_routes[:100]: # Limit to prevent huge responses
843
- try:
844
- if isinstance(route, dict):
845
- # Validate route has required fields
846
- route_id = route.get("path") or route.get("name") or route.get("id")
847
- if route_id:
848
- all_apis.append(route)
849
- if route.get("category"):
850
- categories_set.add(str(route["category"]))
851
- else:
852
- logger.warning("Skipping route without ID/name/path")
853
- else:
854
- logger.warning(f"Skipping invalid route: {type(route)}")
855
- except Exception as route_error:
856
- logger.warning(f"Error processing route: {route_error}", exc_info=True)
857
- continue
858
-
859
- if local_routes:
860
- categories_set.add("local")
861
- else:
862
- logger.warning(f"local_backend_routes is not a list: {type(local_routes)}")
863
- else:
864
- logger.warning(f"Registry is not a dict: {type(registry)}")
865
- else:
866
- logger.warning(f"Resources data is not a dict: {type(resources_data)}")
867
- except json.JSONDecodeError as e:
868
- logger.error(f"JSON decode error loading local routes from {resources_file}: {e}", exc_info=True)
869
- except IOError as io_error:
870
- logger.error(f"IO error reading resources file {resources_file}: {io_error}", exc_info=True)
871
- except Exception as e:
872
- logger.error(f"Error loading local routes from {resources_file}: {e}", exc_info=True)
873
- else:
874
- logger.info(f"Resources file not found at {resources_file}")
875
 
876
- # Ensure all_apis is a list
877
- if not isinstance(all_apis, list):
878
- logger.warning("all_apis is not a list, resetting to empty list")
879
- all_apis = []
 
880
 
881
- # Build categories list safely
882
- try:
883
- categories_list = list(categories_set) if categories_set else []
884
- except Exception as cat_error:
885
- logger.warning(f"Error building categories list: {cat_error}")
886
- categories_list = []
 
 
887
 
888
- logger.info(f"Successfully loaded {len(all_apis)} APIs")
 
 
 
889
 
890
- return jsonify({
891
- 'apis': all_apis,
892
- 'total': len(all_apis),
893
- 'total_apis': len(all_apis),
894
- 'categories': categories_list,
895
- 'ok': True,
896
- 'success': True
897
- })
898
-
899
- except Exception as e:
900
- error_trace = traceback.format_exc()
901
- logger.error(f"Critical error in resources_apis: {e}", exc_info=True)
902
- logger.error(f"Full traceback: {error_trace}")
903
 
904
- # Always return valid JSON even on error
905
- return jsonify({
906
- 'error': True,
907
- 'ok': False,
908
- 'success': False,
909
- 'message': f'Failed to load API resources: {str(e)}',
910
- 'apis': [],
911
- 'total': 0,
912
- 'total_apis': 0,
913
- 'categories': []
914
- }), 500
915
-
916
- @app.route('/api/ai/signals')
917
- def ai_signals():
918
- """AI trading signals endpoint"""
919
- symbol = request.args.get('symbol', 'BTC').upper()
920
-
921
- # Get market data
922
- market_data = get_market_data()
923
- coin = next((c for c in market_data if c['symbol'].upper() == symbol), None)
924
-
925
- if not coin:
926
- return jsonify({
927
- 'symbol': symbol,
928
- 'signal': 'HOLD',
929
- 'strength': 'weak',
930
- 'price': 0,
931
- 'targets': [],
932
- 'indicators': {}
933
- })
934
-
935
- price_change = coin.get('price_change_percentage_24h', 0)
936
- current_price = coin.get('current_price', 0)
937
-
938
- # Generate signal based on price action
939
- if price_change > 5:
940
- signal = 'STRONG_BUY'
941
- strength = 'strong'
942
- targets = [
943
- {'level': current_price * 1.05, 'type': 'short'},
944
- {'level': current_price * 1.10, 'type': 'medium'},
945
- {'level': current_price * 1.15, 'type': 'long'}
946
- ]
947
- elif price_change > 2:
948
- signal = 'BUY'
949
- strength = 'medium'
950
- targets = [
951
- {'level': current_price * 1.03, 'type': 'short'},
952
- {'level': current_price * 1.07, 'type': 'medium'}
953
- ]
954
- elif price_change < -5:
955
- signal = 'STRONG_SELL'
956
- strength = 'strong'
957
- targets = [
958
- {'level': current_price * 0.95, 'type': 'short'},
959
- {'level': current_price * 0.90, 'type': 'medium'}
960
- ]
961
- elif price_change < -2:
962
- signal = 'SELL'
963
- strength = 'medium'
964
- targets = [
965
- {'level': current_price * 0.97, 'type': 'short'}
966
- ]
967
- else:
968
- signal = 'HOLD'
969
- strength = 'weak'
970
- targets = [
971
- {'level': current_price * 1.02, 'type': 'short'}
972
- ]
973
-
974
- return jsonify({
975
- 'symbol': symbol,
976
- 'signal': signal,
977
- 'strength': strength,
978
- 'price': current_price,
979
- 'change_24h': price_change,
980
- 'targets': targets,
981
- 'stop_loss': current_price * 0.95 if signal in ['BUY', 'STRONG_BUY'] else current_price * 1.05,
982
- 'indicators': {
983
- 'rsi': 50 + (price_change * 2),
984
- 'macd': 'bullish' if price_change > 0 else 'bearish',
985
- 'trend': 'up' if price_change > 0 else 'down'
986
- },
987
- 'timestamp': datetime.utcnow().isoformat()
988
- })
989
-
990
- @app.route('/api/ai/decision', methods=['POST'])
991
- def ai_decision():
992
- """AI-powered trading decision endpoint"""
993
- data = request.json
994
- symbol = data.get('symbol', 'BTC').upper()
995
- timeframe = data.get('timeframe', '1d')
996
-
997
- # Get market data for the symbol
998
- market_data = get_market_data()
999
- coin = next((c for c in market_data if c['symbol'].upper() == symbol), None)
1000
-
1001
- if not coin:
1002
- # Fallback to demo decision
1003
- return jsonify({
1004
- 'symbol': symbol,
1005
- 'decision': 'HOLD',
1006
- 'confidence': 0.65,
1007
- 'timeframe': timeframe,
1008
- 'price_target': None,
1009
- 'stop_loss': None,
1010
- 'reasoning': 'Insufficient data for analysis',
1011
- 'signals': {
1012
- 'technical': 'neutral',
1013
- 'sentiment': 'neutral',
1014
- 'trend': 'neutral'
1015
- }
1016
- })
1017
-
1018
- # Calculate decision based on price change
1019
- price_change = coin.get('price_change_percentage_24h', 0)
1020
- current_price = coin.get('current_price', 0)
1021
-
1022
- # Simple decision logic
1023
- if price_change > 5:
1024
- decision = 'BUY'
1025
- confidence = min(0.75 + (price_change / 100), 0.95)
1026
- price_target = current_price * 1.15
1027
- stop_loss = current_price * 0.95
1028
- reasoning = f'{symbol} showing strong upward momentum (+{price_change:.1f}%). Technical indicators suggest continuation.'
1029
- signals = {'technical': 'bullish', 'sentiment': 'bullish', 'trend': 'uptrend'}
1030
- elif price_change < -5:
1031
- decision = 'SELL'
1032
- confidence = min(0.75 + (abs(price_change) / 100), 0.95)
1033
- price_target = current_price * 0.85
1034
- stop_loss = current_price * 1.05
1035
- reasoning = f'{symbol} experiencing significant decline ({price_change:.1f}%). Consider taking profits or cutting losses.'
1036
- signals = {'technical': 'bearish', 'sentiment': 'bearish', 'trend': 'downtrend'}
1037
- elif price_change > 2:
1038
- decision = 'BUY'
1039
- confidence = 0.65
1040
- price_target = current_price * 1.10
1041
- stop_loss = current_price * 0.97
1042
- reasoning = f'{symbol} showing moderate gains (+{price_change:.1f}%). Cautious entry recommended.'
1043
- signals = {'technical': 'bullish', 'sentiment': 'neutral', 'trend': 'uptrend'}
1044
- elif price_change < -2:
1045
- decision = 'SELL'
1046
- confidence = 0.60
1047
- price_target = current_price * 0.92
1048
- stop_loss = current_price * 1.03
1049
- reasoning = f'{symbol} declining ({price_change:.1f}%). Monitor closely for further weakness.'
1050
- signals = {'technical': 'bearish', 'sentiment': 'neutral', 'trend': 'downtrend'}
1051
- else:
1052
- decision = 'HOLD'
1053
- confidence = 0.70
1054
- price_target = current_price * 1.05
1055
- stop_loss = current_price * 0.98
1056
- reasoning = f'{symbol} consolidating ({price_change:.1f}%). Wait for clearer directional move.'
1057
- signals = {'technical': 'neutral', 'sentiment': 'neutral', 'trend': 'sideways'}
1058
-
1059
- return jsonify({
1060
- 'symbol': symbol,
1061
- 'decision': decision,
1062
- 'confidence': confidence,
1063
- 'timeframe': timeframe,
1064
- 'current_price': current_price,
1065
- 'price_target': round(price_target, 2),
1066
- 'stop_loss': round(stop_loss, 2),
1067
- 'reasoning': reasoning,
1068
- 'signals': signals,
1069
- 'risk_level': 'moderate',
1070
- 'timestamp': datetime.utcnow().isoformat()
1071
- })
1072
-
1073
- @app.route('/api/chart/<symbol>')
1074
- def chart_data(symbol):
1075
- """Price chart data for symbol"""
1076
- try:
1077
- coin_id = symbol.lower()
1078
- response = requests.get(
1079
- f'https://api.coingecko.com/api/v3/coins/{coin_id}/market_chart',
1080
- params={'vs_currency': 'usd', 'days': '7'},
1081
- timeout=5
1082
- )
1083
 
1084
- if response.status_code == 200:
1085
- data = response.json()
1086
- return jsonify({
1087
- 'prices': data.get('prices', []),
1088
- 'market_caps': data.get('market_caps', []),
1089
- 'volumes': data.get('total_volumes', [])
1090
- })
1091
- except:
1092
- pass
1093
-
1094
- return jsonify({'prices': [], 'market_caps': [], 'volumes': []})
1095
-
1096
- @app.route('/api/market/ohlc')
1097
- def market_ohlc():
1098
- """Get OHLC data for a symbol (compatible with ai-analyst.js)"""
1099
- symbol = request.args.get('symbol', 'BTC').upper()
1100
- interval = request.args.get('interval', '1h')
1101
- limit = int(request.args.get('limit', 100))
1102
-
1103
- # Map interval formats
1104
- interval_map = {
1105
- '1m': '1m', '5m': '5m', '15m': '15m', '30m': '30m',
1106
- '1h': '1h', '4h': '4h', '1d': '1d', '1w': '1w'
1107
- }
1108
- binance_interval = interval_map.get(interval, '1h')
1109
-
1110
- try:
1111
- binance_symbol = f"{symbol}USDT"
1112
- response = requests.get(
1113
- 'https://api.binance.com/api/v3/klines',
1114
- params={
1115
- 'symbol': binance_symbol,
1116
- 'interval': binance_interval,
1117
- 'limit': min(limit, 1000)
1118
- },
1119
- timeout=10
1120
- )
1121
 
1122
- if response.status_code == 200:
1123
- data = response.json()
1124
- ohlc_data = []
1125
- for item in data:
1126
- ohlc_data.append({
1127
- 'timestamp': item[0],
1128
- 'open': float(item[1]),
1129
- 'high': float(item[2]),
1130
- 'low': float(item[3]),
1131
- 'close': float(item[4]),
1132
- 'volume': float(item[5])
1133
- })
1134
-
1135
- return jsonify({
1136
- 'symbol': symbol,
1137
- 'interval': interval,
1138
- 'data': ohlc_data,
1139
- 'count': len(ohlc_data)
1140
- })
1141
- except Exception as e:
1142
- print(f"Market OHLC error: {e}")
1143
-
1144
- # Fallback to CoinGecko
1145
- try:
1146
- coin_id = symbol.lower()
1147
- days = 7 if interval in ['1h', '4h'] else 30
1148
- response = requests.get(
1149
- f'https://api.coingecko.com/api/v3/coins/{coin_id}/ohlc',
1150
- params={'vs_currency': 'usd', 'days': str(days)},
1151
- timeout=10
1152
- )
1153
 
1154
- if response.status_code == 200:
1155
- data = response.json()
1156
- ohlc_data = []
1157
- for item in data[:limit]:
1158
- if len(item) >= 5:
1159
- ohlc_data.append({
1160
- 'timestamp': item[0],
1161
- 'open': item[1],
1162
- 'high': item[2],
1163
- 'low': item[3],
1164
- 'close': item[4],
1165
- 'volume': None
1166
- })
1167
-
1168
- return jsonify({
1169
- 'symbol': symbol,
1170
- 'interval': interval,
1171
- 'data': ohlc_data,
1172
- 'count': len(ohlc_data)
1173
- })
1174
- except Exception as e:
1175
- print(f"CoinGecko OHLC fallback error: {e}")
1176
-
1177
- return jsonify({'error': 'OHLC data not available', 'symbol': symbol}), 404
1178
-
1179
- @app.route('/api/ohlcv')
1180
- def ohlcv_endpoint():
1181
- """Get OHLCV data (query parameter version)"""
1182
- symbol = request.args.get('symbol', 'BTC').upper()
1183
- timeframe = request.args.get('timeframe', '1h')
1184
- limit = int(request.args.get('limit', 100))
1185
-
1186
- # Redirect to existing endpoint
1187
- return ohlcv_data(symbol)
1188
-
1189
- @app.route('/api/ohlcv/<symbol>')
1190
- def ohlcv_data(symbol):
1191
- """Get OHLCV data for a cryptocurrency"""
1192
- # Get query parameters
1193
- interval = request.args.get('interval', '1d')
1194
- limit = int(request.args.get('limit', 30))
1195
-
1196
- # Map interval to days for CoinGecko
1197
- interval_days_map = {
1198
- '1d': 30,
1199
- '1h': 7,
1200
- '4h': 30,
1201
- '1w': 90
1202
- }
1203
- days = interval_days_map.get(interval, 30)
1204
-
1205
- try:
1206
- # Try CoinGecko first
1207
- coin_id = symbol.lower()
1208
- response = requests.get(
1209
- f'https://api.coingecko.com/api/v3/coins/{coin_id}/ohlc',
1210
- params={'vs_currency': 'usd', 'days': str(days)},
1211
- timeout=10
1212
- )
1213
 
1214
- if response.status_code == 200:
1215
- data = response.json()
1216
- # CoinGecko returns [timestamp, open, high, low, close]
1217
- formatted_data = []
1218
- for item in data:
1219
- if len(item) >= 5:
1220
- formatted_data.append({
1221
- 'timestamp': item[0],
1222
- 'datetime': datetime.fromtimestamp(item[0] / 1000).isoformat(),
1223
- 'open': item[1],
1224
- 'high': item[2],
1225
- 'low': item[3],
1226
- 'close': item[4],
1227
- 'volume': None # CoinGecko OHLC doesn't include volume
1228
- })
1229
-
1230
- # Limit results if needed
1231
- if limit and len(formatted_data) > limit:
1232
- formatted_data = formatted_data[-limit:]
1233
-
1234
- return jsonify({
1235
- 'symbol': symbol.upper(),
1236
- 'source': 'CoinGecko',
1237
- 'interval': interval,
1238
- 'data': formatted_data
1239
- })
1240
- except Exception as e:
1241
- print(f"CoinGecko OHLCV error: {e}")
1242
-
1243
- # Fallback: Try Binance
1244
- try:
1245
- binance_symbol = f"{symbol.upper()}USDT"
1246
- # Map interval for Binance
1247
- binance_interval_map = {
1248
- '1d': '1d',
1249
- '1h': '1h',
1250
- '4h': '4h',
1251
- '1w': '1w'
1252
  }
1253
- binance_interval = binance_interval_map.get(interval, '1d')
1254
 
1255
- response = requests.get(
1256
- 'https://api.binance.com/api/v3/klines',
1257
- params={
1258
- 'symbol': binance_symbol,
1259
- 'interval': binance_interval,
1260
- 'limit': limit
1261
- },
1262
- timeout=10
1263
- )
1264
 
1265
- if response.status_code == 200:
1266
- data = response.json()
1267
- formatted_data = []
1268
- for item in data:
1269
- if len(item) >= 7:
1270
- formatted_data.append({
1271
- 'timestamp': item[0],
1272
- 'datetime': datetime.fromtimestamp(item[0] / 1000).isoformat(),
1273
- 'open': float(item[1]),
1274
- 'high': float(item[2]),
1275
- 'low': float(item[3]),
1276
- 'close': float(item[4]),
1277
- 'volume': float(item[5])
1278
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1279
 
1280
- return jsonify({
1281
- 'symbol': symbol.upper(),
1282
- 'source': 'Binance',
1283
- 'interval': interval,
1284
- 'data': formatted_data
1285
- })
1286
- except Exception as e:
1287
- print(f"Binance OHLCV error: {e}")
1288
-
1289
- return jsonify({
1290
- 'error': 'OHLCV data not available',
1291
- 'symbol': symbol
1292
- }), 404
1293
-
1294
- @app.route('/api/ohlcv/multi')
1295
- def ohlcv_multi():
1296
- """Get OHLCV data for multiple cryptocurrencies"""
1297
- symbols = request.args.get('symbols', 'btc,eth,bnb').split(',')
1298
- interval = request.args.get('interval', '1d')
1299
- limit = int(request.args.get('limit', 30))
1300
-
1301
- results = {}
1302
-
1303
- for symbol in symbols[:10]: # Limit to 10 symbols
1304
- try:
1305
- symbol = symbol.strip().upper()
1306
- binance_symbol = f"{symbol}USDT"
1307
 
1308
- response = requests.get(
1309
- 'https://api.binance.com/api/v3/klines',
1310
- params={
1311
- 'symbol': binance_symbol,
1312
- 'interval': interval,
1313
- 'limit': limit
1314
- },
1315
- timeout=5
1316
- )
1317
 
1318
- if response.status_code == 200:
1319
- data = response.json()
1320
- formatted_data = []
1321
- for item in data:
1322
- if len(item) >= 7:
1323
- formatted_data.append({
1324
- 'timestamp': item[0],
1325
- 'open': float(item[1]),
1326
- 'high': float(item[2]),
1327
- 'low': float(item[3]),
1328
- 'close': float(item[4]),
1329
- 'volume': float(item[5])
1330
- })
1331
-
1332
- results[symbol] = {
1333
- 'success': True,
1334
- 'data': formatted_data
1335
- }
1336
- else:
1337
- results[symbol] = {
1338
- 'success': False,
1339
- 'error': f'HTTP {response.status_code}'
1340
- }
1341
- except Exception as e:
1342
- results[symbol] = {
1343
- 'success': False,
1344
- 'error': str(e)
1345
  }
1346
-
1347
- return jsonify({
1348
- 'interval': interval,
1349
- 'limit': limit,
1350
- 'results': results
1351
- })
1352
-
1353
- @app.route('/api/ohlcv/verify/<symbol>')
1354
- def verify_ohlcv(symbol):
1355
- """Verify OHLCV data quality from multiple sources"""
1356
- results = {}
1357
-
1358
- # Test CoinGecko
1359
- try:
1360
- response = requests.get(
1361
- f'https://api.coingecko.com/api/v3/coins/{symbol.lower()}/ohlc',
1362
- params={'vs_currency': 'usd', 'days': '7'},
1363
- timeout=10
1364
- )
1365
- if response.status_code == 200:
1366
- data = response.json()
1367
- valid_records = sum(1 for item in data if len(item) >= 5 and all(x is not None for x in item[:5]))
1368
- results['coingecko'] = {
1369
- 'status': 'success',
1370
- 'records': len(data),
1371
- 'valid_records': valid_records,
1372
- 'sample': data[0] if data else None
1373
- }
1374
- else:
1375
- results['coingecko'] = {'status': 'failed', 'error': f'HTTP {response.status_code}'}
1376
- except Exception as e:
1377
- results['coingecko'] = {'status': 'error', 'error': str(e)}
1378
-
1379
- # Test Binance
1380
- try:
1381
- response = requests.get(
1382
- 'https://api.binance.com/api/v3/klines',
1383
- params={'symbol': f'{symbol.upper()}USDT', 'interval': '1d', 'limit': 7},
1384
- timeout=10
1385
- )
1386
- if response.status_code == 200:
1387
- data = response.json()
1388
- valid_records = sum(1 for item in data if len(item) >= 7)
1389
- results['binance'] = {
1390
- 'status': 'success',
1391
- 'records': len(data),
1392
- 'valid_records': valid_records,
1393
- 'sample': {
1394
- 'timestamp': data[0][0],
1395
- 'open': data[0][1],
1396
- 'high': data[0][2],
1397
- 'low': data[0][3],
1398
- 'close': data[0][4],
1399
- 'volume': data[0][5]
1400
- } if data else None
1401
  }
1402
- else:
1403
- results['binance'] = {'status': 'failed', 'error': f'HTTP {response.status_code}'}
1404
- except Exception as e:
1405
- results['binance'] = {'status': 'error', 'error': str(e)}
1406
-
1407
- # Test CryptoCompare
1408
- try:
1409
- response = requests.get(
1410
- 'https://min-api.cryptocompare.com/data/v2/histoday',
1411
- params={'fsym': symbol.upper(), 'tsym': 'USD', 'limit': 7},
1412
- timeout=10
1413
- )
1414
- if response.status_code == 200:
1415
- data = response.json()
1416
- if data.get('Response') != 'Error' and 'Data' in data and 'Data' in data['Data']:
1417
- records = data['Data']['Data']
1418
- valid_records = sum(1 for r in records if all(k in r for k in ['time', 'open', 'high', 'low', 'close']))
1419
- results['cryptocompare'] = {
1420
- 'status': 'success',
1421
- 'records': len(records),
1422
- 'valid_records': valid_records,
1423
- 'sample': records[0] if records else None
1424
- }
1425
- else:
1426
- results['cryptocompare'] = {'status': 'failed', 'error': data.get('Message', 'Unknown error')}
1427
- else:
1428
- results['cryptocompare'] = {'status': 'failed', 'error': f'HTTP {response.status_code}'}
1429
- except Exception as e:
1430
- results['cryptocompare'] = {'status': 'error', 'error': str(e)}
1431
-
1432
- return jsonify({
1433
- 'symbol': symbol.upper(),
1434
- 'verification_time': datetime.utcnow().isoformat(),
1435
- 'sources': results
1436
- })
1437
 
1438
- @app.route('/api/test-source/<source_id>')
1439
- def test_source(source_id):
1440
- """Test a specific data source connection"""
1441
-
1442
- # Map of source IDs to test endpoints
1443
- test_endpoints = {
1444
- 'coingecko': 'https://api.coingecko.com/api/v3/ping',
1445
- 'binance_public': 'https://api.binance.com/api/v3/ping',
1446
- 'cryptocompare': 'https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD',
1447
- 'coinpaprika': 'https://api.coinpaprika.com/v1/tickers/btc-bitcoin',
1448
- 'coincap': 'https://api.coincap.io/v2/assets/bitcoin',
1449
- 'alternative_me': 'https://api.alternative.me/fng/?limit=1',
1450
- 'cryptopanic': 'https://cryptopanic.com/api/v1/posts/?public=true',
1451
- 'coinstats_news': 'https://api.coinstats.app/public/v1/news',
1452
- 'messari': 'https://data.messari.io/api/v1/assets/btc/metrics',
1453
- 'defillama': 'https://coins.llama.fi/prices/current/coingecko:bitcoin'
1454
  }
1455
-
1456
- url = test_endpoints.get(source_id)
1457
-
1458
- if not url:
1459
- return jsonify({'error': 'Unknown source'}), 404
1460
-
1461
- try:
1462
- response = requests.get(url, timeout=10)
1463
-
1464
- return jsonify({
1465
- 'source_id': source_id,
1466
- 'status': 'success' if response.status_code == 200 else 'failed',
1467
- 'http_code': response.status_code,
1468
- 'response_time_ms': int(response.elapsed.total_seconds() * 1000),
1469
- 'tested_at': datetime.utcnow().isoformat()
1470
- })
1471
- except requests.exceptions.Timeout:
1472
- return jsonify({
1473
- 'source_id': source_id,
1474
- 'status': 'timeout',
1475
- 'error': 'Request timeout'
1476
- }), 408
1477
- except Exception as e:
1478
- return jsonify({
1479
- 'source_id': source_id,
1480
- 'status': 'error',
1481
- 'error': str(e)
1482
- }), 500
1483
 
1484
- @app.route('/api/sources/all')
1485
- def get_all_sources():
1486
- """Get list of all available data sources"""
1487
-
1488
- sources = [
1489
- {'id': 'coingecko', 'name': 'CoinGecko', 'category': 'market', 'free': True},
1490
- {'id': 'binance', 'name': 'Binance', 'category': 'ohlcv', 'free': True},
1491
- {'id': 'cryptocompare', 'name': 'CryptoCompare', 'category': 'ohlcv', 'free': True},
1492
- {'id': 'coinpaprika', 'name': 'CoinPaprika', 'category': 'market', 'free': True},
1493
- {'id': 'coincap', 'name': 'CoinCap', 'category': 'market', 'free': True},
1494
- {'id': 'alternative_me', 'name': 'Fear & Greed Index', 'category': 'sentiment', 'free': True},
1495
- {'id': 'cryptopanic', 'name': 'CryptoPanic', 'category': 'news', 'free': True},
1496
- {'id': 'messari', 'name': 'Messari', 'category': 'market', 'free': True},
1497
- {'id': 'defillama', 'name': 'DefiLlama', 'category': 'defi', 'free': True}
1498
- ]
1499
 
1500
- return jsonify({
1501
- 'total': len(sources),
1502
- 'sources': sources
1503
- })
 
1504
 
1505
- @app.route('/api/providers')
1506
- def get_providers():
1507
- """
1508
- Get list of API providers with status and details
1509
- Returns comprehensive information about available data providers
1510
- """
1511
- providers = [
1512
- {
1513
- 'id': 'coingecko',
1514
- 'name': 'CoinGecko',
1515
- 'endpoint': 'api.coingecko.com/api/v3',
1516
- 'category': 'Market Data',
1517
- 'status': 'active',
1518
- 'type': 'free',
1519
- 'rate_limit': '50 calls/min',
1520
- 'uptime': '99.9%',
1521
- 'description': 'Comprehensive cryptocurrency data including prices, market caps, and historical data'
1522
- },
1523
- {
1524
- 'id': 'binance',
1525
- 'name': 'Binance',
1526
- 'endpoint': 'api.binance.com/api/v3',
1527
- 'category': 'Market Data',
1528
- 'status': 'active',
1529
- 'type': 'free',
1530
- 'rate_limit': '1200 calls/min',
1531
- 'uptime': '99.9%',
1532
- 'description': 'Real-time trading data and market information from Binance exchange'
1533
- },
1534
- {
1535
- 'id': 'alternative_me',
1536
- 'name': 'Alternative.me',
1537
- 'endpoint': 'api.alternative.me/fng',
1538
- 'category': 'Sentiment',
1539
- 'status': 'active',
1540
- 'type': 'free',
1541
- 'rate_limit': 'Unlimited',
1542
- 'uptime': '99.5%',
1543
- 'description': 'Crypto Fear & Greed Index - Market sentiment indicator'
1544
- },
1545
- {
1546
- 'id': 'cryptopanic',
1547
- 'name': 'CryptoPanic',
1548
- 'endpoint': 'cryptopanic.com/api/v1',
1549
- 'category': 'News',
1550
- 'status': 'active',
1551
- 'type': 'free',
1552
- 'rate_limit': '100 calls/day',
1553
- 'uptime': '98.5%',
1554
- 'description': 'Cryptocurrency news aggregation from multiple sources'
1555
- },
1556
- {
1557
- 'id': 'huggingface',
1558
- 'name': 'Hugging Face',
1559
- 'endpoint': 'api-inference.huggingface.co',
1560
- 'category': 'AI & ML',
1561
- 'status': 'active',
1562
- 'type': 'free',
1563
- 'rate_limit': '1000 calls/day',
1564
- 'uptime': '99.8%',
1565
- 'description': 'AI-powered sentiment analysis and NLP models'
1566
- },
1567
- {
1568
- 'id': 'coinpaprika',
1569
- 'name': 'CoinPaprika',
1570
- 'endpoint': 'api.coinpaprika.com/v1',
1571
- 'category': 'Market Data',
1572
- 'status': 'active',
1573
- 'type': 'free',
1574
- 'rate_limit': '25000 calls/month',
1575
- 'uptime': '99.7%',
1576
- 'description': 'Cryptocurrency market data and analytics'
1577
- },
1578
- {
1579
- 'id': 'messari',
1580
- 'name': 'Messari',
1581
- 'endpoint': 'data.messari.io/api/v1',
1582
- 'category': 'Analytics',
1583
- 'status': 'active',
1584
- 'type': 'free',
1585
- 'rate_limit': '20 calls/min',
1586
- 'uptime': '99.5%',
1587
- 'description': 'Crypto research and market intelligence data'
1588
- }
1589
- ]
1590
 
1591
- return jsonify({
1592
- 'providers': providers,
1593
- 'total': len(providers),
1594
- 'active': len([p for p in providers if p['status'] == 'active']),
1595
- 'timestamp': datetime.utcnow().isoformat()
1596
- })
1597
 
1598
- @app.route('/api/data/aggregate/<symbol>')
1599
- def aggregate_data(symbol):
1600
- """Aggregate data from multiple sources for a symbol"""
1601
-
1602
- results = {}
1603
- symbol = symbol.upper()
1604
-
1605
- # CoinGecko
1606
- try:
1607
- response = requests.get(
1608
- f'https://api.coingecko.com/api/v3/simple/price',
1609
- params={'ids': symbol.lower(), 'vs_currencies': 'usd', 'include_24hr_change': 'true'},
1610
- timeout=5
1611
  )
1612
- if response.status_code == 200:
1613
- results['coingecko'] = response.json()
1614
- except:
1615
- results['coingecko'] = None
1616
 
1617
- # Binance
1618
- try:
1619
- response = requests.get(
1620
- 'https://api.binance.com/api/v3/ticker/24hr',
1621
- params={'symbol': f'{symbol}USDT'},
1622
- timeout=5
1623
- )
1624
- if response.status_code == 200:
1625
- results['binance'] = response.json()
1626
- except:
1627
- results['binance'] = None
1628
 
1629
- # CoinPaprika
1630
- try:
1631
- response = requests.get(
1632
- f'https://api.coinpaprika.com/v1/tickers/{symbol.lower()}-{symbol.lower()}',
1633
- timeout=5
1634
  )
1635
- if response.status_code == 200:
1636
- results['coinpaprika'] = response.json()
1637
- except:
1638
- results['coinpaprika'] = None
1639
-
1640
- return jsonify({
1641
- 'symbol': symbol,
1642
- 'sources': results,
1643
- 'timestamp': datetime.utcnow().isoformat()
1644
- })
1645
-
1646
- # Unified Service API Endpoints
1647
- @app.route('/api/service/rate')
1648
- def service_rate():
1649
- """Get exchange rate for a currency pair"""
1650
- pair = request.args.get('pair', 'BTC/USDT')
1651
- base, quote = pair.split('/') if '/' in pair else (pair, 'USDT')
1652
- base = base.upper()
1653
- quote = quote.upper()
1654
 
1655
- # Symbol to CoinGecko ID mapping
1656
- symbol_to_id = {
1657
- 'BTC': 'bitcoin', 'ETH': 'ethereum', 'BNB': 'binancecoin',
1658
- 'SOL': 'solana', 'ADA': 'cardano', 'XRP': 'ripple',
1659
- 'DOT': 'polkadot', 'DOGE': 'dogecoin', 'MATIC': 'matic-network',
1660
- 'AVAX': 'avalanche-2', 'LINK': 'chainlink', 'UNI': 'uniswap',
1661
- 'LTC': 'litecoin', 'ATOM': 'cosmos', 'ALGO': 'algorand'
1662
  }
1663
-
1664
- # Try Binance first (faster, more reliable for major pairs)
1665
- if quote == 'USDT':
1666
- try:
1667
- binance_symbol = f"{base}USDT"
1668
- response = requests.get(
1669
- 'https://api.binance.com/api/v3/ticker/price',
1670
- params={'symbol': binance_symbol},
1671
- timeout=5
1672
- )
1673
-
1674
- if response.status_code == 200:
1675
- data = response.json()
1676
- return jsonify({
1677
- 'pair': pair,
1678
- 'price': float(data['price']),
1679
- 'quote': quote,
1680
- 'source': 'Binance',
1681
- 'timestamp': datetime.utcnow().isoformat()
1682
- })
1683
- except Exception as e:
1684
- print(f"Binance rate error: {e}")
1685
-
1686
- # Fallback to CoinGecko
1687
- try:
1688
- coin_id = symbol_to_id.get(base, base.lower())
1689
- vs_currency = quote.lower() if quote != 'USDT' else 'usd'
1690
-
1691
- response = requests.get(
1692
- f'https://api.coingecko.com/api/v3/simple/price',
1693
- params={'ids': coin_id, 'vs_currencies': vs_currency},
1694
- timeout=10
1695
- )
1696
-
1697
- if response.status_code == 200:
1698
- data = response.json()
1699
- if coin_id in data and vs_currency in data[coin_id]:
1700
- return jsonify({
1701
- 'pair': pair,
1702
- 'price': data[coin_id][vs_currency],
1703
- 'quote': quote,
1704
- 'source': 'CoinGecko',
1705
- 'timestamp': datetime.utcnow().isoformat()
1706
- })
1707
- except Exception as e:
1708
- print(f"CoinGecko rate error: {e}")
1709
-
1710
- return jsonify({'error': 'Rate not available', 'pair': pair}), 404
1711
 
1712
- @app.route('/api/service/market-status')
1713
- def service_market_status():
1714
- """Get overall market status"""
1715
- try:
1716
- response = requests.get(
1717
- 'https://api.coingecko.com/api/v3/global',
1718
- timeout=10
1719
- )
1720
-
1721
- if response.status_code == 200:
1722
- data = response.json()
1723
- market_data = data.get('data', {})
1724
- return jsonify({
1725
- 'status': 'active',
1726
- 'market_cap': market_data.get('total_market_cap', {}).get('usd', 0),
1727
- 'volume_24h': market_data.get('total_volume', {}).get('usd', 0),
1728
- 'btc_dominance': market_data.get('market_cap_percentage', {}).get('btc', 0),
1729
- 'timestamp': datetime.utcnow().isoformat()
1730
  })
1731
- except Exception as e:
1732
- print(f"Market status error: {e}")
1733
-
1734
- return jsonify({
1735
- 'status': 'unknown',
1736
- 'timestamp': datetime.utcnow().isoformat()
1737
- })
1738
-
1739
- @app.route('/api/service/top')
1740
- def service_top():
1741
- """Get top N cryptocurrencies"""
1742
- n = int(request.args.get('n', 10))
1743
- limit = min(n, 100) # Cap at 100
1744
 
1745
- try:
1746
- response = requests.get(
1747
- 'https://api.coingecko.com/api/v3/coins/markets',
1748
- params={
1749
- 'vs_currency': 'usd',
1750
- 'order': 'market_cap_desc',
1751
- 'per_page': limit,
1752
- 'page': 1
1753
- },
1754
- timeout=10
1755
- )
1756
-
1757
- if response.status_code == 200:
1758
- data = response.json()
1759
- coins = []
1760
- for coin in data:
1761
- coins.append({
1762
- 'symbol': coin['symbol'].upper(),
1763
- 'name': coin['name'],
1764
- 'price': coin['current_price'],
1765
- 'market_cap': coin['market_cap'],
1766
- 'volume_24h': coin['total_volume'],
1767
- 'change_24h': coin['price_change_percentage_24h']
1768
- })
1769
-
1770
- return jsonify({
1771
- 'data': coins,
1772
- 'count': len(coins),
1773
- 'timestamp': datetime.utcnow().isoformat()
1774
- })
1775
- except Exception as e:
1776
- print(f"Service top error: {e}")
1777
-
1778
- return jsonify({'error': 'Top coins not available'}), 404
1779
 
1780
- @app.route('/api/service/history')
1781
- def service_history():
1782
- """Get historical OHLC data"""
1783
- symbol = request.args.get('symbol', 'BTC')
1784
- interval = request.args.get('interval', '60') # minutes
1785
- limit = int(request.args.get('limit', 100))
1786
 
1787
  try:
1788
- # Map interval to Binance format
1789
- interval_map = {
1790
- '60': '1h',
1791
- '240': '4h',
1792
- '1440': '1d'
1793
- }
1794
- binance_interval = interval_map.get(interval, '1h')
1795
-
1796
- binance_symbol = f"{symbol.upper()}USDT"
1797
- response = requests.get(
1798
- 'https://api.binance.com/api/v3/klines',
1799
- params={
1800
- 'symbol': binance_symbol,
1801
- 'interval': binance_interval,
1802
- 'limit': min(limit, 1000)
1803
- },
1804
- timeout=10
1805
- )
1806
 
1807
- if response.status_code == 200:
1808
- data = response.json()
1809
- history = []
1810
- for item in data:
1811
- history.append({
1812
- 'timestamp': item[0],
1813
- 'open': float(item[1]),
1814
- 'high': float(item[2]),
1815
- 'low': float(item[3]),
1816
- 'close': float(item[4]),
1817
- 'volume': float(item[5])
 
1818
  })
1819
-
1820
- return jsonify({
1821
- 'symbol': symbol.upper(),
1822
- 'interval': interval,
1823
- 'data': history,
1824
- 'count': len(history)
1825
- })
1826
- except Exception as e:
1827
- print(f"Service history error: {e}")
1828
 
1829
- return jsonify({'error': 'Historical data not available', 'symbol': symbol}), 404
1830
-
1831
- if __name__ == '__main__':
1832
- try:
1833
- port = int(os.getenv('PORT', 7860))
1834
- logger.info(f"🚀 Starting server on port {port}")
1835
- app.run(host='0.0.0.0', port=port, debug=False)
1836
  except Exception as e:
1837
- logger.error(f" Server startup failed: {e}")
1838
- import traceback
1839
- traceback.print_exc()
1840
- sys.exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )