Compare commits

...

2 Commits

Author SHA1 Message Date
Carlos Galindo bc4e7ed51d documentacion y pistas de tipo en metodos 2023-09-08 12:15:30 +02:00
Carlos Galindo 03522304fa Rediseño de la interfaz y arreglos menores
- ¡Ahora con tema oscuro! Se activa según el navegador del usuario.
- URL de audios simplificada.
- Nuevos iconos para canciones con acordes/audios.
- Enlaces de la cabecera actualizados.
- Siempre mostramos los ajustes.
- Insertar espacio forzoso para separar algunas palabras que se juntaban.
- Soporte para canciones con dos líneas de acordes (e.g. Engrandece).
2023-09-08 00:37:38 +02:00
17 changed files with 269 additions and 110 deletions

View File

@ -14,11 +14,11 @@ public:
mkdir public
public/index.html: $(ALL_TEMPLATES) $(PY_SRC)
python3 src/latex_scanner.py --latex latex/cancionero.tex --audios audios/Canciones --other-latex latex/canciones/
python3 src/latex_scanner.py --latex latex/cancionero.tex --audios audios --other-latex latex/canciones/
public/audios: audios public
rm -f public/audios
ln -s ../audios/Canciones public/audios
#ln -s ../audios/Canciones public/audios
public/main.css: res/less/main.less res/less/colors.less
lessc $< $@

View File

@ -1,12 +1,19 @@
@nochordscolor: #FBB;
@nochordscolor-hover: #FAA;
@background: #FFF;
@background-dark: darken(@background, 100%);
@title: #369;
@title-dark: lighten(@title, 50%);
@subtitle: #444;
@subtitle-dark: lighten(@subtitle, 100%);
@text: #333;
@text-dark: lighten(@text, 100%);
@text-hover: #607D8B;
@text-hover-dark: @text-hover;
@secondary: #EEE;
@secondary-dark: #333;
@secondary-hover: #CCC;
@secondary-hover-dark: #444;
@chord-color: darkgreen;
@chord-color-dark: lightgreen;
@transition: .5s;
@border-radius: 4px;
@border-radius: 10px;

View File

