257 lines
12 KiB
Python
257 lines
12 KiB
Python
|
|
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
|