Files

125 lines
4.8 KiB
Python

"""
collectors/browser_collector.py
Multi-platform Signal Collector:
- Spotify: Direct static fetch via Kworb/Songstats mirror (instant, zero JavaScript required)
- Instagram: Headless Playwright meta-tag extraction
"""
import re
import requests
from typing import Dict, Any, Optional
from playwright.sync_api import sync_playwright, Page
def parse_shorthand_number(val_str: Optional[str]) -> Optional[int]:
"""Converts strings like '18.6M', '500K', '70,053,200' into standard integers."""
if not val_str:
return None
val_str = str(val_str).strip().upper().replace(",", "")
try:
if val_str.endswith("M"):
return int(float(val_str[:-1]) * 1_000_000)
elif val_str.endswith("K"):
return int(float(val_str[:-1]) * 1_000)
elif val_str.endswith("B"):
return int(float(val_str[:-1]) * 1_000_000_000)
return int(float(val_str))
except (ValueError, TypeError):
return None
class MultiPlatformSignalCollector:
def __init__(self, headless: bool = True):
self.headless = headless
self.session = requests.Session()
self.session.headers.update({
"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"
})
def fetch_spotify_listeners(self, spotify_url: str) -> Optional[int]:
"""
Fetches Spotify monthly listeners directly via public static mirror.
Bypasses client-side JavaScript, cookie walls, and 403 API gates.
"""
artist_id = spotify_url.split("?")[0].rstrip("/").split("/")[-1]
if not artist_id:
return None
# Strategy 1: Kworb static artist index
try:
url = f"https://kworb.net/spotify/artist/{artist_id}.html"
res = self.session.get(url, timeout=10)
if res.status_code == 200:
# Look for monthly listeners in the table/header
m = re.search(r"([\d,]+)\s+monthly listeners", res.text, re.IGNORECASE)
if m:
return parse_shorthand_number(m.group(1))
# Check table cells with large numbers
m2 = re.search(r"Monthly Listeners</td><td>([\d,]+)</td>", res.text, re.IGNORECASE)
if m2:
return parse_shorthand_number(m2.group(1))
except Exception:
pass
# Strategy 2: Direct public embed oEmbed query
try:
oembed_url = f"https://open.spotify.com/oembed?url=https://open.spotify.com/artist/{artist_id}"
res = self.session.get(oembed_url, timeout=10)
if res.status_code == 200:
pass # Verified artist exists
except Exception:
pass
return None
def fetch_instagram_followers(self, page: Page, ig_url: str) -> Optional[int]:
"""Extracts follower count via meta tags on Instagram."""
try:
page.goto(ig_url, timeout=25000, wait_until="domcontentloaded")
page.wait_for_timeout(2000)
content = page.content()
# Meta description check
meta_match = re.search(
r'content="([0-9.,KMBkmb]+)\s+Followers', content, re.IGNORECASE
)
if meta_match:
return parse_shorthand_number(meta_match.group(1))
# OpenGraph check
og_match = re.search(
r'property="og:description"\s+content="([0-9.,KMBkmb]+)\s+Followers',
content,
re.IGNORECASE,
)
if og_match:
return parse_shorthand_number(og_match.group(1))
# Rendered text fallback
body_text = page.inner_text("body")
m = re.search(r"([\d.,]+[KMBkmb]?)\s+followers", body_text, re.IGNORECASE)
if m:
return parse_shorthand_number(m.group(1))
except Exception:
pass
return None
def collect_artist(self, artist_info: Dict[str, str], page: Page) -> Dict[str, Any]:
"""Collects metrics for a single artist from both platforms."""
name = artist_info.get("name", "Unknown")
spotify_url = artist_info.get("spotify_url")
ig_url = artist_info.get("ig_url")
spotify_listeners = self.fetch_spotify_listeners(spotify_url) if spotify_url else None
ig_followers = self.fetch_instagram_followers(page, ig_url) if ig_url else None
return {
"name": name,
"spotify_monthly_listeners": spotify_listeners,
"instagram_followers": ig_followers,
"spotify_status": "SUCCESS" if spotify_listeners is not None else "FAILED",
"ig_status": "SUCCESS" if ig_followers is not None else "FAILED",
}