128 lines
3.7 KiB
Python
128 lines
3.7 KiB
Python
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()
|