Create definitions file for my webshop script
[orchestrallily.git] / generate_oly_score.py
blob3b1110b24f9ed988f4c57be975629d81cdb621c1
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.templatepath = script_path + '/Templates/' + templatename;
190 self.template_env = Environment (
191 loader = FileSystemLoader(self.templatepath),
192 block_start_string = '<$', block_end_string = '$>',
193 variable_start_string = '<<', variable_end_string = '>>',
194 comment_start_string = '<#', comment_end_string = '#>',
195 #line_statement_prefix = '#'
197 def get_template (self, template):
198 return self.template_env.get_template (template);
201 def init_arrays (self):
202 # Globals
203 self.globals = self.raw_data.copy ();
204 del self.globals["defaults"];
205 if "latex" in self.globals:
206 del self.globals["latex"];
207 for i in self.globals.get ("scores", []):
208 if i in self.globals:
209 del self.globals[i];
211 def assemble_filename (self, base, id, name, ext):
212 parts = [base, id, name];
213 if "" in parts:
214 parts.remove ("")
215 return "_".join (parts) + "." + ext;
216 def assemble_movement_filename (self, basename, mvmnt):
217 return self.assemble_filename( basename, "Music", mvmnt, "ily");
218 def assemble_instrument_filename (self, basename, instr):
219 return self.assemble_filename( basename, "Instrument", instr, "ly");
220 def assemble_score_filename (self, basename, instr):
221 return self.assemble_filename( basename, "Score", instr, "ly");
222 def assemble_settings_filename (self, basename, s):
223 return self.assemble_filename( basename, "Settings", s, "ily");
224 def assemble_itex_filename (self, basename, filename):
225 return self.assemble_filename( "TeX", basename, filename, "itex");
226 def assemble_texscore_filename (self, basename, score):
227 return self.assemble_filename( "TeX", basename, "Score_" + score, "tex");
229 ######################################################################
230 # File Writing
231 ######################################################################
233 def write_file (path, fname, contents):
234 file_name = path + fname;
235 fn = file_name
236 if os.path.exists (file_name):
237 fn += ".new"
239 dir = os.path.dirname (fn);
240 if not os.path.exists (dir):
241 os.mkdir (dir)
243 try:
244 out_f = codecs.open (fn, "w", "utf-8")
245 s = out_f.write(contents)
246 out_f.write ("\n")
247 out_f.close()
248 except IOError:
249 print ("Unable to write to output file '%s'. Exiting..." % fn)
250 exit (-1);
252 # If the file already existed, check if the new file is identical.
253 if (fn != file_name):
254 patchfile = os.path.join (dir, "patches", os.path.basename(file_name) + ".patch")
255 if os.path.exists (patchfile):
256 try:
257 retcode = subprocess.call("patch -Ns \""+ fn+ "\" \"" + patchfile + "\"", shell=True)
258 if retcode < 0:
259 print >>sys.stderr, "Unable to apply patch to file \"", fn, "\"."
260 except OSError, e:
261 print >>sys.stderr, "Execution failed:", e
263 if filecmp.cmp (fn, file_name):
264 os.unlink (fn);
265 else:
266 print ("A file %s already existed, created new file %s." % (os.path.basename(file_name), os.path.basename(fn)))
270 ######################################################################
271 # Creating movement files
272 ######################################################################
275 def generate_movement_files (score_name, score_settings, settings):
276 parts_files = [];
277 for part_settings in settings.get_score_parts (score_settings):
278 template = settings.get_template ("Lily_Music_Movement.ily");
279 basename = part_settings.get ("basename", score_name);
280 filename = part_settings.get ("filename", score_name + "Part");
281 filename = settings.assemble_movement_filename (basename, filename);
282 write_file (settings.out_dir, filename, template.render (part_settings));
283 parts_files.append (filename);
285 return parts_files;
289 ######################################################################
290 # Creating instrumental score files
291 ######################################################################
293 def generate_instrument_files (score_name, score_settings, settings):
294 instrument_files = [];
295 settings_files = set ();
296 instrument_settings = score_settings.copy ();
297 instrument_settings["parts"] = settings.get_score_parts (score_settings);
299 template = settings.get_template ("Lily_Instrument.ly");
300 basename = score_settings.get ("basename", score_name );
302 noscore_instruments = score_settings.get ("noscore_instruments", [])
303 for i in score_settings.get ("instruments", []):
304 if i in noscore_instruments:
305 continue;
306 instrument_settings["instrument"] = i;
307 if i in score_settings.get ("vocalvoices"):
308 instrument_settings["settings"] = "VocalVoice";
309 else:
310 instrument_settings["settings"] = "Instrument";
311 settings_files.add (instrument_settings["settings"]);
312 filename = settings.assemble_instrument_filename (basename, i);
313 write_file (settings.out_dir, filename, template.render (instrument_settings));
314 instrument_files.append (filename);
316 return (instrument_files, settings_files);
320 ######################################################################
321 # Creating score files
322 ######################################################################
325 score_name_map = {
326 "Particell": "Particell",
328 settings_map = {
329 "OrganScore": "VocalScore",
330 "ChoralScore": "ChoralScore",
331 "LongScore": "FullScore",
332 "OriginalScore": "FullScore",
333 "Particell": "FullScore"
335 scores_cues = ["ChoralScore", "VocalScore"];
337 def generate_score_files (score_name, score_settings, settings):
338 score_files = [];
339 settings_files = set ();
340 s_settings = score_settings.copy ();
341 s_settings["parts"] = settings.get_score_parts (score_settings);
342 s_settings["fullscore"] = True;
344 template = settings.get_template ("Lily_Score.ly");
345 basename = score_settings.get ("basename", score_name );
347 for s in score_settings.get ("scores", []):
348 fullsn = score_name_map.get (s, (s + "Score"));
349 s_settings["score"] = fullsn;
350 s_settings["nocues"] = fullsn not in scores_cues;
351 s_settings["settings"] = settings_map.get (fullsn,fullsn)
352 settings_files.add (s_settings["settings"]);
353 filename = settings.assemble_score_filename (basename, s);
354 write_file (settings.out_dir, filename, template.render (s_settings));
355 score_files.append (filename);
357 return (score_files, settings_files);
361 ######################################################################
362 # Creating settings files
363 ######################################################################
365 def write_settings_file_if_exists (settings, score_settings, template, filename):
366 try:
367 template = settings.get_template (template);
368 basename = score_settings.get ("basename", "");
369 filename = settings.assemble_settings_filename (basename, filename);
370 write_file (settings.out_dir, filename, template.render (score_settings));
371 return filename;
372 except jinja2.exceptions.TemplateNotFound:
373 return None;
375 def generate_settings_files (score_name, score_settings, settings, settings_files):
376 out_files = [];
377 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Global.ily", "Global" ));
378 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings.ily", "" ));
380 for s in settings_files:
381 score_settings["settings"] = s.lower ();
382 out_files.append (write_settings_file_if_exists (settings, score_settings, "Lily_Settings_Generic.ily", s));
384 return out_files;
388 ######################################################################
389 # Generation of Lilypond Files
390 ######################################################################
392 def generate_scores (settings):
393 files = {}
394 for s in settings.score_names ():
395 score_settings = settings.get_score_settings (s);
396 parts_files = generate_movement_files (s, score_settings, settings);
397 (instrument_files, isettings_files) = generate_instrument_files (s, score_settings, settings);
398 (score_files, ssettings_files) = generate_score_files (s, score_settings, settings);
399 included_settings_files = ssettings_files | isettings_files;
400 score_settings["parts_files"] = parts_files;
401 settings_files = generate_settings_files (s, score_settings, settings, included_settings_files );
402 files[s] = {"settings": settings_files,
403 "scores": score_files,
404 "instruments": instrument_files,
405 "parts": parts_files };
406 return files
410 ######################################################################
411 # Creating LaTeX files
412 ######################################################################
414 def write_itex_file (settings, tex_settings, template, filename):
415 template = settings.get_template (template);
416 basename = tex_settings.get ("basename", "");
417 filename = settings.assemble_itex_filename (basename, filename);
418 write_file (settings.out_dir, filename, template.render (tex_settings));
419 return filename;
421 def write_texscore_file (settings, tex_settings, template, score):
422 template = settings.get_template (template);
423 basename = tex_settings.get ("basename", "");
424 filename = settings.assemble_texscore_filename (basename, score);
425 write_file (settings.out_dir, filename, template.render (tex_settings));
426 return filename;
428 no_criticalcomments_scores = ["Vocal", "Choral", "Organ"];
429 tex_options_map = {
430 "Organ": "vocalscore",
431 "Choral": "choralscore",
432 "Vocal": "vocalscore",
433 "Long": "fullscore",
434 "Full": "fullscore",
435 "Original": "fullscore",
436 "Particell": "vocalscore",
437 "InstrumentalParts": "instrumentalparts",
438 # TODO: chambermusic
441 def generate_tex_files (settings, lily_files):
442 tex_files = [];
443 tex_includes = [];
444 tex_settings = settings.get_tex_settings ();
445 tex_settings["lily_files"] = lily_files;
447 score_map = {};
448 instruments_scores = [];
449 for s in settings.score_names ():
450 score_settings = settings.get_score_settings (s);
451 instruments_scores.append (score_settings);
452 for p in score_settings.get ("scores", []):
453 score_map[p] = score_map.get (p, []) + [score_settings];
454 for i in tex_settings.get ("instruments", []):
455 if i in tex_settings.get ("instrumentalscores", []):
456 score_map[i] = score_map.get (i, []) + [score_settings];
457 tex_settings["works"] = instruments_scores;
459 tex_include_templates = settings.get_template_names("TeX_*.itex");
460 for t in tex_include_templates:
461 base = re.sub( r'TeX_(.*)\.itex', r'\1', t);
462 tex_includes.append (write_itex_file (settings, tex_settings, t, base));
464 for (score, parts) in score_map.items ():
465 this_settings = copy.deepcopy (tex_settings);
466 this_settings["scoretype"] = score;
467 this_settings["scores"] = parts;
468 #this_settings["scorebasetype"] = "Score";
469 #this_settings["scorenamebase"] = "ScoreType";
470 tmpopts = this_settings.get ("tex_options", [])
471 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
472 this_settings["tex_options"] = tmpopts
473 if "createCriticalComments" not in tex_settings:
474 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
475 this_settings["is_instrument"] = score in tex_settings.get ("instruments", []);
476 #this_settings["scorebasetype"] = "Instrument";
477 #this_settings["scorenamebase"] = "Name";
478 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Score.tex", score))
480 this_settings = copy.deepcopy (tex_settings);
481 this_settings["scoretype"] = "InstrumentalParts";
482 tmpopts = this_settings.get ("tex_options", [])
483 tmpopts.append (tex_options_map.get (this_settings["scoretype"], ""))
484 this_settings["tex_options"] = tmpopts
485 if "createCriticalComments" not in tex_settings:
486 this_settings["createCriticalComments"] = score not in no_criticalcomments_scores;
487 tex_files.append (write_texscore_file (settings, this_settings, "TeX_Instruments.tex", "Instruments"))
489 return [tex_files, tex_includes];
493 ######################################################################
494 # Creating Makefile
495 ######################################################################
497 def generate_make_files (settings, lily_files, tex_files):
498 make_settings = settings.raw_data.copy ();
500 if settings.has_tex ():
501 tex_settings = settings.get_tex_settings ();
502 tex_settings["includes"] = tex_files[1];
503 tex_settings["files"] = tex_files[0];
504 make_settings["latex"] = tex_settings;
506 score_map = {};
507 instruments_scores = [];
508 nr = 0;
509 for s in settings.score_names ():
510 nr += 1;
511 score_settings = settings.get_score_settings (s);
512 if nr > 1:
513 score_settings["nr"] = nr;
514 else:
515 score_settings["nr"] = "";
516 score_settings["srcfiles"] = lily_files[s];
517 instruments_scores.append (score_settings);
518 for p in score_settings.get ("scores", []):
519 score_map[p] = score_map.get (p, []) + [score_settings];
520 make_settings["works"] = instruments_scores;
522 template = settings.get_template ("Makefile");
523 #basename = tex_settings.get ("basename", "");
524 file = write_file (settings.out_dir, "Makefile", template.render (make_settings));
525 return file;
530 ######################################################################
531 # Creating webshop_descriptions.def
532 ######################################################################
534 def generate_webshop_files (settings, lily_files, tex_files):
535 webshop_settings = settings.raw_data.copy ();
536 template = settings.get_template ("webshop_descriptions.def");
537 #basename = tex_settings.get ("basename", "");
538 #print pp.pprint(settings.raw_data);
539 #print pp.pprint(settings.score_names ())
541 scores=[];
542 noscore_instruments = webshop_settings.get ("noscore_instruments", [])
544 for s in settings.score_names ():
545 score_settings = settings.get_score_settings (s);
546 for i in score_settings.get ("scores", []) + ["Instruments"] + score_settings.get ("instruments", []):
547 if i in noscore_instruments:
548 continue;
549 score_info = score_types.get (i, {});
550 score_type = score_info.get ("Name", "");
551 score_id = score_info.get ("Number", "XXX");
552 scores.append({"sku": score_settings.get ("scorenumber")+"-"+score_id, "type": score_type });
554 webshop_settings["webshop_editions"] = scores;
555 file = write_file (settings.out_dir, "webshop_descriptions.def", template.render (webshop_settings));
559 ######################################################################
560 # Link the orchestrallily package
561 ######################################################################
563 def generate_oly_link (settings):
564 global script_path;
565 try:
566 os.symlink ("../"+script_path, settings.out_dir+"orchestrallily")
567 except OSError:
568 pass
570 ######################################################################
571 # Main function
572 ######################################################################
574 def main ():
575 settings = Settings ();
576 print ("Creating OrchestralLily template in \"%s\", using template \"%s\"." %
577 (settings.out_dir, settings.get_template_name () ));
579 print ("Creating Lilypond files")
580 lily_files = generate_scores (settings);
582 tex_files = []
583 if settings.has_tex ():
584 print ("Creating LaTeX files")
585 tex_files = generate_tex_files (settings, lily_files);
586 print ("Creating OrchestralLily package link")
587 generate_oly_link (settings);
588 print ("Creating webshop_descriptions.def")
589 generate_webshop_files (settings, lily_files, tex_files);
590 print ("Creating Makefile")
591 generate_make_files (settings, lily_files, tex_files);
593 main ();