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 @@
{% 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