diff --git a/README.org b/README.org index 2afa9db..5d8d176 100644 --- a/README.org +++ b/README.org @@ -4,12 +4,12 @@ ** TODO Ordenar els resultats de la cerca de temes ** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat) ** TODO Usuaris i permisos granulars -** TODO Lilypond support (o similar) -*** TODO Fer cançoners "en directe" -*** TODO Suport de caràcters especials (al títol i més llocs?) -*** TODO Mostrar partitura als enllaços (resultats de cerca) -** DONE Arreglar estadístiques de temes (dos temes tocats a la mateixa sessió compten un cop) ** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina +** Lilypond support (o similar) +*** TODO Fer cançoners "en directe" +*** DONE Suport de caràcters especials (al títol i més llocs?) +*** DONE Mostrar partitura als enllaços (resultats de cerca) +** DONE Arreglar estadístiques de temes (dos temes tocats a la mateixa sessió compten un cop) * Idees ** Jams *** Properes jams diff --git a/folkugat_web/assets/templates/lilypond/score_template.ly b/folkugat_web/assets/templates/lilypond/score_template.ly index cbe45fe..093f1fa 100644 --- a/folkugat_web/assets/templates/lilypond/score_template.ly +++ b/folkugat_web/assets/templates/lilypond/score_template.ly @@ -59,3 +59,8 @@ \bar "|." } } +\addlyrics { + % ----------------------------------------------------- + % Lletra + % ----------------------------------------------------- +} diff --git a/folkugat_web/assets/templates/lilypond/single_score.ly b/folkugat_web/assets/templates/lilypond/single_score.ly index cde298f..a332991 100644 --- a/folkugat_web/assets/templates/lilypond/single_score.ly +++ b/folkugat_web/assets/templates/lilypond/single_score.ly @@ -1,3 +1,4 @@ +\version "2.24.4" \paper { top-margin = 10 left-margin = 15 @@ -7,9 +8,9 @@ \header { title = \markup { "{{ tema.title | safe }}" } {% if tema.composer() %} - composer = "{{ tema.composer() }}" + composer = "{{ tema.composer() | safe }}" {% elif tema.origin() %} - composer = "{{ tema.origin() }}" + composer = "{{ tema.origin() | safe }}" {% endif %} tagline = "Partitura generada amb LilyPond" @@ -25,3 +26,21 @@ {{ score_source | safe }} >> } +{% if lyrics_text is not none %} +\markup { + \fill-line { + \hspace #1 + {% for column in lyrics_text.columns %} + \center-column { + {% for paragraph in column.paragraphs %} + {% for line in paragraph.lines %} + \line { {{ line | safe }} } + {% endfor %} + \vspace #1 + {% endfor %} + } + \hspace #1 + {% endfor %} + } +} +{% endif %} diff --git a/folkugat_web/model/lilypond.py b/folkugat_web/model/lilypond.py index 0bdeeea..e042be5 100644 --- a/folkugat_web/model/lilypond.py +++ b/folkugat_web/model/lilypond.py @@ -22,3 +22,18 @@ class RenderError: pos=str(self.pos), error=self.error, ) + + +@dataclasses.dataclass +class LyricsParagraph: + lines: list[str] + + +@dataclasses.dataclass +class LyricsColumn: + paragraphs: list[LyricsParagraph] + + +@dataclasses.dataclass +class LyricsText: + columns: list[LyricsColumn] diff --git a/folkugat_web/services/lilypond.py b/folkugat_web/services/lilypond.py index 2265c33..23d13fb 100644 --- a/folkugat_web/services/lilypond.py +++ b/folkugat_web/services/lilypond.py @@ -1,12 +1,14 @@ import asyncio import dataclasses +import itertools import os import re from pathlib import Path from typing import Literal import aiofiles -from folkugat_web.model.lilypond import RenderError +from folkugat_web.model.lilypond import (LyricsColumn, LyricsParagraph, + LyricsText, RenderError) from folkugat_web.model.temes import Tema from folkugat_web.services import files as files_service from folkugat_web.templates import templates @@ -17,10 +19,15 @@ RenderFormat = Literal["png"] | Literal["pdf"] def source_to_single_score(source: str, tema: Tema) -> str: + if tema.lyrics: + lyrics_text = build_lyrics(tema.lyrics[0].content) + else: + lyrics_text = None return templates.get_template("lilypond/single_score.ly").render( score_beginning=SCORE_BEGINNING, score_source=source, tema=tema, + lyrics_text=lyrics_text, ) @@ -68,3 +75,15 @@ async def render(source: str, fmt: RenderFormat, output_filename: Path | None = output_filename = output_filename.with_suffix(".pdf") return output_filename, [] + + +def build_lyrics(text: str, max_cols: int = 2, min_pars: int = 2) -> LyricsText: + paragraphs = [LyricsParagraph(lines=par_str.splitlines()) for par_str in text.split("\n\n")] + n_cols = next(filter(lambda nc: len(paragraphs) // nc >= min_pars, range(max_cols, 0, -1)), 1) + n_long_cols = len(paragraphs) % n_cols + pars_per_col = [len(paragraphs) // n_cols + 1] * n_long_cols + [len(paragraphs) // n_cols] * (n_cols - n_long_cols) + acc_pars_per_col = [0] + list(itertools.accumulate(pars_per_col)) + return LyricsText(columns=[ + LyricsColumn(paragraphs=paragraphs[acc_pars_per_col[i]:acc_pars_per_col[i+1]]) + for i in range(n_cols) + ]) diff --git a/folkugat_web/services/temes/lyrics.py b/folkugat_web/services/temes/lyrics.py index 4429637..798a268 100644 --- a/folkugat_web/services/temes/lyrics.py +++ b/folkugat_web/services/temes/lyrics.py @@ -1,7 +1,26 @@ +import dataclasses +import re from collections.abc import Iterable, Iterator +from typing import Callable from folkugat_web.dal.sql.temes import lyrics as lyrics_dal from folkugat_web.model import temes as model +from folkugat_web.utils import FnChain + + +def _sub(pattern: str, sub: str) -> Callable[[str], str]: + def _inner_sub(text: str) -> str: + return re.sub(pattern, sub, text) + return _inner_sub + + +def _clean_string(lyrics_str: str) -> str: + return ( + FnChain.transform(lyrics_str) | + _sub(r" *", " ") | + _sub(r"\s*\n\s*", "\n") | + _sub(r"\n*", "\n") + ).result() def add_lyrics_to_tema(tema: model.Tema) -> model.Tema: @@ -19,6 +38,7 @@ def get_lyric_by_id(lyric_id: int, tema_id: int | None = None) -> model.Lyrics | def update_lyric(lyric: model.Lyrics): + lyric = dataclasses.replace(lyric, content=_clean_string(lyric.content)) lyrics_dal.update_lyric(lyric=lyric) diff --git a/folkugat_web/services/temes/scores.py b/folkugat_web/services/temes/scores.py index f75b2d7..a5567a5 100644 --- a/folkugat_web/services/temes/scores.py +++ b/folkugat_web/services/temes/scores.py @@ -4,6 +4,7 @@ from fastapi import HTTPException from folkugat_web.dal.sql.temes import scores as scores_dal from folkugat_web.model import temes as model from folkugat_web.services import lilypond +from folkugat_web.services.temes import lyrics as lyrics_service from folkugat_web.services.temes import properties as properties_service from folkugat_web.services.temes import query as temes_q from folkugat_web.utils import FnChain @@ -51,6 +52,7 @@ def build_single_tune_full_source(tema_id: int, source: str) -> str: raise HTTPException(status_code=404, detail="Could not find tema!") tema = ( FnChain.transform(tema) | - properties_service.add_properties_to_tema + properties_service.add_properties_to_tema | + lyrics_service.add_lyrics_to_tema ).result() return lilypond.source_to_single_score(source=source, tema=tema)