#!/usr/bin/env python3 """Filter SMF token dump to only valid (refreshable) tokens. Attempts to refresh each token and writes successful ones to an output file. Does NOT call Spotify data APIs — just validates the token refresh works. Usage: export SPOTIFY_CLIENT_ID="..." export SPOTIFY_CLIENT_SECRET="..." python3 scripts/filter_valid_tokens.py \ --file ~/Downloads/spotify-smf/spotify.json \ --output /tmp/smf-valid-tokens.json \ --limit 100000 \ --workers 50 """ import argparse import base64 import json import logging import os import sys import time from concurrent.futures import ThreadPoolExecutor, as_completed import requests logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)-7s %(message)s", datefmt="%H:%M:%S", ) logger = logging.getLogger(__name__) def load_tokens(path, limit, offset=0): """Load tokens from a local JSON file (flat array of strings).""" logger.info("Loading tokens from %s (offset=%d, limit=%s)", path, offset, limit) tokens = [] skipped = 0 with open(path) as f: for line in f: line = line.strip().strip(",") if line.startswith('"AQ'): if skipped < offset: skipped += 1 continue tokens.append(line.strip('"')) if limit and len(tokens) >= limit: break logger.info("Loaded %d tokens", len(tokens)) return tokens def validate_token(token, client_id, client_secret): """Try to refresh a token. Returns the new refresh token (or original) on success, None on failure.""" auth_b64 = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() try: resp = requests.post( "https://accounts.spotify.com/api/token", data={"grant_type": "refresh_token", "refresh_token": token}, headers={ "Authorization": f"Basic {auth_b64}", "Content-Type": "application/x-www-form-urlencoded", }, timeout=10, ) if resp.status_code == 200: data = resp.json() # Return the new refresh token if rotated, otherwise original new_token = data.get("refresh_token", token) return new_token return None except requests.RequestException: return None def main(): parser = argparse.ArgumentParser(description="Filter SMF tokens to valid-only") parser.add_argument("--file", required=True, help="Input token JSON file") parser.add_argument("--output", required=True, help="Output file for valid tokens") parser.add_argument("--limit", type=int, default=0, help="Max tokens to check (0=all)") parser.add_argument("--offset", type=int, default=0, help="Skip first N tokens") parser.add_argument("--workers", type=int, default=50, help="Concurrent workers (default: 50)") args = parser.parse_args() client_id = os.environ.get("SPOTIFY_CLIENT_ID") client_secret = os.environ.get("SPOTIFY_CLIENT_SECRET") if not client_id or not client_secret: print("ERROR: Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET env vars") sys.exit(1) tokens = load_tokens(args.file, args.limit, args.offset) if not tokens: print("No tokens loaded.") sys.exit(1) logger.info("Validating %d tokens with %d workers", len(tokens), args.workers) valid_tokens = [] failed = 0 start = time.monotonic() with ThreadPoolExecutor(max_workers=args.workers) as executor: futures = { executor.submit(validate_token, t, client_id, client_secret): i for i, t in enumerate(tokens) } done = 0 for future in as_completed(futures): done += 1 result = future.result() if result: valid_tokens.append(result) else: failed += 1 if done % 1000 == 0: elapsed = time.monotonic() - start rate = done / elapsed logger.info( "Progress: %d/%d checked (%.0f/s), %d valid, %d failed", done, len(tokens), rate, len(valid_tokens), failed, ) elapsed = time.monotonic() - start logger.info( "Done in %.1fs: %d valid, %d failed (%.1f%% valid rate)", elapsed, len(valid_tokens), failed, len(valid_tokens) / len(tokens) * 100 if tokens else 0, ) # Write valid tokens as JSON array with open(args.output, "w") as f: json.dump(valid_tokens, f) logger.info("Wrote %d valid tokens to %s", len(valid_tokens), args.output) if __name__ == "__main__": main()