|
|
| """
|
| K1RL QUASAR — Visitor Tracking Module (HuggingFace Spaces)
|
| Tracks IPs, locations, devices with container-safe persistence
|
| """
|
|
|
| import os
|
| import json
|
| import time
|
| import threading
|
| import requests
|
| from datetime import datetime
|
| from pathlib import Path
|
|
|
| try:
|
| from user_agents import parse as parse_ua
|
| except ImportError:
|
|
|
| def parse_ua(ua_string):
|
| return type('UA', (), {
|
| 'browser': type('Browser', (), {'family': 'Unknown', 'version_string': ''}),
|
| 'os': type('OS', (), {'family': 'Unknown', 'version_string': ''}),
|
| 'device': type('Device', (), {'family': 'Unknown'}),
|
| 'is_mobile': False,
|
| 'is_tablet': False,
|
| 'is_pc': True,
|
| 'is_bot': False
|
| })
|
|
|
|
|
|
|
|
|
|
|
| BASE_DIR = Path('/home/user/app')
|
| VISITOR_FILE = BASE_DIR / 'data' / 'visitors.json'
|
| VISITOR_TIMEOUT = 120
|
|
|
|
|
| VISITOR_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| active_visitors = {}
|
|
|
|
|
| all_time_visitors = {}
|
|
|
| visitor_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
| def get_location(ip):
|
| """Get location from IP using free API with fallbacks"""
|
| try:
|
|
|
| if ip in ['127.0.0.1', 'localhost'] or ip.startswith(('192.168.', '10.', '172.')):
|
| return {'country': 'Local', 'city': 'Local', 'region': '', 'country_code': 'LO', 'isp': 'Local'}
|
|
|
|
|
| response = requests.get(
|
| f'http://ip-api.com/json/{ip}?fields=status,country,countryCode,region,city,isp',
|
| timeout=3
|
| )
|
|
|
| if response.status_code == 200:
|
| data = response.json()
|
| if data.get('status') == 'success':
|
| return {
|
| 'country': data.get('country', 'Unknown'),
|
| 'country_code': data.get('countryCode', '??'),
|
| 'city': data.get('city', 'Unknown'),
|
| 'region': data.get('region', ''),
|
| 'isp': data.get('isp', 'Unknown ISP')
|
| }
|
|
|
| except Exception as e:
|
| print(f"⚠️ Geolocation API failed for {ip}: {e}")
|
|
|
|
|
| return {
|
| 'country': 'Unknown',
|
| 'city': 'Unknown',
|
| 'country_code': '??',
|
| 'region': '',
|
| 'isp': 'Unknown ISP'
|
| }
|
|
|
| def parse_device(user_agent_string):
|
| """Parse user agent to get device info with fallback"""
|
| try:
|
| ua = parse_ua(user_agent_string)
|
| return {
|
| 'browser': f"{ua.browser.family} {ua.browser.version_string}".strip(),
|
| 'os': f"{ua.os.family} {ua.os.version_string}".strip(),
|
| 'device': ua.device.family,
|
| 'is_mobile': ua.is_mobile,
|
| 'is_tablet': ua.is_tablet,
|
| 'is_pc': ua.is_pc,
|
| 'is_bot': ua.is_bot
|
| }
|
| except Exception as e:
|
| print(f"⚠️ User agent parsing failed: {e}")
|
|
|
| return {
|
| 'browser': 'Unknown',
|
| 'os': 'Unknown',
|
| 'device': 'Unknown',
|
| 'is_mobile': False,
|
| 'is_tablet': False,
|
| 'is_pc': True,
|
| 'is_bot': False
|
| }
|
|
|
|
|
|
|
|
|
|
|
| def load_visitors():
|
| """Load all-time visitors from file"""
|
| global all_time_visitors
|
| try:
|
| if VISITOR_FILE.exists():
|
| with open(VISITOR_FILE, 'r') as f:
|
| data = json.load(f)
|
| all_time_visitors = data.get('visitors', {})
|
| count = len(all_time_visitors)
|
| print(f"📊 Loaded {count} historical visitors from {VISITOR_FILE}")
|
| else:
|
| print(f"📊 No existing visitor data found at {VISITOR_FILE}")
|
| all_time_visitors = {}
|
| except Exception as e:
|
| print(f"⚠️ Could not load visitor data: {e}")
|
| all_time_visitors = {}
|
|
|
| def save_visitors():
|
| """Save all-time visitors to file"""
|
| try:
|
| visitor_data = {
|
| 'visitors': all_time_visitors,
|
| 'total_count': len(all_time_visitors),
|
| 'last_updated': datetime.now().isoformat(),
|
| 'platform': 'huggingface-spaces'
|
| }
|
|
|
|
|
| temp_file = VISITOR_FILE.with_suffix('.tmp')
|
| with open(temp_file, 'w') as f:
|
| json.dump(visitor_data, f, indent=2)
|
|
|
| temp_file.rename(VISITOR_FILE)
|
| print(f"💾 Saved {len(all_time_visitors)} visitors to {VISITOR_FILE}")
|
|
|
| except Exception as e:
|
| print(f"⚠️ Could not save visitor data: {e}")
|
|
|
|
|
|
|
|
|
|
|
| def track_visitor(ip, user_agent=''):
|
| """Track a visitor by IP with location and device"""
|
| with visitor_lock:
|
| now = time.time()
|
| now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
| active_visitors[ip] = {
|
| 'last_seen': now,
|
| 'user_agent': user_agent
|
| }
|
|
|
|
|
| is_new = ip not in all_time_visitors
|
|
|
| if is_new:
|
| print(f"👤 New visitor detected: {ip}")
|
|
|
|
|
| def get_visitor_info():
|
| try:
|
| location = get_location(ip)
|
| device = parse_device(user_agent)
|
|
|
| with visitor_lock:
|
| all_time_visitors[ip] = {
|
| 'first_seen': now_str,
|
| 'last_seen': now_str,
|
| 'location': location,
|
| 'device': device,
|
| 'visits': 1,
|
| 'user_agent_history': [user_agent] if user_agent else []
|
| }
|
|
|
| save_visitors()
|
| count = len(all_time_visitors)
|
| city = location.get('city', '?')
|
| country = location.get('country', '?')
|
| print(f"👤 Visitor #{count}: {ip} from {city}, {country}")
|
|
|
| except Exception as e:
|
| print(f"⚠️ Error processing new visitor {ip}: {e}")
|
|
|
|
|
| info_thread = threading.Thread(target=get_visitor_info, daemon=True)
|
| info_thread.start()
|
|
|
| else:
|
|
|
| visitor = all_time_visitors[ip]
|
| visitor['last_seen'] = now_str
|
| visitor['visits'] = visitor.get('visits', 0) + 1
|
|
|
|
|
| if user_agent:
|
| ua_history = visitor.setdefault('user_agent_history', [])
|
| if user_agent not in ua_history:
|
| ua_history.append(user_agent)
|
| visitor['user_agent_history'] = ua_history[-5:]
|
|
|
|
|
| if user_agent and visitor.get('device', {}).get('browser') == 'Unknown':
|
| visitor['device'] = parse_device(user_agent)
|
|
|
|
|
| if visitor['visits'] % 10 == 0:
|
| save_visitors()
|
|
|
| def cleanup_inactive():
|
| """Remove inactive visitors from active list"""
|
| with visitor_lock:
|
| now = time.time()
|
| stale = [ip for ip, data in active_visitors.items()
|
| if now - data['last_seen'] > VISITOR_TIMEOUT]
|
| for ip in stale:
|
| del active_visitors[ip]
|
|
|
| def get_stats():
|
| """Get basic visitor statistics (for API compatibility)"""
|
| cleanup_inactive()
|
| with visitor_lock:
|
| return {
|
| 'active_now': len(active_visitors),
|
| 'active_ips': list(active_visitors.keys()),
|
| 'all_time_unique': len(all_time_visitors),
|
| }
|
|
|
| def get_detailed_stats():
|
| """Get detailed visitor statistics"""
|
| cleanup_inactive()
|
| with visitor_lock:
|
| active_ips = list(active_visitors.keys())
|
|
|
|
|
| countries = {}
|
| cities = {}
|
| devices = {'mobile': 0, 'tablet': 0, 'pc': 0, 'bot': 0}
|
| browsers = {}
|
|
|
| for ip, data in all_time_visitors.items():
|
|
|
| country = data.get('location', {}).get('country', 'Unknown')
|
| countries[country] = countries.get(country, 0) + 1
|
|
|
|
|
| city = data.get('location', {}).get('city', 'Unknown')
|
| cities[city] = cities.get(city, 0) + 1
|
|
|
|
|
| device_info = data.get('device', {})
|
| if device_info.get('is_mobile'):
|
| devices['mobile'] += 1
|
| elif device_info.get('is_tablet'):
|
| devices['tablet'] += 1
|
| elif device_info.get('is_bot'):
|
| devices['bot'] += 1
|
| else:
|
| devices['pc'] += 1
|
|
|
|
|
| browser = device_info.get('browser', 'Unknown').split()[0]
|
| browsers[browser] = browsers.get(browser, 0) + 1
|
|
|
| return {
|
| 'total_unique': len(all_time_visitors),
|
| 'active_now': len(active_ips),
|
| 'countries': dict(sorted(countries.items(), key=lambda x: x[1], reverse=True)[:10]),
|
| 'cities': dict(sorted(cities.items(), key=lambda x: x[1], reverse=True)[:10]),
|
| 'devices': devices,
|
| 'browsers': dict(sorted(browsers.items(), key=lambda x: x[1], reverse=True)[:10]),
|
| }
|
|
|
| def get_all_visitors():
|
| """Get ALL visitor details with location and device"""
|
| cleanup_inactive()
|
| with visitor_lock:
|
| active_ips = list(active_visitors.keys())
|
|
|
| visitors_list = []
|
| for ip, data in all_time_visitors.items():
|
| visitors_list.append({
|
| 'ip': ip,
|
| 'is_active': ip in active_ips,
|
| 'first_seen': data.get('first_seen', ''),
|
| 'last_seen': data.get('last_seen', ''),
|
| 'visits': data.get('visits', 1),
|
| 'location': data.get('location', {}),
|
| 'device': data.get('device', {}),
|
| 'user_agent_count': len(data.get('user_agent_history', []))
|
| })
|
|
|
|
|
| visitors_list.sort(
|
| key=lambda x: (not x['is_active'], -x['visits'], x['last_seen']),
|
| reverse=False
|
| )
|
|
|
| return {
|
| 'total_unique': len(all_time_visitors),
|
| 'active_now': len(active_ips),
|
| 'visitors': visitors_list[:100],
|
| 'showing_count': min(100, len(visitors_list))
|
| }
|
|
|
|
|
|
|
|
|
|
|
| def background_cleanup():
|
| """Background task to periodically clean up and save data"""
|
| while True:
|
| try:
|
| time.sleep(300)
|
| cleanup_inactive()
|
|
|
|
|
| current_time = int(time.time())
|
| if current_time % 3600 < 300:
|
| save_visitors()
|
|
|
| except Exception as e:
|
| print(f"⚠️ Background cleanup error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| load_visitors()
|
|
|
|
|
| cleanup_thread = threading.Thread(target=background_cleanup, daemon=True)
|
| cleanup_thread.start()
|
|
|
| print(f"✅ K1RL QUASAR Visitor Tracker ready")
|
| print(f" Platform: HuggingFace Spaces")
|
| print(f" Historical visitors: {len(all_time_visitors)}")
|
| print(f" Data file: {VISITOR_FILE}")
|
| print("=" * 50)
|
|
|
|
|
| __all__ = ['track_visitor', 'get_stats', 'get_detailed_stats', 'get_all_visitors', 'cleanup_inactive']
|
|
|