Added tune previews

This commit is contained in:
marc
2025-04-27 17:00:04 +02:00
parent 695e0b54bf
commit bc3d98aba2
14 changed files with 123 additions and 131 deletions

View File

@@ -4,16 +4,16 @@
** TODO Ordenar els resultats de la cerca de temes ** TODO Ordenar els resultats de la cerca de temes
** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat) ** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat)
** TODO Usuaris i permisos granulars ** TODO Usuaris i permisos granulars
** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina
** Lilypond support (o similar) ** Lilypond support (o similar)
*** TODO Fer cançoners "en directe" *** TODO Fer cançoners "en directe"
*** DONE Suport de caràcters especials (al títol i més llocs?) *** DONE Suport de caràcters especials (al títol i més llocs?)
*** DONE Mostrar partitura als enllaços (resultats de cerca) *** 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) ** 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 * Idees
** Jams ** Jams
*** Properes 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 ** Temes
*** Navegació *** Navegació
**** Cerca de temes **** Cerca de temes

View File

@@ -35,64 +35,12 @@ async def set_score(
score = scores_service.get_score_by_id(score_id=score_id, tema_id=tema_id) score = scores_service.get_score_by_id(score_id=score_id, tema_id=tema_id)
if not score: if not score:
raise HTTPException(status_code=404, detail="Could not find lyric!") raise HTTPException(status_code=404, detail="Could not find lyric!")
new_score = await scores_service.render_score(score=score, title=title, source=source)
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=[],
)
scores_service.update_score(score=new_score) scores_service.update_score(score=new_score)
files.clean_orphan_files() files.clean_orphan_files()
return scores_fragments.score(request=request, logged_in=logged_in, score=new_score) 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") @router.post("/api/tema/{tema_id}/score")
def add_score( def add_score(
request: Request, request: Request,

View File

@@ -693,10 +693,6 @@ video {
display: flex; display: flex;
} }
.table {
display: table;
}
.hidden { .hidden {
display: none; display: none;
} }
@@ -899,10 +895,6 @@ video {
border-width: 0px; border-width: 0px;
} }
.border-b {
border-bottom-width: 1px;
}
.border-none { .border-none {
border-style: none; border-style: none;
} }

View File

@@ -12,32 +12,34 @@
<i>{{ query }}</i> <i>{{ query }}</i>
</button> </button>
{% endif %} {% endif %}
<table class="text-left min-w-full w-full"> <ul class="text-left min-w-full w-full">
<tr class="border-b border-beige">
<td class="font-bold py-2 px-4">Nom</td>
<td class="font-bold py-2 px-4">Enllaços</td>
<td class="font-bold py-2 px-4">Cops tocat</td>
</tr>
{% for tema in temes %} {% for tema in temes %}
<tr class="border-b border-beige"> <li class="flex flex-col
<td class="py-2 px-4"> m-4 rounded-lg
bg-white">
<div class="py-2 px-4 text-brown font-bold">
<a href="/tema/{{ tema.id }}"> <a href="/tema/{{ tema.id }}">
{{ tema.title }} {{ tema.title }}
</a> </a>
</td> </div>
<td class="py-2 px-4"> {% if tema.main_score() and tema.main_score().preview_url %}
{% include "fragments/temes/result_links.html" %} <img class="p-2 max-w"
</td> src="{{ tema.main_score().preview_url }}" />
<td class="py-2 px-4">
{% if tema.stats is none %}
-
{% else %}
{{ tema.stats.times_played }}
{% endif %} {% endif %}
</td> {% if tema.properties %}
</tr> <ul class="flex flex-wrap text-sm
py-2 px-4">
{% for property in tema.properties %}
<div class="bg-beige text-white rounded
m-1 px-2">
{{ property.value }}
</div>
{% endfor %} {% endfor %}
</table> </ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% if prev_offset is not none or next_offset is not none %} {% if prev_offset is not none or next_offset is not none %}
<div class="py-2"> <div class="py-2">
{% if prev_offset is not none %} {% if prev_offset is not none %}

