157 lines
5.7 KiB
Python
157 lines
5.7 KiB
Python
"""
|
|||
|
|
scripts/fetch_spotify_verified_faces.py
|
||
|
|
Fetches the 100% authentic, verified label profile portrait for every artist
|
||
|
|
in the 500 cohort directly from Spotify's Web API.
|
||
|
|
"""
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import base64
|
||
|
|
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
|
||
|
|
|
||
|
|
SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
|
||
|
|
SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
|
||
|
|
|
||
|
|
def get_spotify_token():
|
||
|
|
if not SPOTIFY_CLIENT_ID or not SPOTIFY_CLIENT_SECRET:
|
||
|
|
print("ERROR: SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET is missing from .env")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
auth_str = f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}"
|
||
|
|
b64_auth = base64.b64encode(auth_str.encode()).decode()
|
||
|
|
|
||
|
|
url = "https://accounts.spotify.com/api/token"
|
||
|
|
headers = {
|
||
|
|
"Authorization": f"Basic {b64_auth}",
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
||
|
|
}
|
||
|
|
data = {"grant_type": "client_credentials"}
|
||
|
|
r = requests.post(url, headers=headers, data=data, timeout=10)
|
||
|
|
if r.status_code == 200:
|
||
|
|
return r.json().get("access_token")
|
||
|
|
else:
|
||
|
|
print(f"Failed to obtain Spotify token: {r.status_code} - {r.text}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
def chunk_list(lst, size=50):
|
||
|
|
for i in range(0, len(lst), size):
|
||
|
|
yield lst[i:i + size]
|
||
|
|
|
||
|
|
def main():
|
||
|
|
print("=" * 65)
|
||
|
|
print("HIFFI INTELLIGENCE: SPOTIFY OFFICIAL ARTIST PORTRAIT ENGINE")
|
||
|
|
print("=" * 65)
|
||
|
|
|
||
|
|
token = get_spotify_token()
|
||
|
|
headers = {"Authorization": f"Bearer {token}"}
|
||
|
|
|
||
|
|
csv_path = ROOT_DIR / "data" / "hiffi_multiplatform_500.csv"
|
||
|
|
if not csv_path.exists():
|
||
|
|
csv_path = ROOT_DIR / "cleaned_artists_inventory.csv"
|
||
|
|
|
||
|
|
df_csv = pd.read_csv(csv_path)
|
||
|
|
name_col = "name" if "name" in df_csv.columns else "artist_name"
|
||
|
|
cohort = [str(n).strip() for n in df_csv[name_col].dropna()]
|
||
|
|
|
||
|
|
conn = get_connection()
|
||
|
|
cur = conn.cursor()
|
||
|
|
|
||
|
|
placeholders = ",".join(["?"] * len(cohort))
|
||
|
|
query = f"""
|
||
|
|
SELECT artist_id, artist_name, spotify_url
|
||
|
|
FROM artists
|
||
|
|
WHERE artist_name IN ({placeholders})
|
||
|
|
"""
|
||
|
|
cur.execute(query, cohort)
|
||
|
|
rows = cur.fetchall()
|
||
|
|
|
||
|
|
id_to_artist = {}
|
||
|
|
direct_search_artists = []
|
||
|
|
|
||
|
|
for artist_id, name, sp_url in rows:
|
||
|
|
sp_url = sp_url or ""
|
||
|
|
if "/artist/" in sp_url:
|
||
|
|
sp_id = sp_url.split("/artist/")[-1].split("?")[0].strip("/")
|
||
|
|
id_to_artist[sp_id] = (artist_id, name)
|
||
|
|
else:
|
||
|
|
direct_search_artists.append((artist_id, name))
|
||
|
|
|
||
|
|
print(f"Direct Spotify Artist IDs mapped: {len(id_to_artist)}")
|
||
|
|
print(f"Artists needing search lookup : {len(direct_search_artists)}\n")
|
||
|
|
|
||
|
|
updated = 0
|
||
|
|
|
||
|
|
# 1. Batch fetch official portraits via Spotify's /v1/artists endpoint (50 at a time)
|
||
|
|
sp_ids = list(id_to_artist.keys())
|
||
|
|
for batch_num, batch in enumerate(chunk_list(sp_ids, 50), start=1):
|
||
|
|
ids_str = ",".join(batch)
|
||
|
|
url = f"https://api.spotify.com/v1/artists?ids={ids_str}"
|
||
|
|
try:
|
||
|
|
r = requests.get(url, headers=headers, timeout=12)
|
||
|
|
if r.status_code == 200:
|
||
|
|
artists_data = r.json().get("artists", [])
|
||
|
|
for a in artists_data:
|
||
|
|
if not a:
|
||
|
|
continue
|
||
|
|
sp_id = a.get("id")
|
||
|
|
images = a.get("images", [])
|
||
|
|
if images and sp_id in id_to_artist:
|
||
|
|
# Grab the highest resolution portrait
|
||
|
|
img_url = images[0].get("url")
|
||
|
|
art_id, art_name = id_to_artist[sp_id]
|
||
|
|
cur.execute("""
|
||
|
|
UPDATE artists
|
||
|
|
SET image_url = ?,
|
||
|
|
image_verified_flag = 1
|
||
|
|
WHERE artist_id = ?
|
||
|
|
""", (img_url, art_id))
|
||
|
|
updated += 1
|
||
|
|
conn.commit()
|
||
|
|
print(f"Batch {batch_num:>2}: Updated {len(artists_data)} verified portraits.")
|
||
|
|
else:
|
||
|
|
print(f"Batch {batch_num} error {r.status_code}: {r.text[:80]}")
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Batch {batch_num} exception: {e}")
|
||
|
|
|
||
|
|
# 2. Resolve the remaining search artists
|
||
|
|
for idx, (artist_id, name) in enumerate(direct_search_artists, start=1):
|
||
|
|
url = f"https://api.spotify.com/v1/search?q={name}&type=artist&limit=1"
|
||
|
|
try:
|
||
|
|
r = requests.get(url, headers=headers, timeout=8)
|
||
|
|
if r.status_code == 200:
|
||
|
|
items = r.json().get("artists", {}).get("items", [])
|
||
|
|
if items:
|
||
|
|
images = items[0].get("images", [])
|
||
|
|
if images:
|
||
|
|
img_url = images[0].get("url")
|
||
|
|
sp_canonical = items[0].get("external_urls", {}).get("spotify")
|
||
|
|
cur.execute("""
|
||
|
|
UPDATE artists
|
||
|
|
SET image_url = ?,
|
||
|
|
spotify_url = COALESCE(NULLIF(spotify_url, ''), ?),
|
||
|
|
image_verified_flag = 1
|
||
|
|
WHERE artist_id = ?
|
||
|
|
""", (img_url, sp_canonical, artist_id))
|
||
|
|
updated += 1
|
||
|
|
print(f"[Search {idx:>2}/{len(direct_search_artists)}] Verified portrait for {name}")
|
||
|
|
conn.commit()
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Error on {name}: {e}")
|
||
|
|
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
print("\n" + "=" * 65)
|
||
|
|
print(f"Successfully populated authentic Spotify portraits for {updated} artists.")
|
||
|
|
print("=" * 65)
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|