@ -7,24 +7,22 @@
color: inherit;
text-decoration: none;
> li {
background-color: @secondary;
display: flex;
gap: 5px;
margin: .5em;
padding: .6em;
border: 1px solid @text;
border-radius: @border-radius;
.number {
font-weight: bolder;
color: black;
@media (prefers-color-scheme: dark) {
border: 1px solid @text-dark;
}
.number { font-weight: bolder; }
.name { flex-grow: 99; }
&:hover {
color: @text-hover;
background-color: @secondary-hover;
background-color: @secondary;
transition-duration: @transition;
}
&.noChords {
background-color: @nochordscolor;
&:hover {
color: @text-hover;
background-color: @nochordscolor-hover;
@media (prefers-color-scheme: dark) {
background-color: @secondary-dark;
}
}
}
@ -38,9 +36,18 @@ ul.songs {
margin: .2em;
}
}
span.noChords {
background-color: @nochordscolor;
padding: 0.3em 0.4em;
border-radius: @border-radius;
display: inline-block;
img.guitar-icon, img.music-icon {
width: 1em;
padding: 0 0.2em;
}
img.guitar-icon {
/* Green color filter */
filter: invert(42%) sepia(93%) saturate(1352%) hue-rotate(55deg) brightness(119%) contrast(119%);
}
img.music-icon {
/* Orange color filter */
filter: invert(46%) sepia(100%) saturate(681%) hue-rotate(360deg) brightness(106%) contrast(105%);
}
p { img.guitar-icon, img.music-icon {
vertical-align: -0.25em;
}}

View File

@ -6,8 +6,14 @@ h1 {
width: max-content;
max-width: 100%;
text-align: center;
@media (prefers-color-scheme: dark) {
color: @title-dark;
}
> a, > a:hover, > a:active, > a:visited {
color: @title;
@media (prefers-color-scheme: dark) {
color: @title-dark;
}
}
}
@ -15,6 +21,9 @@ h2, h3 {
color: @subtitle;
font-weight: lighter;
margin-bottom: 0;
@media (prefers-color-scheme: dark) {
color: @subtitle-dark;
}
}
footer {
@ -24,8 +33,13 @@ footer {
body {
font-family: Helvetica, Arial, sans-serif;
color: @text;
background: @background;
max-width: 75em;
margin: 1em;
@media (prefers-color-scheme: dark) {
color: @text-dark;
background: @background-dark;
}
@media (min-width: 75em + 1em) {
margin: auto;
}
@ -43,6 +57,9 @@ nav {
}
}
nav a > span {
border-width: 4px;
}
a {
text-decoration: none;
> span {
@ -50,11 +67,20 @@ a {
padding: 0.5em;
margin: 0.2em;
display: inline-block;
background: @secondary;
border-radius: @border-radius;
border-radius: @border-radius * 5;
border: 1px solid @text;
@media (prefers-color-scheme: dark) {
color: @text-dark;
border-color: @text-dark;
}
&:hover {
background: @secondary-hover;
transition-duration: @transition;
@media (prefers-color-scheme: dark) {
background: @secondary-hover-dark;
}
}
&#nav-web { border-color: #6ecf00; }
&#nav-audios { border-color: orange; }
}
}

View File

@ -5,10 +5,6 @@ table.chordedline {
display: inline-block;
}
.song div {
padding: 0.5em 0;
}
.chordedline td {
padding: 0px;
}
@ -16,11 +12,21 @@ table.chordedline {
.chord {
font-style: italic;
color: @chord-color;
@media (prefers-color-scheme: dark) {
color: @chord-color-dark;
}
}
div.chorus {
.song div {
margin: 0.5em 0;
}
div.chorus, p.chorus {
font-weight: bold;
border-left: black 2px solid;
border-left: @text 2px solid;
@media (prefers-color-scheme: dark) {
border-left: @text-dark 2px solid;
}
padding-left: 0.5em;
div {
padding-left: 0.5em;
@ -37,6 +43,10 @@ div.chorus {
display: inline-block;
width: 1em;
height: 2em;
@media (prefers-color-scheme: dark) {
/* Black to white filter for svgs */
filter: invert(100%);
}
}
.lrep {
@ -63,10 +73,18 @@ button, select#transposeSelect {
padding: .4em;
border-style: none;
border-radius: @border-radius;
color: @text;
background-color: @secondary;
@media (prefers-color-scheme: dark) {
color: @text-dark;
background-color: @secondary-dark;
}
&:hover {
background-color: @secondary-hover;
transition-duration: @transition;
@media (prefers-color-scheme: dark) {
background-color: @secondary-hover-dark;
}
}
}
@ -75,4 +93,7 @@ select#transposeSelect {
-moz-appearance: none;
font-weight: bold;
color: black;
@media (prefers-color-scheme: dark) {
color: @text-dark;
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zm2 226.3c37.1 22.4 62 63.1 62 109.7s-24.9 87.3-62 109.7c-7.6 4.6-17.4 2.1-22-5.4s-2.1-17.4 5.4-22C269.4 401.5 288 370.9 288 336s-18.6-65.5-46.5-82.3c-7.6-4.6-10-14.4-5.4-22s14.4-10 22-5.4zm-91.9 30.9c6 2.5 9.9 8.3 9.9 14.8V400c0 6.5-3.9 12.3-9.9 14.8s-12.9 1.1-17.4-3.5L113.4 376H80c-8.8 0-16-7.2-16-16V312c0-8.8 7.2-16 16-16h33.4l35.3-35.3c4.6-4.6 11.5-5.9 17.4-3.5zm51 34.9c6.6-5.9 16.7-5.3 22.6 1.3C249.8 304.6 256 319.6 256 336s-6.2 31.4-16.3 42.7c-5.9 6.6-16 7.1-22.6 1.3s-7.1-16-1.3-22.6c5.1-5.7 8.1-13.1 8.1-21.3s-3.1-15.7-8.1-21.3c-5.9-6.6-5.3-16.7 1.3-22.6z"/></svg>

After

Width:  |  Height:  |  Size: 946 B

1
res/static/gear.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
res/static/guitar.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M465 7c-9.4-9.4-24.6-9.4-33.9 0L383 55c-2.4 2.4-4.3 5.3-5.5 8.5l-15.4 41-77.5 77.6c-45.1-29.4-99.3-30.2-131 1.6c-11 11-18 24.6-21.4 39.6c-3.7 16.6-19.1 30.7-36.1 31.6c-25.6 1.3-49.3 10.7-67.3 28.6C-16 328.4-7.6 409.4 47.5 464.5s136.1 63.5 180.9 18.7c17.9-17.9 27.4-41.7 28.6-67.3c.9-17 15-32.3 31.6-36.1c15-3.4 28.6-10.5 39.6-21.4c31.8-31.8 31-85.9 1.6-131l77.6-77.6 41-15.4c3.2-1.2 6.1-3.1 8.5-5.5l48-48c9.4-9.4 9.4-24.6 0-33.9L465 7zM208 256a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/></svg>

After

Width:  |  Height:  |  Size: 727 B

1
res/static/music.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M499.1 6.3c8.1 6 12.9 15.6 12.9 25.7v72V368c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V147L192 223.8V432c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V200 128c0-14.1 9.3-26.6 22.8-30.7l320-96c9.7-2.9 20.2-1.1 28.3 5z"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -1,6 +1,6 @@
<footer>
<p>
&copy; 2020 Carlos Galindo, Parroquia San Leandro
&copy; 2023 Carlos Galindo, Parroquia San Leandro
<a href="https://gitlab.com/parroquia-san-leandro/cancionero-web"><span>Código fuente</span></a>
</p>
</footer>

View File

@ -2,9 +2,9 @@
<h1><a href="{{ path }}">Cancionero San Leandro</a></h1>
<nav class="nav">
<ul>
<li><a href="https://sanleandro-obispo.net/"><span>Parroquia San Leandro</span></a></li>
<li><a href="https://nube.sanleandro-obispo.net/s/X23Jzz5A6dpCfr2"><span>Grabaciones</span></a></li>
<li><a href="https://sanleandro-obispo.net/cancionero/"><span>Cancionero en PDF</span></a></li>
<li><a href="https://sanleandrovalencia.es/"><span id="nav-web">Parroquia San Leandro</span></a></li>
<li><a href="https://nube.sanleandrovalencia.es/s/X23Jzz5A6dpCfr2"><span id="nav-audios">Grabaciones</span></a></li>
<li><a href="https://sanleandrovalencia.es/cancionero/"><span>Cancionero en PDF</span></a></li>
</ul>
</nav>
</header>

View File

@ -15,17 +15,25 @@
</a>
{% endfor %}
</ul>
Las canciones sin acordes están marcadas en <span class="noChords">rojo</span>.
<p><strong>Leyenda</strong>: <img src="music.svg" class="music-icon"> tiene música grabada; <img src="guitar.svg" class="guitar-icon"> tiene acordes.</p>
<ol class="songs">
{% for category, songs in sorted_categories.items %}
<h3 id="{{ category|slugify }}">{{ category|title }}</h3>
{% for song in songs %}
<a href="{{ song.url }}">
<li {% if not song.chorded %}class="noChords" {% endif %}>
<li>
<span class="number">{{ song.number }}.</span>
{{ song.name }}
{% if song.author %} por {{ song.author }} {% endif %}
{% if song.origin %} basada en {{ song.origin }} {% endif %}
<span class="name">
{{ song.name }}
{% if song.author %} por {{ song.author }} {% endif %}
{% if song.origin %} basada en {{ song.origin }} {% endif %}
</span>
{% if song.has_audios %}
<img src="music.svg" class="music-icon filter-green">
{% endif %}
{% if song.chorded %}
<img src="guitar.svg" class="guitar-icon filter-green">
{% endif %}
</li>
</a>
{% endfor %}
@ -34,4 +42,4 @@
</main>
{% include "footer.html" %}
</body>
</html>
</html>

View File

@ -16,8 +16,7 @@
{% if song.origin %}
<span>basada en: <strong>{{ song.origin }}</strong></span>
{% endif %}
<details>
<summary><h3 style="display: inline-block;">Ajustes</h3></summary>
<h3>Ajustes</h3>
<div>
<label>Cambiar tamaño de letra </label>
<button class="small" onclick="size(-1)">-</button>
@ -29,7 +28,7 @@
<label><input id="showChords" type="checkbox" checked onchange="showChords(this.checked)"/> Mostrar acordes</label>
</div>
<div id="transposeControls" class="showChords">
<label>Transponer acordes </label>
<label for="transposeSelect">Transponer acordes </label>
<button class="small" onclick="transposeAdd(-2)">-2</button>
<button class="small" onclick="transposeAdd(-1)">-1</button>
<select id="transposeSelect" disabled>
@ -58,16 +57,15 @@
</div>
{% endif %}
{% endif %}
</details>
<h3>Canción</h3>
<div id="wholeSongDiv">
<div id="songLyrics" class="showLyrics" style="display: none;">
{% for verse in song.verses %}
<div class="{{ verse.kind }}">
<p class="{{ verse.kind }}">
{% for line in verse.lines %}
{{ line }}<br/>
{% endfor %}
</div>
</p>
{% endfor %}
</div>
<div id="songChords" class="showChords">
@ -82,13 +80,45 @@
{% if chord.class %}
<span class="{{ chord.class }}"></span>
{% endif %}
{% for c in chord.chord.items %}
{% if c.chord %}
<span class="c">{{ c.text|safe }}</span>
{% else %}
<span>{{ c.text|safe }}</span>
{% if chord.chord.trfmt == "normal" %}
{% for c in chord.chord.items_t %}
{% if c.chord %}
<span class="c">{{ c.text|safe }}</span>
{% else %}
<span>{{ c.text|safe }}</span>
{% endif %}
{% endfor %}
{% endif %}
{% if chord.chord.trfmt == "double" %}
<span>
{% for c in chord.chord.items_n %}
{% if c.chord %}
<span class="c">{{ c.text|safe }}</span>
{% else %}
<span>{{ c.text|safe }}</span>
{% endif %}
{% endfor %}
<br/>
{% for c in chord.chord.items_t %}
{% if c.chord %}
<span class="c">{{ c.text|safe }}</span>
{% else %}
<span>{{ c.text|safe }}</span>
{% endif %}
{% endfor %}
<span/>
{% endif %}
{% if chord.chord.trfmt == "hover" %}
<span>
{% for c in chord.chord.items_n %}
{% if c.chord %}
<span class="c">{{ c.text|safe }}</span>
{% else %}
<span>{{ c.text|safe }}</span>
{% endif %}
{% endfor %}
<br/>&nbsp;</span>
{% endif %}
{% endfor %}
</td>
</tr>
<tr class="lyric">

View File

@ -1,8 +1,8 @@
import re
from os import listdir
from os.path import isfile, join
from model import Audio
import re
from datetime import datetime
from model import Audio
def find_audios(index, audio_dir):

View File

@ -6,6 +6,7 @@ import pickle
def generate_songbook(songs, song_numbers, out_file, dj_engine):
'''Generate an HTML file with a sequence of songs.'''
# Build the list of songs
song_list = []
for n in song_numbers:
@ -24,6 +25,7 @@ def generate_songbook(songs, song_numbers, out_file, dj_engine):
def create_argparser():
'''Parse main's arguments.'''
parser = latex_scanner.create_argparser()
parser.add_argument("--songs", required=True, nargs='+', type=int, help="A list of song numbers to include.")
parser.add_argument("--output-file", required=False, nargs=1, default=["misa.html"],

View File

@ -11,7 +11,7 @@ from audio_scanner import find_audios
from model import Chord, Line, Song, Verse
def mkdir(path):
def mkdir(path: str) -> None:
if not os.path.exists(path):
os.mkdir(path)
@ -19,33 +19,40 @@ def mkdir(path):
# Note that re.match prepends ^ to the pattern, whereas re.search doesn't
def read_property(text, key):
def read_property(text: str, key: str) -> str | None:
'''A parser for \\beginsong attributes.'''
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):
def extra_put(extra: list, index: int, the_type: str, data: dict|None = None):
'''Adds the given data (if any) to the extra list, \
at the given index, and tagged with the given the_type.'''
payload = {'type': the_type, 'data': data} if data else {'type': the_type}
if index not in extra:
extra[index] = []
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 = []
'''Parses LaTeX files to build a collection of song objects.'''
def __init__(self, latex_file: str, audio_dir: str | None = None):
'''Initializes and populates a LaTeX reader.'''
self.index: int = 1
self.category: str | None = None
self.categories: list[str] = []
self.songs: list[Song] = []
if audio_dir:
self.audio_dir = audio_dir
self.scan(latex_file)
def scan(self, latex_file):
def scan(self, latex_file: str) -> None:
'''Reads through an index file and scans each song, with the same numbers.
:param latex_file: The main latex file, which includes songs via \\input.'''
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':
@ -61,14 +68,18 @@ class SongLoader:
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)
re_input_match = re.search(r"\\input{(.*?)}", line)
if re_input_match is not None:
input_file = join(canciones_dir, re_input_match.group(1))
input_file = join(str(Path(latex_file).parent), 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):
def scan_others(self, folder: str, start_index: int) -> None:
'''Looks for songs not found during self.scan(...).
:param folder: A folder to scan through, looking for .tex files.
:param start_index: When numbering new songs, the first index to use.
'''
self.index = start_index
self.category = "Nuevas"
self.categories.append(self.category)
@ -81,21 +92,46 @@ class SongLoader:
print("Scanning extra file", f)
self.scan_song_file(f)
def scan_song_file(self, song_file):
def scan_song_file(self, song_file: str) -> None:
'''Scan a single song file and store any songs found.'''
# Variables
ignore = False
current_song = None
current_verse = None
memory = None
memorizing = False
replay_index = 0
transpose = 0
ignore: bool = False
current_song: Song | None = None
current_verse: Verse | None = None
memory: str | None = None
memorizing: bool = False
replay_index: int = 0
transpose: int = 0
trfmt: str = "normal"
# General behaviour: read the file and scan line-by-line
# In each line, read char-by-char, searching for common LaTeX commands
# Apply the effects of these commands, and add each verse (line inside
# song after removing all commands) to build up each song.
# Commands include:
### Comments: % Something something -> Ignored
### Line break locations: \brk -> Ignored
### Transpose: \transpose{SEMITONES}
### Begin song: \beginsong{NAME}[METADATA]
### End song: \endsong
### Verse begin/end: \beginverse, \endverse
### Chorus begin/end: \beginchorus, \endchorus
### Capo: \capo{FRET}
### Chord-excl. txt.: \ifchorded, \else, \fi
### (the contents between \else and \fi are discarded)
### Echoes: \echo{TEXT}
### Chord: \[CHORD]
### Chord repetition: \^
### Music repetition: \lrep, \rrep
### Lyric repetition: \rep{TIMES}
### Chord memory: \memorize, \replay
### Transpose format: \renewcommand{\trchordformat}[2]{.*}
### Other unrecognized commands: \NAME([ARG]|{ARG})*
for line in open(song_file, "r").readlines():
# Remove newline
if line[-1] == '\n':
line = line[:-1]
# Remove commends and \brk commands
# Remove comments and \brk commands
text = re.sub(r"%.*$", "", line)
text = re.sub(r"\\brk({})?", '', text)
text = re.sub(r"``", u"\u201C", text)
@ -117,13 +153,11 @@ class SongLoader:
continue
# Command lookup
re_transpose_match = re.match(r"\\transpose *?{(-?\d+?)}", remain)
if re_transpose_match:
if re_transpose_match := re.match(r"\\transpose *?{(-?\d+?)}", remain):
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:
if re_song_begin_match := re.match(r"\\beginsong *?{(.*?)}(\[.*?])?", remain):
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))
@ -135,6 +169,7 @@ class SongLoader:
category=self.category,
latex_file=song_file[song_file.index('/') + 1:])
transpose = 0
trfmt = "normal"
memory = None
memorizing = False
replay_index = 0
@ -148,8 +183,7 @@ class SongLoader:
current_song = None
self.index += 1
continue
re_verse_cmd_match = re.match(r"\\(begin|end)(verse|chorus)", remain)
if re_verse_cmd_match:
if re_verse_cmd_match := re.match(r"\\(begin|end)(verse|chorus)", remain):
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:
@ -170,8 +204,7 @@ class SongLoader:
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:
if (re_capo_match := re.match(r"\\capo{(\d+?)}", remain)) and current_song:
text = beginning + text[i + len(re_capo_match.group(0)):]
current_song.set_capo(int(re_capo_match.group(1)))
continue
@ -182,8 +215,7 @@ class SongLoader:
ignore = True
text = beginning + text[i + len("\\else"):]
continue
re_echo_match = re.match(r"\\echo[ \t]*?{((.|{.*?})*?)}", remain)
if re_echo_match:
if re_echo_match := re.match(r"\\echo[ \t]*?{((.|{.*?})*?)}", remain):
text = beginning + re_echo_match.group(1) + "\\echoend" + text[i + len(re_echo_match.group(0)):]
extra_put(extras, i, "echo")
continue
@ -191,10 +223,9 @@ class SongLoader:
text = beginning + text[i + len("\\echoend"):]
extra_put(extras, i, "echo")
continue
re_chord_match = re.match(r"\\\[(.+?)]", remain)
if re_chord_match:
if re_chord_match := re.match(r"\\\[(.+?)]", remain):
text = beginning + text[i + len(re_chord_match.group(0)):]
c = Chord(re_chord_match.group(1), transpose)
c = Chord(re_chord_match.group(1), transpose, trfmt)
extra_put(extras, i, "chord", c)
if memorizing:
memory.append(c)
@ -205,13 +236,11 @@ class SongLoader:
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:
if re_dir_rep_match := re.match(r"\\([lr]rep)", remain):
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:
if re_rep_match := re.match(r"\\rep{(\d+?)}", remain):
text = beginning + text[i + len(re_rep_match.group(0)):]
extra_put(extras, i, 'rep', int(re_rep_match.group(1)))
continue
@ -224,6 +253,19 @@ class SongLoader:
text = beginning + text[i + len("\\replay"):]
replay_index = 0
continue
# Double or single transpose mode
if re_trfmt := re.match(r"\\renewcommand{\\trchordformat}\[2\]{\\vbox{\\hbox{#1}\\hbox{#2}}}", remain):
text = beginning + text[i + len(re_trfmt.group(0)):]
trfmt = "double"
continue
if re_trfmt := re.match(r"\\renewcommand{\\trchordformat}\[2\]{\\vbox{\\hbox{#1}\\hbox{}}}", remain):
text = beginning + text[i + len(re_trfmt.group(0)):]
trfmt = "hover"
continue
if re_trfmt := re.match(r"\\renewcommand{\\trchordformat}\[2\]{\\hbox{#2}}", remain):
text = beginning + text[i + len(re_trfmt.group(0)):]
trfmt = "normal"
continue
# Command lookup end, removing any unrecognized command
re_macro_match = re.match(r"\\([^ \t{\[]+)[ \t]*?({.*?}|\[.*?])*", remain)
if re_macro_match:
@ -238,7 +280,8 @@ class SongLoader:
continue
current_verse.add_line(Line(text, extras))
def sort_categories(self):
def sort_categories(self) -> dict[str, list[Song]]:
'''Returns a dictionary of categories to lists of songs (sorted by number).'''
result = {}
for c in self.categories:
result[c] = sorted([s for s in self.songs if s.category == c],

View File

@ -4,7 +4,8 @@ from datetime import datetime
import locale
def join_list(the_list, separator="\n"):
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, "")
@ -40,7 +41,7 @@ class Song:
self.verses.append(verse)
def url(self):
return "%03d %s" % (self.number, self.name.replace("¿", "").replace("?", ""))
return "%03d %s/" % (self.number, self.name.replace("¿", "").replace("?", ""))
def chorded(self):
for v in self.verses:
@ -48,6 +49,9 @@ class Song:
return True
return False
def has_audios(self):
return len(self.audios) > 0
class Verse:
def __init__(self, is_chorus=False):
@ -149,6 +153,7 @@ class Line:
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):
@ -162,8 +167,8 @@ class Line:
return False
def chord_eng2lat(text):
return Chord.CHORDS_LAT[Chord.ENG_INDEX[text]]
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:
@ -172,28 +177,34 @@ class Chord:
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):
def __init__(self, text, transpose = 0, trfmt = "normal"):
self.text = text + "&nbsp;"
self.items = []
self.base_transpose = base_transpose
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) > 0 and not self.items[-1]['text'].endswith("&nbsp;"):
self.items[-1]['text'] += "&nbsp;"
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.append({'text': chord_eng2lat(char + text[i + 1]), 'chord': True})
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.append({'text': chord_eng2lat(char), 'chord': True})
self.items_n.append({'text': chord_eng2lat(char), 'chord': True})
self.items_t.append({'text': chord_eng2lat(char, transpose), 'chord': True})
else:
self.items.append({'text': char, 'chord': False})
if len(self.items) > 0 and not self.items[-1]['text'].endswith("&nbsp;"):
self.items[-1]['text'] += "&nbsp;"
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