Skip to content

Instantly share code, notes, and snippets.

@stig
Last active March 5, 2026 22:22
Show Gist options
  • Select an option

  • Save stig/302cb0e9c87dcad29f0b5e5e54f16719 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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()
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
@stig
Copy link
Author

stig commented Mar 5, 2026

Relies on a convention of putting "@ venue name" in set names to detect venues. Without it, will omit venue stats.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment