Last active
March 5, 2026 22:22
-
-
Save stig/302cb0e9c87dcad29f0b5e5e54f16719 to your computer and use it in GitHub Desktop.
Parses a SongBook Pro .sbpbackup file and prints venue statistics and song play counts. Requires no dependencies beyond the Python standard library.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| SongBook Pro Backup - Statistics Generator | |
| Analyses gigs, venues, and song play counts from a .sbpbackup file. | |
| Usage: python3 sbp_stats.py [path/to/file.sbpbackup] | |
| """ | |
| import json | |
| import sys | |
| import zipfile | |
| from collections import defaultdict, Counter | |
| from datetime import datetime | |
| # ── helpers ────────────────────────────────────────────────────────────────── | |
| def load_backup(path: str) -> dict: | |
| with zipfile.ZipFile(path) as zf: | |
| with zf.open("dataFile.txt") as f: | |
| f.readline() # version line | |
| return json.load(f) | |
| def infer_venue(name: str) -> str: | |
| """Extract venue from set name. | |
| Supports 'event @ venue' convention; falls back to the full name.""" | |
| if ' @ ' in name: | |
| return name.split(' @ ', 1)[1].strip() | |
| return name.strip() | |
| def is_gig(set_detail: dict) -> bool: | |
| """Exclude future / template / deleted sets.""" | |
| if set_detail.get('Deleted'): | |
| return False | |
| date_str = set_detail.get('date', '') | |
| try: | |
| date = datetime.fromisoformat(date_str.replace('Z', '')) | |
| return date <= datetime.now() | |
| except Exception: | |
| return True | |
| # ── main ───────────────────────────────────────────────────────────────────── | |
| def main(): | |
| path = sys.argv[1] if len(sys.argv) > 1 else "SongbookPro Backup.sbpbackup" | |
| print(f"Loading: {path}\n") | |
| data = load_backup(path) | |
| songs_by_id = {s['Id']: s for s in data['songs']} | |
| deleted_song_ids = {s['Id'] for s in data['songs'] if s.get('Deleted')} | |
| def active_items(contents): | |
| """Set content items that are not deleted and reference a non-deleted song.""" | |
| return [ | |
| c for c in contents | |
| if not c.get('Deleted') | |
| and c.get('ItemType', 1) == 1 | |
| and c.get('SongId') not in deleted_song_ids | |
| ] | |
| # ── filter to past gigs ─────────────────────────────────────────────────── | |
| gigs = [s for s in data['sets'] if is_gig(s['details'])] | |
| gigs.sort(key=lambda s: s['details']['date']) | |
| total_songs_played = sum(len(active_items(g['contents'])) for g in gigs) | |
| print(f"Total gigs: {len(gigs)} Songs played (with repeats): {total_songs_played}") | |
| # ── venue stats (only if any set names use the '@ venue' convention) ──────── | |
| has_venues = any(' @ ' in g['details']['name'] for g in gigs) | |
| if has_venues: | |
| venue_gigs = Counter() | |
| venue_songs = Counter() | |
| for g in gigs: | |
| d = g['details'] | |
| venue = infer_venue(d['name']) | |
| songs = active_items(g['contents']) | |
| venue_gigs[venue] += 1 | |
| venue_songs[venue] += len(songs) | |
| print() | |
| print("=" * 65) | |
| print("VENUE STATISTICS") | |
| print("=" * 65) | |
| print(f"{'Venue':<38} {'Gigs':>6} {'Songs played':>13}") | |
| print("-" * 65) | |
| for venue, count in venue_gigs.most_common(): | |
| print(f"{venue:<38} {count:>6} {venue_songs[venue]:>13}") | |
| # ── song play counts ───────────────────────────────────────────────────── | |
| song_plays = Counter() # song_id -> times played | |
| song_gig_dates = defaultdict(list) # song_id -> [date, ...] | |
| for g in gigs: | |
| date = g['details']['date'][:10] | |
| seen_in_gig = set() | |
| for item in active_items(g['contents']): | |
| sid = item.get('SongId') | |
| if sid and sid not in seen_in_gig: | |
| song_plays[sid] += 1 | |
| song_gig_dates[sid].append(date) | |
| seen_in_gig.add(sid) | |
| print() | |
| print("=" * 65) | |
| print("SONG PLAY COUNTS (songs played at 3+ gigs)") | |
| print("=" * 65) | |
| print(f"{'Song':<32} {'Artist':<22} {'Plays':>5} {'Last played':<12}") | |
| print("-" * 65) | |
| for sid, plays in song_plays.most_common(): | |
| if plays < 3: | |
| break | |
| song = songs_by_id.get(sid) | |
| if not song: | |
| continue | |
| name = song['name'][:32] | |
| artist = song['author'][:22] | |
| last = max(song_gig_dates[sid]) | |
| print(f"{name:<32} {artist:<22} {plays:>5} {last:<12}") | |
| # ── songs played only once ──────────────────────────────────────────────── | |
| once = sum(1 for p in song_plays.values() if p == 1) | |
| print() | |
| print(f"Songs played exactly once: {once}") | |
| # ── yearly summary ──────────────────────────────────────────────────────── | |
| year_gigs = Counter() | |
| year_songs = Counter() | |
| for g in gigs: | |
| year = g['details']['date'][:4] | |
| songs = active_items(g['contents']) | |
| year_gigs[year] += 1 | |
| year_songs[year] += len(songs) | |
| print() | |
| print("=" * 65) | |
| print("YEARLY SUMMARY") | |
| print("=" * 65) | |
| print(f"{'Year':<8} {'Gigs':>6} {'Songs played':>13}") | |
| print("-" * 65) | |
| for year in sorted(year_gigs): | |
| print(f"{year:<8} {year_gigs[year]:>6} {year_songs[year]:>13}") | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Loading: SongbookPro Backup.sbpbackup | |
| Total gigs: 61 Songs played (with repeats): 387 | |
| ================================================================= | |
| VENUE STATISTICS | |
| ================================================================= | |
| Venue Gigs Songs played | |
| ----------------------------------------------------------------- | |
| The Anchor 18 98 | |
| The Maltings 15 72 | |
| The Wheatsheaf 9 61 | |
| The Green Man 6 34 | |
| Town Hall 2 48 | |
| Riverside Festival 2 19 | |
| Village Hall 2 13 | |
| The Tap Room 1 8 | |
| Community Centre 1 10 | |
| Summer Fair 1 14 | |
| The Old Library 1 5 | |
| Arts Centre 1 15 | |
| ================================================================= | |
| SONG PLAY COUNTS (songs played at 3+ gigs) | |
| ================================================================= | |
| Song Artist Plays Last played | |
| ----------------------------------------------------------------- | |
| Wonderwall Oasis 28 2026-01-18 | |
| Fast Car Tracy Chapman 24 2026-02-07 | |
| Jolene Dolly Parton 17 2025-12-12 | |
| Creep Radiohead 14 2026-01-18 | |
| Valerie The Zutons 13 2025-11-22 | |
| Africa Toto 12 2026-02-07 | |
| Wish You Were Here Pink Floyd 11 2025-10-04 | |
| Iris Goo Goo Dolls 10 2026-01-18 | |
| Somebody That I Used To Know Gotye 9 2025-12-12 | |
| Mr. Brightside The Killers 8 2025-12-12 | |
| Running Up That Hill Kate Bush 8 2025-11-22 | |
| Dreams Fleetwood Mac 7 2026-02-07 | |
| Shallow Lady Gaga 7 2025-10-04 | |
| Rolling In The Deep Adele 6 2025-09-13 | |
| Take Me To Church Hozier 6 2026-01-18 | |
| Knockin' On Heaven's Door Bob Dylan 6 2025-11-22 | |
| House Of The Rising Sun The Animals 5 2026-02-07 | |
| The Sound Of Silence Simon & Garfunkel 5 2025-10-04 | |
| Blackbird The Beatles 5 2025-08-23 | |
| Landslide Fleetwood Mac 5 2025-06-14 | |
| Yellow Coldplay 4 2025-12-12 | |
| Fade To Black Metallica 4 2025-09-13 | |
| Layla Eric Clapton 4 2025-07-19 | |
| Hallelujah Leonard Cohen 4 2025-06-14 | |
| Losing My Religion R.E.M. 3 2025-11-22 | |
| Behind Blue Eyes The Who 3 2025-10-04 | |
| Nothing Else Matters Metallica 3 2025-08-23 | |
| Wonderwall (slow version) Oasis 3 2025-05-03 | |
| Songs played exactly once: 19 | |
| ================================================================= | |
| YEARLY SUMMARY | |
| ================================================================= | |
| Year Gigs Songs played | |
| ----------------------------------------------------------------- | |
| 2022 2 6 | |
| 2023 9 31 | |
| 2024 24 172 | |
| 2025 21 157 | |
| 2026 5 21 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Relies on a convention of putting "@ venue name" in set names to detect venues. Without it, will omit venue stats.