diff --git a/stats-distributed-net.py b/stats-distributed-net.py new file mode 100644 index 0000000..1a3f639 --- /dev/null +++ b/stats-distributed-net.py @@ -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)