From bc3d98aba276c14740a9efbc0758fb71e73163dc Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 27 Apr 2025 17:00:04 +0200 Subject: [PATCH] Added tune previews --- README.org | 4 +- folkugat_web/api/tema/scores.py | 54 +----------- folkugat_web/assets/static/css/main.css | 8 -- .../templates/fragments/temes/results.html | 44 +++++----- .../assets/templates/lilypond/preview.ly | 10 +++ folkugat_web/dal/sql/temes/ddl.py | 1 + folkugat_web/dal/sql/temes/scores.py | 15 ++-- folkugat_web/fragments/temes.py | 6 +- folkugat_web/model/temes.py | 2 + folkugat_web/services/files.py | 3 +- folkugat_web/services/lilypond/render.py | 8 +- folkugat_web/services/lilypond/source.py | 7 ++ folkugat_web/services/temes/scores.py | 84 ++++++++++++------- scripts/03_add_preview_url.py | 8 ++ 14 files changed, 123 insertions(+), 131 deletions(-) create mode 100644 folkugat_web/assets/templates/lilypond/preview.ly create mode 100644 scripts/03_add_preview_url.py diff --git a/README.org b/README.org index 5d8d176..5e5e88f 100644 --- a/README.org +++ b/README.org @@ -4,16 +4,16 @@ ** 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 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) +** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina * Idees ** Jams *** Properes jams -**** Info dels temes que es tocaràn (a la slow jam) +**** Info dels temes que es tocaran (a la slow jam) ** Temes *** Navegació **** Cerca de temes diff --git a/folkugat_web/api/tema/scores.py b/folkugat_web/api/tema/scores.py index ce7daca..7475e0c 100644 --- a/folkugat_web/api/tema/scores.py +++ b/folkugat_web/api/tema/scores.py @@ -35,64 +35,12 @@ async def set_score( score = scores_service.get_score_by_id(score_id=score_id, tema_id=tema_id) if not score: raise HTTPException(status_code=404, detail="Could not find lyric!") - - tune = lilypond_build.tune_from_tema_id(tema_id=tema_id, score_source=source) - tune_source = lilypond_source.tune_source(tune=tune) - pdf_result, png_result = await render_tune(tune_source=tune_source, tema_id=tema_id) - if errors := pdf_result.error: - new_score = dataclasses.replace( - score, - source=source, - title=title, - errors=errors, - ) - elif png_result is None: - raise RuntimeError(f"Received empty png_result with pdf_result: {pdf_result}") - elif errors := png_result.error: - new_score = dataclasses.replace( - score, - source=source, - title=title, - errors=errors, - ) - else: - pdf_file = pdf_result.result - png_file = png_result.result - new_score = dataclasses.replace( - score, - source=source, - title=title, - pdf_url=files.get_db_file_path(pdf_file) if pdf_file else None, - img_url=files.get_db_file_path(png_file) if png_file else None, - errors=[], - ) + new_score = await scores_service.render_score(score=score, title=title, source=source) scores_service.update_score(score=new_score) files.clean_orphan_files() return scores_fragments.score(request=request, logged_in=logged_in, score=new_score) -async def render_tune( - tune_source: str, - tema_id: int, -) -> tuple[lilypond_render.RenderResult, lilypond_render.RenderResult | None]: - async with files.tmp_file(content=tune_source) as source_file: - pdf_file = files.create_tema_filename(tema_id=tema_id) - pdf_result = await lilypond_render.render_file( - input_file=source_file, - output=lilypond_render.RenderOutput.PDF, - output_file=pdf_file, - ) - if pdf_result.error: - return pdf_result, None - png_file = files.create_tema_filename(tema_id=tema_id) - png_result = await lilypond_render.render_file( - input_file=source_file, - output=lilypond_render.RenderOutput.PNG_CROPPED, - output_file=png_file, - ) - return pdf_result, png_result - - @router.post("/api/tema/{tema_id}/score") def add_score( request: Request, diff --git a/folkugat_web/assets/static/css/main.css b/folkugat_web/assets/static/css/main.css index c27b609..8a566bb 100644 --- a/folkugat_web/assets/static/css/main.css +++ b/folkugat_web/assets/static/css/main.css @@ -693,10 +693,6 @@ video { display: flex; } -.table { - display: table; -} - .hidden { display: none; } @@ -899,10 +895,6 @@ video { border-width: 0px; } -.border-b { - border-bottom-width: 1px; -} - .border-none { border-style: none; } diff --git a/folkugat_web/assets/templates/fragments/temes/results.html b/folkugat_web/assets/templates/fragments/temes/results.html index c699170..4341411 100644 --- a/folkugat_web/assets/templates/fragments/temes/results.html +++ b/folkugat_web/assets/templates/fragments/temes/results.html @@ -12,32 +12,34 @@ {{ query }} {% endif %} - - - - - - + - - - - + + {% if tema.main_score() and tema.main_score().preview_url %} + + {% endif %} + {% if tema.properties %} + + {% endif %} + {% endfor %} -
NomEnllaçosCops tocat
+
  • +
  • - {% include "fragments/temes/result_links.html" %} - - {% if tema.stats is none %} - - - {% else %} - {{ tema.stats.times_played }} - {% endif %} -
    + {% if prev_offset is not none or next_offset is not none %}
    {% if prev_offset is not none %} diff --git a/folkugat_web/assets/templates/lilypond/preview.ly b/folkugat_web/assets/templates/lilypond/preview.ly new file mode 100644 index 0000000..34c4bc9 --- /dev/null +++ b/folkugat_web/assets/templates/lilypond/preview.ly @@ -0,0 +1,10 @@ +\version "2.24.4" +{% if tune.score_source is not none %} +\score { + \language "english" + << +{{ score_beginning }} +{{ tune.score_source | safe }} + >> +} +{% endif %} diff --git a/folkugat_web/dal/sql/temes/ddl.py b/folkugat_web/dal/sql/temes/ddl.py index 68d409e..ddf4bf6 100644 --- a/folkugat_web/dal/sql/temes/ddl.py +++ b/folkugat_web/dal/sql/temes/ddl.py @@ -79,6 +79,7 @@ def create_scores_table(con: Connection): errors TEXT NOT NULL, img_url TEXT, pdf_url TEXT, + preview_url TEXT, hidden BOOLEAN, FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE ) diff --git a/folkugat_web/dal/sql/temes/scores.py b/folkugat_web/dal/sql/temes/scores.py index b74418b..e50f165 100644 --- a/folkugat_web/dal/sql/temes/scores.py +++ b/folkugat_web/dal/sql/temes/scores.py @@ -6,7 +6,7 @@ from folkugat_web.dal.sql import Connection, get_connection from folkugat_web.model import temes as model from folkugat_web.model.lilypond.processing import RenderError -ScoreRowTuple = tuple[int, int, str, str, str, str | None, str | None, bool] +ScoreRowTuple = tuple[int, int, str, str, str, str | None, str | None, str | None, bool] class ScoreRowDict(TypedDict): @@ -17,6 +17,7 @@ class ScoreRowDict(TypedDict): source: str img_url: str | None pdf_url: str | None + preview_url: str | None hidden: bool @@ -29,6 +30,7 @@ def score_to_row(score: model.Score) -> ScoreRowDict: "source": score.source, "img_url": score.img_url, "pdf_url": score.pdf_url, + "preview_url": score.preview_url, "hidden": score.hidden, } @@ -43,7 +45,8 @@ def row_to_score(row: ScoreRowTuple) -> model.Score: errors=list(map(RenderError.from_dict, errors_dicts)), img_url=row[5], pdf_url=row[6], - hidden=bool(row[7]), + preview_url=row[7], + hidden=bool(row[8]), ) @@ -78,7 +81,7 @@ def get_scores(score_id: int | None = None, tema_id: int | None = None, con: Con query = f""" SELECT - id, tema_id, title, source, errors, img_url, pdf_url, hidden + id, tema_id, title, source, errors, img_url, pdf_url, preview_url, hidden FROM tema_scores {filter_clause} """ @@ -92,9 +95,9 @@ def insert_score(score: model.Score, con: Connection | None = None) -> model.Sco data = score_to_row(score) query = f""" INSERT INTO tema_scores - (id, tema_id, title, source, errors, img_url, pdf_url, hidden) + (id, tema_id, title, source, errors, img_url, pdf_url, preview_url, hidden) VALUES - (:id, :tema_id, :title, :source, :errors, :img_url, :pdf_url, :hidden) + (:id, :tema_id, :title, :source, :errors, :img_url, :pdf_url, :preview_url, :hidden) RETURNING * """ with get_connection(con) as con: @@ -110,7 +113,7 @@ def update_score(score: model.Score, con: Connection | None = None): UPDATE tema_scores SET tema_id = :tema_id, title = :title, source = :source, errors = :errors, - img_url = :img_url, pdf_url = :pdf_url, hidden = :hidden + img_url = :img_url, pdf_url = :pdf_url, preview_url = :preview_url, hidden = :hidden WHERE id = :id """ diff --git a/folkugat_web/fragments/temes.py b/folkugat_web/fragments/temes.py index 2283228..219ccc7 100644 --- a/folkugat_web/fragments/temes.py +++ b/folkugat_web/fragments/temes.py @@ -3,8 +3,7 @@ from folkugat_web.model import temes as model from folkugat_web.model.lilypond.processing import RenderError from folkugat_web.model.pagines import Pages from folkugat_web.services import sessions as sessions_service -from folkugat_web.services.temes import links as links_service -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.services.temes import scores as scores_service from folkugat_web.services.temes import search as temes_s @@ -43,8 +42,7 @@ def temes_busca(request: Request, logged_in: bool, query: str, offset: int = 0, temes = ( FnChain.transform(temes) | temes_q.temes_compute_stats | - links_service.add_links_to_temes | - lyrics_service.add_lyrics_to_temes | + properties_service.add_properties_to_temes | scores_service.add_scores_to_temes | list ).result() diff --git a/folkugat_web/model/temes.py b/folkugat_web/model/temes.py index 1b28dcf..d34af26 100644 --- a/folkugat_web/model/temes.py +++ b/folkugat_web/model/temes.py @@ -67,6 +67,7 @@ class Score: source: str img_url: str | None pdf_url: str | None + preview_url: str | None hidden: bool @classmethod @@ -81,6 +82,7 @@ class Score: errors=[], img_url=link.url if link.link_type is LinkType.IMAGE else None, pdf_url=link.url if link.link_type is LinkType.PDF else None, + preview_url=None, hidden=False, ) diff --git a/folkugat_web/services/files.py b/folkugat_web/services/files.py index 979388f..1efd34b 100644 --- a/folkugat_web/services/files.py +++ b/folkugat_web/services/files.py @@ -97,7 +97,8 @@ def get_orphan_files() -> Iterator[Path]: link_urls = {link.url for link in links_dal.get_links()} score_pdf_urls = {score.pdf_url for score in scores_dal.get_scores() if score.pdf_url is not None} score_img_urls = {score.img_url for score in scores_dal.get_scores() if score.img_url is not None} - alive_urls = link_urls | score_pdf_urls | score_img_urls + score_preview_urls = {score.preview_url for score in scores_dal.get_scores() if score.preview_url is not None} + alive_urls = link_urls | score_pdf_urls | score_img_urls | score_preview_urls return filter( lambda p: p.is_file() and get_db_file_path(p) not in alive_urls, itertools.chain( diff --git a/folkugat_web/services/lilypond/render.py b/folkugat_web/services/lilypond/render.py index 72e8e4e..b976f11 100644 --- a/folkugat_web/services/lilypond/render.py +++ b/folkugat_web/services/lilypond/render.py @@ -18,7 +18,7 @@ RenderResult = Result[Path, list[RenderError]] class RenderOutput(enum.Enum): PDF = "pdf" PNG_CROPPED = "png-cropped" - PNG_PREVIEW = "png-preview" + PREVIEW = "preview" async def render( @@ -61,16 +61,16 @@ async def render_file( ] output_file = output_file.with_suffix(".cropped.png") # output_file = output_file.with_suffix(".png") - case RenderOutput.PNG_PREVIEW: + case RenderOutput.PREVIEW: command = [ "lilypond", - "-f", "png", + "-f", "svg", "-dpreview=#t", "-dno-print-pages", "-o", str(output_file), str(input_file) ] - output_file = output_file.with_suffix(".preview.png") + output_file = output_file.with_suffix(".preview.svg") proc = await asyncio.create_subprocess_exec( *command, diff --git a/folkugat_web/services/lilypond/source.py b/folkugat_web/services/lilypond/source.py index de7f98e..46397a3 100644 --- a/folkugat_web/services/lilypond/source.py +++ b/folkugat_web/services/lilypond/source.py @@ -11,6 +11,13 @@ def tune_source(tune: LilypondTune) -> str: ) +def preview_source(tune: LilypondTune) -> str: + return templates.get_template("lilypond/preview.ly").render( + score_beginning=SCORE_BEGINNING, + tune=tune, + ) + + def set_source(tune_set: LilypondSet) -> str: return templates.get_template("lilypond/tune_set.ly").render( score_beginning=SCORE_BEGINNING, diff --git a/folkugat_web/services/temes/scores.py b/folkugat_web/services/temes/scores.py index fd5d56f..b603dce 100644 --- a/folkugat_web/services/temes/scores.py +++ b/folkugat_web/services/temes/scores.py @@ -1,14 +1,12 @@ +import dataclasses from collections.abc import Iterable, Iterator -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.model.lilypond import score as lilypond_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 +from folkugat_web.services import files as files_service +from folkugat_web.services.lilypond import build as lilypond_build +from folkugat_web.services.lilypond import render as lilypond_render +from folkugat_web.services.lilypond import source as lilypond_source def add_scores_to_tema(tema: model.Tema) -> model.Tema: @@ -42,34 +40,56 @@ def create_score(tema_id: int) -> model.Score: errors=[], img_url=None, pdf_url=None, + preview_url=None, hidden=True, ) return scores_dal.insert_score(score=new_score) -def build_tune_set_full_source(tema_ids: list[int]) -> str: - temes = ( - FnChain.transform(temes_q.get_temes_by_ids(tema_ids=tema_ids)) | - properties_service.add_properties_to_temes | - lyrics_service.add_lyrics_to_temes | - add_scores_to_temes | - list - ).result() - if not temes: - return "" - set_title = " i ".join(filter(bool, [ - ", ".join([tema.title for tema in temes[:-1]]), - temes[-1].title - ])) - tune_set = lilypond_model.LilypondSet( - title=set_title, - tunes=[ - lilypond_model.LilypondTune( - header=lilypond_model.HeaderData.from_tema(tema=tema), - score_source=tema.scores[0].source if tema.scores else None, - lyrics=lilypond_model.LyricsText.from_lyrics(lyrics=tema.lyrics[0]) if tema.lyrics else None, - ) for tema in temes - ] +async def render_score(score: model.Score, title: str, source: str) -> model.Score: + tune = lilypond_build.tune_from_tema_id(tema_id=score.tema_id, score_source=source) + output_file = files_service.create_tema_filename(tema_id=score.tema_id) + + tune_source = lilypond_source.tune_source(tune=tune) + async with files_service.tmp_file(content=tune_source) as source_file: + # Render PDF + pdf_result = await lilypond_render.render_file( + input_file=source_file, + output=lilypond_render.RenderOutput.PDF, + output_file=output_file, + ) + if errors := pdf_result.error: + return dataclasses.replace(score, source=source, title=title, errors=errors) + # Render IMAGE + img_result = await lilypond_render.render_file( + input_file=source_file, + output=lilypond_render.RenderOutput.PNG_CROPPED, + output_file=output_file, + ) + if errors := img_result.error: + return dataclasses.replace(score, source=source, title=title, errors=errors) + + preview_source = lilypond_source.preview_source(tune=tune) + async with files_service.tmp_file(content=preview_source) as source_file: + # Render PREVIEW + preview_result = await lilypond_render.render_file( + input_file=source_file, + output=lilypond_render.RenderOutput.PREVIEW, + output_file=output_file, + ) + if errors := preview_result.error: + return dataclasses.replace(score, source=source, title=title, errors=errors) + + pdf_file = pdf_result.result + img_file = img_result.result + preview_file = preview_result.result if preview_result.result and preview_result.result.exists() else None + + return dataclasses.replace( + score, + source=source, + title=title, + pdf_url=files_service.get_db_file_path(pdf_file) if pdf_file else None, + img_url=files_service.get_db_file_path(img_file) if img_file else None, + preview_url=files_service.get_db_file_path(preview_file) if preview_file else None, + errors=[], ) - print("TUNE SET HASH: ", tune_set.hash().hex()) - return lilypond.tune_set_score(tune_set=tune_set) diff --git a/scripts/03_add_preview_url.py b/scripts/03_add_preview_url.py new file mode 100644 index 0000000..e146c17 --- /dev/null +++ b/scripts/03_add_preview_url.py @@ -0,0 +1,8 @@ +from folkugat_web.dal.sql import get_connection + +with get_connection() as con: + cur = con.cursor() + alter_query = """ ALTER TABLE tema_scores ADD COLUMN preview_url TEXT""" + _ = cur.execute(alter_query) + +print("DONE!")