import argparse import os import re from django.conf import settings from django.template import Engine, Context from os.path import join from pathlib import Path from audio_scanner import find_audios from model import Chord, Line, Song, Verse def mkdir(path): if not os.path.exists(path): os.mkdir(path) # Note that re.match prepends ^ to the pattern, whereas re.search doesn't def read_property(text, key): if text is None: return None match = re.search(key + "={(.*?)}", text) return match.group(1) if match else None def extra_put(extra, index, the_type, data=None): payload = {'type': the_type, 'data': data} if data else {'type': the_type} if index not in extra: extra[index] = [] extra[index].append(payload) class SongLoader: def __init__(self, latex_file, audio_dir=None): self.index = 1 self.category = None self.categories = [] self.songs = [] if audio_dir: self.audio_dir = audio_dir self.scan(latex_file) def scan(self, latex_file): main_file = open(latex_file, 'r') canciones_dir = join(str(Path(latex_file).parent), "canciones") for line in main_file.readlines(): # Remove newline if line[-1] == '\n': line = line[:-1] # Remove comments line = re.sub(r"%.*$", "", line) # Read counter and category change (max 1 per line) re_set_counter_match = re.search(r"\\setcounter{songnum}{(\d+)}", line) if re_set_counter_match is not None: self.index = int(re_set_counter_match.group(1)) re_chapter_match = re.search(r"\\songchapter{(.*?)}", line) if re_chapter_match is not None: self.category = re_chapter_match.group(1) self.categories.append(self.category) # Traverse into \input commands if path starts w/ 'canciones/' re_input_match = re.search(r"\\input{canciones/(.*?)}", line) if re_input_match is not None: input_file = join(canciones_dir, re_input_match.group(1)) if not input_file.endswith(".tex"): input_file += ".tex" self.scan_song_file(input_file) def scan_others(self, folder, start_index): self.index = start_index self.category = "Nuevas" self.categories.append(self.category) files_scanned = [s.latex_file for s in self.songs] files_to_scan = [os.path.join(root, name) for root, dirs, files in os.walk(folder, topdown=False) for name in files] files_to_scan = [f for f in files_to_scan if f.endswith(".tex") and f[f.index('/') + 1:] not in files_scanned] files_to_scan = sorted(files_to_scan) for f in files_to_scan: print("Scanning extra file", f) self.scan_song_file(f) def scan_song_file(self, song_file): # Variables ignore = False current_song = None current_verse = None memory = None memorizing = False replay_index = 0 transpose = 0 for line in open(song_file, "r").readlines(): # Remove newline if line[-1] == '\n': line = line[:-1] # Remove commends and \brk commands text = re.sub(r"%.*$", "", line) text = re.sub(r"\\brk({})?", '', text) text = re.sub(r"``", u"\u201C", text) text = re.sub(r"''", u"\u201D", text) text = re.sub(r"`", u"\u2018", text) text = re.sub(r"'", u"\u2019", text) extras = {} i = 0 while i <= len(text): beginning = text[:i] remain = text[i:] if re.match(r"\\fi", remain): ignore = False text = beginning + text[i + len("\\fi"):] continue if ignore: i += 1 continue # Command lookup re_transpose_match = re.match(r"\\transpose *?{(-?\d+?)}", remain) if re_transpose_match: text = beginning + text[i + len(re_transpose_match.group(0)):] transpose = int(re_transpose_match.group(1)) continue re_song_begin_match = re.match(r"\\beginsong *?{(.*?)}(\[.*?])?", remain) if re_song_begin_match: text = beginning + text[i + len(re_song_begin_match.group(0)):] if current_song is not None: print("error end-begin song! %s at %s" % (line, song_file)) self.songs.append(current_song) self.index += 1 current_song = Song(re_song_begin_match.group(1), self.index, author=read_property(re_song_begin_match.group(2), "by"), origin=read_property(re_song_begin_match.group(2), "m"), category=self.category, latex_file=song_file[song_file.index('/') + 1:]) transpose = 0 memory = None memorizing = False replay_index = 0 if hasattr(self, "audio_dir"): for a in find_audios(self.index, self.audio_dir): current_song.add_audio(a) continue if re.match(r"\\endsong", remain): text = beginning + text[i + len("\\endsong"):] self.songs.append(current_song) current_song = None self.index += 1 continue re_verse_cmd_match = re.match(r"\\(begin|end)(verse|chorus)", remain) if re_verse_cmd_match: text = beginning + text[i + len(re_verse_cmd_match.group(0)):] is_chorus = re_verse_cmd_match.group(2) == "chorus" if current_song is None: print("verse %s found outside song in %s" % (line, song_file)) if re_verse_cmd_match.group(1) == "begin": if current_verse is not None: print("error end-begin verse! %s at %s" % (line, song_file)) current_song.add_verse(current_verse) if not is_chorus and memory is None: memory = [] memorizing = True replay_index = 0 current_verse = Verse(is_chorus) else: # end of verse/chorus if current_verse.is_chorus != is_chorus: print("ended chorus-verse with wrong command?") memorizing = False current_song.add_verse(current_verse) current_verse = None continue re_capo_match = re.match(r"\\capo{(\d+?)}", remain) if re_capo_match and current_song: text = beginning + text[i + len(re_capo_match.group(0)):] current_song.set_capo(int(re_capo_match.group(1))) continue if re.match(r"\\ifchorded", remain): text = beginning + text[i + len("\\ifchorded"):] continue if re.match(r"\\else", remain): ignore = True text = beginning + text[i + len("\\else"):] continue re_echo_match = re.match(r"\\echo[ \t]*?{((.|{.*?})*?)}", remain) if re_echo_match: text = beginning + re_echo_match.group(1) + "\\echoend" + text[i + len(re_echo_match.group(0)):] extra_put(extras, i, "echo") continue if re.match(r"\\echoend", remain): text = beginning + text[i + len("\\echoend"):] extra_put(extras, i, "echo") continue re_chord_match = re.match(r"\\\[(.+?)]", remain) if re_chord_match: text = beginning + text[i + len(re_chord_match.group(0)):] c = Chord(re_chord_match.group(1), transpose) extra_put(extras, i, "chord", c) if memorizing: memory.append(c) continue if re.match(r"\^", remain): text = beginning + text[i + len("^"):] if memory is not None and replay_index < len(memory): extra_put(extras, i, "chord", memory[replay_index]) replay_index += 1 continue re_dir_rep_match = re.match(r"\\([lr]rep)", remain) if re_dir_rep_match: text = beginning + text[i + len(re_dir_rep_match.group(0)):] extra_put(extras, i, "dir-rep", re_dir_rep_match.group(1)) continue re_rep_match = re.match(r"\\rep{(\d+?)}", remain) if re_rep_match: text = beginning + text[i + len(re_rep_match.group(0)):] extra_put(extras, i, 'rep', int(re_rep_match.group(1))) continue if re.match(r"\\memorize", remain): text = beginning + text[i + len("\\memorize"):] memory = [] memorizing = True continue if re.match(r"\\replay", remain): text = beginning + text[i + len("\\replay"):] replay_index = 0 continue # Command lookup end, removing any unrecognized command re_macro_match = re.match(r"\\([^ \t{\[]+)[ \t]*?({.*?}|\[.*?])*", remain) if re_macro_match: text = beginning + text[i + len(re_macro_match.group(0)):] print("Removed an unrecognized command:", re_macro_match.group(0)) continue i += 1 if not current_verse and text.strip() != '': print("l outside v:", text) continue if ignore or text.strip() == '': continue current_verse.add_line(Line(text, extras)) def sort_categories(self): result = {} for c in self.categories: result[c] = sorted([s for s in self.songs if s.category == c], key=lambda s: s.number) return result def print_index(self, index_file, dj_engine): context = Context({'sorted_categories': self.sort_categories()}) html = dj_engine.get_template("index.html").render(context) with open(index_file, 'w') as f: f.write(html) @staticmethod def print_song(song, directory, dj_engine): context = Context({'song': song}) num_dir = join(directory, "%03d" % song.number) mkdir(num_dir) with open(join(num_dir, "index.html"), 'w') as f: f.write(dj_engine.get_template("song_redir.html").render(context)) song_dir = join(directory, song.url()) mkdir(song_dir) with open(join(song_dir, "index.html"), 'w') as f: f.write(dj_engine.get_template("song.html").render(context)) def generate_html(self, output_dir, dj_engine): mkdir(output_dir) for song in self.songs: self.print_song(song, output_dir, dj_engine) self.print_index(join(output_dir, "index.html"), dj_engine) def create_argparser(): parser = argparse.ArgumentParser() parser.add_argument("--latex", required=True, nargs=1, help="The main LaTeX file. It may include other documents") parser.add_argument("--other-latex", required=False, nargs=1, default=[None], help="A folder with songs, those not referenced in the main file will be included at the end.") parser.add_argument("--other-index", required=False, nargs=1, type=int, default=[400], help="The first song number for songs outside the main songbook.") parser.add_argument("--audios", required=False, nargs=1, default=[None], help="The folder containing the audio files.") parser.add_argument("--output-dir", required=False, nargs=1, default=["public"]) return parser if __name__ == '__main__': args = create_argparser().parse_args() loader = SongLoader(args.latex[0], args.audios[0]) if args.other_latex: loader.scan_others(args.other_latex[0], int(args.other_index[0])) settings.configure(USE_TZ=False, USE_I18N=False) e = Engine(dirs=["res/templates/"]) loader.generate_html(args.output_dir[0], e)