Update to lily 2.17, layout fix for backpage
[orchestrallily.git] / generate_oly_score.py
blob33e0f931eecb987189f768b08b338ac002d741ee
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 import sys
5 import os
6 import os.path
7 import getopt
8 import re
9 import filecmp
10 import string
11 import subprocess
12 import copy
13 import codecs
14 import fnmatch
15 from jinja2 import Environment, Template, FileSystemLoader
16 import jinja2
18 import pprint
19 pp = pprint.PrettyPrinter(indent=4)
21 program_name = 'generate_oly_score';
22 settings_file = 'oly_structure.def';
23 script_path = os.path.dirname(__file__);
25 ######################################################################
26 # Options handling
27 ######################################################################
29 help_text = r"""Usage: %(program_name)s [OPTIONS]... [DEF-FILE]
30 Create a complete file structure for a score using OrchestralLily.
31 If no definitions file it given, the file oly_structure.def is used
33 Options:
34 -h, --help print this help
35 -o, --output=DIR write output files to DIR (default: read from settings file)
36 """
38 def help (text):
39 sys.stdout.write (text)
40 sys.exit (0)
45 def import_file (filename):
46 res={}
47 try:
48 in_f = codecs.open (filename, "r", "utf-8")
49 s = in_f.read()
50 in_f.close()
51 res = eval(s);
52 except IOError:
53 print ("Unable to load file '%s'. Exiting..." % filename)
54 exit (-1);
55 except SyntaxError as ex:
56 print ex;
57 print ("Unable to interpret settings file '%s', it's syntax is invalid. Exiting..." % filename);
58 exit (-1);
59 return res
62 # Load the score type/number definitions from the oly directory
63 score_types = import_file (os.path.join(sys.path[0], "oly_defs.py"));
67 ######################################################################
68 # Settings
69 ######################################################################
71 class Settings:
72 options = {};
73 arguments = [];
74 globals={};
75 raw_data={};
76 out_dir = "";
77 template_env = None;
79 def __init__ (self):
80 settings_file = self.load_options ();
81 self.load (settings_file);
82 self.raw_data["settings_file_path"] = settings_file;
83 self.raw_data["settings_file"] = os.path.basename(settings_file);
84 self.init_template_env ();
85 self.init_arrays ();
87 def output_dir (self):
88 return self.out_dir
89 def defaults (self):
90 return self.raw_data.get ("defaults", {});
91 def score_names (self):
92 return self.raw_data.get ("scores", ["Score"]);
94 def get_score_settings (self, id):
95 settings = self.globals.copy();
96 settings.update (self.defaults ());
97 settings.update ({"name": id})
98 settings.update (self.raw_data.get (id, {}));
99 self.normalize_part_definitions (settings);
100 return settings;
102 def normalize_part_definitions (self, score_settings):
103 scorename = score_settings.get ("name", "Score");
104 parts = score_settings.get ("parts", [scorename]);
105 result = [];
106 for this_part in parts:
107 if isinstance (this_part, basestring):
108 this_part = {"id": this_part, "filename": this_part }
109 elif not isinstance (this_part, dict):
110 warning ("Invalid part list for score %a: %a" % (scorename, p));
111 return [];
112 if "id" not in this_part:
113 this_part["id"] = scorename;
114 if "filename" not in this_part:
115 this_part["filename"] = this_part["id"];
116 if "piece" not in this_part:
117 this_part["piece"] = this_part["id"];
118 this_part["score"] = scorename;
119 this_part.update (score_settings);
120 result.append (this_part);
121 score_settings["parts"] = result;
123 def has_tex (self):
124 return "latex" in self.raw_data;
125 def get_tex_settings (self):
126 settings = self.globals.copy();
127 settings.update (self.defaults ());
128 settings.update (self.raw_data.get ("latex", {}));
129 return settings;
131 def get_score_parts (self, score_settings):
132 scorename = score_settings.get ("name", "Score");
133 parts = score_settings.get ("parts", [scorename]);
134 result = [];
135 for p in parts:
136 this_part = p;
137 if isinstance (this_part, basestring):
138 this_part = {"id": this_part, "filename": this_part }
139 elif not isinstance (this_part, dict):
140 warning ("Invalid part list for score %a: %a" % (scorename, p));
141 return [];
142 if "id" not in this_part:
143 this_part["id"] = scorename;
144 if "filename" not in this_part:
145 this_part["filename"] = this_part["id"];
146 if "piece" not in this_part:
147 this_part["piece"] = this_part["id"];
148 this_part["score"] = scorename;
149 this_part.update (score_settings);
150 result.append (this_part);
151 return result;
153 def load_options (self):
154 (self.options, self.arguments) = getopt.getopt (sys.argv[1:], 'ho:', ['help', 'output='])
155 for opt in self.options:
156 o = opt[0]
157 a = opt[1]
158 if o == '-h' or o == '--help':
159 help (help_text % globals ())
160 elif o == '-o' or o == '--output':
161 self.out_dir = a
162 else:
163 raise Exception ('unknown option: ' + o)
164 if self.arguments:
165 return self.arguments[0]
166 else:
167 return "oly_structure.def";
169 def load (self, filename):
170 self.raw_data = import_file (filename)
171 if not self.out_dir:
172 self.out_dir = self.raw_data.get ("output_dir", self.out_dir) + "/";
174 def get_template_name (self):
175 return self.raw_data.get ("template", "Full");
176 def get_template_names (self, pattern):
177 allfiles = os.listdir (self.templatepath);
178 files = [];
179 for f in allfiles:
180 if fnmatch.fnmatch(f, pattern):
181 files.append (f);
182 return files;
185 def init_template_env (self):
186 global program_name;
187 global script_path;
188 templatename = self.get_template_name ();
189 self.defaulttemplatepath = script_path + '/Templates/default';
190 self.templatepath = script_path + '/Templates/' + templatename;
191 self.template_env = Environment (
192 loader = FileSystemLoader([self.templatepath,self.defaulttemplatepath]),
193 block_start_string = '<$', block_end_string = '$>',
194 variable_start_string = '<<', variable_end_string = '>>',
195 comment_start_string = '<#', comment_end_string = '#>',
196 #line_statement_prefix = '#'
198 def get_template (self, template):
199 return self.template_env.get_template (template);
202 def init_arrays (self):
203 # Globals
204 self.globals = self.raw_data.copy ();
205 del self.globals["defaults"];
206 if "latex" in self.globals:
207 del self.globals["latex"];
208 for i in self.globals.get ("scores", []):
209 if i in self.globals:
210 del self.globals[i];
212 def assemble_filename (self, base, id, name, ext):
213 parts = [base, id, name];
214 if "" in parts:
215 parts.remove ("")
216 return "_".join (parts) + "." + ext;
217 def assemble_movement_filename (self, basename, mvmnt):
218 return self.assemble_filename( basename, "Music", mvmnt, "ily");
219 def assemble_instrument_filename (self, basename, instr):
220 return self.assemble_filename( basename, "Instrument", instr, "ly");
221 def assemble_score_filename (self, basename, instr):
222 return self.assemble_filename( basename, "Score", instr, "ly");
223 def assemble_settings_filename (self, basename, s):
224 return self.assemble_filename( basename, "Settings", s, "ily");
225 def assemble_itex_filename (self, basename, filename):
226 return self.assemble_filename( "TeX", basename, filename, "itex");
227 def assemble_texscore_filename (self, basename, score):
228 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
230 ######################################################################
231 # File Writing
232 ######################################################################
234 def write_file (path, fname, contents):
235 file_name = path + fname;
236 fn = file_name
237 if os.path.exists (file_name):
238 fn += ".new"
240 dir = os.path.dirname (fn);
241 if not os.path.exists (dir):
242 os.mkdir (dir)
244 try:
245 out_f = codecs.open (fn, "w", "utf-8")
246 s = out_f.write(contents)
247 out_f.write ("\n")
248 out_f.close()
249 except IOError:
250 print ("Unable to write to output file '%s'. Exiting..." % fn)
251 exit (-1);
253 # If the file already existed, check if the new file is identical.
254 if (fn != file_name):
255 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
256 if os.path.exists (patchfile):
257 try:
258 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
259 if retcode < 0:
260 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
261 except OSError, e:
262 print >>sys.stderr, "Execution failed:", e
264 if filecmp.cmp (fn, file_name):
265 os.unlink (fn);
266 else:
267 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
271 ######################################################################
272 # Creating movement files
273 ######################################################################
276 def generate_movement_files (score_name, score_settings, settings):
277 parts_files = [];
278 for part_settings in settings.get_score_parts (score_settings):
279 template = settings.get_template ("Lily_Music_Movement.ily");
280 basename = part_settings.get ("basename", score_name);
281 filename = part_settings.get ("filename", score_name + "Part");
282 filename = settings.assemble_movement_filename (basename, filename);
283 write_file (settings.out_dir, filename, template.render (part_settings));
284 parts_files.append (filename);
286 return parts_files;
290 ######################################################################
291 # Creating instrumental score files
292 ######################################################################
294 def generate_instrument_files (score_name, score_settings, settings):
295 instrument_files = [];
296 settings_files = set ();
297 instrument_settings = score_settings.copy ();
298 instrument_settings["parts"] = settings.get_score_parts (score_settings);
300 template = settings.get_template ("Lily_Instrument.ly");
301 basename = score_settings.get ("basename", score_name );
303 noscore_instruments = score_settings.get ("noscore_instruments", [])
304 for i in score_settings.get ("instruments", []):
305 if i in noscore_instruments:
306 continue;
307 instrument_settings["instrument"] = i;
308 if i in score_settings.get ("vocalvoices"):
309 instrument_settings["settings"] = "VocalVoice";
310 else:
311 instrument_settings["settings"] = "Instrument";
312 settings_files.add (instrument_settings["settings"]);
313 filename = settings.assemble_instrument_filename (basename, i);
314 write_file (settings.out_dir, filename, template.render (instrument_settings));
315 instrument_files.append (filename);
317 return (instrument_files, settings_files);
321 ######################################################################
322 # Creating score files
323 ######################################################################
326 score_name_map = {
327 "Particell": "Particell",
329 settings_map = {
330 "OrganScore": "VocalScore",
331 "ChoralScore": "ChoralScore",
332 "LongScore": "FullScore",
333 "OriginalScore": "FullScore",
334 "Particell": "FullScore"
336 scores_cues = ["ChoralScore", "VocalScore"];
338 def generate_score_files (score_name, score_settings, settings):
339 score_files = [];
340 settings_files = set ();
341 s_settings = score_settings.copy ();
342 s_settings["parts"] = settings.get_score_parts (score_settings);
343 s_settings["fullscore"] = True;
345 template = settings.get_template ("Lily_Score.ly");
346 basename = score_settings.get ("basename", score_name );
348 for s in score_settings.get ("scores", []):
349 fullsn = score_name_map.get (s, (s + "Score"));
350 s_settings["score"] = fullsn;
351 s_settings["nocues"] = fullsn not in scores_cues;
352 s_settings["settings"] = settings_map.get (fullsn,fullsn)
353 settings_files.add (s_settings["settings"]);
354 filename = settings.assemble_score_filename (basename, s);
355 write_file (settings.out_dir, filename, template.render (s_settings));
356 score_files.append (filename);
358 return (score_files, settings_files);
362 ######################################################################
363 # Creating settings files
364 ######################################################################
366 def write_settings_file_if_exists (settings, score_settings, template, filename):
367 try:
368 template = settings.get_template (template);
369 basename = score_settings.get ("basename", "");
370 filename = settings.assemble_settings_filename (basename, filename);
371 write_file (settings.out_dir, filename, template.render (score_settings));
372 return filename;
373 except jinja2.exceptions.TemplateNotFound as e:
374 return None;
376 def generate_settings_files (score_name, score_settings, settings, settings_files):
377 out_files = [];
378 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Global.ily", "Global" ));
379 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings.ily", "" ));
381 for s in settings_files:
382 score_settings["settings"] = s.lower ();
383 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Generic.ily", s));
385 return out_files;
389 ######################################################################
390 # Generation of Lilypond Files
391 ######################################################################
393 def generate_scores (settings):
394 files = {}
395 for s in settings.score_names ():
396 score_settings = settings.get_score_settings (s);
397 parts_files = generate_movement_files (s, score_settings, settings);
398 (instrument_files, isettings_files) = generate_instrument_files (s, score_settings, settings);
399 (score_files, ssettings_files) = generate_score_files (s, score_settings, settings);
400 included_settings_files = ssettings_files | isettings_files;
401 score_settings["parts_files"] = parts_files;
402 settings_files = generate_settings_files (s, score_settings, settings, included_settings_files );
403 files[s] = {"settings": settings_files,
404 "scores": score_files,
405 "instruments": instrument_files,
406 "parts": parts_files };
407 return files
411 ######################################################################
412 # Creating LaTeX files
413 ######################################################################
415 def write_itex_file (settings, tex_settings, template, filename):
416 template = settings.get_template (template);
417 basename = tex_settings.get ("basename", "");
418 filename = settings.assemble_itex_filename (basename, filename);
419 write_file (settings.out_dir, filename, template.render (tex_settings));
420 return filename;
422 def write_texscore_file (settings, tex_settings, template, score):
423 template = settings.get_template (template);
424 basename = tex_settings.get ("basename", "");
425 filename = settings.assemble_texscore_filename (basename, score);
426 write_file (settings.out_dir, filename, template.render (tex_settings));
427 return filename;
429 no_criticalcomments_scores = ["Vocal", "Choral", "Organ"];
430 tex_options_map = {
431 "Organ": "vocalscore",
432 "Choral": "choralscore",
433 "Vocal": "vocalscore",
434 "Long": "fullscore",
435 "Full": "fullscore",
436 "Original": "fullscore",
437 "Particell": "vocalscore",
438 "InstrumentalParts": "instrumentalparts",
439 # TODO: chambermusic
442 def generate_tex_files (settings, lily_files):
443 tex_files = [];
444 tex_includes = [];
445 tex_settings = settings.get_tex_settings ();
446 tex_settings["lily_files"] = lily_files;
448 score_map = {};
449 instruments_scores = [];
450 for s in settings.score_names ():
451 score_settings = settings.get_score_settings (s);
452 instruments_scores.append (score_settings);
453 for p in score_settings.get ("scores", []):
454 score_map[p] = score_map.get (p, []) + [score_settings];
455 for i in tex_settings.get ("instruments", []):
456 if i in tex_settings.get ("instrumentalscores", []):
457 score_map[i] = score_map.get (i, []) + [score_settings];
458 tex_settings["works"] = instruments_scores;
460 tex_include_templates = settings.get_template_names("TeX_*.itex");
461 for t in tex_include_templates:
462 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
463 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
465 for (score, parts) in score_map.items ():
466 this_settings = copy.deepcopy (tex_settings);
467 this_settings["scoretype"] = score;
468 this_settings["scores"] = parts;
469 #this_settings["scorebasetype"] = "Score";
470 #this_settings["scorenamebase"] = "ScoreType";
471 tmpopts = this_settings.get ("tex_options", [])
472 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
473 this_settings["tex_options"] = tmpopts
474 if "createCriticalComments" not in tex_settings:
475 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
476 this_settings["is_instrument"] = score in tex_settings.get ("instruments", []);
477 #this_settings["scorebasetype"] = "Instrument";
478 #this_settings["scorenamebase"] = "Name";
479 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
482 # Create the tex instruments file only if we have instruments!
483 if set(this_settings.get("instruments",[])) != set(this_settings.get("noscore_instruments",[])):
484 this_settings = copy.deepcopy (tex_settings);
485 this_settings["scoretype"] = "InstrumentalParts";
486 tmpopts = this_settings.get ("tex_options", [])
487 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
488 this_settings["tex_options"] = tmpopts
489 if "createCriticalComments" not in tex_settings:
490 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
491 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
492 else:
493 print " No separate instrumental scores, not creating instruments tex file."
495 return [tex_files, tex_includes];
499 ######################################################################
500 # Creating Makefile
501 ######################################################################
503 def generate_make_files (settings, lily_files, tex_files):
504 make_settings = settings.raw_data.copy ();
506 if settings.has_tex ():
507 tex_settings = settings.get_tex_settings ();
508 tex_settings["includes"] = tex_files[1];
509 tex_settings["files"] = tex_files[0];
510 make_settings["latex"] = tex_settings;
512 score_map = {};
513 instruments_scores = [];
514 nr = 0;
515 for s in settings.score_names ():
516 nr += 1;
517 score_settings = settings.get_score_settings (s);
518 if nr > 1:
519 score_settings["nr"] = nr;
520 else:
521 score_settings["nr"] = "";
522 score_settings["srcfiles"] = lily_files[s];
523 instruments_scores.append (score_settings);
524 for p in score_settings.get ("scores", []):
525 score_map[p] = score_map.get (p, []) + [score_settings];
526 make_settings["works"] = instruments_scores;
528 template = settings.get_template ("Makefile");
529 #basename = tex_settings.get ("basename", "");
530 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
531 return file;
536 ######################################################################
537 # Creating webshop_descriptions.def
538 ######################################################################
540 def generate_webshop_files (settings, lily_files, tex_files):
541 webshop_settings = settings.raw_data.copy ();
542 template = settings.get_template ("webshop_descriptions.def");
543 #basename = tex_settings.get ("basename", "");
544 #print pp.pprint(settings.raw_data);
545 #print pp.pprint(settings.score_names ())
547 scores=[];
549 for s in settings.score_names ():
550 score_settings = settings.get_score_settings (s);
551 noscore_instruments = score_settings.get ("noscore_instruments", [])
552 for i in score_settings.get ("scores", []) + ["Instruments"] + score_settings.get ("instruments", []):
553 if i in noscore_instruments:
554 continue;
555 score_info = score_types.get (i, {});
556 score_type = score_info.get ("Name", i);
557 score_id = score_info.get ("Number", "XXX");
558 try:
559 # Replace '1a' to 1.01 (i.e. appended letters will indicate decimals, so they are sorted after 1)
560 def chartoindex(matchobj):
561 return "%s.%02d" % (matchobj.group(1), ord(matchobj.group(2).lower())-96);
562 sid = re.sub(r'^([0-9]+)([a-zA-Z])$', chartoindex, score_id);
563 sid = float(sid);
564 except ValueError as e:
565 sid = score_id;
566 scores.append({"id": sid, "sku": score_settings.get ("scorenumber")+"-"+score_id, "type": score_type });
568 webshop_settings["webshop_editions"] = sorted (scores, key=lambda k: k.get("id", 0));
570 webshop_settings.update (webshop_settings.get("defaults", {}));
571 file = write_file (settings.out_dir, "webshop_descriptions.def", template.render (webshop_settings));
575 ######################################################################
576 # Link the orchestrallily package
577 ######################################################################
579 def generate_oly_link (settings):
580 global script_path;
581 try:
582 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
583 except OSError:
584 pass
586 ######################################################################
587 # Main function
588 ######################################################################
590 def main ():
591 settings = Settings ();
592 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
593 (settings.out_dir, settings.get_template_name () ));
595 print ("Creating Lilypond files")
596 lily_files = generate_scores (settings);
598 tex_files = []
599 if settings.has_tex ():
600 print ("Creating LaTeX files")
601 tex_files = generate_tex_files (settings, lily_files);
602 print ("Creating OrchestralLily package link")
603 generate_oly_link (settings);
604 print ("Creating webshop_descriptions.def")
605 generate_webshop_files (settings, lily_files, tex_files);
606 print ("Creating Makefile")
607 generate_make_files (settings, lily_files, tex_files);
609 main ();