Skip to content

Instantly share code, notes, and snippets.

@psobot
Created August 25, 2024 16:40
Show Gist options
  • Save psobot/4a1728d143ec6948a6fa6090d7c03e9a to your computer and use it in GitHub Desktop.
Save psobot/4a1728d143ec6948a6fa6090d7c03e9a to your computer and use it in GitHub Desktop.
setlistizer - Automatically create a New Yacht City setlist doc
# coding: utf-8
# @psobot on Aug 25, 2024
# Usage:
# pbpaste | python setlistizer.py && open setlist.pdf
# Update the key mapping as needed.
# Details below specialized for https://newyacht.city
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import inch
from io import BytesIO
from tqdm import tqdm
import sys
import string
KEY_MAPPING = {
"How Long": "Cm",
"Dancing in the Moonlight": "Cm",
"What You Won't Do for Love": "Cm",
"Brandy": "E",
"Bennie and the Jets": "G",
"Peg": "G",
"Somebody's Baby": "D",
"Reelin' In The Years": "A",
"My Old School": "G",
"Lido Shuffle": "G",
"Easy": "Ab",
"Sailing": "A",
"Baker Street": "Dm",
"That's All": "E",
"Everybody Wants To Rule The World": "D",
"What a Fool Believes": {"original": "Db", "transposed": "B"},
"Steal Away": "A",
"Dreams": "Am",
"Maneater": "Bm",
"Rosanna": "G",
"Takin' It To The Streets": "C",
"Africa": "Dbm",
"I Keep Forgettin' (Every Time You're Near)": "A",
"Ride Like The Wind": "Cm",
"I Will Survive": "A",
"Private Eyes": {"original": "A", "transposed": "G"},
"Valerie": "Eb",
"You Can Call Me Al": "F",
"Sledgehammer": "Eb",
"Let's Dance": "Bb",
"Go Your Own Way": "F",
"The Power Of Love": "C",
"Easy Lover": "Fm",
"Escape": "C",
"Kiss On My List": {"original": "C", "transposed": "Bb"},
"You Make My Dreams (Come True)": "F",
"Don't Stop Believin'": "E",
"Wouldn't It Be Nice": "F",
"Love Shack": "C",
"Human Nature": "D",
}
def fix_misspellings(song):
if "pina colada" in song.lower():
return "Escape (The Piña Colada Song)"
if "takin it" in song.lower():
return "Takin' It To The Streets"
if "baker st" in song.lower():
return "Baker Street"
if "reelin" in song.lower():
return "Reelin' In The Years"
return song
def normalize(song):
return song.lower().replace("’", "'").replace(" ", "")
def resolve_song_and_key(song):
if not song:
return None, None
if song.lower().endswith("minor)") and "(" in song:
parts = song.split("(")
song = parts[0]
key = parts[1].split(" ")[0]
return song, f"<u>{key.upper()}m</u>"
for k, v in KEY_MAPPING.items():
if normalize(song) in normalize(k) or normalize(k) in normalize(song):
if isinstance(v, dict):
if "original key" in song.lower() or "orig key" in song.lower():
return song.split("(")[0], f'<u>{v["original"]}</u>'
elif "transposed" in song.lower():
return song.split("(")[0], f'<u>{v["transposed"]}</u>'
else:
# Assume transposed:
return song, f'<u>{v["transposed"]}</u>'
else:
return song, v
return song, None
def format_song_title(song: str) -> str:
song = string.capwords(song)
if "(" in song:
# capwords doesn't properly re-capitalize the first letter after a parenthesis:
song = (
song[: song.index("(")] + "(" + string.capwords(song[song.index("(") + 1 :])
)
return song
FONT_INCREMENT = 0.5
def create_setlist(songs, output_file, line_height_multiplier=1.1):
max_font_size = 100
style = ParagraphStyle(
name="Song",
fontName="Helvetica",
fontSize=max_font_size,
leading=max_font_size * line_height_multiplier,
)
with tqdm(total=max_font_size / FONT_INCREMENT) as pbar:
while max_font_size > 0:
style.fontSize = max_font_size
style.leading = max_font_size * line_height_multiplier
buf = BytesIO()
doc = SimpleDocTemplate(
buf,
pagesize=letter,
rightMargin=0,
leftMargin=0.1 * inch,
topMargin=0,
bottomMargin=0,
)
elements = []
for song in songs:
song, key = resolve_song_and_key(song)
text = ""
if song:
text += f"<strong>{format_song_title(song)}</strong>"
else:
text += "&nbsp;"
if key:
text += f" <font color='#555555'>in {key}</font>"
elements.append(Paragraph(text, style))
doc.build(elements)
if doc.page == 1:
break
max_font_size -= FONT_INCREMENT
pbar.update(1)
else:
raise ValueError("Could not fit the setlist on a single page.")
with open(output_file, "wb") as f:
f.write(buf.getvalue())
OMIT = {
"Set List",
"setlist",
"set break",
"break",
"/",
"midnight",
"1am",
"2am",
"3pm",
"4pm",
"5pm",
"6pm",
"7pm",
"8pm",
"9pm",
"10pm",
"11pm",
"12am",
"[",
}
def format_song_name(song):
song = song.strip()
song = song.split(" - ")[0]
song = song.split("(feat")[0]
if "remaster" in song.lower():
song = song.split("(")[0]
return song
def parse_setlist(input):
lines = [
fix_misspellings(format_song_name(line.strip()))
for line in input.split("\n")
if not any(key.lower() in line.lower() for key in OMIT)
]
if lines:
while lines[0].strip() == "":
lines.pop(0)
while lines[-1].strip() == "":
lines.pop(-1)
# Remove consecutive blank lines:
lines = [
line
for i, line in enumerate(lines)
if i == 0 or line.strip() != "" or lines[i - 1].strip() != ""
]
return lines
def main():
print("Reading set list from stdin...")
create_setlist(parse_setlist(sys.stdin.read()), "setlist.pdf")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment