Added tune previews
This commit is contained in:
@@ -4,16 +4,16 @@
|
||||
** TODO Ordenar els resultats de la cerca de temes
|
||||
** TODO Suport per a diverses organitzacions (no només jam de Sant Cugat)
|
||||
** TODO Usuaris i permisos granulars
|
||||
** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina
|
||||
** Lilypond support (o similar)
|
||||
*** TODO Fer cançoners "en directe"
|
||||
*** DONE Suport de caràcters especials (al títol i més llocs?)
|
||||
*** 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)
|
||||
** TODO Arreglar visualitzador de pdf, suportar més d'un visualitzador per pàgina
|
||||
* Idees
|
||||
** 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
|
||||
*** Navegació
|
||||
**** Cerca de temes
|
||||
|
||||
@@ -35,64 +35,12 @@ async def set_score(
|
||||
score = scores_service.get_score_by_id(score_id=score_id, tema_id=tema_id)
|
||||
if not score:
|
||||
raise HTTPException(status_code=404, detail="Could not find lyric!")
|
||||
|
||||
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=[],
|
||||
)
|
||||
new_score = await scores_service.render_score(score=score, title=title, source=source)
|
||||
scores_service.update_score(score=new_score)
|
||||
files.clean_orphan_files()
|
||||
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")
|
||||
def add_score(
|
||||
request: Request,
|
||||
|
||||
@@ -693,10 +693,6 @@ video {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -899,10 +895,6 @@ video {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-none {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
@@ -12,32 +12,34 @@
|
||||
<i>{{ query }}</i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<table 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>
|
||||
<ul class="text-left min-w-full w-full">
|
||||
{% for tema in temes %}
|
||||
<tr class="border-b border-beige">
|
||||
<td class="py-2 px-4">
|
||||
<li class="flex flex-col
|
||||
m-4 rounded-lg
|
||||
bg-white">
|
||||
<div class="py-2 px-4 text-brown font-bold">
|
||||
<a href="/tema/{{ tema.id }}">
|
||||
{{ tema.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="py-2 px-4">
|
||||
{% include "fragments/temes/result_links.html" %}
|
||||
</td>
|
||||
<td class="py-2 px-4">
|
||||
{% if tema.stats is none %}
|
||||
-
|
||||
{% else %}
|
||||
{{ tema.stats.times_played }}
|
||||
</div>
|
||||
{% if tema.main_score() and tema.main_score().preview_url %}
|
||||
<img class="p-2 max-w"
|
||||
src="{{ tema.main_score().preview_url }}" />
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if tema.properties %}
|
||||
<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 %}
|
||||
</table>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if prev_offset is not none or next_offset is not none %}
|
||||
<div class="py-2">
|
||||
{% if prev_offset is not none %}
|
||||
|
||||
10
folkugat_web/assets/templates/lilypond/preview.ly
Normal file
10
folkugat_web/assets/templates/lilypond/preview.ly
Normal 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 %}
|
||||
@@ -79,6 +79,7 @@ def create_scores_table(con: Connection):
|
||||
errors TEXT NOT NULL,
|
||||
img_url TEXT,
|
||||
pdf_url TEXT,
|
||||
preview_url TEXT,
|
||||
hidden BOOLEAN,
|
||||
FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE
|
||||
)
|
||||
|
||||
@@ -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.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):
|
||||
@@ -17,6 +17,7 @@ class ScoreRowDict(TypedDict):
|
||||
source: str
|
||||
img_url: str | None
|
||||
pdf_url: str | None
|
||||
preview_url: str | None
|
||||
hidden: bool
|
||||
|
||||
|
||||
@@ -29,6 +30,7 @@ def score_to_row(score: model.Score) -> ScoreRowDict:
|
||||
"source": score.source,
|
||||
"img_url": score.img_url,
|
||||
"pdf_url": score.pdf_url,
|
||||
"preview_url": score.preview_url,
|
||||
"hidden": score.hidden,
|
||||
}
|
||||
|
||||
@@ -43,7 +45,8 @@ def row_to_score(row: ScoreRowTuple) -> model.Score:
|
||||
errors=list(map(RenderError.from_dict, errors_dicts)),
|
||||
img_url=row[5],
|
||||
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"""
|
||||
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
|
||||
{filter_clause}
|
||||
"""
|
||||
@@ -92,9 +95,9 @@ def insert_score(score: model.Score, con: Connection | None = None) -> model.Sco
|
||||
data = score_to_row(score)
|
||||
query = f"""
|
||||
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
|
||||
(: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 *
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
@@ -110,7 +113,7 @@ def update_score(score: model.Score, con: Connection | None = None):
|
||||
UPDATE tema_scores
|
||||
SET
|
||||
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
|
||||
id = :id
|
||||
"""
|
||||
|
||||
@@ -3,8 +3,7 @@ from folkugat_web.model import temes as model
|
||||
from folkugat_web.model.lilypond.processing import RenderError
|
||||
from folkugat_web.model.pagines import Pages
|
||||
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 lyrics as lyrics_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 scores as scores_service
|
||||
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 = (
|
||||
FnChain.transform(temes) |
|
||||
temes_q.temes_compute_stats |
|
||||
links_service.add_links_to_temes |
|
||||
lyrics_service.add_lyrics_to_temes |
|
||||
properties_service.add_properties_to_temes |
|
||||
scores_service.add_scores_to_temes |
|
||||
list
|
||||
).result()
|
||||
|
||||
@@ -67,6 +67,7 @@ class Score:
|
||||
source: str
|
||||
img_url: str | None
|
||||
pdf_url: str | None
|
||||
preview_url: str | None
|
||||
hidden: bool
|
||||
|
||||
@classmethod
|
||||
@@ -81,6 +82,7 @@ class Score:
|
||||
errors=[],
|
||||
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,
|
||||
preview_url=None,
|
||||
hidden=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@ def get_orphan_files() -> Iterator[Path]:
|
||||
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_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(
|
||||
lambda p: p.is_file() and get_db_file_path(p) not in alive_urls,
|
||||
itertools.chain(
|
||||
|
||||
@@ -18,7 +18,7 @@ RenderResult = Result[Path, list[RenderError]]
|
||||
class RenderOutput(enum.Enum):
|
||||
PDF = "pdf"
|
||||
PNG_CROPPED = "png-cropped"
|
||||
PNG_PREVIEW = "png-preview"
|
||||
PREVIEW = "preview"
|
||||
|
||||
|
||||
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(".png")
|
||||
case RenderOutput.PNG_PREVIEW:
|
||||
case RenderOutput.PREVIEW:
|
||||
command = [
|
||||
"lilypond",
|
||||
"-f", "png",
|
||||
"-f", "svg",
|
||||
"-dpreview=#t",
|
||||
"-dno-print-pages",
|
||||
"-o", str(output_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(
|
||||
*command,
|
||||
|
||||
@@ -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:
|
||||
return templates.get_template("lilypond/tune_set.ly").render(
|
||||
score_beginning=SCORE_BEGINNING,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import dataclasses
|
||||
from collections.abc import Iterable, Iterator
|
||||
|
||||
from fastapi import HTTPException
|
||||
from folkugat_web.dal.sql.temes import scores as scores_dal
|
||||
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
|
||||
from folkugat_web.utils import FnChain
|
||||
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
|
||||
|
||||
|
||||
def add_scores_to_tema(tema: model.Tema) -> model.Tema:
|
||||
@@ -42,34 +40,56 @@ def create_score(tema_id: int) -> model.Score:
|
||||
errors=[],
|
||||
img_url=None,
|
||||
pdf_url=None,
|
||||
preview_url=None,
|
||||
hidden=True,
|
||||
)
|
||||
return scores_dal.insert_score(score=new_score)
|
||||
|
||||
|
||||
def build_tune_set_full_source(tema_ids: list[int]) -> str:
|
||||
temes = (
|
||||
FnChain.transform(temes_q.get_temes_by_ids(tema_ids=tema_ids)) |
|
||||
properties_service.add_properties_to_temes |
|
||||
lyrics_service.add_lyrics_to_temes |
|
||||
add_scores_to_temes |
|
||||
list
|
||||
).result()
|
||||
if not temes:
|
||||
return ""
|
||||
set_title = " i ".join(filter(bool, [
|
||||
", ".join([tema.title for tema in temes[:-1]]),
|
||||
temes[-1].title
|
||||
]))
|
||||
tune_set = lilypond_model.LilypondSet(
|
||||
title=set_title,
|
||||
tunes=[
|
||||
lilypond_model.LilypondTune(
|
||||
header=lilypond_model.HeaderData.from_tema(tema=tema),
|
||||
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,
|
||||
) for tema in temes
|
||||
]
|
||||
async def render_score(score: model.Score, title: str, source: str) -> model.Score:
|
||||
tune = lilypond_build.tune_from_tema_id(tema_id=score.tema_id, score_source=source)
|
||||
output_file = files_service.create_tema_filename(tema_id=score.tema_id)
|
||||
|
||||
tune_source = lilypond_source.tune_source(tune=tune)
|
||||
async with files_service.tmp_file(content=tune_source) as source_file:
|
||||
# Render PDF
|
||||
pdf_result = await lilypond_render.render_file(
|
||||
input_file=source_file,
|
||||
output=lilypond_render.RenderOutput.PDF,
|
||||
output_file=output_file,
|
||||
)
|
||||
if errors := pdf_result.error:
|
||||
return dataclasses.replace(score, source=source, title=title, errors=errors)
|
||||
# Render IMAGE
|
||||
img_result = await lilypond_render.render_file(
|
||||
input_file=source_file,
|
||||
output=lilypond_render.RenderOutput.PNG_CROPPED,
|
||||
output_file=output_file,
|
||||
)
|
||||
if errors := img_result.error:
|
||||
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)
|
||||
|
||||
8
scripts/03_add_preview_url.py
Normal file
8
scripts/03_add_preview_url.py
Normal 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!")
|
||||
Reference in New Issue
Block a user