from web.database import query_db class FeatureService: @staticmethod def get_player_features(steam_id): sql = "SELECT * FROM dm_player_features WHERE steam_id_64 = ?" return query_db('l3', sql, [steam_id], one=True) @staticmethod def get_players_list(page=1, per_page=20, sort_by='rating', search=None): offset = (page - 1) * per_page # Sort Mapping sort_map = { 'rating': 'basic_avg_rating', 'kd': 'basic_avg_kd', 'kast': 'basic_avg_kast', 'matches': 'matches_played' } order_col = sort_map.get(sort_by, 'basic_avg_rating') from web.services.stats_service import StatsService # Helper to attach match counts def attach_match_counts(player_list): if not player_list: return ids = [p['steam_id_64'] for p in player_list] # Batch query for counts from L2 placeholders = ','.join('?' for _ in ids) sql = f""" SELECT steam_id_64, COUNT(*) as cnt FROM fact_match_players WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64 """ counts = query_db('l2', sql, ids) cnt_dict = {r['steam_id_64']: r['cnt'] for r in counts} for p in player_list: p['matches_played'] = cnt_dict.get(p['steam_id_64'], 0) if search: # ... existing search logic ... # Get all matching players l2_players, _ = StatsService.get_players(page=1, per_page=100, search=search) if not l2_players: return [], 0 # ... (Merge logic) ... # I need to insert the match count logic inside the merge loop or after steam_ids = [p['steam_id_64'] for p in l2_players] placeholders = ','.join('?' for _ in steam_ids) sql = f"SELECT * FROM dm_player_features WHERE steam_id_64 IN ({placeholders})" features = query_db('l3', sql, steam_ids) f_dict = {f['steam_id_64']: f for f in features} # Get counts for sorting count_sql = f"SELECT steam_id_64, COUNT(*) as cnt FROM fact_match_players WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64" counts = query_db('l2', count_sql, steam_ids) cnt_dict = {r['steam_id_64']: r['cnt'] for r in counts} merged = [] for p in l2_players: f = f_dict.get(p['steam_id_64']) m = dict(p) if f: m.update(dict(f)) else: # Fallback Calc stats = StatsService.get_player_basic_stats(p['steam_id_64']) if stats: m['basic_avg_rating'] = stats['rating'] m['basic_avg_kd'] = stats['kd'] m['basic_avg_kast'] = stats['kast'] else: m['basic_avg_rating'] = 0 m['basic_avg_kd'] = 0 m['basic_avg_kast'] = 0 # Ensure kast exists m['matches_played'] = cnt_dict.get(p['steam_id_64'], 0) merged.append(m) merged.sort(key=lambda x: x.get(order_col, 0) or 0, reverse=True) total = len(merged) start = (page - 1) * per_page end = start + per_page return merged[start:end], total else: # Browse mode # Check L3 l3_count = query_db('l3', "SELECT COUNT(*) as cnt FROM dm_player_features", one=True)['cnt'] if l3_count == 0 or sort_by == 'matches': # If sorting by matches, we MUST use L2 counts because L3 might not have it or we want dynamic. # OR if L3 is empty. # Since L3 schema is unknown regarding 'matches_played', let's assume we fallback to L2 logic # but paginated in memory if dataset is small, or just fetch all L2 players? # Fetching all L2 players is bad if many. # But for 'matches' sort, we need to know counts for ALL to sort correctly. # Solution: Query L2 for top N players by match count. if sort_by == 'matches': # Query L2 for IDs ordered by count sql = """ SELECT steam_id_64, COUNT(*) as cnt FROM fact_match_players GROUP BY steam_id_64 ORDER BY cnt DESC LIMIT ? OFFSET ? """ top_ids = query_db('l2', sql, [per_page, offset]) if not top_ids: return [], 0 total = query_db('l2', "SELECT COUNT(DISTINCT steam_id_64) as cnt FROM fact_match_players", one=True)['cnt'] ids = [r['steam_id_64'] for r in top_ids] # Fetch details for these IDs l2_players = StatsService.get_players_by_ids(ids) # Merge logic (reuse) merged = [] # Fetch L3 features for these IDs to show stats p_ph = ','.join('?' for _ in ids) f_sql = f"SELECT * FROM dm_player_features WHERE steam_id_64 IN ({p_ph})" features = query_db('l3', f_sql, ids) f_dict = {f['steam_id_64']: f for f in features} cnt_dict = {r['steam_id_64']: r['cnt'] for r in top_ids} # Map L2 players to dict for easy access (though list order matters for sort?) # Actually top_ids is sorted. p_dict = {p['steam_id_64']: p for p in l2_players} for r in top_ids: # Preserve order sid = r['steam_id_64'] p = p_dict.get(sid) if not p: continue m = dict(p) f = f_dict.get(sid) if f: m.update(dict(f)) else: stats = StatsService.get_player_basic_stats(sid) if stats: m['basic_avg_rating'] = stats['rating'] m['basic_avg_kd'] = stats['kd'] m['basic_avg_kast'] = stats['kast'] else: m['basic_avg_rating'] = 0 m['basic_avg_kd'] = 0 m['basic_avg_kast'] = 0 m['matches_played'] = r['cnt'] merged.append(m) return merged, total # L3 empty fallback (existing logic) l2_players, total = StatsService.get_players(page, per_page, sort_by=None) merged = [] attach_match_counts(l2_players) # Helper for p in l2_players: m = dict(p) stats = StatsService.get_player_basic_stats(p['steam_id_64']) if stats: m['basic_avg_rating'] = stats['rating'] m['basic_avg_kd'] = stats['kd'] m['basic_avg_kast'] = stats['kast'] else: m['basic_avg_rating'] = 0 m['basic_avg_kd'] = 0 m['basic_avg_kast'] = 0 m['matches_played'] = p.get('matches_played', 0) merged.append(m) if sort_by != 'rating': merged.sort(key=lambda x: x.get(order_col, 0) or 0, reverse=True) return merged, total # Normal L3 browse (sort by rating/kd/kast) sql = f"SELECT * FROM dm_player_features ORDER BY {order_col} DESC LIMIT ? OFFSET ?" features = query_db('l3', sql, [per_page, offset]) total = query_db('l3', "SELECT COUNT(*) as cnt FROM dm_player_features", one=True)['cnt'] if not features: return [], total steam_ids = [f['steam_id_64'] for f in features] l2_players = StatsService.get_players_by_ids(steam_ids) p_dict = {p['steam_id_64']: p for p in l2_players} merged = [] for f in features: m = dict(f) p = p_dict.get(f['steam_id_64']) if p: m.update(dict(p)) else: m['username'] = f['steam_id_64'] # Fallback m['avatar_url'] = None merged.append(m) return merged, total @staticmethod def get_top_players(limit=20, sort_by='basic_avg_rating'): # Safety check for sort_by to prevent injection allowed_sorts = ['basic_avg_rating', 'basic_avg_kd', 'basic_avg_kast', 'basic_avg_rws'] if sort_by not in allowed_sorts: sort_by = 'basic_avg_rating' sql = f""" SELECT f.*, p.username, p.avatar_url FROM dm_player_features f LEFT JOIN l2.dim_players p ON f.steam_id_64 = p.steam_id_64 ORDER BY {sort_by} DESC LIMIT ? """ # Note: Cross-database join (l2.dim_players) works in SQLite if attached. # But `query_db` connects to one DB. # Strategy: Fetch features, then fetch player infos from L2. Or attach DB. # Simple strategy: Fetch features, then extract steam_ids and batch fetch from L2 in StatsService. # Or simpler: Just return features and let the controller/template handle the name/avatar via another call or pre-fetching. # Actually, for "Player List" view, we really want L3 data joined with L2 names. # I will change this to just return features for now, and handle joining in the route handler or via a helper that attaches databases. # Attaching is better. return query_db('l3', f"SELECT * FROM dm_player_features ORDER BY {sort_by} DESC LIMIT ?", [limit]) @staticmethod def get_player_trend(steam_id, limit=30): # This requires `fact_match_features` or querying L2 matches for historical data. # WebRDD says: "Trend graph: Recent 10/20 matches Rating trend (Chart.js)." # We can get this from L2 fact_match_players. sql = """ SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id FROM fact_match_players mp JOIN fact_matches m ON mp.match_id = m.match_id WHERE mp.steam_id_64 = ? ORDER BY m.start_time DESC LIMIT ? """ # This query needs to run against L2. # So this method should actually be in StatsService or FeatureService connecting to L2. # I will put it here but note it uses L2. Actually, better to put in StatsService if it uses L2 tables. # But FeatureService conceptualizes "Trends". I'll move it to StatsService for implementation correctness (DB context). pass