Afegir partitures de llistes (cançoners)

This commit is contained in:
marc
2025-12-21 15:40:11 +01:00
parent 089e61fb0b
commit ac79785cf0
16 changed files with 238 additions and 6 deletions

View File

@@ -34,12 +34,12 @@ def page(
@router.get("/api/content/llista/{playlist_id}") @router.get("/api/content/llista/{playlist_id}")
def contingut( async def contingut(
request: Request, request: Request,
logged_in: auth.LoggedIn, logged_in: auth.LoggedIn,
playlist_id: int, 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") @router.get("/api/llista/{playlist_id}/name")

View File

@@ -653,6 +653,10 @@ video {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.mb-6 {
margin-bottom: 1.5rem;
}
.ml-2 { .ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }

View File

@@ -4,6 +4,7 @@
<div id="llista-name"> <div id="llista-name">
{% include "fragments/playlist/name.html" %} {% include "fragments/playlist/name.html" %}
</div> </div>
{% include "fragments/playlist/score.html" %}
<div class="text-left"> <div class="text-left">
{% set playlist_id = playlist.id %} {% set playlist_id = playlist.id %}
{% set playlist = playlist %} {% set playlist = playlist %}

View File

@@ -0,0 +1,9 @@
{% if playlist.playlist_score is not none and playlist.playlist_score.pdf_url is not none %}
<div class="flex flex-col items-center mt-4 mb-6">
<div class="bg-beige border rounded border-beige m-2 p-2">
<a href="{{ playlist.playlist_score.pdf_url }}" target="_blank" class="text-white">
Obre en PDF
</a>
</div>
</div>
{% endif %}

View File

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

View File

@@ -9,6 +9,7 @@
} }
} }
} }
\noPageBreak
{% if tune.score_source is not none %} {% if tune.score_source is not none %}
\score { \score {
\language "english" \language "english"

View File

@@ -14,12 +14,14 @@ DB_FILES_DIR = DB_DIR / "fitxer"
DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema" DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema"
DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio" DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio"
DB_FILES_SET_DIR = DB_FILES_DIR / "set" DB_FILES_SET_DIR = DB_FILES_DIR / "set"
DB_FILES_PLAYLIST_DIR = DB_FILES_DIR / "llistes"
DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp" DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp"
for path in [ for path in [
DB_FILES_DIR, DB_FILES_DIR,
DB_FILES_TEMA_DIR, DB_FILES_TEMA_DIR,
DB_FILES_SET_DIR, DB_FILES_SET_DIR,
DB_FILES_PLAYLIST_DIR,
DB_FILES_SET_DIR, DB_FILES_SET_DIR,
]: ]:
path.mkdir(exist_ok=True) path.mkdir(exist_ok=True)

View File

@@ -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) playlist = playlists_service.get_playlist(playlist_id=playlist_id)
if not playlist: if not playlist:
from fastapi import HTTPException from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Could not find playlist") raise HTTPException(status_code=404, detail="Could not find playlist")
playlist = playlists_service.add_temes_to_playlist(playlist) playlist = playlists_service.add_temes_to_playlist(playlist)
playlist = await playlists_service.add_playlist_score_to_playlist(playlist)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/playlist/pagina.html", "fragments/playlist/pagina.html",
{ {

View File

@@ -89,3 +89,15 @@ class LilypondSet:
self.title.encode(), self.title.encode(),
*(tune.hash() for tune in self.tunes), *(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),
)

View File

@@ -49,6 +49,12 @@ class SetScore:
pdf_url: str | None pdf_url: str | None
@dataclasses.dataclass
class PlaylistScore:
img_url: str | None
pdf_url: str | None
@dataclasses.dataclass @dataclasses.dataclass
class Set: class Set:
id: int id: int
@@ -81,6 +87,7 @@ class Playlist:
id: int id: int
name: str | None name: str | None
sets: list[Set] sets: list[Set]
playlist_score: PlaylistScore | None = None
def to_playlist_entries(self) -> Iterator[PlaylistEntry]: def to_playlist_entries(self) -> Iterator[PlaylistEntry]:
for set_entry in self.sets: for set_entry in self.sets:

View File

@@ -100,6 +100,10 @@ def get_set_filename(filename: str) -> Path:
return db.DB_FILES_SET_DIR / filename return db.DB_FILES_SET_DIR / filename
def get_playlist_filename(filename: str) -> Path:
return db.DB_FILES_PLAYLIST_DIR / filename
@asynccontextmanager @asynccontextmanager
async def tmp_file(content: str): async def tmp_file(content: str):
input_filename = create_tmp_filename(extension=".ly") input_filename = create_tmp_filename(extension=".ly")

View File

@@ -4,6 +4,7 @@ from fastapi import HTTPException
from folkugat_web.model import playlists as playlists_model from folkugat_web.model import playlists as playlists_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.model.lilypond import score as lilypond_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 lyrics as lyrics_service
from folkugat_web.services.temes import properties as properties_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 query as temes_q
@@ -96,3 +97,20 @@ def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet:
title=set_title, title=set_title,
tunes=tunes 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
)

View File

@@ -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 from folkugat_web.templates import templates
SCORE_BEGINNING = "% --- SCORE BEGINNING --- %" SCORE_BEGINNING = "% --- SCORE BEGINNING --- %"
@@ -23,3 +23,10 @@ def set_source(tune_set: LilypondSet) -> str:
score_beginning=SCORE_BEGINNING, score_beginning=SCORE_BEGINNING,
tune_set=tune_set, tune_set=tune_set,
) )
def playlist_source(playlist: LilypondPlaylist) -> str:
return templates.get_template("lilypond/playlist.ly").render(
score_beginning=SCORE_BEGINNING,
playlist=playlist,
)

View File

@@ -170,3 +170,49 @@ async def add_set_score_to_set(tune_set: playlists.Set) -> playlists.Set:
) )
else: else:
return tune_set 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

View File

@@ -184,8 +184,8 @@ def _create_session_playlists(session_id: int):
setlist_name, slowjam_name = _get_playlist_names(session=session) setlist_name, slowjam_name = _get_playlist_names(session=session)
setlist_playlist_id = playlists_write.create_playlist(name=setlist_name, con=None) setlist_playlist_id = playlists_write.create_playlist(name=setlist_name)
slowjam_playlist_id = playlists_write.create_playlist(name=slowjam_name, con=None) slowjam_playlist_id = playlists_write.create_playlist(name=slowjam_name)
with get_connection() as con: with get_connection() as con:
session_playlists.insert_playlist( session_playlists.insert_playlist(

View File

@@ -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()