From e6b8b3e809351350a7e4df504b08c096b93823ba Mon Sep 17 00:00:00 2001 From: marc Date: Sat, 26 Apr 2025 21:56:25 +0200 Subject: [PATCH] Set scores --- folkugat_web/api/sessio/index.py | 55 +++++++-------- folkugat_web/api/sessio/set_page.py | 4 +- .../fragments/sessio/set/set_page.html | 15 ++++ .../fragments/sessio/set/set_score.html | 13 ++++ .../templates/fragments/sessio/set/tema.html | 8 +-- .../fragments/sessio/set/tema_title.html | 7 ++ folkugat_web/config/db.py | 12 +++- folkugat_web/fragments/set_page.py | 3 +- folkugat_web/model/playlists.py | 8 +++ folkugat_web/services/files.py | 18 +++-- folkugat_web/services/lilypond/build.py | 22 +++--- folkugat_web/services/lilypond/render.py | 2 +- folkugat_web/services/playlists.py | 68 +++++++++++++++++++ 13 files changed, 174 insertions(+), 61 deletions(-) create mode 100644 folkugat_web/assets/templates/fragments/sessio/set/set_score.html create mode 100644 folkugat_web/assets/templates/fragments/sessio/set/tema_title.html diff --git a/folkugat_web/api/sessio/index.py b/folkugat_web/api/sessio/index.py index f8cdd62..afcb52a 100644 --- a/folkugat_web/api/sessio/index.py +++ b/folkugat_web/api/sessio/index.py @@ -1,15 +1,9 @@ from typing import Annotated -from fastapi import Form, HTTPException, Request -from fastapi.responses import HTMLResponse +from fastapi import Form, Request from folkugat_web.api import router from folkugat_web.fragments import live, sessio -from folkugat_web.services import auth, files -from folkugat_web.services import playlists as playlists_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 -from folkugat_web.services.temes import scores as scores_service +from folkugat_web.services import auth from folkugat_web.services.temes import write as temes_service from folkugat_web.templates import templates @@ -205,25 +199,26 @@ def set_tema_new( ) -@router.get("/api/sessio/{session_id}/set/{set_id}/score") -async def render( - request: Request, - _: auth.RequireLogin, - session_id: int, - set_id: int, -): - set_entry = playlists_service.get_set(session_id=session_id, set_id=set_id) - if not set_entry: - raise HTTPException(status_code=404, detail="Could not find set!") - tune_set = lilypond_build.set_from_set(set_entry=set_entry) - set_source = lilypond_source.set_source(tune_set=tune_set) - pdf_result = await lilypond_render.render( - source=set_source, - output=lilypond_render.RenderOutput.PDF, - ) - if output_filename := pdf_result.result: - score_render_url = files.get_db_file_path(output_filename) - # return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url) - return HTMLResponse(content=score_render_url) - else: - return HTMLResponse() +# @router.get("/api/sessio/{session_id}/set/{set_id}/score") +# async def render( +# request: Request, +# _: auth.RequireLogin, +# session_id: int, +# set_id: int, +# ): +# set_entry = playlists_service.get_set(session_id=session_id, set_id=set_id) +# if not set_entry: +# raise HTTPException(status_code=404, detail="Could not find set!") +# set_entry = playlists_service.add_temes_to_set(set_entry) +# tune_set = lilypond_build.set_from_set(set_entry=set_entry) +# set_source = lilypond_source.set_source(tune_set=tune_set) +# pdf_result = await lilypond_render.render( +# source=set_source, +# output=lilypond_render.RenderOutput.PDF, +# ) +# if output_filename := pdf_result.result: +# score_render_url = files.get_db_file_path(output_filename) +# # return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url) +# return HTMLResponse(content=score_render_url) +# else: +# return HTMLResponse() diff --git a/folkugat_web/api/sessio/set_page.py b/folkugat_web/api/sessio/set_page.py index 7502321..17dac00 100644 --- a/folkugat_web/api/sessio/set_page.py +++ b/folkugat_web/api/sessio/set_page.py @@ -24,10 +24,10 @@ def page( @router.get("/api/content/sessio/{session_id}/set/{set_id}") -def contingut( +async def contingut( request: Request, logged_in: auth.LoggedIn, session_id: int, set_id: int, ): - return set_page.pagina(request, session_id, set_id, logged_in) + return await set_page.pagina(request, session_id, set_id, logged_in) diff --git a/folkugat_web/assets/templates/fragments/sessio/set/set_page.html b/folkugat_web/assets/templates/fragments/sessio/set/set_page.html index 1732cb8..b3d3827 100644 --- a/folkugat_web/assets/templates/fragments/sessio/set/set_page.html +++ b/folkugat_web/assets/templates/fragments/sessio/set/set_page.html @@ -9,6 +9,20 @@ {% endif %} +{% if set.score %} + {% for tema_in_set in set.temes %} + {% if tema_in_set.tema is not none %} + {% set tema = tema_in_set.tema %} + {% include "fragments/sessio/set/tema_title.html" %} + {% else %} +

