commit c8c7eae33c1107adf34d20b55c7bd3128e54c49b Author: Carlos Galindo Date: Wed Oct 14 00:54:52 2020 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..254c49b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +public/ +audios/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..82a8446 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "latex"] + path = latex + url = https://gitlab.com/parroquia-san-leandro/cancionero-25.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8733d10 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: clean + mkdir -p public + ln -s ../audios public/audios + python3 src/latex_scanner.py + +clean: + rm -rf public diff --git a/latex b/latex new file mode 160000 index 0000000..312ecba --- /dev/null +++ b/latex @@ -0,0 +1 @@ +Subproject commit 312ecbac9f86f3bad0f474018c25c5344e207470 diff --git a/res/index.css b/res/index.css new file mode 100644 index 0000000..7389f52 --- /dev/null +++ b/res/index.css @@ -0,0 +1,57 @@ +.songs { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; +} + +.songs li { + cursor: pointer; + position: relative; + left: 0; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + min-height: 1.6em; + border-radius: 4px; +} + +.songs li:hover { + color: #607D8B; + background-color: #DDD; + transition-duration: 500ms; +} + +/* .songs li.selected { + background-color: #CFD8DC; + transition-duration: 100ms; +} + +.songs li.selected:hover { + background-color: #BBD8DC; + color: white; + transition-duration: 500ms; +} */ + +.songs li.hasChords { + background-color: #FBB; +} + +.songs li.hasChords:hover { + color: #607D8B; + background-color: #FAA; +} + +.songs .numberBadge { + display: inline-block; + font-size: small; + color: white; + padding: 0.8em 0.7em 0 0.7em; + background-color: #405061; + line-height: 1em; + position: relative; + left: -1px; + top: -4px; + min-height: 1.8em; + margin-right: .8em; + border-radius: 4px 0 0 4px; +} \ No newline at end of file diff --git a/res/index.html b/res/index.html new file mode 100644 index 0000000..1cd14ce --- /dev/null +++ b/res/index.html @@ -0,0 +1,4 @@ +

Índice

+ diff --git a/res/main.css b/res/main.css new file mode 100644 index 0000000..0372680 --- /dev/null +++ b/res/main.css @@ -0,0 +1,44 @@ +h1 { + color: #369; + font-size: 250%; +} +h2, h3 { + color: #444; + font-weight: lighter; +} +body { + font-family: Helvetica, Arial, sans-serif; + margin: 2em; + color: #333; +} + +.nav * { + display: inline; +} + +.nav li { + padding: 0.5em; + margin: 0.2em; + background: #eee; + border-radius: 5px; +} + +.nav li:hover { + background: #ccc; + transition-duration: 500ms; +} + +.nav li.selected { + background: #bbb; + transition-duration: 100ms; +} + +.nav .selected a { + color: #111; + transition-duration: 100ms; +} + +.nav a { + color: #333; + text-decoration: none; +} diff --git a/res/page.html b/res/page.html new file mode 100644 index 0000000..e38672c --- /dev/null +++ b/res/page.html @@ -0,0 +1,20 @@ + + + + + Cancionero - Parroquia San Leandro + + %s + + +
+

Cancionero San Leandro

+
+
+ %s +
+ + + diff --git a/res/sizes.js b/res/sizes.js new file mode 100644 index 0000000..b031f78 --- /dev/null +++ b/res/sizes.js @@ -0,0 +1,12 @@ +SIZE_STEPS = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150, 170, 200, 240, 300]; + +currSize = SIZE_STEPS.indexOf(100) + +/** Changes the size of the lyrics and chords. */ +function size(steps) { + if (steps === 0) { + currSize = SIZE_STEPS.indexOf(100); + } + currSize += steps; + document.getElementById('wholeSongDiv').style.fontSize = SIZE_STEPS[currSize] + '%'; +} diff --git a/res/song.css b/res/song.css new file mode 100644 index 0000000..80a15a7 --- /dev/null +++ b/res/song.css @@ -0,0 +1,65 @@ +table.chordedline { + border-spacing: 0px; + display: inline; +} + +.song div { + padding: 0.5em 0; +} + +.chordedline td { + padding: 0px; +} + +.chord { + font-style: italic; + color: darkgreen; +} + +.chorus { + font-weight: bold; + border-left: black 2px solid; + padding-left: 0.5em !important; +} + +.chorus div { + padding-left: 0.5em; +} + +.chorus table { + margin-left: 0; + padding-left: 0; + padding-right: 0; +} + +.lrep { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipV; + -ms-filter: "FlipV"; + background-image: url(img/repeat-sign.svg); + display: inline-block; + width: 1em; + height: 2em; +} + +.rrep { + background-image: url(img/repeat-sign.svg); + display: inline-block; + width: 1em; + height: 2em; +} + +.rep { + font-style: italic; +} + +button.small { + padding: 0.1em; +} + +.echo { + font-style: italic; +} diff --git a/res/song.html b/res/song.html new file mode 100644 index 0000000..299dad4 --- /dev/null +++ b/res/song.html @@ -0,0 +1,48 @@ +
+ + +

