Created
August 25, 2024 16:40
-
-
Save psobot/4a1728d143ec6948a6fa6090d7c03e9a to your computer and use it in GitHub Desktop.
setlistizer - Automatically create a New Yacht City setlist doc
This file contains 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
# 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 += " " | |
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