cancionero-web/src/model.py

219 lines
7.7 KiB
Python

import re
import functools as ft
from datetime import datetime
import locale
def join_list(the_list: list, separator: str = "\n") -> str:
'''Join a list of objects in a string, with a separator.'''
return ft.reduce(lambda x, y: x + (separator if x else "") + str(y), the_list, "")
locale.setlocale(locale.LC_ALL, "es_ES.UTF-8")
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.replace("\\\\", "-").replace("\\", "")
self.number = number
self.author = author.replace("\\", "") if author else author
self.origin = origin.replace("\\", "") if origin else origin
self.latex_file = latex_file
self.audios = audios
self.capo = capo
self.category = category
def __str__(self):
return self.name
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 url(self):
return "%03d %s/" % (self.number, self.name.replace("¿", "").replace("?", ""))
def chorded(self):
for v in self.verses:
if v.chorded():
return True
return False
def has_audios(self):
return len(self.audios) > 0
class Verse:
def __init__(self, is_chorus=False):
self.is_chorus = is_chorus
self.lines = []
self.kind = "chorus" if is_chorus else "verse"
def __str__(self):
return join_list(self.lines, " ")
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 = '<span class="echo">'
ECHO_END = '</span>'
def __init__(self, text, extras):
self.text = text
self.extras = extras
self.chord_arr = []
self.lyric_arr = []
self.build()
self.remove_brackets()
assert len(self.chord_arr) == len(self.lyric_arr)
self.zipped_arr = zip(self.chord_arr, self.lyric_arr)
def __str__(self):
return self.text
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('<span class="rep">(x%d)</span>' % (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]
self.lyric_arr = [re.sub(r'^ | $', '&nbsp;', l) for l in self.lyric_arr]
self.lyric_arr = [l if l != "" else "&nbsp;" for l in self.lyric_arr]
def remove_brackets(self):
self.lyric_arr = [l.replace('}', '').replace('{', '') for l in self.lyric_arr]
def chorded(self):
for key in self.extras:
for i in self.extras[key]:
if i["type"] == "chord":
return True
return False
def chord_eng2lat(text, transpose = 0):
return Chord.CHORDS_LAT[(Chord.ENG_INDEX[text] + transpose + len(Chord.CHORDS_LAT)) % len(Chord.CHORDS_LAT)]
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, transpose = 0, trfmt = "normal"):
self.text = text + "&nbsp;"
self.items_n = [] # normal version
self.items_t = [] # transposed version
self.transpose = transpose
self.trfmt = trfmt
ignore = False
for i, char in enumerate(text):
if ignore:
ignore = False
continue
if "A" <= char <= "G":
if len(self.items_n) > 0 and not self.items_n[-1]['text'].endswith("&nbsp;"):
self.items_n[-1]['text'] += "&nbsp;"
self.items_t[-1]['text'] += "&nbsp;"
if len(text) > i + 1 and (text[i + 1] == "#" or text[i + 1] == "&"):
self.items_n.append({'text': chord_eng2lat(char + text[i + 1]), 'chord': True})
self.items_t.append({'text': chord_eng2lat(char + text[i + 1], transpose), 'chord': True})
ignore = True
else:
self.items_n.append({'text': chord_eng2lat(char), 'chord': True})
self.items_t.append({'text': chord_eng2lat(char, transpose), 'chord': True})
else:
self.items_n.append({'text': char, 'chord': False})
self.items_t.append({'text': char, 'chord': False})
if len(self.items_n) > 0 and not self.items_n[-1]['text'].endswith("&nbsp;"):
self.items_n[-1]['text'] += "&nbsp;"
self.items_t[-1]['text'] += "&nbsp;"
def __str__(self):
return self.text
class Audio:
def __init__(self, audio_file):
self.audio_file = audio_file
def __str__(self):
return self.audio_file