%s

+ %s + %s +

Ajustes

+
+ + + + +
+
+ + + + + + + +
+ %s +

Canción

+
+ %s +
+
+ %s + %s +
+ Ver archivo original + +
diff --git a/res/song_li.html b/res/song_li.html new file mode 100644 index 0000000..02c689e --- /dev/null +++ b/res/song_li.html @@ -0,0 +1,4 @@ +
  • + %d. + %s%s%s +
  • diff --git a/res/transpose.js b/res/transpose.js new file mode 100644 index 0000000..1f295db --- /dev/null +++ b/res/transpose.js @@ -0,0 +1,39 @@ +ENG_INDEX = {'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3, 'E': 4, 'Fb': 4, 'F': 5, 'E#': 5, 'F#': 6, 'Gb': 6, 'G': 7, 'G#': 8, 'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11, 'Cb': 11, 'B#': 0} +LAT_INDEX = {'Do': 0, 'Do#': 1, 'Reb': 1, 'Re': 2, 'Re#': 3, 'Mib': 3, 'Mi': 4, 'Fab': 4, 'Fa': 5, 'Mi#': 5, 'Fa#': 6, 'Solb': 6, 'Sol': 7, 'Sol#': 8, 'Lab': 8, 'La': 9, 'La#': 10, 'Sib': 10, 'Si': 11, 'Dob': 11, 'Si#': 0} +CHORDS_LAT = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'Sib', 'Si'] +CHORDS_ENG = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B'] + + +/** Changes all chords to a given semitone relative to the original */ +function transpose(n) { + transposeAdd(n - getTranspose()) +} + +/** Transposes all chords by n steps */ +function transposeAdd(n) { + for (c of document.getElementsByClassName('c')) { + chord = c.innerHTML + if (LAT_INDEX[chord] == undefined) { + throw Error("Unknown chord: " + chord) + } + i = LAT_INDEX[chord] + j = (i + n + 12) % 12 + c.innerHTML = CHORDS_LAT[j] + } + + setTransposeSelector(getTranspose() + n) +} + +function setTransposeSelector(n) { + while (n > 6) { + n -= 12 + } + while (n < -6) { + n += 12 + } + document.getElementById("transposeSelect").value = n +} + +function getTranspose() { + return Number.parseInt(document.getElementById("transposeSelect").value) +} diff --git a/src/audio_scanner.py b/src/audio_scanner.py new file mode 100644 index 0000000..dfda494 --- /dev/null +++ b/src/audio_scanner.py @@ -0,0 +1,25 @@ +from os import listdir +from os.path import isfile, join +from song_types import Audio +import re +from datetime import datetime + + +AUDIO_DIR = "audios/" + +def find_audios(index): + """ + Finds all audios in a folder that match the given index. + Audios must be in the format [index]_[YYYY]-[MM]-[DD].mp3 + :param index: An integer denoting the song's index + :return: A list of matching Audio objects + """ + res = [] + for f in listdir(AUDIO_DIR): + full_file = join(AUDIO_DIR, f) + re_date_match = re.match(r"^%03d_(\d{4}-[01]\d-[0-3]\d).mp3$" % index, f) + if not isfile(full_file) or not re_date_match: + continue + date = datetime.strptime(re_date_match.group(1), "%Y-%m-%d") + res.append(Audio(date, join("../", full_file))) + return res diff --git a/src/latex_scanner.py b/src/latex_scanner.py new file mode 100644 index 0000000..b08668d --- /dev/null +++ b/src/latex_scanner.py @@ -0,0 +1,269 @@ +from song_types import * +from audio_scanner import find_audios +from os.path import join +from pathlib import Path +import shutil +import os +import re + + +# 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) + + +page_template = readfile("res/page.html") +index_template = readfile("res/index.html") +index_per_song_template = readfile("res/song_li.html") +index_css = '\n\t' +song_css = '\n\t' + + +class SongLoader: + def __init__(self, latex_file): + self.index = 1 + self.category = None + self.categories = [] + self.songs = [] + self.scan(latex_file) + self.memory = {} + + 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_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) + + extras = {} + for i in range(len(text)): + beginning = text[:i] + remain = text[i:] + if re.match(r"\\fi", remain): + ignore = False + text = beginning + text[i + len("\\fi"):] + i -= 1 + continue + if ignore: + 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)) + i -= 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) + transpose = 0 + memory = None + memorizing = False + replay_index = 0 + for a in find_audios(self.index): + current_song.add_audio(a) + i -= 1 + continue + if re.match(r"\\endsong", remain): + text = beginning + text[i + len("\\endsong"):] + self.songs.append(current_song) + current_song = None + self.index += 1 + i -= 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 + i -= 1 + 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))) + i -= 1 + continue + if re.match(r"\\ifchorded", remain): + text = beginning + text[i + len("\\ifchorded"):] + i -= 1 + continue + if re.match(r"\\else", remain): + ignore = True + text = beginning + text[i + len("\\else"):] + i -= 1 + 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") + i -= 1 + continue + if re.match(r"\\echoend", remain): + text = beginning + text[i + len("\\echoend"):] + extra_put(extras, i, "echo") + i -= 1 + 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) + i -= 1 + 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 + i -= 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)) + i -= 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))) + i -= 1 + continue + if re.match(r"\\memorize", remain): + text = beginning + text[i + len("\\memorize"):] + memory = [] + memorizing = True + i -= 1 + continue + if re.match(r"\\replay", remain): + text = beginning + text[i + len("\\replay"):] + replay_index = 0 + i -= 1 + 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)) + i -= 1 + continue + 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 print_index(self, index_file="index.html"): + self.songs = sorted(self.songs, key=lambda s: s.number) + body = index_template % join_list([index_per_song_template % + (s.get_url(), + ' class="hasChords"' if not s.chorded() else '', + s.number, s.name, + " por %s " % s.author if s.author else "", + " basado en %s " % s.origin if s.origin else "") + for s in self.songs]) + with open(index_file, 'w') as f: + f.write(page_template % (index_css, body)) + + def print_songs(self, directory="."): + for song in self.songs: + song_dir = join(directory, song.get_url()) + if not os.path.exists(song_dir): + os.mkdir(song_dir) + with open(join(song_dir, "index.html"), 'w') as f: + f.write(page_template % (song_css, str(song))) + + +def copy_static(source_dir, target_dir): + for f in os.listdir(source_dir): + if re.search(r"\.(css|js)$", f): + shutil.copy2(join(source_dir, f), target_dir) + + +if __name__ == '__main__': + loader = SongLoader("latex/cancionero.tex") + target_dir = "public/" + if not os.path.exists(target_dir): + os.mkdir(target_dir) + loader.print_songs(target_dir) + loader.print_index(target_dir + "index.html") + copy_static("res", target_dir) diff --git a/src/song_types.py b/src/song_types.py new file mode 100644 index 0000000..3cc88ba --- /dev/null +++ b/src/song_types.py @@ -0,0 +1,234 @@ +import re +import functools as ft +from datetime import datetime +import locale + + +def join_list(the_list, separator="\n"): + return ft.reduce(lambda x, y: x + (separator if x else "") + str(y), the_list, "") + + +def readfile(file): + with open(file, 'r') as f: + return join_list(f.readlines(), '') + + +locale.setlocale(locale.LC_ALL, "es_ES.UTF-8") +song_template = readfile("res/song.html") + + +class Song: + def __init__(self, name, number, author=None, origin=None, latex_file=None, audios=None, capo=0, category=None): + if audios is None: + audios = [] + self.verses = [] + self.name = name + self.number = number + self.author = author + self.origin = origin + self.latex_file = latex_file + self.audios = audios + self.capo = capo + self.category = category + + def __str__(self): + return song_template % ( + self.name, + "
    Autor: %s
    " % self.author if self.author else "", + "
    Basada en: %s
    " % self.origin if self.origin else "", + """
    Tono original: Cejilla {s.capo} +
    """ + .format(s=self) if self.capo != 0 else "", + join_list(self.verses), + "

    Audios

    " if len(self.audios) > 0 else "", + join_list(self.audios), + self.latex_file) + + def set_capo(self, capo): + self.capo = capo + + def add_audio(self, audio): + assert isinstance(audio, Audio) + self.audios.append(audio) + + def add_verse(self, verse): + assert isinstance(verse, Verse) + self.verses.append(verse) + + def get_url(self): + return self.name + + def chorded(self): + for v in self.verses: + if v.chorded(): + return True + return False + + +class Verse: + def __init__(self, is_chorus=False): + self.is_chorus = is_chorus + self.lines = [] + + def __str__(self): + return """ +
    + %s +
    + """ % ("chorus" if self.is_chorus else "verse", join_list([str(l) for l in self.lines], "\n
    \n")) + + def add_line(self, line): + assert isinstance(line, Line) + self.lines.append(line) + + def chorded(self): + for line in self.lines: + if line.chorded(): + return True + return False + + +class Line: + ECHO_BEGIN = '' + ECHO_END = '' + + def __init__(self, text, extras): + self.text = text + self.extras = extras + self.chord_arr = [] + self.lyric_arr = [] + self.build() + + def __str__(self): + assert len(self.chord_arr) == len(self.lyric_arr) + return join_list(["""%s
    %s%s
    """ + % (self.chord_arr[i]["rowspan"] if "rowspan" in self.chord_arr[i] else 1, + '' % self.chord_arr[i]["class"] if "class" in self.chord_arr[i] else "", + self.chord_arr[i]["chord"] if "chord" in self.chord_arr[i] else "", + '%s' % self.lyric_arr[i] if "rowspan" not in self.chord_arr[i] else '' + ) for i in range(len(self.chord_arr))], separator='') + + def add_chord(self, index, chord): + self.add_item(index, "chord", chord) + + def add_dir_rep(self, index, data): + self.add_item(index, "dir-rep", data) + + def add_rep(self, index, data): + self.add_item(index, "rep", data) + + def add_echo(self, index): + self.add_item(index, "echo", None) + + def add_item(self, index, the_type, data): + if index not in self.extras: + self.extras[index] = [] + self.extras[index].append({'type': the_type, 'data': data}) + + def build(self): + self.chord_arr = [{}] + self.lyric_arr = [""] + + inside_echo = False + mid = True + for i in range(len(self.text) + 1): + for e in self.extras[i] if i in self.extras else []: + if e["type"] == "chord": + floating = (i < len(self.text) and self.text[i] == ' ' and + (i == 0 or self.text[i - 1] == ' ')) or \ + (i >= len(self.text) and self.text[i - 1] == ' ') + self.chord_arr.append({'chord': e["data"]}) + if inside_echo and self.lyric_arr[-1] != '': + self.lyric_arr[-1] += Line.ECHO_END + if floating: + self.lyric_arr.append('') + self.chord_arr.append({}) + self.lyric_arr.append(Line.ECHO_BEGIN if inside_echo else '') + mid = True + elif e["type"] == "dir-rep": + self.chord_arr.append({'class': e["data"], 'rowspan': 2}) + self.lyric_arr.append('') + if mid and inside_echo: + self.lyric_arr[-1] += Line.ECHO_END + mid = False + elif e["type"] == "rep": + self.lyric_arr.append('(x%d)' % (e["data"])) + self.chord_arr.append({}) + mid = False + elif e["type"] == "echo": + if not mid: + self.lyric_arr.append(Line.ECHO_BEGIN if inside_echo else '') + mid = True + start = Line.ECHO_BEGIN + '(' + end = ')' + Line.ECHO_END + self.lyric_arr[-1] += end if inside_echo else start + inside_echo = not inside_echo + else: + print("Unrecognized type", e["type"]) + if i != len(self.text): + if not mid: + self.chord_arr.append({}) + self.lyric_arr.append(Line.ECHO_BEGIN if inside_echo else '') + mid = True + self.lyric_arr[-1] += self.text[i] + for i in range(len(self.lyric_arr)): + self.lyric_arr[i] = re.sub(r"(^ | $)", " ", self.lyric_arr[i]) + + def chorded(self): + for key in self.extras: + for i in self.extras[key]: + if i["type"] == "chord": + return True + return False + + +class Chord: + N_CHORDS = 12 + CHORDS_LAT = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'Sib', 'Si'] + CHORDS_ENG = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B&', 'B'] + ENG_INDEX = {'C': 0, 'C#': 1, 'D&': 1, 'D': 2, 'D#': 3, 'E&': 3, 'E': 4, 'F&': 4, 'F': 5, 'E#': 5, 'F#': 6, 'G&': 6, 'G': 7, 'G#': 8, 'A&': 8, 'A': 9, 'A#': 10, 'B&': 10, 'B': 11, 'C&': 11, 'B#': 0} + + def __init__(self, text, base_transpose=0): + self.text = text + self.chords = [] + self.base_transpose = base_transpose + ignore = False + for i, char in enumerate(text): + if ignore: + ignore = False + continue + if "A" <= char <= "G": + if len(text) > i + 1 and (text[i + 1] == "#" or text[i + 1] == "&"): + self.chords.append({'text': char + text[i + 1], 'chord': True}) + ignore = True + else: + self.chords.append({'text': char, 'chord': True}) + else: + self.chords.append({'text': char, 'chord': False}) + + def __str__(self): + res = "" + for c in self.chords: + if c['chord']: + res += "%s" % Chord.CHORDS_LAT[Chord.ENG_INDEX[c['text']]] + else: + res += c['text'] + return res + + +class Audio: + def __init__(self, date, audio_file): + assert isinstance(date, datetime) + self.date = date + self.date_text = date.strftime("%d de %B del %Y") + self.audio_file = audio_file + + def __str__(self): + return """ +
    + Audio del %s (descarga) + +
    + """ % (self.date_text, self.audio_file, self.audio_file)