Spaces:
Sleeping
Sleeping
| """ | |
| NationStates High-Value Trading Card Tracker (CLI) | |
| =================================================== | |
| Runs a full scan pipeline using the NationStates API directly. | |
| 1. Scans cards by ID range for Legendary/Epic (Low ID filter) | |
| 2. Checks if flagged nations are dead (CTE filter) | |
| 3. Matches against historical names | |
| 4. Checks live market prices | |
| 5. Generates dashboard.html | |
| COMPLIANCE: This script NEVER executes automated trades. | |
| All purchases are manual via generated URL links. | |
| Rate limit: 1.2s between every API request. | |
| Usage: python tracker.py [--max-id 10000] [--price-checks 30] | |
| """ | |
| import sys | |
| import os | |
| import time | |
| import sqlite3 | |
| from datetime import datetime | |
| # Add script directory to path for imports | |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, SCRIPT_DIR) | |
| from core import ( | |
| init_db, scan_low_id_cards, scan_cte_cards, scan_historical_cards, | |
| check_target_prices, SEASON, RATE_LIMIT_SECONDS, THRESHOLDS | |
| ) | |
| DB_PATH = os.path.join(SCRIPT_DIR, "cards.db") | |
| DASHBOARD_PATH = os.path.join(SCRIPT_DIR, "dashboard.html") | |
| # Defaults (can be overridden via command line) | |
| DEFAULT_MAX_ID = 10000 | |
| DEFAULT_PRICE_CHECKS = 30 | |
| # ========================================== | |
| # CLI CALLBACKS | |
| # ========================================== | |
| def scan_callback(status, card_id, card): | |
| if status == "found": | |
| print(f" β ID:{card_id} β {card['name']} ({card['rarity']}) MV:{card['market_value']}") | |
| else: | |
| name = card["name"] if card else "not found" | |
| rarity = card["rarity"] if card else "" | |
| if card: | |
| print(f" Β· ID:{card_id} β {name} ({rarity}) skip") | |
| else: | |
| print(f" Β· ID:{card_id} β not found") | |
| def cte_callback(status, card_id, name): | |
| if status == "cte": | |
| print(f" π {name} (ID:{card_id}) β DEAD") | |
| else: | |
| print(f" β {name} (ID:{card_id}) β alive") | |
| # ========================================== | |
| # DASHBOARD GENERATION | |
| # ========================================== | |
| def generate_dashboard(alerts): | |
| """Generate HTML dashboard.""" | |
| print("\n[HTML] Generating dashboard...") | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute("SELECT COUNT(*) FROM cards") | |
| total_cards = c.fetchone()[0] | |
| c.execute("SELECT COUNT(*) FROM targets") | |
| total_targets = c.fetchone()[0] | |
| c.execute(""" | |
| SELECT card_id, name, rarity, flag_reason, lowest_ask, last_checked, card_url | |
| FROM targets ORDER BY | |
| CASE rarity WHEN 'legendary' THEN 1 WHEN 'epic' THEN 2 ELSE 3 END, lowest_ask ASC | |
| """) | |
| all_targets = c.fetchall() | |
| conn.close() | |
| now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| rarity_colors = { | |
| "legendary": "#FFD700", "epic": "#FF9800", "ultra-rare": "#9C27B0", | |
| "rare": "#2196F3", "uncommon": "#4CAF50", "common": "#8B8B8B" | |
| } | |
| alerts_html = "" | |
| if alerts: | |
| alerts_html = f'<div class="alert-box"><h2>π¨ {len(alerts)} Undervalued Cards!</h2>' | |
| for a in alerts: | |
| color = rarity_colors.get(a["rarity"], "#fff") | |
| alerts_html += ( | |
| f'<div class="alert-card"><span style="color:{color}">{a["rarity"].upper()}</span> ' | |
| f'<strong>{a["name"]}</strong> (ID:{a["card_id"]})<br>' | |
| f'Reason: {a["reason"]}<br>' | |
| f'Ask: <strong>{a["ask"]:.2f}</strong> (threshold: {a["threshold"]})<br>' | |
| f'<a href="{a["url"]}" target="_blank">π View / Buy</a></div>' | |
| ) | |
| alerts_html += "</div>" | |
| else: | |
| alerts_html = '<div class="no-alert">β No undervalued cards below threshold.</div>' | |
| table_rows = "" | |
| for row in all_targets: | |
| card_id, name, rarity, flag_reason, lowest_ask, last_checked, card_url = row | |
| color = rarity_colors.get(rarity, "#fff") | |
| ask_display = f"{lowest_ask:.2f}" if lowest_ask is not None else "β" | |
| checked_display = last_checked[:16] if last_checked else "β" | |
| table_rows += ( | |
| f'<tr><td>{card_id}</td><td>{name}</td>' | |
| f'<td style="color:{color};font-weight:bold">{rarity}</td>' | |
| f'<td>{flag_reason}</td><td>{ask_display}</td>' | |
| f'<td>{checked_display}</td>' | |
| f'<td><a href="{card_url}" target="_blank">View</a></td></tr>' | |
| ) | |
| html = f"""<!DOCTYPE html> | |
| <html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>NS Card Tracker</title> | |
| <style> | |
| *{{margin:0;padding:0;box-sizing:border-box}} | |
| body{{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;padding:20px}} | |
| h1{{color:#FFD700;margin-bottom:5px}} | |
| .sub{{color:#aaa;margin-bottom:20px;font-size:13px}} | |
| .stats{{display:flex;gap:15px;margin-bottom:20px;flex-wrap:wrap}} | |
| .stat{{background:#16213e;padding:12px 20px;border-radius:8px;text-align:center}} | |
| .stat .n{{font-size:22px;font-weight:bold;color:#4fc3f7}} | |
| .stat .l{{font-size:11px;color:#aaa}} | |
| .alert-box{{background:#2d1b00;border:2px solid #FF9800;border-radius:8px;padding:15px;margin-bottom:20px}} | |
| .alert-card{{background:#1a1a2e;padding:10px;margin:8px 0;border-radius:6px;border-left:4px solid #FF9800}} | |
| .no-alert{{background:#0d2818;border:2px solid #4CAF50;border-radius:8px;padding:15px;margin-bottom:20px}} | |
| table{{width:100%;border-collapse:collapse;margin-top:10px}} | |
| th{{background:#16213e;padding:8px;text-align:left;font-size:11px;color:#aaa;text-transform:uppercase}} | |
| td{{padding:6px 8px;border-bottom:1px solid #2a2a4a;font-size:12px}} | |
| tr:hover{{background:#16213e}} | |
| a{{color:#4fc3f7}} | |
| .foot{{margin-top:20px;font-size:10px;color:#555;border-top:1px solid #333;padding-top:10px}} | |
| </style></head><body> | |
| <h1>π NS High-Value Card Tracker</h1> | |
| <p class="sub">Generated: {now} | Season {SEASON} | API-only (no dumps)</p> | |
| <div class="stats"> | |
| <div class="stat"><div class="n">{total_cards}</div><div class="l">Cards Scanned</div></div> | |
| <div class="stat"><div class="n">{total_targets}</div><div class="l">Targets</div></div> | |
| <div class="stat"><div class="n">{len(alerts)}</div><div class="l">Undervalued</div></div> | |
| </div> | |
| {alerts_html} | |
| <h2>π All Targets</h2> | |
| <table><thead><tr><th>ID</th><th>Name</th><th>Rarity</th><th>Reason</th><th>Ask</th><th>Checked</th><th>Link</th></tr></thead> | |
| <tbody>{table_rows}</tbody></table> | |
| <div class="foot">β οΈ Read-only tool. No automated trades. Rate limit: {RATE_LIMIT_SECONDS}s/request.</div> | |
| </body></html>""" | |
| with open(DASHBOARD_PATH, "w", encoding="utf-8") as f: | |
| f.write(html) | |
| print(f"[HTML] Saved: {DASHBOARD_PATH}") | |
| # ========================================== | |
| # MAIN | |
| # ========================================== | |
| def main(): | |
| max_id = DEFAULT_MAX_ID | |
| price_checks = DEFAULT_PRICE_CHECKS | |
| for i, arg in enumerate(sys.argv[1:]): | |
| if arg == "--max-id" and i + 2 <= len(sys.argv[1:]): | |
| max_id = int(sys.argv[i + 2]) | |
| if arg == "--price-checks" and i + 2 <= len(sys.argv[1:]): | |
| price_checks = int(sys.argv[i + 2]) | |
| print("=" * 50) | |
| print(" NS High-Value Card Tracker (API-Only)") | |
| print(" No dump files needed. No automated trades.") | |
| print(f" Scan range: 1β{max_id} | Price checks: {price_checks}") | |
| print("=" * 50) | |
| start = time.time() | |
| init_db(DB_PATH) | |
| # Step 1: Low ID scan | |
| print(f"\n[SCAN] Scanning cards 1β{max_id} for Legendary/Epic...") | |
| found = scan_low_id_cards(DB_PATH, max_id, callback=scan_callback) | |
| print(f"[SCAN] Done. Found {len(found)} Legendary/Epic cards.") | |
| # Step 2: CTE check | |
| print(f"\n[CTE] Checking cards for dead nations...") | |
| cte = scan_cte_cards(DB_PATH, callback=cte_callback) | |
| print(f"[CTE] Found {len(cte)} dead nation cards.") | |
| # Step 3: Historical check | |
| print(f"\n[HIST] Checking against historical names...") | |
| hist = scan_historical_cards(DB_PATH) | |
| print(f"[HIST] Found {len(hist)} historical name matches.") | |
| # Step 4: Price check | |
| print(f"\n[PRICE] Checking live prices for up to {price_checks} targets...") | |
| alerts, errors = check_target_prices(DB_PATH, price_checks) | |
| if errors: | |
| print(f" β οΈ {errors} API errors during price check") | |
| if alerts: | |
| print(f"\nπ¨ {len(alerts)} UNDERVALUED CARDS:") | |
| for a in alerts: | |
| print(f" {a['name']} (ID:{a['card_id']}) {a['rarity']} β Ask:{a['ask']:.2f} < {a['threshold']}") | |
| print(f" {a['url']}") | |
| else: | |
| print("\nβ No undervalued cards below threshold.") | |
| # Step 5: Dashboard | |
| generate_dashboard(alerts) | |
| elapsed = time.time() - start | |
| print(f"\n{'=' * 50}") | |
| print(f" Done in {elapsed:.0f}s. Open dashboard.html to view.") | |
| print(f"{'=' * 50}") | |
| if __name__ == "__main__": | |
| main() | |