Added lilypond edition
This commit is contained in:
@@ -1 +1 @@
|
||||
from . import editor, index, links, lyrics, properties
|
||||
from . import editor, index, links, lyrics, properties, scores
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 write as temes_w
|
||||
from folkugat_web.templates import templates
|
||||
from folkugat_web.utils import FnChain
|
||||
@@ -39,6 +40,7 @@ def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||
temes_q.tema_compute_stats |
|
||||
links_service.add_links_to_tema |
|
||||
lyrics_service.add_lyrics_to_tema |
|
||||
scores_service.add_scores_to_tema |
|
||||
properties_service.add_properties_to_tema
|
||||
).result()
|
||||
return temes.tema(request, logged_in, tema)
|
||||
|
||||
147
folkugat_web/api/tema/scores.py
Normal file
147
folkugat_web/api/tema/scores.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import dataclasses
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.params import Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.fragments import temes
|
||||
from folkugat_web.fragments.tema import scores as scores_fragments
|
||||
from folkugat_web.services import auth, files, lilypond
|
||||
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.utils import FnChain
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/score/{score_id}")
|
||||
def score(request: Request, logged_in: auth.LoggedIn, tema_id: int, score_id: int):
|
||||
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!")
|
||||
return scores_fragments.score(request=request, logged_in=logged_in, score=score)
|
||||
|
||||
|
||||
@router.put("/api/tema/{tema_id}/score/{score_id}")
|
||||
async def set_score(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
score_id: int,
|
||||
title: Annotated[str, Form()],
|
||||
source: Annotated[str, Form()],
|
||||
):
|
||||
source = source.strip()
|
||||
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!")
|
||||
|
||||
full_source = scores_service.build_single_tune_full_source(tema_id=tema_id, source=source)
|
||||
pdf_filename = files.create_tema_filename(tema_id=tema_id)
|
||||
pdf_filename, errors = await lilypond.render(source=full_source, fmt="pdf", output_filename=pdf_filename)
|
||||
if errors:
|
||||
new_score = dataclasses.replace(
|
||||
score,
|
||||
source=source,
|
||||
title=title,
|
||||
errors=errors,
|
||||
)
|
||||
else:
|
||||
png_filename = files.create_tema_filename(tema_id=tema_id)
|
||||
png_filename, errors = await lilypond.render(source=full_source, fmt="png", output_filename=png_filename)
|
||||
new_score = dataclasses.replace(
|
||||
score,
|
||||
source=source,
|
||||
title=title,
|
||||
pdf_url=files.get_db_file_path(pdf_filename) if pdf_filename else None,
|
||||
img_url=files.get_db_file_path(png_filename) if png_filename else None,
|
||||
errors=errors,
|
||||
)
|
||||
scores_service.update_score(score=new_score)
|
||||
files.clean_orphan_files()
|
||||
return scores_fragments.score(request=request, logged_in=logged_in, score=new_score)
|
||||
|
||||
|
||||
@router.post("/api/tema/{tema_id}/score")
|
||||
def add_score(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
):
|
||||
score = scores_service.create_score(tema_id=tema_id)
|
||||
return scores_fragments.score_editor(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
score=score,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/api/tema/{tema_id}/score/{score_id}")
|
||||
def delete_score(request: Request, logged_in: auth.RequireLogin, tema_id: int, score_id: int):
|
||||
scores_service.delete_score(score_id=score_id, tema_id=tema_id)
|
||||
files.clean_orphan_files()
|
||||
return HTMLResponse()
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/editor/score/{score_id}")
|
||||
def score_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
score_id: int,
|
||||
):
|
||||
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!")
|
||||
return scores_fragments.score_editor(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
score=score,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/tema/{tema_id}/editor/score/{score_id}/render")
|
||||
async def render(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
score_id: int,
|
||||
source: Annotated[str, Form()],
|
||||
):
|
||||
full_source = scores_service.build_single_tune_full_source(tema_id=tema_id, source=source)
|
||||
output_filename, errors = await lilypond.render(source=full_source, fmt="png")
|
||||
if output_filename:
|
||||
score_render_url = files.get_db_file_path(output_filename)
|
||||
return temes.score_render(request=request, score_id=score_id, score_render_url=score_render_url)
|
||||
else:
|
||||
return temes.score_render(request=request, score_id=score_id, errors=errors)
|
||||
|
||||
|
||||
@router.put("/api/tema/{tema_id}/score/{score_id}/show")
|
||||
async def show_score(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
score_id: int,
|
||||
):
|
||||
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!")
|
||||
new_score = dataclasses.replace(score, hidden=False)
|
||||
scores_service.update_score(score=new_score)
|
||||
return scores_fragments.score(request=request, logged_in=logged_in, score=new_score)
|
||||
|
||||
|
||||
@router.put("/api/tema/{tema_id}/score/{score_id}/hide")
|
||||
async def hide_score(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
score_id: int,
|
||||
):
|
||||
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!")
|
||||
new_score = dataclasses.replace(score, hidden=True)
|
||||
scores_service.update_score(score=new_score)
|
||||
return scores_fragments.score(request=request, logged_in=logged_in, score=new_score)
|
||||
35
folkugat_web/assets/static/css/lilypond.css
Normal file
35
folkugat_web/assets/static/css/lilypond.css
Normal file
@@ -0,0 +1,35 @@
|
||||
/* Override background and text color for the entire editor */
|
||||
.CodeMirror {
|
||||
background: #1e1e1e; /* Dark background */
|
||||
color: #f8f8f2; /* Light text */
|
||||
}
|
||||
|
||||
/* Optional: Style the gutter (line numbers) */
|
||||
.CodeMirror-gutters {
|
||||
background: #1e1e1e;
|
||||
border-right: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Optional: Style active line */
|
||||
.CodeMirror-activeline-background {
|
||||
background: #2a2a2a !important;
|
||||
}
|
||||
|
||||
/* Make the cursor white */
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid white !important;
|
||||
}
|
||||
|
||||
/* Optional: Change the cursor color when focused/unfocused */
|
||||
.CodeMirror-focused .CodeMirror-cursor {
|
||||
border-left: 1px solid white !important;
|
||||
}
|
||||
|
||||
.cm-keyword {
|
||||
/* Commands like \relative */
|
||||
color: #569cd6;
|
||||
}
|
||||
.cm-comment {
|
||||
/* Notes like c4, d'8 */
|
||||
color: #5c5c5c;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
17
folkugat_web/assets/static/js/lilypond.js
Normal file
17
folkugat_web/assets/static/js/lilypond.js
Normal file
@@ -0,0 +1,17 @@
|
||||
CodeMirror.defineMode("lilypond", function() {
|
||||
return {
|
||||
token: function(stream) {
|
||||
// Highlight Comments
|
||||
if (stream.match(/%.*$/)) {
|
||||
return "comment";
|
||||
}
|
||||
// Highlight LilyPond commands (e.g., \relative, \time, \key)
|
||||
if (stream.match(/\\[a-zA-Z]+/)) {
|
||||
return "keyword";
|
||||
}
|
||||
// Move to the next character
|
||||
stream.next();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -7,7 +7,8 @@
|
||||
</h3>
|
||||
<div class="mx-12 text-left">
|
||||
|
||||
{% if tema.score() is not none %}
|
||||
{% if tema.main_score() is not none %}
|
||||
{% set score = tema.main_score() %}
|
||||
{% include "fragments/tema/score.html" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<div id="tema-score-{{ score.id }}">
|
||||
<h4 class="mp-4 text-xl text-beige">
|
||||
<i class="fa fa-code mr-2" aria-hidden="true"></i> Editor de partitura
|
||||
</h4>
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
<input name="title"
|
||||
id="score-{{ score.id }}-editor-title"
|
||||
placeholder="Nom de la partitura"
|
||||
value="{{ score.title }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded
|
||||
bg-brown px-2"
|
||||
/>
|
||||
<button title="Desa els canvis"
|
||||
class="mx-1"
|
||||
hx-put="/api/tema/{{ score.tema_id }}/score/{{ score.id }}"
|
||||
hx-vals="js:{title: document.getElementById('score-{{ score.id }}-editor-title').value, source: editor_{{ score.id }}.getValue()}"
|
||||
hx-target="#tema-score-{{ score.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Descarta els canvis"
|
||||
class="mx-1"
|
||||
hx-get="/api/tema/{{ score.tema_id }}/score/{{ score.id }}"
|
||||
hx-target="#tema-score-{{ score.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</h5>
|
||||
<h5 class="mp-4 text text-beige">
|
||||
Codi Lilypond
|
||||
<a href="https://lilypond.org/doc/v2.24/Documentation/learning/simple-notation"
|
||||
target="_blank">
|
||||
<i class="fa fa-info-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h5>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<form class="flex flex-col"
|
||||
hx-post="/api/tema/{{ score.tema_id }}/editor/score/{{ score.id }}/render"
|
||||
hx-target="#score-{{ score.id }}-render-target"
|
||||
hx-swap="outerHTML"
|
||||
onsubmit="document.getElementById('lilypond-editor-{{ score.id }}').value = editor_{{ score.id }}.getValue()"
|
||||
>
|
||||
{% if score.source %}
|
||||
<textarea id="lilypond-editor-{{ score.id }}"
|
||||
name="source">{{ score.source | safe }}</textarea>
|
||||
{% else %}
|
||||
<textarea id="lilypond-editor-{{ score.id }}"
|
||||
name="source">{% include "lilypond/score_template.ly" %}</textarea>
|
||||
{% endif %}
|
||||
<script>
|
||||
var editor_{{ score.id }} = CodeMirror.fromTextArea(
|
||||
document.getElementById("lilypond-editor-{{ score.id }}"), {
|
||||
mode: "lilypond",
|
||||
lineNumbers: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<button title="Previsualitza els canvis"
|
||||
type="submit"
|
||||
class="flex flex-col items-center">
|
||||
<div class="m-2 mt-3 p-1 text-beige
|
||||
border border-beige rounded">
|
||||
Previsualitza els canvis
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
<div id="score-{{ score.id }}-render-target">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div id="score-{{ score_id }}-render-target">
|
||||
<h5 class="text text-beige"> Previsualització </h5>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
{% if score_render_url %}
|
||||
<img class="my-2" src="{{ score_render_url }}" />
|
||||
{% else %}
|
||||
<ol class="text-red-400">
|
||||
{% for error in errors %}
|
||||
<li class="text-red-500">Línia {{ error.line }} posició {{ error.pos }}: {{ error.error }}</p>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -4,12 +4,13 @@
|
||||
{% include "fragments/tema/title.html" %}
|
||||
</div>
|
||||
<div class="p-12 text-left">
|
||||
<div id="tema-{{ tema.id }}-score"
|
||||
hx-get="/api/tema/{{ tema.id }}/score"
|
||||
hx-trigger="load, reload-tema-{{ tema.id }}-score from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
</div>
|
||||
<!-- <div id="tema-{{ tema.id }}-score" -->
|
||||
<!-- hx-get="/api/tema/{{ tema.id }}/score" -->
|
||||
<!-- hx-trigger="load, reload-tema-{{ tema.id }}-score from:body" -->
|
||||
<!-- hx-swap="innerHTML" -->
|
||||
<!-- > -->
|
||||
<!-- </div> -->
|
||||
{% include "fragments/tema/scores.html" %}
|
||||
{% include "fragments/tema/lyrics.html" %}
|
||||
{% include "fragments/tema/links.html" %}
|
||||
{% include "fragments/tema/properties.html" %}
|
||||
|
||||
@@ -1,15 +1,63 @@
|
||||
{% set score = tema.score() %}
|
||||
{% if score %}
|
||||
<h4 class="mp-4 text-xl text-beige">Partitura</h4>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
{% if score.link_type == LinkType.PDF %}
|
||||
{% set pdf_url = score.url %}
|
||||
{% include "fragments/pdf_viewer.html" %}
|
||||
{% elif score.link_type == LinkType.IMAGE %}
|
||||
<div class="flex justify-center">
|
||||
<a href="{{ score.url }}" target="_blank">
|
||||
<img class="m-2" src="{{ score.url }}" />
|
||||
</a>
|
||||
{% if logged_in or not score.hidden %}
|
||||
<div id="tema-score-{{ score.id }}">
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
{{ score.title }}
|
||||
{% if logged_in and score.id is not none %}
|
||||
{% if score.source == "" %}
|
||||
<i>(Partitura buida)</i>
|
||||
{% elif score.errors %}
|
||||
<i>(Partitura amb errors)</i>
|
||||
{% endif %}
|
||||
{% if score.hidden %}
|
||||
<i>(Privada)</i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if logged_in and score.id is not none %}
|
||||
{% if score.hidden %}
|
||||
<button title="Mostra la partitura"
|
||||
class="mx-1"
|
||||
hx-put="/api/tema/{{ score.tema_id }}/score/{{ score.id }}/show"
|
||||
hx-target="#tema-score-{{ score.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye-slash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button title="Amaga la partitura"
|
||||
class="mx-1"
|
||||
hx-put="/api/tema/{{ score.tema_id }}/score/{{ score.id }}/hide"
|
||||
hx-target="#tema-score-{{ score.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button title="Modifica la partitura"
|
||||
class="mx-1"
|
||||
hx-get="/api/tema/{{ score.tema_id }}/editor/score/{{ score.id }}"
|
||||
hx-target="#tema-score-{{ score.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Esborra la partitura"
|
||||
class="mx-1"
|
||||
hx-delete="/api/tema/{{ score.tema_id }}/score/{{ score.id }}"
|
||||
hx-target="#tema-score-{{ score.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</h5>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
<div>
|
||||
{% if score.img_url is not none %}
|
||||
<div class="flex justify-center">
|
||||
<a href="{{ score.pdf_url or score.img_url }}" target="_blank">
|
||||
<img class="m-2" src="{{ score.img_url }}" />
|
||||
</a>
|
||||
</div>
|
||||
{% elif score.pdf_url is not none %}
|
||||
{% set pdf_url = score.pdf_url %}
|
||||
{% include "fragments/pdf_viewer.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
31
folkugat_web/assets/templates/fragments/tema/scores.html
Normal file
31
folkugat_web/assets/templates/fragments/tema/scores.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% set main_score = tema.main_score() %}
|
||||
{% if logged_in or tema.scores or main_score %}
|
||||
<h4 class="pt-4 text-xl text-beige">Partitures</h4>
|
||||
{% endif %}
|
||||
|
||||
{% if main_score is not none %}
|
||||
{% set score = main_score %}
|
||||
{% include "fragments/tema/score.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if tema.scores %}
|
||||
{% for score in tema.scores %}
|
||||
{% if score.id != main_score.id %}
|
||||
{% include "fragments/tema/score.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if logged_in %}
|
||||
<div id="new-score-target"></div>
|
||||
<div class="flex flex-row my-2 justify-end">
|
||||
<button title="Afegeix una partitura"
|
||||
class="text-sm text-beige text-right"
|
||||
hx-post="/api/tema/{{ tema.id }}/score"
|
||||
hx-target="#new-score-target"
|
||||
hx-swap="beforebegin">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix una partitura
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -12,16 +12,5 @@
|
||||
hx-trigger="revealed, keyup delay:500ms changed"
|
||||
hx-target="#search-results"
|
||||
hx-swap="outerHTML">
|
||||
{% if logged_in %}
|
||||
<button title="Afegeix un tema"
|
||||
class="text-beige m-2"
|
||||
hx-post="/api/tema"
|
||||
hx-include="[name=query]"
|
||||
hx-vals="js:{title: document.getElementById('query-input').value}"
|
||||
>
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix un tema
|
||||
</button>
|
||||
{% endif %}
|
||||
<div id="search-results" class="flex-col"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<div id="search-results"
|
||||
class="min-w-full w-full">
|
||||
{% if logged_in and query %}
|
||||
<button title="Afegeix un tema"
|
||||
class="border border-beige text-beige rounded
|
||||
m-1 px-2"
|
||||
hx-post="/api/tema"
|
||||
hx-include="[name=query]"
|
||||
hx-vals="js:{title: document.getElementById('query-input').value}"
|
||||
>
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
<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>
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
<!-- HTMX -->
|
||||
<script src="{{ url_for(request, 'static', path='js/htmx.min.js') }}"></script>
|
||||
|
||||
<!-- CodeMirror -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
||||
|
||||
<script src="{{ url_for(request, 'static', path='js/lilypond.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for(request, 'static', path='css/lilypond.css') }}" type="text/css" />
|
||||
|
||||
|
||||
<!-- Favicon! -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for(request, 'static', path='favicon/apple-touch-icon.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for(request, 'static', path='favicon/favicon-32x32.png') }}">
|
||||
|
||||
75
folkugat_web/assets/templates/lilypond/score.ly
Normal file
75
folkugat_web/assets/templates/lilypond/score.ly
Normal file
@@ -0,0 +1,75 @@
|
||||
\new ChordNames {
|
||||
\chords {
|
||||
\set chordChanges = ##t
|
||||
\set noChordSymbol = ""
|
||||
|
||||
\partial 2
|
||||
r2
|
||||
|
||||
% A
|
||||
\repeat volta 2 {
|
||||
d1 g a:5 r4 a2.:aug | d1 g a
|
||||
\alternative {
|
||||
\volta 1 { a4 a2.:aug }
|
||||
\volta 2 { d4 d2.:7 }
|
||||
}
|
||||
}
|
||||
|
||||
% B
|
||||
g1 d/fs g2 a b:m c/a
|
||||
g1 d/fs g2 a d a/cs
|
||||
|
||||
% C
|
||||
b1:m d/fs a g2 a
|
||||
b1:m d/fs a2 g d a/cs
|
||||
b1:m d/fs a g2 a
|
||||
b1:m g a2 a:aug a1:aug
|
||||
|
||||
}
|
||||
}
|
||||
\new Staff {
|
||||
\relative {
|
||||
\numericTimeSignature
|
||||
\time 4/4
|
||||
\set Timing.beamExceptions = #'()
|
||||
\set Timing.baseMoment = #(ly:make-moment 1/4)
|
||||
\set Timing.beatStructure = 2,2
|
||||
|
||||
\key d \major
|
||||
|
||||
\partial 2
|
||||
a8 b d e
|
||||
|
||||
% A
|
||||
\mark \default
|
||||
|
||||
\repeat volta 2 {
|
||||
fs4. fs8 e fs e d | g, r r4 e'8 fs e d |
|
||||
a16 b8( b8.) c16 b a8 b d e | e d( d) a( a) b d e |
|
||||
fs4. fs8 e fs e d | g, r r4 e'8 fs e d |
|
||||
a b d e e d( d) e |
|
||||
\alternative {
|
||||
\volta 1 { d4 a( a8) b d e }
|
||||
\volta 2 { d4 fs( fs) e8 d }
|
||||
}
|
||||
}
|
||||
|
||||
% B
|
||||
\mark \default
|
||||
|
||||
b2 e8 fs e d | a2 e'8 fs e d | b4. b8 a b d e | e d( d) fs( fs) a e d |
|
||||
b2 e8 fs e d | a2 e'8 fs e d | a b d e e d( d) e | d4 d16 d d8 d fs a cs |
|
||||
|
||||
\bar "||" \break
|
||||
|
||||
% C
|
||||
\mark \default
|
||||
|
||||
d4. d8 cs d cs a | fs4 e8 d a' b a fs | e2 e8 fs e d | a b d e d fs a cs |
|
||||
d4. d8 cs d cs b | fs4 e8 d a' b a fs | e2 d4. e8 | d4 d16 d d8 d fs a cs |
|
||||
d2 cs4 b8 a | fs4 e8 d a b a fs | e'2 e8 fs e d | a b d e d fs a cs |
|
||||
d2 d8 cs a fs | b2 b8 a fs d | e-> r r fs-> d-> r r4 | r8 c' c c c b a g |
|
||||
|
||||
\bar "|."
|
||||
}
|
||||
}
|
||||
61
folkugat_web/assets/templates/lilypond/score_template.ly
Normal file
61
folkugat_web/assets/templates/lilypond/score_template.ly
Normal file
@@ -0,0 +1,61 @@
|
||||
% -----------------------------------------------------
|
||||
% Comandes últils
|
||||
% -----------------------------------------------------
|
||||
% Anacrusa: \partial 2 (2 = blanca)
|
||||
%
|
||||
% Repetició amb caselles:
|
||||
% \repeat volta 2 {
|
||||
% %%%
|
||||
% \alternative {
|
||||
% \volta 1 { %%% }
|
||||
% \volta 2 { %%% }
|
||||
% }
|
||||
% }
|
||||
%
|
||||
% -----------------------------------------------------
|
||||
|
||||
\new ChordNames {
|
||||
\chords {
|
||||
\set chordChanges = ##t
|
||||
\set noChordSymbol = ""
|
||||
% -----------------------------------------------------
|
||||
% Acords
|
||||
% -----------------------------------------------------
|
||||
|
||||
% A
|
||||
|
||||
% B
|
||||
|
||||
}
|
||||
}
|
||||
\new Staff {
|
||||
\relative {
|
||||
% -----------------------------------------------------
|
||||
% Compàs
|
||||
% -----------------------------------------------------
|
||||
\numericTimeSignature
|
||||
\time 4/4
|
||||
\set Timing.beamExceptions = #'()
|
||||
\set Timing.baseMoment = #(ly:make-moment 1/4)
|
||||
\set Timing.beatStructure = 2,2
|
||||
|
||||
% -----------------------------------------------------
|
||||
% Armadura
|
||||
% -----------------------------------------------------
|
||||
\key c \major
|
||||
|
||||
% -----------------------------------------------------
|
||||
% Melodia
|
||||
% -----------------------------------------------------
|
||||
|
||||
% A
|
||||
\mark \default
|
||||
|
||||
\bar "||" \break
|
||||
|
||||
% B
|
||||
\mark \default
|
||||
|
||||
\bar "|."
|
||||
}
|
||||
}
|
||||
27
folkugat_web/assets/templates/lilypond/single_score.ly
Normal file
27
folkugat_web/assets/templates/lilypond/single_score.ly
Normal file
@@ -0,0 +1,27 @@
|
||||
\paper {
|
||||
top-margin = 10
|
||||
left-margin = 15
|
||||
right-margin = 15
|
||||
}
|
||||
|
||||
\header {
|
||||
title = \markup { "{{ tema.title }}" }
|
||||
{% if tema.composer() %}
|
||||
composer = "{{ tema.composer() }}"
|
||||
{% elif tema.origin() %}
|
||||
composer = "{{ tema.origin() }}"
|
||||
{% endif %}
|
||||
|
||||
tagline = "Partitura generada amb LilyPond"
|
||||
copyright = "Folkugat"
|
||||
}
|
||||
|
||||
\markup \vspace #2
|
||||
|
||||
\score {
|
||||
\language "english"
|
||||
<<
|
||||
{{ score_beginning }}
|
||||
{{ score_source | safe }}
|
||||
>>
|
||||
}
|
||||
@@ -7,6 +7,7 @@ def create_db(con: Connection | None = None):
|
||||
create_links_table(con)
|
||||
create_lyrics_table(con)
|
||||
create_properties_table(con)
|
||||
create_scores_table(con)
|
||||
|
||||
|
||||
def create_temes_table(con: Connection):
|
||||
@@ -66,3 +67,21 @@ def create_properties_table(con: Connection):
|
||||
"""
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
|
||||
|
||||
def create_scores_table(con: Connection):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS tema_scores (
|
||||
id INTEGER PRIMARY KEY,
|
||||
tema_id INTEGER,
|
||||
title TEXT,
|
||||
source TEXT NOT NULL,
|
||||
errors TEXT NOT NULL,
|
||||
img_url TEXT,
|
||||
pdf_url TEXT,
|
||||
hidden BOOLEAN,
|
||||
FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
|
||||
133
folkugat_web/dal/sql/temes/scores.py
Normal file
133
folkugat_web/dal/sql/temes/scores.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from typing import TypedDict
|
||||
|
||||
from folkugat_web.dal.sql import Connection, get_connection
|
||||
from folkugat_web.model import lilypond as lilypond_model
|
||||
from folkugat_web.model import temes as model
|
||||
|
||||
ScoreRowTuple = tuple[int, int, str, str, str, str | None, str | None, bool]
|
||||
|
||||
|
||||
class ScoreRowDict(TypedDict):
|
||||
id: int | None
|
||||
tema_id: int
|
||||
title: str
|
||||
errors: str
|
||||
source: str
|
||||
img_url: str | None
|
||||
pdf_url: str | None
|
||||
hidden: bool
|
||||
|
||||
|
||||
def score_to_row(score: model.Score) -> ScoreRowDict:
|
||||
return {
|
||||
"id": score.id,
|
||||
"tema_id": score.tema_id,
|
||||
"title": score.title,
|
||||
"errors": json.dumps(list(error.to_dict() for error in score.errors)),
|
||||
"source": score.source,
|
||||
"img_url": score.img_url,
|
||||
"pdf_url": score.pdf_url,
|
||||
"hidden": score.hidden,
|
||||
}
|
||||
|
||||
|
||||
def row_to_score(row: ScoreRowTuple) -> model.Score:
|
||||
errors_dicts: list[dict[str, str]] = json.loads(row[4])
|
||||
return model.Score(
|
||||
id=row[0],
|
||||
tema_id=row[1],
|
||||
title=row[2],
|
||||
source=row[3],
|
||||
errors=list(map(lilypond_model.RenderError.from_dict, errors_dicts)),
|
||||
img_url=row[5],
|
||||
pdf_url=row[6],
|
||||
hidden=bool(row[7]),
|
||||
)
|
||||
|
||||
|
||||
class QueryData(TypedDict, total=False):
|
||||
id: int
|
||||
tema_id: int
|
||||
|
||||
|
||||
def _filter_clause(
|
||||
score_id: int | None,
|
||||
tema_id: int | None,
|
||||
) -> tuple[str, QueryData]:
|
||||
filter_clauses: list[str] = []
|
||||
filter_data: QueryData = {}
|
||||
|
||||
if score_id is not None:
|
||||
filter_clauses.append("id = :id")
|
||||
filter_data["id"] = score_id
|
||||
|
||||
if tema_id is not None:
|
||||
filter_clauses.append("tema_id = :tema_id")
|
||||
filter_data["tema_id"] = tema_id
|
||||
|
||||
filter_clause = " AND ".join(filter_clauses)
|
||||
return filter_clause, filter_data
|
||||
|
||||
|
||||
def get_scores(score_id: int | None = None, tema_id: int | None = None, con: Connection | None = None) -> Iterable[model.Score]:
|
||||
filter_clause, data = _filter_clause(score_id=score_id, tema_id=tema_id)
|
||||
if filter_clause:
|
||||
filter_clause = f"WHERE {filter_clause}"
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
id, tema_id, title, source, errors, img_url, pdf_url, hidden
|
||||
FROM tema_scores
|
||||
{filter_clause}
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return map(row_to_score, cur.fetchall())
|
||||
|
||||
|
||||
def insert_score(score: model.Score, con: Connection | None = None) -> model.Score:
|
||||
data = score_to_row(score)
|
||||
query = f"""
|
||||
INSERT INTO tema_scores
|
||||
(id, tema_id, title, source, errors, img_url, pdf_url, hidden)
|
||||
VALUES
|
||||
(:id, :tema_id, :title, :source, :errors, :img_url, :pdf_url, :hidden)
|
||||
RETURNING *
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
row: ScoreRowTuple = cur.fetchone()
|
||||
return row_to_score(row)
|
||||
|
||||
|
||||
def update_score(score: model.Score, con: Connection | None = None):
|
||||
data = score_to_row(score)
|
||||
query = """
|
||||
UPDATE tema_scores
|
||||
SET
|
||||
tema_id = :tema_id, title = :title, source = :source, errors = :errors,
|
||||
img_url = :img_url, pdf_url = :pdf_url, hidden = :hidden
|
||||
WHERE
|
||||
id = :id
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
|
||||
|
||||
def delete_score(score_id: int, tema_id: int | None = None, con: Connection | None = None):
|
||||
filter_clause, data = _filter_clause(score_id=score_id, tema_id=tema_id)
|
||||
if filter_clause:
|
||||
filter_clause = f"WHERE {filter_clause}"
|
||||
|
||||
query = f"""
|
||||
DELETE FROM tema_scores
|
||||
{filter_clause}
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
36
folkugat_web/fragments/tema/scores.py
Normal file
36
folkugat_web/fragments/tema/scores.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
|
||||
def score(request: Request, logged_in: bool, score: model.Score):
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/score.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"score": score,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# def score_code(request: Request, logged_in: bool, tema: model.Tema):
|
||||
# return templates.TemplateResponse(
|
||||
# "fragments/tema/score.html",
|
||||
# {
|
||||
# "request": request,
|
||||
# "logged_in": logged_in,
|
||||
# "tema": tema,
|
||||
# }
|
||||
# )
|
||||
|
||||
|
||||
def score_editor(request: Request, logged_in: bool, score: model.Score):
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/editor/score.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"score": score,
|
||||
}
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.model.lilypond 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
|
||||
@@ -72,3 +73,20 @@ def tema(request: Request, logged_in: bool, tema: model.Tema):
|
||||
"date_names": sessions_service.get_date_names,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def score_render(
|
||||
request: Request,
|
||||
score_id: int,
|
||||
score_render_url: str = "",
|
||||
errors: list[RenderError] | None = None,
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/editor/score_render.html",
|
||||
{
|
||||
"request": request,
|
||||
"score_id": score_id,
|
||||
"score_render_url": score_render_url,
|
||||
"errors": errors,
|
||||
}
|
||||
)
|
||||
|
||||
24
folkugat_web/model/lilypond.py
Normal file
24
folkugat_web/model/lilypond.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import dataclasses
|
||||
from typing import Self
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RenderError:
|
||||
line: int
|
||||
pos: int
|
||||
error: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, error_match: dict[str, str]) -> Self:
|
||||
return cls(
|
||||
line=int(error_match["line"]),
|
||||
pos=int(error_match["pos"]),
|
||||
error=error_match["error"],
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
return dict(
|
||||
line=str(self.line),
|
||||
pos=str(self.pos),
|
||||
error=self.error,
|
||||
)
|
||||
@@ -1,7 +1,10 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
import enum
|
||||
import itertools
|
||||
from typing import Self
|
||||
|
||||
from folkugat_web.model.lilypond import RenderError
|
||||
from folkugat_web.model.search import NGrams
|
||||
from folkugat_web.model.sessions import Session
|
||||
from folkugat_web.services import ngrams
|
||||
@@ -55,6 +58,33 @@ class Lyrics:
|
||||
content: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Score:
|
||||
id: int | None
|
||||
tema_id: int
|
||||
title: str
|
||||
errors: list[RenderError]
|
||||
source: str
|
||||
img_url: str | None
|
||||
pdf_url: str | None
|
||||
hidden: bool
|
||||
|
||||
@classmethod
|
||||
def from_link(cls, link: Link) -> Self:
|
||||
if link.link_type not in {LinkType.IMAGE, LinkType.PDF}:
|
||||
raise ValueError("Link not supported to build a score!")
|
||||
return cls(
|
||||
id=None,
|
||||
tema_id=link.tema_id,
|
||||
title=link.title,
|
||||
source="",
|
||||
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,
|
||||
hidden=False,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Stats:
|
||||
times_played: int
|
||||
@@ -69,6 +99,7 @@ class Tema:
|
||||
properties: list[Property] = dataclasses.field(default_factory=list)
|
||||
links: list[Link] = dataclasses.field(default_factory=list)
|
||||
lyrics: list[Lyrics] = dataclasses.field(default_factory=list)
|
||||
scores: list[Score] = dataclasses.field(default_factory=list)
|
||||
# Search related
|
||||
alternatives: list[str] = dataclasses.field(default_factory=list)
|
||||
hidden: bool = True
|
||||
@@ -89,9 +120,18 @@ class Tema:
|
||||
return link.url.startswith("/")
|
||||
return link.link_type is LinkType.IMAGE
|
||||
|
||||
def score(self) -> Link | None:
|
||||
result = next(filter(self._is_score, self.links), None)
|
||||
return result
|
||||
def main_score(self) -> Score | None:
|
||||
candidate_scores = filter(lambda s: s.hidden is False, self.scores)
|
||||
candidate_links = map(Score.from_link, filter(self._is_score, self.links))
|
||||
return next(itertools.chain(candidate_scores, candidate_links), None)
|
||||
|
||||
def composer(self) -> str | None:
|
||||
result = next(filter(lambda prop: prop.field is PropertyField.AUTOR, self.properties), None)
|
||||
return result.value if result else None
|
||||
|
||||
def origin(self) -> str | None:
|
||||
result = next(filter(lambda prop: prop.field is PropertyField.ORIGEN, self.properties), None)
|
||||
return result.value if result else None
|
||||
|
||||
|
||||
class TemaCols(enum.Enum):
|
||||
|
||||
@@ -9,6 +9,7 @@ import magic
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from folkugat_web.config import db
|
||||
from folkugat_web.dal.sql.temes import links as links_dal
|
||||
from folkugat_web.dal.sql.temes import scores as scores_dal
|
||||
from folkugat_web.log import logger
|
||||
|
||||
|
||||
@@ -46,10 +47,7 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
|
||||
check_mimetype(mimetype)
|
||||
|
||||
extension = mimetypes.guess_extension(mimetype) or ""
|
||||
filename = str(uuid.uuid4().hex) + extension
|
||||
filedir = db.DB_FILES_DIR / str(tema_id)
|
||||
filedir.mkdir(exist_ok=True)
|
||||
filepath = filedir / filename
|
||||
filepath = create_tema_filename(tema_id=tema_id, extension=extension)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
_ = f.write(await upload_file.read())
|
||||
@@ -57,15 +55,34 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
|
||||
return get_db_file_path(filepath)
|
||||
|
||||
|
||||
def create_tema_filename(tema_id: int, extension: str = "") -> Path:
|
||||
filename = str(uuid.uuid4().hex) + extension
|
||||
filedir = db.DB_FILES_DIR / str(tema_id)
|
||||
filedir.mkdir(exist_ok=True)
|
||||
filepath = filedir / filename
|
||||
return filepath
|
||||
|
||||
|
||||
def create_tmp_filename(extension: str = "") -> Path:
|
||||
filename = str(uuid.uuid4().hex) + extension
|
||||
filedir = db.DB_FILES_DIR / "tmp"
|
||||
filedir.mkdir(exist_ok=True)
|
||||
filepath = filedir / filename
|
||||
return filepath
|
||||
|
||||
|
||||
def list_files(tema_id: str) -> list[str]:
|
||||
filedir = db.DB_FILES_DIR / str(tema_id)
|
||||
return [get_db_file_path(f) for f in filedir.iterdir()]
|
||||
|
||||
|
||||
def get_orphan_files() -> Iterator[Path]:
|
||||
alive_files = {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_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
|
||||
return filter(
|
||||
lambda p: p.is_file() and get_db_file_path(p) not in alive_files,
|
||||
lambda p: p.is_file() and get_db_file_path(p) not in alive_urls,
|
||||
db.DB_FILES_DIR.rglob("*"),
|
||||
)
|
||||
|
||||
|
||||
70
folkugat_web/services/lilypond.py
Normal file
70
folkugat_web/services/lilypond.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import aiofiles
|
||||
from folkugat_web.model.lilypond import RenderError
|
||||
from folkugat_web.model.temes import Tema
|
||||
from folkugat_web.services import files as files_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
SCORE_BEGINNING = "% --- SCORE BEGINNING --- %"
|
||||
|
||||
RenderFormat = Literal["png"] | Literal["pdf"]
|
||||
|
||||
|
||||
def source_to_single_score(source: str, tema: Tema) -> str:
|
||||
return templates.get_template("lilypond/single_score.ly").render(
|
||||
score_beginning=SCORE_BEGINNING,
|
||||
score_source=source,
|
||||
tema=tema,
|
||||
)
|
||||
|
||||
|
||||
async def render(source: str, fmt: RenderFormat, output_filename: Path | None = None) -> tuple[Path | None, list[RenderError]]:
|
||||
input_filename = files_service.create_tmp_filename(extension=".ly")
|
||||
async with aiofiles.open(input_filename, "w") as f:
|
||||
_ = await f.write(source)
|
||||
|
||||
output_filename = output_filename or files_service.create_tmp_filename()
|
||||
|
||||
if fmt == "png":
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"lilypond", "--png", "-o", output_filename, input_filename,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
else:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"lilypond", "-o", output_filename, input_filename,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
_stdout, stderr = await proc.communicate()
|
||||
|
||||
if proc.returncode:
|
||||
esc_input_filename = re.escape(str(input_filename))
|
||||
error_re = re.compile(rf"{esc_input_filename}:(?P<line>\d+):(?P<pos>\d+): error: (?P<error>.*)$")
|
||||
error_matches = filter(None, map(error_re.match, stderr.decode("utf-8").splitlines()))
|
||||
errors = map(lambda m: RenderError.from_dict(m.groupdict()), error_matches)
|
||||
line_offset = source.splitlines().index(SCORE_BEGINNING) + 1
|
||||
errors_offset = [dataclasses.replace(e, line=e.line - line_offset) for e in errors if e.line > line_offset]
|
||||
return None, errors_offset
|
||||
|
||||
if fmt == "png":
|
||||
# Check if the output generated multiple pages
|
||||
pages = sorted(output_filename.parent.rglob(f"{output_filename.name}*.png"))
|
||||
if len(pages) > 1:
|
||||
output_filename = pages[0]
|
||||
for page in pages[1:]:
|
||||
os.remove(page)
|
||||
else:
|
||||
output_filename = output_filename.with_suffix(".png")
|
||||
elif fmt == "pdf":
|
||||
output_filename = output_filename.with_suffix(".pdf")
|
||||
|
||||
return output_filename, []
|
||||
@@ -5,6 +5,7 @@ from folkugat_web.log import logger
|
||||
from folkugat_web.model import playlists
|
||||
from folkugat_web.services.temes import links as links_service
|
||||
from folkugat_web.services.temes import query as temes_query
|
||||
from folkugat_web.services.temes import scores as scores_service
|
||||
|
||||
|
||||
def add_temes_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist:
|
||||
@@ -26,6 +27,7 @@ def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaI
|
||||
logger.error("fCould not load tune in set: {tema_in_set}")
|
||||
else:
|
||||
_ = links_service.add_links_to_tema(tema_in_set.tema)
|
||||
_ = scores_service.add_scores_to_tema(tema_in_set.tema)
|
||||
return tema_in_set
|
||||
|
||||
|
||||
|
||||
56
folkugat_web/services/temes/scores.py
Normal file
56
folkugat_web/services/temes/scores.py
Normal file
@@ -0,0 +1,56 @@
|
||||
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.services import lilypond
|
||||
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
|
||||
|
||||
|
||||
def add_scores_to_tema(tema: model.Tema) -> model.Tema:
|
||||
if tema.id is not None:
|
||||
tema.scores = list(scores_dal.get_scores(tema_id=tema.id))
|
||||
return tema
|
||||
|
||||
|
||||
def add_scores_to_temes(temes: Iterable[model.Tema]) -> Iterator[model.Tema]:
|
||||
return map(add_scores_to_tema, temes)
|
||||
|
||||
|
||||
def get_score_by_id(score_id: int, tema_id: int | None = None) -> model.Score | None:
|
||||
return next(iter(scores_dal.get_scores(score_id=score_id, tema_id=tema_id)), None)
|
||||
|
||||
|
||||
def update_score(score: model.Score):
|
||||
scores_dal.update_score(score=score)
|
||||
|
||||
|
||||
def delete_score(score_id: int, tema_id: int | None = None):
|
||||
scores_dal.delete_score(score_id=score_id, tema_id=tema_id)
|
||||
|
||||
|
||||
def create_score(tema_id: int) -> model.Score:
|
||||
new_score = model.Score(
|
||||
id=None,
|
||||
tema_id=tema_id,
|
||||
title="",
|
||||
source="",
|
||||
errors=[],
|
||||
img_url=None,
|
||||
pdf_url=None,
|
||||
hidden=True,
|
||||
)
|
||||
return scores_dal.insert_score(score=new_score)
|
||||
|
||||
|
||||
def build_single_tune_full_source(tema_id: int, source: str) -> str:
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
if not tema:
|
||||
raise HTTPException(status_code=404, detail="Could not find tema!")
|
||||
tema = (
|
||||
FnChain.transform(tema) |
|
||||
properties_service.add_properties_to_tema
|
||||
).result()
|
||||
return lilypond.source_to_single_score(source=source, tema=tema)
|
||||
Reference in New Issue
Block a user