mirror of
https://gitlab.com/parroquia-san-leandro/cancionero-web.git
synced 2024-12-22 08:43:33 +01:00
initial commit
This commit is contained in:
commit
c8c7eae33c
16 changed files with 835 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*.pyc
|
||||||
|
public/
|
||||||
|
audios/
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "latex"]
|
||||||
|
path = latex
|
||||||
|
url = https://gitlab.com/parroquia-san-leandro/cancionero-25.git
|
7
Makefile
Normal file
7
Makefile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
all: clean
|
||||||
|
mkdir -p public
|
||||||
|
ln -s ../audios public/audios
|
||||||
|
python3 src/latex_scanner.py
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf public
|
1
latex
Submodule
1
latex
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 312ecbac9f86f3bad0f474018c25c5344e207470
|
57
res/index.css
Normal file
57
res/index.css
Normal file
|
@ -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;
|
||||||
|
}
|
4
res/index.html
Normal file
4
res/index.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<h2>Índice</h2>
|
||||||
|
<ul class="songs">
|
||||||
|
%s
|
||||||
|
</ul>
|
44
res/main.css
Normal file
44
res/main.css
Normal file
|
@ -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;
|
||||||
|
}
|
20
res/page.html
Normal file
20
res/page.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Cancionero - Parroquia San Leandro</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
%s
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Cancionero San Leandro</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
%s
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>Canciones del <a href="https://sanleandro-obispo.net/cancionero">Cancionero</a> de la <a href="https://sanleandro-obispo.net">Parroquia San Leandro</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
res/sizes.js
Normal file
12
res/sizes.js
Normal file
|
@ -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] + '%';
|
||||||
|
}
|
65
res/song.css
Normal file
65
res/song.css
Normal file
|
@ -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;
|
||||||
|
}
|
48
res/song.html
Normal file
48
res/song.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<div class="song">
|
||||||
|
<script async src="../transpose.js"></script>
|
||||||
|
<script async src="../sizes.js"></script>
|
||||||
|
<h2>%s</h2>
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
<h3>Ajustes</h3>
|
||||||
|
<div>
|
||||||
|
<label>Cambiar tamaño de letra </label>
|
||||||
|
<button class="small" onclick="size(-1)">-</button>
|
||||||
|
<button class="small" onclick="size(0)">Reset</button>
|
||||||
|
<button class="small" onclick="size(+1)">+</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Transponer acordes </label>
|
||||||
|
<button class="small" onclick="transposeAdd(-2)">-2</button>
|
||||||
|
<button class="small" onclick="transposeAdd(-1)">-1</button>
|
||||||
|
<select id="transposeSelect" disabled>
|
||||||
|
<option onclick="transpose(-6)">-6</option>
|
||||||
|
<option onclick="transpose(-5)">-5</option>
|
||||||
|
<option onclick="transpose(-4)">-4</option>
|
||||||
|
<option onclick="transpose(-3)">-3</option>
|
||||||
|
<option onclick="transpose(-2)">-2</option>
|
||||||
|
<option onclick="transpose(-1)">-1</option>
|
||||||
|
<option onclick="transpose(0)" selected="selected">0</option>
|
||||||
|
<option onclick="transpose(1)">1</option>
|
||||||
|
<option onclick="transpose(2)">2</option>
|
||||||
|
<option onclick="transpose(3)">3</option>
|
||||||
|
<option onclick="transpose(4)">4</option>
|
||||||
|
<option onclick="transpose(5)">5</option>
|
||||||
|
<option onclick="transpose(6)">6</option>
|
||||||
|
</select>
|
||||||
|
<button class="small" onclick="transposeAdd(1)">+1</button>
|
||||||
|
<button class="small" onclick="transposeAdd(2)">+2</button>
|
||||||
|
<button onclick="transpose(0)">Reset</button>
|
||||||
|
</div>
|
||||||
|
%s
|
||||||
|
<h3>Canción</h3>
|
||||||
|
<div id="wholeSongDiv">
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
<a href="https://gitlab.com/parroquia-san-leandro/cancionero-25/blob/master/%s">Ver archivo original</a>
|
||||||
|
<button onclick="window.location = window.location.toString().slice(0, window.location.toString().lastIndexOf('/')) + '/../'">Atrás</button>
|
||||||
|
</div>
|
4
res/song_li.html
Normal file
4
res/song_li.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<li onclick="window.location = '%s'"%s>
|
||||||
|
<span class="numberBadge">%d. </span>
|
||||||
|
%s%s%s
|
||||||
|
</li>
|
39
res/transpose.js
Normal file
39
res/transpose.js
Normal file
|
@ -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)
|
||||||
|
}
|
25
src/audio_scanner.py
Normal file
25
src/audio_scanner.py
Normal file
|
@ -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
|
269
src/latex_scanner.py
Normal file
269
src/latex_scanner.py
Normal file
|
@ -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 = '<link rel="stylesheet" href="main.css">\n\t<link rel="stylesheet" href="index.css">'
|
||||||
|
song_css = '<link rel="stylesheet" href="../song.css">\n\t<link rel="stylesheet" href="../main.css">'
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
234
src/song_types.py
Normal file
234
src/song_types.py
Normal file
|
@ -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,
|
||||||
|
"<div>Autor: %s</div>" % self.author if self.author else "",
|
||||||
|
"<div>Basada en: %s</div>" % self.origin if self.origin else "",
|
||||||
|
"""<div *ngIf="song.capo != 0"><span class="capo">Tono original: Cejilla {s.capo}</span>
|
||||||
|
<button style="margin-left: 0.5em;" onclick="transpose({s.capo})">Transponer para quitarla</button></div>"""
|
||||||
|
.format(s=self) if self.capo != 0 else "",
|
||||||
|
join_list(self.verses),
|
||||||
|
"<h3>Audios</h3>" 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 """
|
||||||
|
<div class="%s">
|
||||||
|
%s
|
||||||
|
</div>
|
||||||
|
""" % ("chorus" if self.is_chorus else "verse", join_list([str(l) for l in self.lines], "\n<br>\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 = '<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()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
assert len(self.chord_arr) == len(self.lyric_arr)
|
||||||
|
return join_list(["""<table class="chordedline"><tr class="chord"><td rowspan="%s">%s%s</td></tr>%s</table>"""
|
||||||
|
% (self.chord_arr[i]["rowspan"] if "rowspan" in self.chord_arr[i] else 1,
|
||||||
|
'<span class="%s"></span>' % 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 "",
|
||||||
|
'<tr class="lyric"><td>%s</td></tr>' % 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('<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]
|
||||||
|
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 += "<span class='c'>%s</span>" % 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 """
|
||||||
|
<div>
|
||||||
|
Audio del %s (<a href="%s">descarga</a>)
|
||||||
|
<audio controls style='width: 100%%;'>
|
||||||
|
<source src='%s' type='audio/mpeg'/>
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
""" % (self.date_text, self.audio_file, self.audio_file)
|
Loading…
Reference in a new issue