Desconegut

+ {% endif %} + {% endfor %} +
+
+ {% include "fragments/sessio/set/set_score.html"%} +
+{% else %} {% for tema_in_set in set.temes %} {% if loop.index != 1 %}
@@ -28,3 +42,4 @@ {% endif %}
{% endfor %} +{% endif %} diff --git a/folkugat_web/assets/templates/fragments/sessio/set/set_score.html b/folkugat_web/assets/templates/fragments/sessio/set/set_score.html new file mode 100644 index 0000000..5f9efaf --- /dev/null +++ b/folkugat_web/assets/templates/fragments/sessio/set/set_score.html @@ -0,0 +1,13 @@ +
+
+ + + +
+ {% if set.score.pdf_url is not none %} + + {% endif %} +
diff --git a/folkugat_web/assets/templates/fragments/sessio/set/tema.html b/folkugat_web/assets/templates/fragments/sessio/set/tema.html index 8af4bbc..f9660ef 100644 --- a/folkugat_web/assets/templates/fragments/sessio/set/tema.html +++ b/folkugat_web/assets/templates/fragments/sessio/set/tema.html @@ -1,10 +1,4 @@ -

- {{ tema.title }} - - - -

+{% include "fragments/sessio/set/tema_title.html" %}
{% if tema.main_score() is not none %} diff --git a/folkugat_web/assets/templates/fragments/sessio/set/tema_title.html b/folkugat_web/assets/templates/fragments/sessio/set/tema_title.html new file mode 100644 index 0000000..ecd9c56 --- /dev/null +++ b/folkugat_web/assets/templates/fragments/sessio/set/tema_title.html @@ -0,0 +1,7 @@ +

+ {{ tema.title }} + + + +

