From ac79785cf011945e960335cd35da3b0b3d78db99 Mon Sep 17 00:00:00 2001 From: marc Date: Sun, 21 Dec 2025 15:40:11 +0100 Subject: [PATCH] =?UTF-8?q?Afegir=20partitures=20de=20llistes=20(can=C3=A7?= =?UTF-8?q?oners)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- folkugat_web/api/routes/llista/playlist.py | 4 +- folkugat_web/assets/static/css/main.css | 4 ++ .../templates/fragments/playlist/pagina.html | 1 + .../templates/fragments/playlist/score.html | 9 +++ .../assets/templates/lilypond/playlist.ly | 49 +++++++++++++ .../assets/templates/lilypond/tune_in_set.ly | 1 + folkugat_web/config/db.py | 2 + folkugat_web/fragments/playlist.py | 3 +- folkugat_web/model/lilypond/score.py | 12 ++++ folkugat_web/model/playlists.py | 7 ++ folkugat_web/services/files.py | 4 ++ folkugat_web/services/lilypond/build.py | 18 +++++ folkugat_web/services/lilypond/source.py | 9 ++- folkugat_web/services/playlists.py | 46 ++++++++++++ folkugat_web/services/sessions.py | 4 +- scripts/06_migrate_session_playlist_names.py | 71 +++++++++++++++++++ 16 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 folkugat_web/assets/templates/fragments/playlist/score.html create mode 100644 folkugat_web/assets/templates/lilypond/playlist.ly create mode 100755 scripts/06_migrate_session_playlist_names.py diff --git a/folkugat_web/api/routes/llista/playlist.py b/folkugat_web/api/routes/llista/playlist.py index 347bb98..cc50e98 100644 --- a/folkugat_web/api/routes/llista/playlist.py +++ b/folkugat_web/api/routes/llista/playlist.py @@ -34,12 +34,12 @@ def page( @router.get("/api/content/llista/{playlist_id}") -def contingut( +async def contingut( request: Request, logged_in: auth.LoggedIn, playlist_id: int, ): - return playlist.pagina(request, playlist_id, logged_in) + return await playlist.pagina(request, playlist_id, logged_in) @router.get("/api/llista/{playlist_id}/name") diff --git a/folkugat_web/assets/static/css/main.css b/folkugat_web/assets/static/css/main.css index 4a6c748..64e8a01 100644 --- a/folkugat_web/assets/static/css/main.css +++ b/folkugat_web/assets/static/css/main.css @@ -653,6 +653,10 @@ video { margin-bottom: 0.75rem; } +.mb-6 { + margin-bottom: 1.5rem; +} + .ml-2 { margin-left: 0.5rem; } diff --git a/folkugat_web/assets/templates/fragments/playlist/pagina.html b/folkugat_web/assets/templates/fragments/playlist/pagina.html index f790b41..585de39 100644 --- a/folkugat_web/assets/templates/fragments/playlist/pagina.html +++ b/folkugat_web/assets/templates/fragments/playlist/pagina.html @@ -4,6 +4,7 @@
{% include "fragments/playlist/name.html" %}
+ {% include "fragments/playlist/score.html" %}
{% set playlist_id = playlist.id %} {% set playlist = playlist %} diff --git a/folkugat_web/assets/templates/fragments/playlist/score.html b/folkugat_web/assets/templates/fragments/playlist/score.html new file mode 100644 index 0000000..0093349 --- /dev/null +++ b/folkugat_web/assets/templates/fragments/playlist/score.html @@ -0,0 +1,9 @@ +{% if playlist.playlist_score is not none and playlist.playlist_score.pdf_url is not none %} + +{% endif %} diff --git a/folkugat_web/assets/templates/lilypond/playlist.ly b/folkugat_web/assets/templates/lilypond/playlist.ly new file mode 100644 index 0000000..7c1c923 --- /dev/null +++ b/folkugat_web/assets/templates/lilypond/playlist.ly @@ -0,0 +1,49 @@ +{{ score_beginning }} +\version "2.24.4" + +{% include "lilypond/lib.ly" %} + +\book { + \paper { + top-margin = 10 + left-margin = 15 + right-margin = 15 + ragged-bottom = ##f + + scoreTitleMarkup = \markup { + \center-column { + \fontsize #3 \bold \fromproperty #'header:piece + \fill-line { + \null + \right-column { + \fromproperty #'header:composer + } + } + } + } + } + + \header { + title = \markup { "{{ playlist.title | safe }}" } + tagline = "Partitura generada amb LilyPond" + copyright = "Folkugat" + } + + {% for set in playlist.sets %} + {% if set.tunes|length > 1 %} + \markup { + \vspace #2 + \fill-line { + \center-column { + \fontsize #2 \bold "{{ set.title | safe }}" + } + } + } + \noPageBreak + {% endif %} + + {% for tune in set.tunes %} + {% include "lilypond/tune_in_set.ly" %} + {% endfor %} + {% endfor %} +} diff --git a/folkugat_web/assets/templates/lilypond/tune_in_set.ly b/folkugat_web/assets/templates/lilypond/tune_in_set.ly index d03b9c8..391275a 100644 --- a/folkugat_web/assets/templates/lilypond/tune_in_set.ly +++ b/folkugat_web/assets/templates/lilypond/tune_in_set.ly @@ -9,6 +9,7 @@ } } } +\noPageBreak {% if tune.score_source is not none %} \score { \language "english" diff --git a/folkugat_web/config/db.py b/folkugat_web/config/db.py index cbc0593..7fbb016 100644 --- a/folkugat_web/config/db.py +++ b/folkugat_web/config/db.py @@ -14,12 +14,14 @@ DB_FILES_DIR = DB_DIR / "fitxer" DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema" DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio" DB_FILES_SET_DIR = DB_FILES_DIR / "set" +DB_FILES_PLAYLIST_DIR = DB_FILES_DIR / "llistes" DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp" for path in [ DB_FILES_DIR, DB_FILES_TEMA_DIR, DB_FILES_SET_DIR, + DB_FILES_PLAYLIST_DIR, DB_FILES_SET_DIR, ]: path.mkdir(exist_ok=True) diff --git a/folkugat_web/fragments/playlist.py b/folkugat_web/fragments/playlist.py index 51100b2..5d7ab27 100644 --- a/folkugat_web/fragments/playlist.py +++ b/folkugat_web/fragments/playlist.py @@ -174,12 +174,13 @@ def set_tema(request: Request, logged_in: bool, playlist_id: int, set_id: int, e ) -def pagina(request: Request, playlist_id: int, logged_in: bool): +async def pagina(request: Request, playlist_id: int, logged_in: bool): playlist = playlists_service.get_playlist(playlist_id=playlist_id) if not playlist: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Could not find playlist") playlist = playlists_service.add_temes_to_playlist(playlist) + playlist = await playlists_service.add_playlist_score_to_playlist(playlist) return templates.TemplateResponse( "fragments/playlist/pagina.html", { diff --git a/folkugat_web/model/lilypond/score.py b/folkugat_web/model/lilypond/score.py index 99feae8..27691b5 100644 --- a/folkugat_web/model/lilypond/score.py +++ b/folkugat_web/model/lilypond/score.py @@ -89,3 +89,15 @@ class LilypondSet: self.title.encode(), *(tune.hash() for tune in self.tunes), ) + + +@dataclasses.dataclass +class LilypondPlaylist: + title: str + sets: list[LilypondSet] + + def hash(self) -> bytes: + return get_hash( + self.title.encode(), + *(lilypond_set.hash() for lilypond_set in self.sets), + ) diff --git a/folkugat_web/model/playlists.py b/folkugat_web/model/playlists.py index 398a265..5d5a6dc 100644 --- a/folkugat_web/model/playlists.py +++ b/folkugat_web/model/playlists.py @@ -49,6 +49,12 @@ class SetScore: pdf_url: str | None +@dataclasses.dataclass +class PlaylistScore: + img_url: str | None + pdf_url: str | None + + @dataclasses.dataclass class Set: id: int @@ -81,6 +87,7 @@ class Playlist: id: int name: str | None sets: list[Set] + playlist_score: PlaylistScore | None = None def to_playlist_entries(self) -> Iterator[PlaylistEntry]: for set_entry in self.sets: diff --git a/folkugat_web/services/files.py b/folkugat_web/services/files.py index 25bd8ab..68f3d83 100644 --- a/folkugat_web/services/files.py +++ b/folkugat_web/services/files.py @@ -100,6 +100,10 @@ def get_set_filename(filename: str) -> Path: return db.DB_FILES_SET_DIR / filename +def get_playlist_filename(filename: str) -> Path: + return db.DB_FILES_PLAYLIST_DIR / filename + + @asynccontextmanager async def tmp_file(content: str): input_filename = create_tmp_filename(extension=".ly") diff --git a/folkugat_web/services/lilypond/build.py b/folkugat_web/services/lilypond/build.py index 15e0679..91a5a46 100644 --- a/folkugat_web/services/lilypond/build.py +++ b/folkugat_web/services/lilypond/build.py @@ -4,6 +4,7 @@ from fastapi import HTTPException 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 playlists as playlists_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 @@ -96,3 +97,20 @@ def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet: title=set_title, tunes=tunes ) + + +def playlist_from_playlist(playlist: playlists_model.Playlist) -> lilypond_model.LilypondPlaylist: + """ + The playlist is assumed to be enriched with tunes + """ + lilypond_sets = [] + for set_entry in playlist.sets: + lilypond_set = set_from_set(set_entry) + if lilypond_set.tunes and all(map(playlists_service._elegible_for_set_score, lilypond_set.tunes)): + lilypond_sets.append(lilypond_set) + + playlist_title = playlist.name or "Llista" + return lilypond_model.LilypondPlaylist( + title=playlist_title, + sets=lilypond_sets + ) diff --git a/folkugat_web/services/lilypond/source.py b/folkugat_web/services/lilypond/source.py index 46397a3..cb16b75 100644 --- a/folkugat_web/services/lilypond/source.py +++ b/folkugat_web/services/lilypond/source.py @@ -1,4 +1,4 @@ -from folkugat_web.model.lilypond.score import LilypondSet, LilypondTune +from folkugat_web.model.lilypond.score import LilypondPlaylist, LilypondSet, LilypondTune from folkugat_web.templates import templates SCORE_BEGINNING = "% --- SCORE BEGINNING --- %" @@ -23,3 +23,10 @@ def set_source(tune_set: LilypondSet) -> str: score_beginning=SCORE_BEGINNING, tune_set=tune_set, ) + + +def playlist_source(playlist: LilypondPlaylist) -> str: + return templates.get_template("lilypond/playlist.ly").render( + score_beginning=SCORE_BEGINNING, + playlist=playlist, + ) diff --git a/folkugat_web/services/playlists.py b/folkugat_web/services/playlists.py index d6db7ea..97a5db4 100644 --- a/folkugat_web/services/playlists.py +++ b/folkugat_web/services/playlists.py @@ -170,3 +170,49 @@ async def add_set_score_to_set(tune_set: playlists.Set) -> playlists.Set: ) else: return tune_set + + +async def get_or_create_playlist_score(playlist: playlists.Playlist) -> playlists.PlaylistScore | None: + # The playlist is assumed to be enriched with tunes + if not playlist.sets: + return None + + lilypond_playlist = lilypond_build.playlist_from_playlist(playlist) + if not lilypond_playlist.sets: + return None + + playlist_score_hash = lilypond_playlist.hash().hex() + + pdf_filepath = files_service.get_playlist_filename(f"{playlist_score_hash}.pdf") + + if not pdf_filepath.exists(): + # No score exists, so we need to create it + playlist_source = lilypond_source.playlist_source(playlist=lilypond_playlist) + out_filepath = files_service.get_playlist_filename(playlist_score_hash) + async with files_service.tmp_file(content=playlist_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 + + return playlists.PlaylistScore( + img_url=None, # Only PDF generation for now + pdf_url=files_service.get_db_file_path(pdf_filepath), + ) + + +async def add_playlist_score_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist: + if score := await get_or_create_playlist_score(playlist=playlist): + return dataclasses.replace( + playlist, + playlist_score=score, + ) + else: + return playlist diff --git a/folkugat_web/services/sessions.py b/folkugat_web/services/sessions.py index c2ac566..c6982f0 100644 --- a/folkugat_web/services/sessions.py +++ b/folkugat_web/services/sessions.py @@ -184,8 +184,8 @@ def _create_session_playlists(session_id: int): setlist_name, slowjam_name = _get_playlist_names(session=session) - setlist_playlist_id = playlists_write.create_playlist(name=setlist_name, con=None) - slowjam_playlist_id = playlists_write.create_playlist(name=slowjam_name, con=None) + setlist_playlist_id = playlists_write.create_playlist(name=setlist_name) + slowjam_playlist_id = playlists_write.create_playlist(name=slowjam_name) with get_connection() as con: session_playlists.insert_playlist( diff --git a/scripts/06_migrate_session_playlist_names.py b/scripts/06_migrate_session_playlist_names.py new file mode 100755 index 0000000..e1a669a --- /dev/null +++ b/scripts/06_migrate_session_playlist_names.py @@ -0,0 +1,71 @@ +import datetime +import json +from collections.abc import Iterable + +from folkugat_web.dal.sql import get_connection +from folkugat_web.dal.sql.sessions import playlists as session_playlists +from folkugat_web.model.playlists import PlaylistType +from folkugat_web.services import sessions as sessions_service +from folkugat_web.dal.sql.playlists import write as playlists_write + +def main(): + """Main migration function.""" + with get_connection() as con: + cur = con.cursor() + query = """ SELECT + sp.playlist_id, + sp.session_id, + s.date, + sp.playlist_type, + p.name as current_name + FROM session_playlists sp + JOIN sessions s ON sp.session_id = s.id + JOIN playlists p ON sp.playlist_id = p.id + WHERE p.name IS NULL OR p.name = '' + ORDER BY sp.session_id, sp.playlist_type """ + _ = cur.execute(query) + rows = cur.fetchall() + + if not rows: + print("No session playlists need renaming.") + return + + updated_count = 0 + for row in rows: + playlist_id, session_id, session_date, playlist_type, current_name = row + + # Parse session date properly + session_date = sessions_service.get_date_names( + datetime.date.fromisoformat(session_date) + ) + + # Generate proper name based on playlist type + if playlist_type == PlaylistType.SESSION_SETLIST.value: + new_name = f"Sessió del {session_date.day} de {session_date.month_name} de {session_date.year}" + elif playlist_type == PlaylistType.SESSION_SLOWJAM.value: + new_name = f"Slow Jam del {session_date.day} de {session_date.month_name} de {session_date.year}" + else: + print(f"Unknown playlist type {playlist_type} for playlist {playlist_id}") + continue + + # Update the playlist name using existing function + playlists_write.update_playlist_name( + playlist_id=playlist_id, + name=new_name, + con=con + ) + + current_name_display = current_name or "None" + print( + f"Updated playlist {playlist_id} (session {session_id}) " + f"from '{current_name_display}' to '{new_name}'" + ) + updated_count += 1 + + con.commit() + print(f"Migration completed! Updated {updated_count} playlist names.") + print("DONE!") + + +if __name__ == "__main__": + main() \ No newline at end of file