a18bfc9 by Aurélien Bompard at 2010-04-11 1
#!/usr/bin/env python
d3eca04 by Aurélien Bompard at 2010-04-12 2
# vim: set fileencoding=utf-8 tabstop=4 shiftwidth=4 expandtab smartindent:
f5600b8 by Aurélien Bompard at 2010-05-12 3
u"""
7581434 by Aurélien Bompard at 2011-04-03 4
5
Make songs list
6
---------------
7
a18bfc9 by Aurélien Bompard at 2010-04-11 8
Create a PDF file with the list of Karaoke songs available to Performous.
9
10
Performous: http://performous.org/
11
7581434 by Aurélien Bompard at 2011-04-03 12
.. :Authors:
13
       Aurélien Bompard <aurelien@bompard.org> <http://aurelien.bompard.org>
a18bfc9 by Aurélien Bompard at 2010-04-11 14
7581434 by Aurélien Bompard at 2011-04-03 15
.. :License:
16
       GNU GPL v3 or later
a18bfc9 by Aurélien Bompard at 2010-04-11 17
"""
18
19
import os
20
import sys
21
import tempfile
22
import optparse
23
import atexit
8255535 by Aurélien Bompard at 2010-06-28 24
import re
a18bfc9 by Aurélien Bompard at 2010-04-11 25
from pprint import pprint
26
8255535 by Aurélien Bompard at 2010-06-28 27
import PIL
28
a18bfc9 by Aurélien Bompard at 2010-04-11 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]
a5106af by Aurélien Bompard at 2010-07-17 39
MARGIN = 8
a18bfc9 by Aurélien Bompard at 2010-04-11 40
COLSEP = 5
41
42
songs = []
43
8255535 by Aurélien Bompard at 2010-06-28 44
def get_songs(cover_size, dpi):
a5106af by Aurélien Bompard at 2010-07-17 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():
a18bfc9 by Aurélien Bompard at 2010-04-11 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
                 ]
8255535 by Aurélien Bompard at 2010-06-28 82
    available_songs = []
a18bfc9 by Aurélien Bompard at 2010-04-11 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
8255535 by Aurélien Bompard at 2010-06-28 91
                available_songs.append(os.path.join(root, cur_file))
a5106af by Aurélien Bompard at 2010-07-17 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
a18bfc9 by Aurélien Bompard at 2010-04-11 108
8255535 by Aurélien Bompard at 2010-06-28 109
def process_file(filepath, cover_size, dpi):
a18bfc9 by Aurélien Bompard at 2010-04-11 110
    song = {}
111
    file_h = open(filepath, "r")
112
    for line in file_h:
b9913f6 by Aurélien Bompard at 2010-09-12 113
        for tag in ["artist", "title", "cover", "background", "video"]:
a18bfc9 by Aurélien Bompard at 2010-04-11 114
            if line.startswith("#%s:" % tag.upper()):
115
                song[tag] = ":".join(line.split(":")[1:]).strip()
116
    file_h.close()
95b8bf4 by Aurélien Bompard at 2010-04-12 117
    if "title" not in song:
118
        song["title"] = ""
a18bfc9 by Aurélien Bompard at 2010-04-11 119
    for tag in ["cover", "background"]:
120
        if tag not in song:
121
            continue
8255535 by Aurélien Bompard at 2010-06-28 122
        song["image"] = os.path.realpath(os.path.join(
a18bfc9 by Aurélien Bompard at 2010-04-11 123
                        os.path.dirname(filepath), song[tag]))
8255535 by Aurélien Bompard at 2010-06-28 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
b9913f6 by Aurélien Bompard at 2010-09-12 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"]
a18bfc9 by Aurélien Bompard at 2010-04-11 140
    return song
141
f0326ee by Aurélien Bompard at 2010-06-09 142
def filter_songs(karaoke):
8255535 by Aurélien Bompard at 2010-06-28 143
    filter_re = re.compile("(.*)\s+\(.*\)")
f0326ee by Aurélien Bompard at 2010-06-09 144
    for song in songs[:]:
8255535 by Aurélien Bompard at 2010-06-28 145
        title = song["title"]
146
        if not karaoke and title.count("(Karaoke)"):
f0326ee by Aurélien Bompard at 2010-06-09 147
            songs.remove(song)
8255535 by Aurélien Bompard at 2010-06-28 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
a18bfc9 by Aurélien Bompard at 2010-04-11 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
a5106af by Aurélien Bompard at 2010-07-17 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
a18bfc9 by Aurélien Bompard at 2010-04-11 165
    frame_left = Frame(MARGIN, MARGIN, col_width, col_height,
a5106af by Aurélien Bompard at 2010-07-17 166
                       leftPadding=0, rightPadding=0,
167
                       topPadding=vspace_left_left + 2*cover_size,
168
                       bottomPadding=0, id="left", showBoundary=0)
81a305c by Aurélien Bompard at 2010-06-09 169
    frame_center = Frame(MARGIN + col_width + COLSEP, MARGIN,
170
                         col_width, col_height, leftPadding=0,
171
                         rightPadding=0, topPadding=vspace_left,
a18bfc9 by Aurélien Bompard at 2010-04-11 172
                         bottomPadding=0, id="center")
81a305c by Aurélien Bompard at 2010-06-09 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")
a18bfc9 by Aurélien Bompard at 2010-04-11 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:
8255535 by Aurélien Bompard at 2010-06-28 192
        try:
193
            image = None
a5106af by Aurélien Bompard at 2010-07-17 194
            text = "%s - %s" % (song["artist"], song["title"])
195
            text = text.replace("&", "&amp;")
b9913f6 by Aurélien Bompard at 2010-09-12 196
            if "video" in song:
197
                text = "<i>%s</i>" % text
a5106af by Aurélien Bompard at 2010-07-17 198
            label = Paragraph(text, text_style)
8255535 by Aurélien Bompard at 2010-06-28 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
a18bfc9 by Aurélien Bompard at 2010-04-11 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)")
6fdcac4 by Aurélien Bompard at 2010-06-09 224
    parser.add_option("-c", "--cover-size", dest="cover_size", default=32,
a18bfc9 by Aurélien Bompard at 2010-04-11 225
                      type="int", help="Cover size (default: %default pt)")
8255535 by Aurélien Bompard at 2010-06-28 226
    parser.add_option("--dpi", dest="dpi", default=96,
227
                  help="Image resolution in Dots Per Inch (default: %default)")
a5106af by Aurélien Bompard at 2010-07-17 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")
a18bfc9 by Aurélien Bompard at 2010-04-11 231
    opts, args = parser.parse_args()
232
    if len(args) != 0:
233
        parser.error("No arguments allowed")
a5106af by Aurélien Bompard at 2010-07-17 234
    if opts.input and not os.path.exists(opts.input):
235
        parser.error("The file %s does not exist" % opts.input)
a18bfc9 by Aurélien Bompard at 2010-04-11 236
    return opts
237
238
if __name__ == '__main__':
239
    opts = parse_opts()
8255535 by Aurélien Bompard at 2010-06-28 240
    get_songs(opts.cover_size, opts.dpi)
a18bfc9 by Aurélien Bompard at 2010-04-11 241
    #pprint(songs)
f0326ee by Aurélien Bompard at 2010-06-09 242
    filter_songs(opts.karaoke)
a18bfc9 by Aurélien Bompard at 2010-04-11 243
    make_pdf(opts.output, opts.title, opts.cover_size)
244
    print "PDF written to", opts.output