feat: finalize root api.py with X-Auth-Secret auth security and clean pure sales cohort
This commit is contained in:
1 parent
85e6395770
commit
78a46a8dbe
629 files changed
+123965
-755
No files matched your search
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
scripts/scrape_spotify_218.py
|
||||
Extracts verified Spotify followers and monthly listeners for the cohort artists
|
||||
and writes ground truth directly into SQLite.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
load_dotenv(ROOT_DIR / ".env")
|
||||
|
||||
from core.database import get_connection
|
||||
|
||||
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
|
||||
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
|
||||
|
||||
HEADERS_WEB = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/124.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
}
|
||||
|
||||
|
||||
def get_spotify_token():
|
||||
"""Fetches an official application access token from Spotify Web API."""
|
||||
if not CLIENT_ID or not CLIENT_SECRET:
|
||||
return None
|
||||
auth_str = f"{CLIENT_ID}:{CLIENT_SECRET}"
|
||||
b64_auth = base64.b64encode(auth_str.encode()).decode()
|
||||
url = "https://accounts.spotify.com/api/token"
|
||||
headers = {"Authorization": f"Basic {b64_auth}"}
|
||||
data = {"grant_type": "client_credentials"}
|
||||
try:
|
||||
r = requests.post(url, headers=headers, data=data, timeout=10)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("access_token")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def search_spotify_artist(name, token):
|
||||
"""Searches official Spotify API for canonical artist ID and follower count."""
|
||||
if not token:
|
||||
return None
|
||||
url = "https://api.spotify.com/v1/search"
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
params = {"q": name, "type": "artist", "limit": 1}
|
||||
try:
|
||||
r = requests.get(url, headers=headers, params=params, timeout=10)
|
||||
if r.status_code == 200:
|
||||
items = r.json().get("artists", {}).get("items", [])
|
||||
if items:
|
||||
artist = items[0]
|
||||
return {
|
||||
"id": artist["id"],
|
||||
"name": artist["name"],
|
||||
"followers": artist.get("followers", {}).get("total", 0),
|
||||
"popularity": artist.get("popularity", 0),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def scrape_monthly_listeners(spotify_id):
|
||||
"""Parses monthly listeners from Spotify public artist profile page."""
|
||||
url = f"https://open.spotify.com/artist/{spotify_id}"
|
||||
try:
|
||||
r = requests.get(url, headers=HEADERS_WEB, timeout=10)
|
||||
if r.status_code == 200:
|
||||
# Match monthlyListeners in JSON hydration or state payload
|
||||
match_json = re.search(r'"monthlyListeners":\s*(\d+)', r.text)
|
||||
if match_json:
|
||||
return int(match_json.group(1))
|
||||
|
||||
match_text = re.search(r'([\d,]+)\s+monthly\s+listeners', r.text, re.IGNORECASE)
|
||||
if match_text:
|
||||
return int(match_text.group(1).replace(",", ""))
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 65)
|
||||
print("HIFFI INTELLIGENCE: SPOTIFY 218 LIVE EXTRACTION PIPELINE")
|
||||
print("=" * 65)
|
||||
|
||||
token = get_spotify_token()
|
||||
if not token:
|
||||
print("[!] ERROR: SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET is missing or invalid in .env")
|
||||
return
|
||||
|
||||
# Check for cohort CSV
|
||||
csv_path = ROOT_DIR / "data" / "hiffi_multiplatform_500.csv"
|
||||
if not csv_path.exists():
|
||||
csv_path = ROOT_DIR / "cleaned_artists_inventory.csv"
|
||||
|
||||
df = pd.read_csv(csv_path)
|
||||
name_col = "name" if "name" in df.columns else "artist_name"
|
||||
cohort_names = [str(n).strip() for n in df[name_col].dropna()]
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Identify artists missing verified monthly listeners in DB
|
||||
cur.execute("""
|
||||
SELECT a.artist_name
|
||||
FROM artists a
|
||||
JOIN telemetry_metrics t ON a.artist_id = t.artist_id
|
||||
WHERE t.spotify_monthly_listeners IS NULL OR t.spotify_monthly_listeners = 0
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
missing_targets = [r[0] for r in rows if r[0] in cohort_names]
|
||||
|
||||
if not missing_targets:
|
||||
print("[*] All artists already have verified monthly listeners. Checking cohort followers...")
|
||||
missing_targets = cohort_names[:50]
|
||||
|
||||
print(f"Targeting {len(missing_targets)} artists for Spotify collection...\n")
|
||||
|
||||
checkpoint_records = []
|
||||
success = 0
|
||||
|
||||
for idx, artist_name in enumerate(missing_targets, 1):
|
||||
artist_meta = search_spotify_artist(artist_name, token)
|
||||
listeners = 0
|
||||
followers = 0
|
||||
sp_id = None
|
||||
|
||||
if artist_meta:
|
||||
sp_id = artist_meta["id"]
|
||||
followers = artist_meta["followers"]
|
||||
listeners = scrape_monthly_listeners(sp_id)
|
||||
|
||||
# Update DB if we resolved listeners or followers
|
||||
if listeners > 0 or followers > 0:
|
||||
success += 1
|
||||
if listeners > 0:
|
||||
cur.execute("""
|
||||
UPDATE telemetry_metrics
|
||||
SET spotify_monthly_listeners = ?
|
||||
WHERE artist_id IN (
|
||||
SELECT artist_id FROM artists WHERE LOWER(artist_name) = LOWER(?)
|
||||
)
|
||||
""", (listeners, artist_name))
|
||||
|
||||
print(f"[{idx:>3}/{len(missing_targets)}] {artist_name:<25} -> {listeners:,} monthly listeners | {followers:,} followers")
|
||||
else:
|
||||
print(f"[{idx:>3}/{len(missing_targets)}] {artist_name:<25} -> Unresolved on Spotify")
|
||||
|
||||
checkpoint_records.append({
|
||||
"name": artist_name,
|
||||
"spotify_id": sp_id,
|
||||
"spotify_monthly_listeners": listeners,
|
||||
"spotify_followers": followers,
|
||||
})
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
chk_dir = ROOT_DIR / "data" / "checkpoints"
|
||||
chk_dir.mkdir(parents=True, exist_ok=True)
|
||||
chk_path = chk_dir / "spotify_verification_checkpoint.csv"
|
||||
pd.DataFrame(checkpoint_records).to_csv(chk_path, index=False)
|
||||
|
||||
print("\n" + "=" * 65)
|
||||
print(f"Finished. Extracted metrics for {success} / {len(missing_targets)} artists.")
|
||||
print(f"Checkpoint saved to: {chk_path}")
|
||||
print("=" * 65)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user