View File

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

View File

@@ -79,6 +79,7 @@ def create_scores_table(con: Connection):
errors TEXT NOT NULL, errors TEXT NOT NULL,
img_url TEXT, img_url TEXT,
pdf_url TEXT, pdf_url TEXT,
preview_url TEXT,
hidden BOOLEAN, hidden BOOLEAN,
FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE
) )

View File

@@ -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 import temes as model
from folkugat_web.model.lilypond.processing import RenderError 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): class ScoreRowDict(TypedDict):
@@ -17,6 +17,7 @@ class ScoreRowDict(TypedDict):
source: str source: str
img_url: str | None img_url: str | None
pdf_url: str | None pdf_url: str | None
preview_url: str | None
hidden: bool hidden: bool
@@ -29,6 +30,7 @@ def score_to_row(score: model.Score) -> ScoreRowDict:
"source": score.source, "source": score.source,
"img_url": score.img_url, "img_url": score.img_url,
"pdf_url": score.pdf_url, "pdf_url": score.pdf_url,
"preview_url": score.preview_url,
"hidden": score.hidden, "hidden": score.hidden,
} }
@@ -43,7 +45,8 @@ def row_to_score(row: ScoreRowTuple) -> model.Score:
errors=list(map(RenderError.from_dict, errors_dicts)), errors=list(map(RenderError.from_dict, errors_dicts)),
img_url=row[5], img_url=row[5],
pdf_url=row[6], 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""" query = f"""
SELECT 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 FROM tema_scores
{filter_clause} {filter_clause}
""" """
@@ -92,9 +95,9 @@ def insert_score(score: model.Score, con: Connection | None = None) -> model.Sco
data = score_to_row(score) data = score_to_row(score)
query = f""" query = f"""
INSERT INTO tema_scores 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 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 * RETURNING *
""" """
with get_connection(con) as con: with get_connection(con) as con:
@@ -110,7 +113,7 @@ def update_score(score: model.Score, con: Connection | None = None):
UPDATE tema_scores UPDATE tema_scores
SET SET
tema_id = :tema_id, title = :title, source = :source, errors = :errors, 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 WHERE
id = :id id = :id
""" """

View File

@@ -3,8 +3,7 @@ from folkugat_web.model import temes as model
from folkugat_web.model.lilypond.processing import RenderError from folkugat_web.model.lilypond.processing import RenderError
from folkugat_web.model.pagines import Pages from folkugat_web.model.pagines import Pages
from folkugat_web.services import sessions as sessions_service 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 properties as properties_service
from folkugat_web.services.temes import lyrics as lyrics_service
from folkugat_web.services.temes import query as temes_q 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 scores as scores_service
from folkugat_web.services.temes import search as temes_s 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 = ( temes = (
FnChain.transform(temes) | FnChain.transform(temes) |
temes_q.temes_compute_stats | temes_q.temes_compute_stats |
links_service.add_links_to_temes | properties_service.add_properties_to_temes |
lyrics_service.add_lyrics_to_temes |
scores_service.add_scores_to_temes | scores_service.add_scores_to_temes |
list list
).result() ).result()

View File

@@ -67,6 +67,7 @@ class Score:
source: str source: str
img_url: str | None img_url: str | None
pdf_url: str | None pdf_url: str | None
preview_url: str | None
hidden: bool hidden: bool
@classmethod @classmethod
@@ -81,6 +82,7 @@ class Score:
errors=[], errors=[],
img_url=link.url if link.link_type is LinkType.IMAGE else None, 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, pdf_url=link.url if link.link_type is LinkType.PDF else None,
preview_url=None,
hidden=False, hidden=False,
) )

View File

