2026-11-06 17:27:51 +05:30
|
|
|
|
"""
|
|
|
|
|
|
scripts/fast_sales_ingestion_17k.py
|
2026-11-10 14:43:27 +05:30
|
|
|
|
High-speed multithreaded Wikipedia pure sales crawler for HIFFI catalog.
|
|
|
|
|
|
Directly populates pure_album_sales, audited_sales_units, and provenance_flag.
|
2026-11-06 17:27:51 +05:30
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
|
import json
|
|
|
|
|
|
import time
|
|
|
|
|
|
import re
|
|
|
|
|
|
import urllib.parse
|
|
|
|
|
|
import urllib.request
|
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
|
|
DB_PATH = "data/hiffi_catalog.db"
|
|
|
|
|
|
WIKI_API_URL = "https://en.wikipedia.org/w/api.php"
|
|
|
|
|
|
HEADERS = {"User-Agent": "HIFFI-FastScraper/1.0 (sri.lekha@superlabs.co)"}
|
|
|
|
|
|
MAX_WORKERS = 8
|
|
|
|
|
|
db_lock = threading.Lock()
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
|
2026-11-06 17:27:51 +05:30
|
|
|
|
def clean_artist_query(name: str) -> str:
|
|
|
|
|
|
clean = re.sub(r'[,$\'\"]', ' ', name)
|
|
|
|
|
|
return re.sub(r'\s+', ' ', clean).strip()
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
|
2026-11-06 17:27:51 +05:30
|
|
|
|
def search_best_discography_title(artist_name: str) -> str:
|
|
|
|
|
|
clean_name = clean_artist_query(artist_name)
|
|
|
|
|
|
queries = [
|
|
|
|
|
|
f"{artist_name} albums discography",
|
|
|
|
|
|
f"{clean_name} discography",
|
|
|
|
|
|
f"{artist_name} discography",
|
|
|
|
|
|
artist_name
|
|
|
|
|
|
]
|
|
|
|
|
|
for q in queries:
|
|
|
|
|
|
params = {
|
|
|
|
|
|
"action": "opensearch",
|
|
|
|
|
|
"search": q,
|
|
|
|
|
|
"limit": "4",
|
|
|
|
|
|
"namespace": "0",
|
|
|
|
|
|
"format": "json"
|
|
|
|
|
|
}
|
|
|
|
|
|
url = f"{WIKI_API_URL}?{urllib.parse.urlencode(params)}"
|
|
|
|
|
|
try:
|
|
|
|
|
|
req = urllib.request.Request(url, headers=HEADERS)
|
2026-11-10 14:43:27 +05:30
|
|
|
|
with urllib.request.urlopen(req, timeout=6) as resp:
|
2026-11-06 17:27:51 +05:30
|
|
|
|
data = json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
|
titles = data[1] if len(data) > 1 else []
|
|
|
|
|
|
for t in titles:
|
|
|
|
|
|
if "albums discography" in t.lower():
|
|
|
|
|
|
return t
|
|
|
|
|
|
for t in titles:
|
|
|
|
|
|
if "discography" in t.lower():
|
|
|
|
|
|
return t
|
2026-11-10 14:43:27 +05:30
|
|
|
|
if titles:
|
2026-11-06 17:27:51 +05:30
|
|
|
|
return titles[0]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
continue
|
2026-11-10 14:43:27 +05:30
|
|
|
|
return f"{clean_name} discography"
|
2026-11-06 17:27:51 +05:30
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
|
|
|
|
|
|
def parse_pure_sales_from_wikitext(wikitext: str) -> float:
|
|
|
|
|
|
pure_sales = 0.0
|
|
|
|
|
|
sales_patterns = [
|
|
|
|
|
|
r'sales\s*=\s*([0-9,]+)',
|
|
|
|
|
|
r'sales\s*=\s*approx\.\s*([0-9,]+)',
|
|
|
|
|
|
r'US:\s*([0-9,]+)',
|
|
|
|
|
|
r'WW:\s*([0-9,]+)',
|
|
|
|
|
|
r'([0-9,]+)\s*(?:copies|units)\s*sold'
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in sales_patterns:
|
|
|
|
|
|
matches = re.findall(pattern, wikitext, re.IGNORECASE)
|
|
|
|
|
|
for m in matches:
|
|
|
|
|
|
cleaned = re.sub(r'[^0-9]', '', m)
|
|
|
|
|
|
if cleaned and len(cleaned) >= 4:
|
|
|
|
|
|
val = float(cleaned)
|
|
|
|
|
|
if val < 500000000:
|
|
|
|
|
|
pure_sales += val
|
|
|
|
|
|
|
|
|
|
|
|
cert_weights = {
|
|
|
|
|
|
r'\|\s*Gold\b': 500000.0,
|
|
|
|
|
|
r'\|\s*Platinum\b': 1000000.0,
|
|
|
|
|
|
r'\|\s*2×\s*Platinum\b': 2000000.0,
|
|
|
|
|
|
r'\|\s*3×\s*Platinum\b': 3000000.0,
|
|
|
|
|
|
r'\|\s*Diamond\b': 10000000.0
|
|
|
|
|
|
}
|
|
|
|
|
|
for cert_regex, cert_val in cert_weights.items():
|
|
|
|
|
|
count = len(re.findall(cert_regex, wikitext, re.IGNORECASE))
|
|
|
|
|
|
if count > 0:
|
|
|
|
|
|
pure_sales += (count * cert_val)
|
|
|
|
|
|
|
|
|
|
|
|
return pure_sales
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_single_artist(artist: dict) -> tuple:
|
|
|
|
|
|
aid = artist["artist_id"]
|
|
|
|
|
|
name = artist["artist_name"]
|
|
|
|
|
|
|
|
|
|
|
|
discog_title = search_best_discography_title(name)
|
2026-11-06 17:27:51 +05:30
|
|
|
|
params = {
|
|
|
|
|
|
"action": "query",
|
|
|
|
|
|
"prop": "revisions",
|
|
|
|
|
|
"rvprop": "content",
|
|
|
|
|
|
"rvslots": "main",
|
2026-11-10 14:43:27 +05:30
|
|
|
|
"titles": discog_title,
|
|
|
|
|
|
"format": "json"
|
2026-11-06 17:27:51 +05:30
|
|
|
|
}
|
|
|
|
|
|
url = f"{WIKI_API_URL}?{urllib.parse.urlencode(params)}"
|
2026-11-10 14:43:27 +05:30
|
|
|
|
sales_units = 0.0
|
|
|
|
|
|
|
2026-11-06 17:27:51 +05:30
|
|
|
|
try:
|
2026-11-10 14:43:27 +05:30
|
|
|
|
req = urllib.request.Request(url, headers=HEADERS)
|
|
|
|
|
|
with urllib.request.urlopen(req, timeout=8) as resp:
|
2026-11-06 17:27:51 +05:30
|
|
|
|
data = json.loads(resp.read().decode("utf-8"))
|
|
|
|
|
|
pages = data.get("query", {}).get("pages", {})
|
2026-11-10 14:43:27 +05:30
|
|
|
|
for pid, pdata in pages.items():
|
|
|
|
|
|
if pid != "-1":
|
|
|
|
|
|
revisions = pdata.get("revisions", [])
|
|
|
|
|
|
if revisions:
|
|
|
|
|
|
wikitext = revisions[0].get("slots", {}).get("main", {}).get("*", "")
|
|
|
|
|
|
sales_units = parse_pure_sales_from_wikitext(wikitext)
|
2026-11-06 17:27:51 +05:30
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
if sales_units > 0:
|
|
|
|
|
|
prov = "WIKI_DISCOGRAPHY_AUDIT"
|
|
|
|
|
|
awards = int(sales_units // 500000)
|
2026-11-06 17:27:51 +05:30
|
|
|
|
else:
|
|
|
|
|
|
sales_units, awards = 0.0, 0
|
2026-11-10 14:43:27 +05:30
|
|
|
|
prov = "UNVERIFIED_NO_RECORDS"
|
|
|
|
|
|
|
|
|
|
|
|
return (sales_units, sales_units, awards, prov, aid)
|
|
|
|
|
|
|
2026-11-06 17:27:51 +05:30
|
|
|
|
|
|
|
|
|
|
def run_fast_ingestion(limit=3000):
|
|
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
|
|
|
|
conn.row_factory = sqlite3.Row
|
|
|
|
|
|
cur = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
rows = cur.execute("""
|
|
|
|
|
|
SELECT a.artist_id, a.artist_name
|
|
|
|
|
|
FROM artists a
|
2026-11-10 14:43:27 +05:30
|
|
|
|
JOIN commercial_sales cs ON a.artist_id = cs.artist_id
|
|
|
|
|
|
WHERE (cs.pure_album_sales IS NULL OR cs.pure_album_sales = 0)
|
|
|
|
|
|
AND (cs.provenance_flag IS NULL OR cs.provenance_flag NOT IN ('VERIFIED_CSPC', 'VERIFIED_RIAA', 'WIKI_DISCOGRAPHY_AUDIT'))
|
|
|
|
|
|
ORDER BY a.artist_id ASC
|
2026-11-06 17:27:51 +05:30
|
|
|
|
LIMIT ?
|
|
|
|
|
|
""", (limit,)).fetchall()
|
2026-11-10 14:43:27 +05:30
|
|
|
|
|
2026-11-06 17:27:51 +05:30
|
|
|
|
total = len(rows)
|
|
|
|
|
|
if total == 0:
|
2026-11-10 14:43:27 +05:30
|
|
|
|
print("✓ All catalog artists already audited with pure sales data.")
|
2026-11-06 17:27:51 +05:30
|
|
|
|
conn.close()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"🚀 Starting high-speed ingestion for {total} artists with {MAX_WORKERS} concurrent threads...")
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
update_sql = """
|
|
|
|
|
|
UPDATE commercial_sales
|
|
|
|
|
|
SET pure_album_sales = ?,
|
|
|
|
|
|
audited_sales_units = ?,
|
|
|
|
|
|
awards_count = ?,
|
|
|
|
|
|
provenance_flag = ?
|
|
|
|
|
|
WHERE artist_id = ?
|
2026-11-06 17:27:51 +05:30
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
completed = 0
|
|
|
|
|
|
buffer = []
|
|
|
|
|
|
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
|
|
|
|
|
futures = {executor.submit(process_single_artist, dict(r)): r for r in rows}
|
|
|
|
|
|
for future in as_completed(futures):
|
|
|
|
|
|
try:
|
|
|
|
|
|
record = future.result()
|
|
|
|
|
|
buffer.append(record)
|
|
|
|
|
|
completed += 1
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
if len(buffer) >= 25:
|
2026-11-06 17:27:51 +05:30
|
|
|
|
with db_lock:
|
2026-11-10 14:43:27 +05:30
|
|
|
|
cur.executemany(update_sql, buffer)
|
2026-11-06 17:27:51 +05:30
|
|
|
|
conn.commit()
|
|
|
|
|
|
elapsed = time.time() - start_time
|
|
|
|
|
|
rate = round(completed / elapsed, 1)
|
|
|
|
|
|
print(f" ⚡ Ingested {completed}/{total} | {rate} artists/sec | Elapsed: {int(elapsed)}s")
|
|
|
|
|
|
buffer.clear()
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
except Exception:
|
2026-11-06 17:27:51 +05:30
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if buffer:
|
|
|
|
|
|
with db_lock:
|
2026-11-10 14:43:27 +05:30
|
|
|
|
cur.executemany(update_sql, buffer)
|
2026-11-06 17:27:51 +05:30
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
total_time = round(time.time() - start_time, 1)
|
|
|
|
|
|
print(f"✓ Fast batch complete: {completed} artists processed in {total_time}s.")
|
|
|
|
|
|
|
2026-11-10 14:43:27 +05:30
|
|
|
|
|
2026-11-06 17:27:51 +05:30
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
import sys
|
|
|
|
|
|
count = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
|
2026-11-10 14:43:27 +05:30
|
|
|
|
run_fast_ingestion(count)
|