diff --git a/folkugat_web/config/db.py b/folkugat_web/config/db.py index 48afc88..5f59770 100644 --- a/folkugat_web/config/db.py +++ b/folkugat_web/config/db.py @@ -11,7 +11,17 @@ logger.info(f"Using DB_DIR: {DB_DIR}") # Files db DB_FILES_DIR = DB_DIR / "fitxer" -DB_FILES_DIR.mkdir(exist_ok=True) +DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema" +DB_FILES_SET_DIR = DB_FILES_DIR / "set" +DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp" + +for path in [ + DB_FILES_DIR, + DB_FILES_TEMA_DIR, + DB_FILES_SET_DIR, + DB_FILES_SET_DIR, +]: + path.mkdir(exist_ok=True) DB_FILES_URL = "/db/fitxer" diff --git a/folkugat_web/fragments/set_page.py b/folkugat_web/fragments/set_page.py index 0fb6bc5..b25c75c 100644 --- a/folkugat_web/fragments/set_page.py +++ b/folkugat_web/fragments/set_page.py @@ -6,12 +6,13 @@ from folkugat_web.services import sessions as sessions_service from folkugat_web.templates import templates -def pagina(request: Request, session_id: int, set_id: int, logged_in: bool): +async def pagina(request: Request, session_id: int, set_id: int, logged_in: bool): session = sessions_service.get_session(session_id=session_id) set_ = playlists_service.get_set(session_id=session_id, set_id=set_id) if not set_: raise HTTPException(status_code=404, detail="Set not found") set_ = playlists_service.add_temes_to_set(set_) + set_ = await playlists_service.add_set_score_to_set(set_) return templates.TemplateResponse( "fragments/sessio/set/pagina.html", { diff --git a/folkugat_web/model/playlists.py b/folkugat_web/model/playlists.py index fba232f..6ef55fd 100644 --- a/folkugat_web/model/playlists.py +++ b/folkugat_web/model/playlists.py @@ -33,10 +33,17 @@ class TemaInSet: return cls(id=entry.id, tema_id=entry.tema_id, tema=None) +@dataclasses.dataclass +class SetScore: + img_url: str | None + pdf_url: str | None + + @dataclasses.dataclass class Set: id: int temes: list[TemaInSet] + score: SetScore | None def to_playlist_entries(self, session_id: int) -> Iterator[PlaylistEntry]: for tema_in_set in self.temes: @@ -52,6 +59,7 @@ class Set: return cls( id=set_id, temes=[TemaInSet.from_playlist_entry(entry) for entry in sorted(entries, key=lambda e: e.id or 0)], + score=None, ) diff --git a/folkugat_web/services/files.py b/folkugat_web/services/files.py index 55c1878..979388f 100644 --- a/folkugat_web/services/files.py +++ b/folkugat_web/services/files.py @@ -1,3 +1,4 @@ +import itertools import mimetypes import os import re @@ -59,7 +60,7 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str: def create_tema_filename(tema_id: int, extension: str = "") -> Path: filename = str(uuid.uuid4().hex) + extension - filedir = db.DB_FILES_DIR / "tema" / str(tema_id) + filedir = db.DB_FILES_TEMA_DIR / str(tema_id) filedir.mkdir(parents=True, exist_ok=True) filepath = filedir / filename return filepath @@ -67,12 +68,14 @@ def create_tema_filename(tema_id: int, extension: str = "") -> Path: def create_tmp_filename(extension: str = "") -> Path: filename = str(uuid.uuid4().hex) + extension - filedir = db.DB_FILES_DIR / "tmp" - filedir.mkdir(exist_ok=True) - filepath = filedir / filename + filepath = db.DB_FILES_TMP_DIR / filename return filepath +def get_set_filename(filename: str) -> Path: + return db.DB_FILES_SET_DIR / filename + + @asynccontextmanager async def tmp_file(content: str): input_filename = create_tmp_filename(extension=".ly") @@ -86,7 +89,7 @@ async def tmp_file(content: str): def list_files(tema_id: str) -> list[str]: - filedir = db.DB_FILES_DIR / str(tema_id) + filedir = db.DB_FILES_TEMA_DIR / str(tema_id) return [get_db_file_path(f) for f in filedir.iterdir()] @@ -97,7 +100,10 @@ def get_orphan_files() -> Iterator[Path]: alive_urls = link_urls | score_pdf_urls | score_img_urls return filter( lambda p: p.is_file() and get_db_file_path(p) not in alive_urls, - db.DB_FILES_DIR.rglob("*"), + itertools.chain( + db.DB_FILES_TEMA_DIR.rglob("*"), + db.DB_FILES_TMP_DIR.rglob("*"), + ) ) diff --git a/folkugat_web/services/lilypond/build.py b/folkugat_web/services/lilypond/build.py index 8bef2e6..15e0679 100644 --- a/folkugat_web/services/lilypond/build.py +++ b/folkugat_web/services/lilypond/build.py @@ -1,11 +1,9 @@ -from collections.abc import Iterable, Iterator +from collections.abc import Iterable from fastapi import HTTPException -from folkugat_web.dal.sql.temes import scores as scores_dal from folkugat_web.model import playlists as playlists_model 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 @@ -82,17 +80,15 @@ def build_set_title(temes: list[model.Tema | None]) -> str: def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet: - def _build_temes_by_id(temes: Iterable[model.Tema]) -> dict[int, model.Tema]: - return {tema.id: tema for tema in temes if tema.id is not None} + """ + The tune_set is assumed to be enriched with tunes + """ tema_ids = [tema_in_set.tema_id for tema_in_set in set_entry.temes] - temes_by_id = ( - FnChain.transform([tema_id for tema_id in tema_ids if tema_id is not None]) | - temes_q.get_temes_by_ids | - properties_service.add_properties_to_temes | - lyrics_service.add_lyrics_to_temes | - scores_service.add_scores_to_temes | - _build_temes_by_id - ).result() + temes_by_id = { + tema_in_set.tema_id: tema_in_set.tema + for tema_in_set in set_entry.temes + if tema_in_set.id is not None and tema_in_set.tema + } temes = [temes_by_id[tema_id] if tema_id is not None else None for tema_id in tema_ids] set_title = build_set_title(temes=temes) tunes = tunes_from_temes(temes) diff --git a/folkugat_web/services/lilypond/render.py b/folkugat_web/services/lilypond/render.py index 53b369b..8e5ae72 100644 --- a/folkugat_web/services/lilypond/render.py +++ b/folkugat_web/services/lilypond/render.py @@ -91,7 +91,7 @@ async def _build_errors(input_file: Path, stderr: bytes) -> list[RenderError]: stderr_lines = stderr.decode("utf-8").splitlines() logger.warning("Lilypond errors:") for line in stderr_lines: - logger.warning(line) + logger.warning(f"[LILYPOND] {line}") async with aiofiles.open(input_file, "r") as f: lines = await f.readlines() diff --git a/folkugat_web/services/playlists.py b/folkugat_web/services/playlists.py index 6c6e975..042399b 100644 --- a/folkugat_web/services/playlists.py +++ b/folkugat_web/services/playlists.py @@ -1,8 +1,15 @@ +import dataclasses + from folkugat_web.dal.sql import Connection from folkugat_web.dal.sql._connection import get_connection from folkugat_web.dal.sql.playlists import query, write from folkugat_web.log import logger from folkugat_web.model import playlists +from folkugat_web.model.lilypond import score as lilypond_model +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 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 query as temes_query @@ -84,3 +91,64 @@ def set_tema(session_id: int, set_id: int, entry_id: int, tema_id: int | None, with get_connection(con) as con: new_entry = playlists.PlaylistEntry(id=entry_id, session_id=session_id, set_id=set_id, tema_id=tema_id) write.update_playlist_entry(entry=new_entry, con=con) + + +def _elegible_for_set_score(tune: lilypond_model.LilypondTune) -> bool: + # A tune will be eligible for a set score if it has a score, lyrics or is unknown + return tune.is_unknown or bool(tune.score_source) or bool(tune.lyrics) + + +async def get_or_create_set_score(tune_set: playlists.Set) -> playlists.SetScore | None: + # The tune_set is assumed to be enriched with tunes + lilypond_set = lilypond_build.set_from_set(set_entry=tune_set) + if not all(map(_elegible_for_set_score, lilypond_set.tunes)): + return None + set_score_hash = lilypond_set.hash().hex() + + pdf_filepath = files_service.get_set_filename(f"{set_score_hash}.pdf") + png_filepath = files_service.get_set_filename(f"{set_score_hash}.cropped.png") + + if not pdf_filepath.exists() or not png_filepath.exists(): + # No score exists, so we need to create it + set_source = lilypond_source.set_source(tune_set=lilypond_set) + out_filepath = files_service.get_set_filename(set_score_hash) + print("Out filepath: ", str(out_filepath)) + async with files_service.tmp_file(content=set_source) as source_filepath: + if not pdf_filepath.exists(): + pdf_result = await lilypond_render.render_file( + input_file=source_filepath, + output=lilypond_render.RenderOutput.PDF, + output_file=out_filepath, + ) + if pdf_result.error is not None: + return None + if pdf_result.result is None: + raise RuntimeError("This shouldn't happen") + pdf_filepath = pdf_result.result + + if not png_filepath.exists(): + png_result = await lilypond_render.render_file( + input_file=source_filepath, + output=lilypond_render.RenderOutput.PNG_CROPPED, + output_file=out_filepath, + ) + if png_result.error is not None: + return None + if png_result.result is None: + raise RuntimeError("This shouldn't happen") + png_filepath = png_result.result + + return playlists.SetScore( + img_url=files_service.get_db_file_path(png_filepath), + pdf_url=files_service.get_db_file_path(pdf_filepath), + ) + + +async def add_set_score_to_set(tune_set: playlists.Set) -> playlists.Set: + if score := await get_or_create_set_score(tune_set=tune_set): + return dataclasses.replace( + tune_set, + score=score, + ) + else: + return tune_set