Search user or team stats from stats.distributed.net
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/python3
|
||||
# Description: Searches for participant or team statistics from stats.distributed.net
|
||||
# Usage: python3 stats-distributed-net.py
|
||||
# Author: Justin Oros
|
||||
# Source: https://github.com/JustinOros
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_projects():
|
||||
"""Fetch available projects dynamically from stats.distributed.net and return {project_id: project_name}."""
|
||||
url = "https://stats.distributed.net/"
|
||||
|
||||
# Known stable mapping used only if parsing misses anything
|
||||
KNOWN = {
|
||||
"3": "RC5-56",
|
||||
"5": "RC5-64",
|
||||
"8": "RC5-72",
|
||||
"24": "OGR-24",
|
||||
"25": "OGR-25",
|
||||
"26": "OGR-26",
|
||||
"27": "OGR-27",
|
||||
"28": "OGR-28",
|
||||
"205": "RC5-64 (all)",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
print(f"Error fetching projects: {e}")
|
||||
return KNOWN.copy() # fallback to known values
|
||||
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
projects = {}
|
||||
|
||||
# Scrape any link with project_id= in the href
|
||||
for a in soup.find_all('a', href=True):
|
||||
href = a['href']
|
||||
text = a.get_text(strip=True)
|
||||
|
||||
if "project_id=" in href:
|
||||
try:
|
||||
pid = href.split("project_id=")[1].split("&")[0]
|
||||
if pid:
|
||||
# Prefer readable project text if meaningful
|
||||
if text and any(text.startswith(prefix) for prefix in ("RC5", "OGR", "DES")):
|
||||
projects[pid] = text
|
||||
else:
|
||||
projects[pid] = KNOWN.get(pid, text or f"Project {pid}")
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Ensure all known are included (fallback)
|
||||
for pid, name in KNOWN.items():
|
||||
if pid not in projects:
|
||||
projects[pid] = name
|
||||
|
||||
# Notify if new project IDs appear so user can track future additions
|
||||
for pid, name in projects.items():
|
||||
if pid not in KNOWN:
|
||||
print(f"⚠️ New project detected on stats.distributed.net: ID={pid}, Name='{name}'")
|
||||
|
||||
return dict(sorted(projects.items(), key=lambda x: x[0]))
|
||||
|
||||
|
||||
def search_participant(project_id, search_term, debug=False):
|
||||
url = "https://stats.distributed.net/participant/psearch.php"
|
||||
params = {'project_id': project_id, 'st': search_term}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
if debug:
|
||||
with open('debug_participant.html', 'w', encoding='utf-8') as f:
|
||||
f.write(response.text)
|
||||
print("DEBUG: Saved response to debug_participant.html")
|
||||
|
||||
return parse_results(response.text, 'participant')
|
||||
except Exception as e:
|
||||
print(f"Error searching participant: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def search_team(project_id, search_term, debug=False):
|
||||
url = "https://stats.distributed.net/team/tsearch.php"
|
||||
params = {'project_id': project_id, 'st': search_term}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
if debug:
|
||||
with open('debug_team.html', 'w', encoding='utf-8') as f:
|
||||
f.write(response.text)
|
||||
print("DEBUG: Saved response to debug_team.html")
|
||||
|
||||
return parse_results(response.text, 'team')
|
||||
except Exception as e:
|
||||
print(f"Error searching team: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_results(html, search_type):
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
results = []
|
||||
|
||||
if 'psummary.php' in html or 'tsummary.php' in html:
|
||||
result = parse_stats_page(soup, search_type)
|
||||
if result:
|
||||
return [result]
|
||||
|
||||
tables = soup.find_all('table')
|
||||
|
||||
for table in tables:
|
||||
rows = table.find_all('tr')
|
||||
for row in rows:
|
||||
cells = row.find_all('td')
|
||||
if len(cells) < 3:
|
||||
continue
|
||||
|
||||
data = [cell.get_text(strip=True) for cell in cells]
|
||||
if not any(data):
|
||||
continue
|
||||
|
||||
rank_text = data[0].replace(',', '').replace('(', '').replace(')', '').strip()
|
||||
if not rank_text.isdigit():
|
||||
continue
|
||||
|
||||
name = data[1]
|
||||
days = data[2]
|
||||
overall = data[3] if len(data) > 3 else 'N/A'
|
||||
|
||||
results.append({
|
||||
"name": name,
|
||||
"current_rank": rank_text,
|
||||
"overall_rank": rank_text,
|
||||
"updated": f"{days} days active" if days != 'N/A' else 'N/A',
|
||||
"overall_work": overall
|
||||
})
|
||||
|
||||
return results or None
|
||||
|
||||
|
||||
def parse_stats_page(soup, search_type):
|
||||
result = {}
|
||||
h1_tag = soup.find('h1', class_='phead')
|
||||
if h1_tag:
|
||||
text = h1_tag.get_text(strip=True)
|
||||
if "'s stats" in text:
|
||||
text = text.replace("'s stats", "").strip()
|
||||
result['name'] = text
|
||||
|
||||
title_tag = soup.find('title')
|
||||
if 'name' not in result and title_tag:
|
||||
title_text = title_tag.get_text(strip=True)
|
||||
if ' for ' in title_text:
|
||||
result['name'] = title_text.split(' for ')[-1].strip()
|
||||
|
||||
tables = soup.find_all('table', border='0')
|
||||
for table in tables:
|
||||
for row in table.find_all('tr'):
|
||||
cells = row.find_all('td')
|
||||
if len(cells) >= 2:
|
||||
label = cells[0].get_text(strip=True).lower()
|
||||
if label == 'rank:':
|
||||
overall = cells[1].get_text(strip=True)
|
||||
result['overall_rank'] = overall
|
||||
if len(cells) >= 3:
|
||||
current = cells[2].get_text(strip=True).split('(')[0].strip()
|
||||
result['current_rank'] = current
|
||||
else:
|
||||
result['current_rank'] = overall
|
||||
elif label == 'time working:':
|
||||
result['time_working'] = cells[1].get_text(strip=True)
|
||||
|
||||
last_update = soup.find('td', class_='lastupdate')
|
||||
if last_update:
|
||||
text = last_update.get_text(strip=True)
|
||||
if 'as of' in text:
|
||||
date_text = text.split('as of')[1].split('at')[0].strip()
|
||||
result['updated'] = date_text
|
||||
|
||||
result.setdefault('name', 'Unknown')
|
||||
result.setdefault('overall_rank', 'N/A')
|
||||
result.setdefault('current_rank', result['overall_rank'])
|
||||
result.setdefault('updated', 'N/A')
|
||||
|
||||
return result if result['name'] != 'Unknown' else None
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Distributed.net Statistics Search Tool")
|
||||
print("=" * 60)
|
||||
print("\nFetching project list from stats.distributed.net ...")
|
||||
|
||||
projects = get_projects()
|
||||
|
||||
print("\nAvailable Projects:")
|
||||
project_items = list(projects.items())
|
||||
for i, (pid, name) in enumerate(project_items, 1):
|
||||
print(f"{i}. {name} (ID: {pid})")
|
||||
|
||||
while True:
|
||||
choice = input("\nSelect project number (press Enter for RC5-72): ").strip()
|
||||
if choice == "":
|
||||
project_id = "8"
|
||||
break
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(project_items):
|
||||
project_id = project_items[int(choice)-1][0]
|
||||
break
|
||||
print(f"Enter a number between 1 and {len(project_items)}")
|
||||
|
||||
print("\n1. Participant (User)")
|
||||
print("2. Team")
|
||||
while True:
|
||||
search_type = input("\nSelect search type (1 or 2): ").strip()
|
||||
if search_type in ("1", "2"):
|
||||
break
|
||||
print("Invalid choice")
|
||||
|
||||
search_term = input("\nEnter search term: ").strip()
|
||||
if not search_term:
|
||||
print("Search term cannot be empty.")
|
||||
return
|
||||
|
||||
debug = '--debug' in sys.argv
|
||||
|
||||
print(f"\nSearching '{search_term}'...\nPlease wait...")
|
||||
|
||||
results = search_participant(project_id, search_term, debug) if search_type == "1" else search_team(project_id, search_term, debug)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SEARCH RESULTS")
|
||||
print("=" * 60)
|
||||
|
||||
if not results:
|
||||
print("\nNo results found.")
|
||||
else:
|
||||
for result in results:
|
||||
print(f"\nName: {result['name']}")
|
||||
print(f"Current Rank: {result['current_rank']}")
|
||||
print(f"Overall Rank: {result['overall_rank']}")
|
||||
print(f"Updated: {result['updated']}")
|
||||
if "time_working" in result:
|
||||
print(f"Time Working: {result['time_working']}")
|
||||
if "overall_work" in result:
|
||||
print(f"Total Work Units: {result['overall_work']}")
|
||||
print("-" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nCancelled.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nUnexpected error: {e}")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user