| 1 |
#!/usr/bin/env python |
| 2 |
# vim: set fileencoding=utf-8 tabstop=4 shiftwidth=4 expandtab smartindent: |
| 3 |
u""" |
| 4 |
|
| 5 |
Make songs list |
| 6 |
--------------- |
| 7 |
|
| 8 |
Create a PDF file with the list of Karaoke songs available to Performous. |
| 9 |
|
| 10 |
Performous: http://performous.org/ |
| 11 |
|
| 12 |
.. :Authors: |
| 13 |
Aurélien Bompard <aurelien@bompard.org> <http://aurelien.bompard.org> |
| 14 |
|
| 15 |
.. :License: |
| 16 |
GNU GPL v3 or later |
| 17 |
""" |
| 18 |
|
| 19 |
import os |
| 20 |
import sys |
| 21 |
import tempfile |
| 22 |
import optparse |
| 23 |
import atexit |
| 24 |
import re |
| 25 |
from pprint import pprint |
| 26 |
|
| 27 |
import PIL |
| 28 |
|
| 29 |
from reportlab.platypus import BaseDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, Frame, PageTemplate |
| 30 |
from reportlab.lib.styles import ParagraphStyle |
| 31 |
from reportlab.lib import colors |
| 32 |
from reportlab.rl_config import defaultPageSize |
| 33 |
from reportlab.lib.units import inch |
| 34 |
|
| 35 |
|
| 36 |
defaultPageSize = (defaultPageSize[1], defaultPageSize[0]) |
| 37 |
PAGE_HEIGHT=defaultPageSize[1] |
| 38 |
PAGE_WIDTH=defaultPageSize[0] |
| 39 |
MARGIN = 8 |
| 40 |
COLSEP = 5 |
| 41 |
|
| 42 |
songs = [] |
| 43 |
|
| 44 |
def get_songs(cover_size, dpi): |
| 45 |
if opts.input: |
| 46 |
available_songs = scan_file() |
| 47 |
else: |
| 48 |
available_songs = scan_folders() |
| 49 |
for index, available_song in enumerate(available_songs): |
| 50 |
song = process_file(available_song, cover_size, dpi) |
| 51 |
if song: |
| 52 |
songs.append(song) |
| 53 |
sys.stdout.write("\rAdding song %d/%d" |
| 54 |
% (index+1, len(available_songs))) |
| 55 |
sys.stdout.flush() |
| 56 |
print |
| 57 |
print "Building PDF..." |
| 58 |
songs.sort(cmp=sort_songs) |
| 59 |
|
| 60 |
def sort_songs(x, y): |
| 61 |
key_x = x["artist"] |
| 62 |
key_y = y["artist"] |
| 63 |
for ignore in ["The ", "Les "]: |
| 64 |
if key_x.startswith(ignore): |
| 65 |
key_x = key_x[len(ignore):] |
| 66 |
if key_y.startswith(ignore): |
| 67 |
key_y = key_y[len(ignore):] |
| 68 |
if key_x == key_y: |
| 69 |
key_x = x["title"] |
| 70 |
key_y = y["title"] |
| 71 |
return cmp(key_x, key_y) |
| 72 |
|
| 73 |
def scan_folders(): |
| 74 |
songs_dirs = ["/usr/share/performous/songs", |
| 75 |
"/usr/local/share/games/ultrastar/songs", |
| 76 |
"/usr/local/share/games/fretsonfire/data/songs", |
| 77 |
"/usr/share/games/ultrastar/songs", |
| 78 |
"/usr/share/games/fretsonfire/data/songs", |
| 79 |
"~/.ultrastar/songs", |
| 80 |
"~/.fretsonfire/songs", |
| 81 |
] |
| 82 |
available_songs = [] |
| 83 |
for songs_dir in songs_dirs: |
| 84 |
songs_dir = os.path.expanduser(songs_dir) |
| 85 |
if not os.path.exists(songs_dir): |
| 86 |
continue |
| 87 |
for root, dirs, files in os.walk(songs_dir): |
| 88 |
for cur_file in files: |
| 89 |
if not cur_file.lower().endswith(".txt"): |
| 90 |
continue |
| 91 |
available_songs.append(os.path.join(root, cur_file)) |
| 92 |
return available_songs |
| 93 |
|
| 94 |
def scan_file(): |
| 95 |
available_songs = [] |
| 96 |
songs_file = open(opts.input, "r") |
| 97 |
for line in songs_file: |
| 98 |
song_file = line.strip() |
| 99 |
if not song_file.lower().endswith(".txt"): |
| 100 |
continue |
| 101 |
if not os.path.exists(song_file): |
| 102 |
print >>sys.stderr, "WARNING: file %s in the input file " \ |
| 103 |
% song_file + "cannot be found !" |
| 104 |
continue |
| 105 |
available_songs.append(song_file) |
| 106 |
songs_file.close() |
| 107 |
return available_songs |
| 108 |
|
| 109 |
def process_file(filepath, cover_size, dpi): |
| 110 |
song = {} |
| 111 |
file_h = open(filepath, "r") |
| 112 |
for line in file_h: |
| 113 |
for tag in ["artist", "title", "cover", "background", "video"]: |
| 114 |
if line.startswith("#%s:" % tag.upper()): |
| 115 |
song[tag] = ":".join(line.split(":")[1:]).strip() |
| 116 |
file_h.close() |
| 117 |
if "title" not in song: |
| 118 |
song["title"] = "" |
| 119 |
for tag in ["cover", "background"]: |
| 120 |
if tag not in song: |
| 121 |
continue |
| 122 |
song["image"] = os.path.realpath(os.path.join( |
| 123 |
os.path.dirname(filepath), song[tag])) |
| 124 |
if not os.path.exists(song["image"]): |
| 125 |
del song["image"] |
| 126 |
continue |
| 127 |
# Resize the image |
| 128 |
tmpimg, tmpimg_path = tempfile.mkstemp( |
| 129 |
prefix="mksonglist-", suffix=".jpg") |
| 130 |
img = PIL.Image.open(song["image"]) |
| 131 |
img.thumbnail((cover_size * dpi, cover_size * dpi), PIL.Image.ANTIALIAS) |
| 132 |
img.save(os.fdopen(tmpimg, "w"), "JPEG") |
| 133 |
atexit.register(os.remove, tmpimg_path) |
| 134 |
song["image"] = tmpimg_path |
| 135 |
break |
| 136 |
if "video" in song: |
| 137 |
videofile = os.path.join(os.path.dirname(filepath), song["video"]) |
| 138 |
if not os.path.exists(videofile): |
| 139 |
del song["video"] |
| 140 |
return song |
| 141 |
|
| 142 |
def filter_songs(karaoke): |
| 143 |
filter_re = re.compile("(.*)\s+\(.*\)") |
| 144 |
for song in songs[:]: |
| 145 |
title = song["title"] |
| 146 |
if not karaoke and title.count("(Karaoke)"): |
| 147 |
songs.remove(song) |
| 148 |
continue |
| 149 |
filter_match = filter_re.match(title) |
| 150 |
if filter_match: |
| 151 |
short_title = filter_match.group(1) |
| 152 |
if short_title in [ s["title"] for s in songs ]: |
| 153 |
songs.remove(song) |
| 154 |
continue |
| 155 |
|
| 156 |
def make_pdf(output, title, cover_size): |
| 157 |
doc = BaseDocTemplate( |
| 158 |
output, title=title, |
| 159 |
pagesize=defaultPageSize, |
| 160 |
) |
| 161 |
col_width = (PAGE_WIDTH - 2 * MARGIN - 2 * COLSEP) / 3 |
| 162 |
col_height = PAGE_HEIGHT - 2 * MARGIN |
| 163 |
vspace_left = col_height % (cover_size + 2) # used to bottom-align the 2nd and 3rd columns |
| 164 |
vspace_left_left = (col_height - 2*cover_size) % (cover_size + 2) # used to bottom-align the left column |
| 165 |
frame_left = Frame(MARGIN, MARGIN, col_width, col_height, |
| 166 |
leftPadding=0, rightPadding=0, |
| 167 |
topPadding=vspace_left_left + 2*cover_size, |
| 168 |
bottomPadding=0, id="left", showBoundary=0) |
| 169 |
frame_center = Frame(MARGIN + col_width + COLSEP, MARGIN, |
| 170 |
col_width, col_height, leftPadding=0, |
| 171 |
rightPadding=0, topPadding=vspace_left, |
| 172 |
bottomPadding=0, id="center") |
| 173 |
frame_right = Frame(PAGE_WIDTH - MARGIN - col_width, MARGIN, |
| 174 |
col_width, col_height, leftPadding=0, |
| 175 |
rightPadding=0, topPadding=vspace_left, |
| 176 |
bottomPadding=0, id="left") |
| 177 |
doc.addPageTemplates(PageTemplate("threecols", frames=[frame_left, frame_center, frame_right])) |
| 178 |
Story = [] |
| 179 |
text_style = ParagraphStyle(name="labels", fontSize=12) |
| 180 |
table_style = TableStyle([ |
| 181 |
("VALIGN", (0,0), (-1,-1), "MIDDLE"), |
| 182 |
#("LEADING", (0,0), (-1,-1), 1), |
| 183 |
("TOPPADDING", (0,0), (-1,-1), 1), |
| 184 |
("BOTTOMPADDING", (0,0), (-1,-1), 1), |
| 185 |
("LEFTPADDING", (0,0), (0,-1), 0), |
| 186 |
("RIGHTPADDING", (0,0), (-1,-1), 0), |
| 187 |
("LEFTPADDING", (1,0), (1,-1), 5), |
| 188 |
#("BOX", (0,0), (-1,-1), 0.25, colors.gray), |
| 189 |
]) |
| 190 |
table_contents = [] |
| 191 |
for song in songs: |
| 192 |
try: |
| 193 |
image = None |
| 194 |
text = "%s - %s" % (song["artist"], song["title"]) |
| 195 |
text = text.replace("&", "&") |
| 196 |
if "video" in song: |
| 197 |
text = "<i>%s</i>" % text |
| 198 |
label = Paragraph(text, text_style) |
| 199 |
if "image" in song: |
| 200 |
image = Image(song["image"], cover_size, cover_size) |
| 201 |
#image.drawHeight = cover_size *I.drawHeight / I.drawWidth |
| 202 |
#image.drawWidth = cover_size |
| 203 |
else: |
| 204 |
image = Spacer(1, cover_size) |
| 205 |
row = [image, label] |
| 206 |
table_contents.append(row) |
| 207 |
except Exception: |
| 208 |
print >>sys.stderr, "Error when printing %s" % repr(song) |
| 209 |
raise |
| 210 |
Story.append(Table(table_contents, |
| 211 |
colWidths=(cover_size + 2, col_width - (cover_size + 2)), |
| 212 |
style=table_style)) |
| 213 |
doc.build(Story) |
| 214 |
|
| 215 |
def parse_opts(): |
| 216 |
#usage = "usage: %prog [options]" |
| 217 |
parser = optparse.OptionParser() |
| 218 |
parser.add_option("-o", "--output", dest="output", default="songs.pdf", |
| 219 |
help="Output file name (default: %default)") |
| 220 |
parser.add_option("-t", "--title", dest="title", default="Songs", |
| 221 |
help="PDF title (default: %default)") |
| 222 |
parser.add_option("-k", "--karaoke", dest="karaoke", action="store_true", |
| 223 |
help="List the 'karaoke' versions (default: False)") |
| 224 |
parser.add_option("-c", "--cover-size", dest="cover_size", default=32, |
| 225 |
type="int", help="Cover size (default: %default pt)") |
| 226 |
parser.add_option("--dpi", dest="dpi", default=96, |
| 227 |
help="Image resolution in Dots Per Inch (default: %default)") |
| 228 |
parser.add_option("-i", "--input", dest="input", metavar="FILE", |
| 229 |
help="Read the song list from this file instead of " |
| 230 |
"scanning the folders") |
| 231 |
opts, args = parser.parse_args() |
| 232 |
if len(args) != 0: |
| 233 |
parser.error("No arguments allowed") |
| 234 |
if opts.input and not os.path.exists(opts.input): |
| 235 |
parser.error("The file %s does not exist" % opts.input) |
| 236 |
return opts |
| 237 |
|
| 238 |
if __name__ == '__main__': |
| 239 |
opts = parse_opts() |
| 240 |
get_songs(opts.cover_size, opts.dpi) |
| 241 |
#pprint(songs) |
| 242 |
filter_songs(opts.karaoke) |
| 243 |
make_pdf(opts.output, opts.title, opts.cover_size) |
| 244 |
print "PDF written to", opts.output |