import json, html, os from datetime import datetime, timezone with open("/tmp/bulk_results.json") as f: results = json.load(f) now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") STATUS_ORDER = { "significant_mismatch": 0, "not_on_spotify": 1, "not_in_insights": 2, "position_drift": 3, "small_difference": 4, "spotify_error": 5, "up_to_date": 6, } STATUS_LABEL = { "up_to_date": "✅ Up to date", "position_drift": "⚠️ Position drift", "small_difference": "⚠️ Small difference", "significant_mismatch": "❌ Significant mismatch", "not_in_insights": "❌ Not in Insights", "not_on_spotify": "❌ Not on Spotify", "spotify_error": "❓ Spotify error", } STATUS_CSS = { "up_to_date": "green", "position_drift": "amber", "small_difference": "amber", "significant_mismatch": "red", "not_in_insights": "red", "not_on_spotify": "red", "spotify_error": "grey", } counts = {} for r in results: s = r["status"] counts[s] = counts.get(s, 0) + 1 ok = counts.get("up_to_date", 0) warn = counts.get("position_drift", 0) + counts.get("small_difference", 0) err = counts.get("significant_mismatch", 0) + counts.get("not_in_insights", 0) + counts.get("not_on_spotify", 0) other = counts.get("spotify_error", 0) results_sorted = sorted(results, key=lambda r: (STATUS_ORDER.get(r["status"], 99), r["playlist_name"].lower())) def fmt_last_updated(ts): if not ts: return "—" try: s = str(ts).split(".")[0].replace("T", " ") return s[:16] # YYYY-MM-DD HH:MM except Exception: return str(ts)[:16] PERSONALIZED_TYPES = {"PERSONALIZED", "ALGORITHMIC", "RADIO"} def diff_color(d): if d == 0: return "green-text" if d <= 5: return "amber-text" return "red-text" def type_badge(ptype): if not ptype: return "—" if ptype in PERSONALIZED_TYPES: return f'{ptype}' return f'{ptype}' def detail_html(r): parts = [] missing = r.get("missing_from_insights", []) extra = r.get("extra_in_insights", []) pos_detail = r.get("position_detail", []) variants = r.get("possible_variants", []) if not (missing or extra or pos_detail or variants): return "" if variants: rows = "".join( f"{v['position']}" f"{v['spotify_isrc']}" f"{v['insights_isrc']}" f"{html.escape(v.get('spotify_name') or '')}" for v in variants) parts.append( f"
" f"

ℹ️ Possible ISRC variants ({len(variants)}): same track at the same position but different ISRCs in Spotify vs Insights. " f"These are excluded from the ISRC diff count. Likely the same recording released under a different ISRC (e.g. regional re-release).

" f"Possible ISRC variants ({len(variants)})" f"{rows}
PosSpotify ISRCInsights ISRCTrack (Spotify name)
") if missing: rows = "".join(f"{t['position']}{t['isrc']}{html.escape(t.get('name') or '')}" for t in missing) parts.append(f"
Missing from Insights ({len(missing)}){rows}
PosISRCTrack
") if extra: rows = "".join(f"{t['position'] if t['position'] is not None else '—'}{t['isrc']}{html.escape(t.get('name') or '')}" for t in extra) parts.append(f"
Extra in Insights ({len(extra)}){rows}
PosISRCTrack
") if pos_detail: def pos_row(t): dcls = "red-text" if abs(t["diff"]) > 5 else "amber-text" sign = "+" if t["diff"] > 0 else "" sp_positions = t.get("spotify_positions", [t["spotify_pos"]]) sp_display = ", ".join(str(p) for p in sp_positions) if len(sp_positions) > 1: sp_display = f'{sp_display} ⚠️' return f"{t['isrc']}{html.escape(t.get('name') or '')}{sp_display}{t['insights_pos']}{sign}{t['diff']}" rows = "".join(pos_row(t) for t in pos_detail) note = "" dup_isrcs = r.get("duplicate_isrcs", {}) if dup_isrcs: dup_list = ", ".join(f"{i} (pos {', '.join(str(p) for p in ps)})" for i, ps in dup_isrcs.items()) note = f"

ℹ️ This playlist contains tracks with duplicate ISRCs on Spotify ({dup_list}). Insights only shows one position per track (duplicates are filtered out). Position comparisons for these tracks will be more meaningful once Insights supports showing duplicate tracks.

" parts.append(f"
{note}Position mismatches ({len(pos_detail)}){rows}
ISRCTrackSpotify pos(s)Insights posDiff
") return "".join(parts) rows_html = [] for idx, r in enumerate(results_sorted): pid = r["playlist_id"] name = html.escape(r["playlist_name"]) status = r["status"] ptype = r.get("playlist_type") label = STATUS_LABEL.get(status, status) if status == "spotify_error" and r.get("spotify_error_code"): label = f"❓ Spotify error (HTTP {r['spotify_error_code']})" cls = STATUS_CSS.get(status, "grey") sp = r["spotify_count"] ins = r["insights_count"] diff = r["isrc_diff"] only_sp = r["only_spotify"] only_ins = r["only_insights"] pos = r["pos_mismatches"] dcls = diff_color(diff) no_detail = status in ("not_on_spotify", "spotify_error") has_insights = status not in ("not_in_insights", "not_on_spotify", "spotify_error") insights_link = f'(Insights Link)' if has_insights else "" show_last_updated = status != "up_to_date" last_updated = fmt_last_updated(r.get("insights_latest")) if show_last_updated else "" drilldown = detail_html(r) has_drilldown = bool(drilldown) clickable = ' class="clickable"' if has_drilldown else "" rows_html.append(f""" {name}{'   ' + insights_link if insights_link else ""} {label} {type_badge(ptype)} {sp if sp else "—"} {ins if ins else "—"} {diff if not no_detail else "—"} {only_sp if not no_detail else "—"} {only_ins if not no_detail else "—"} {pos if not no_detail else "—"} {last_updated} """) if has_drilldown: rows_html.append(f'
{drilldown}
') rows = "\n".join(rows_html) html_doc = f""" Priority Playlist Status Report

Priority Playlist Status Report

Generated {now}  ·  {len(results)} playlists checked  ·  {ok} ✅   {warn} ⚠️   {err} ❌

{counts.get('up_to_date',0)}
✅ Up to date
{counts.get('position_drift',0)}
⚠️ Position drift
{counts.get('small_difference',0)}
⚠️ Small difference
1–5 ISRC gap — may be null-ISRC or unavailable tracks; requires investigation
{counts.get('significant_mismatch',0)}
❌ Significant mismatch
{counts.get('not_in_insights',0)}
❌ Not in Insights
{counts.get('not_on_spotify',0)}
❌ Not on Spotify
Playlist deleted or unpublished on Spotify (404)
{counts.get('spotify_error',0)}
❓ Spotify error
{rows}
Playlist Status Type Spotify tracks Insights tracks ISRC diff Missing from Insights Extra in Insights Position mismatches Last updated (Insights)
""" out = os.path.expanduser("~/Downloads/priority_playlist_status_report.html") with open(out, "w") as f: f.write(html_doc) print(f"Written: {out}")