@@ -97,7 +97,8 @@ def get_orphan_files() -> Iterator[Path]:
link_urls = {link.url for link in links_dal.get_links()} 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_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} 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( return filter(
lambda p: p.is_file() and get_db_file_path(p) not in alive_urls, lambda p: p.is_file() and get_db_file_path(p) not in alive_urls,
itertools.chain( itertools.chain(

View File

@@ -18,7 +18,7 @@ RenderResult = Result[Path, list[RenderError]]
class RenderOutput(enum.Enum): class RenderOutput(enum.Enum):
PDF = "pdf" PDF = "pdf"
PNG_CROPPED = "png-cropped" PNG_CROPPED = "png-cropped"
PNG_PREVIEW = "png-preview" PREVIEW = "preview"
async def render( 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(".cropped.png")
# output_file = output_file.with_suffix(".png") # output_file = output_file.with_suffix(".png")
case RenderOutput.PNG_PREVIEW: case RenderOutput.PREVIEW:
command = [ command = [
"lilypond", "lilypond",
"-f", "png", "-f", "svg",
"-dpreview=#t", "-dpreview=#t",
"-dno-print-pages", "-dno-print-pages",
"-o", str(output_file), "-o", str(output_file),
str(input_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( proc = await asyncio.create_subprocess_exec(
*command, *command,

View File

@@ -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: def set_source(tune_set: LilypondSet) -> str:
return templates.get_template("lilypond/tune_set.ly").render( return templates.get_template("lilypond/tune_set.ly").render(
score_beginning=SCORE_BEGINNING, score_beginning=SCORE_BEGINNING,

View File

@@ -1,14 +1,12 @@
import dataclasses
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from fastapi import HTTPException
from folkugat_web.dal.sql.temes import scores as scores_dal from folkugat_web.dal.sql.temes import scores as scores_dal
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.services import files as files_service
from folkugat_web.services import lilypond from folkugat_web.services.lilypond import build as lilypond_build
from folkugat_web.services.temes import lyrics as lyrics_service from folkugat_web.services.lilypond import render as lilypond_render
from folkugat_web.services.temes import properties as properties_service from folkugat_web.services.lilypond import source as lilypond_source
from folkugat_web.services.temes import query as temes_q
from folkugat_web.utils import FnChain
def add_scores_to_tema(tema: model.Tema) -> model.Tema: def add_scores_to_tema(tema: model.Tema) -> model.Tema:
@@ -42,34 +40,56 @@ def create_score(tema_id: int) -> model.Score:
errors=[], errors=[],
img_url=None, img_url=None,
pdf_url=None, pdf_url=None,
preview_url=None,
hidden=True, hidden=True,
) )
return scores_dal.insert_score(score=new_score) return scores_dal.insert_score(score=new_score)
def build_tune_set_full_source(tema_ids: list[int]) -> str: async def render_score(score: model.Score, title: str, source: str) -> model.Score:
temes = ( tune = lilypond_build.tune_from_tema_id(tema_id=score.tema_id, score_source=source)
FnChain.transform(temes_q.get_temes_by_ids(tema_ids=tema_ids)) | output_file = files_service.create_tema_filename(tema_id=score.tema_id)
properties_service.add_properties_to_temes |
lyrics_service.add_lyrics_to_temes | tune_source = lilypond_source.tune_source(tune=tune)
add_scores_to_temes | async with files_service.tmp_file(content=tune_source) as source_file:
list # Render PDF
).result() pdf_result = await lilypond_render.render_file(
if not temes: input_file=source_file,
return "" output=lilypond_render.RenderOutput.PDF,
set_title = " i ".join(filter(bool, [ output_file=output_file,
", ".join([tema.title for tema in temes[:-1]]), )
temes[-1].title if errors := pdf_result.error:
])) return dataclasses.replace(score, source=source, title=title, errors=errors)
tune_set = lilypond_model.LilypondSet( # Render IMAGE
title=set_title, img_result = await lilypond_render.render_file(
tunes=[ input_file=source_file,
lilypond_model.LilypondTune( output=lilypond_render.RenderOutput.PNG_CROPPED,
header=lilypond_model.HeaderData.from_tema(tema=tema), output_file=output_file,
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, if errors := img_result.error:
) for tema in temes 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)

View File

@@ -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!")