ns-card-tracker / tracker.py
davidpcm's picture
Upload folder using huggingface_hub
4f5f0d9 verified
Raw
History Blame Contribute Delete
8.95 kB
"""
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()