Files

128 lines
3.7 KiB
Python
Raw Permalink Normal View History

import os
import re
from bs4 import BeautifulSoup
from curl_cffi import requests
import pandas as pd
from rapidfuzz import fuzz
DATA_DIR = "data"
CATALOG_PATH = os.path.join(DATA_DIR, "master_artists_intelligence_17k.csv")
OUTPUT_PATH = os.path.join(DATA_DIR, "master_artists_intelligence_17k.csv")
KWORB_STREAMS_URL = "https://kworb.net/spotify/artists.html"
KWORB_LISTENERS_URL = "https://kworb.net/spotify/listeners.html"
def normalize_name(text: str) -> str:
if not text:
return ""
cleaned = re.sub(r"\(.*?\)|\[.*?\]", "", str(text))
cleaned = re.sub(r"[^\w\s]", "", cleaned)
return cleaned.strip().lower()
def fetch_kworb_master_table() -> dict:
print("Fetching Kworb master stream table...")
session = requests.Session()
resp = session.get(KWORB_STREAMS_URL, impersonate="chrome120", timeout=15)
if resp.status_code != 200:
raise ConnectionError(
f"Failed to fetch Kworb streams table (Status: {resp.status_code})"
)
soup = BeautifulSoup(resp.text, "html.parser")
table = soup.find("table")
if not table:
raise ValueError("No table found on Kworb page.")
kworb_data = {}
rows = table.find_all("tr")
# Table format: Artist | Total Streams (M) | Daily (M) | As Lead (M) | Solo (M) | As Feature (M)
for row in rows[1:]:
cells = row.find_all("td")
if len(cells) < 2:
continue
artist_anchor = cells[0].find("a")
artist_name = (
artist_anchor.get_text(strip=True)
if artist_anchor
else cells[0].get_text(strip=True)
)
try:
# Kworb streams are formatted in millions (e.g., '143,818.8' means 143.8 billion)
stream_str = cells[1].get_text(strip=True).replace(",", "")
total_streams = int(float(stream_str) * 1_000_000)
except (ValueError, IndexError):
total_streams = 0
norm_key = normalize_name(artist_name)
if norm_key and total_streams > 0:
kworb_data[norm_key] = {
"kworb_artist_name": artist_name,
"spotify_total_streams": total_streams,
}
print(f"Loaded {len(kworb_data):,} artists with lifetime stream metrics.")
return kworb_data
def run_kworb_enrichment():
df = pd.read_csv(CATALOG_PATH)
kworb_dict = fetch_kworb_master_table()
if "spotify_total_streams" not in df.columns:
df["spotify_total_streams"] = None
unverified_mask = df["catalog_intelligence_tier"].str.contains(
"TIER_3_UNAUDITED", na=False
)
unverified_indices = df[unverified_mask].index
print(
f"Matching Kworb streaming metrics against {len(unverified_indices):,}"
" unverified artists..."
)
matches = 0
for idx in unverified_indices:
a_name = str(df.at[idx, "artist_name"])
c_name = normalize_name(a_name)
# 1. Exact match
if c_name in kworb_dict:
entry = kworb_dict[c_name]
df.at[idx, "spotify_total_streams"] = entry["spotify_total_streams"]
df.at[idx, "catalog_intelligence_tier"] = (
"TIER_3A_VERIFIED_DIGITAL_CATALOG"
)
matches += 1
continue
# 2. Strict token match for names longer than 5 chars
if len(c_name) > 5:
for k_key, entry in kworb_dict.items():
if fuzz.token_sort_ratio(c_name, k_key) >= 92:
df.at[idx, "spotify_total_streams"] = entry["spotify_total_streams"]
df.at[idx, "catalog_intelligence_tier"] = (
"TIER_3A_VERIFIED_DIGITAL_CATALOG"
)
matches += 1
break
print(
f"\nEnrichment complete! Successfully matched {matches:,} previously"
" unverified artists."
)
print("\nUpdated Catalog Intelligence Tiers:")
print(df["catalog_intelligence_tier"].value_counts())
df.to_csv(OUTPUT_PATH, index=False)
print(f"Saved updated catalog to: {OUTPUT_PATH}")
if __name__ == "__main__":
run_kworb_enrichment()