Added indexed lists and link edition in tunes

This commit is contained in:
marc
2025-03-11 23:05:20 +01:00
parent efd26ce19d
commit a85efd0838
22 changed files with 498 additions and 137 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
.direnv
**/**.pyc **/**.pyc

View File

@@ -13,11 +13,31 @@ def title_editor(
return tema.title_editor(request=request, logged_in=logged_in, tema_id=tema_id) return tema.title_editor(request=request, logged_in=logged_in, tema_id=tema_id)
@router.get("/api/tema/{tema_id}/editor/lyric/{lyric_idx}") @router.get("/api/tema/{tema_id}/editor/lyric/{lyric_id}")
def lyric_editor( def lyric_editor(
request: Request, request: Request,
logged_in: auth.RequireLogin, logged_in: auth.RequireLogin,
tema_id: int, tema_id: int,
lyric_idx: int, lyric_id: int,
): ):
return tema.lyric_editor(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx) return tema.lyric_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
lyric_id=lyric_id,
)
@router.get("/api/tema/{tema_id}/editor/link/{link_id}")
def link_editor(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
):
return tema.link_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
link_id=link_id,
)

View File

@@ -1,13 +1,14 @@
from typing import Annotated from typing import Annotated
from fastapi import Request from fastapi import Request
from fastapi.params import Form from fastapi.params import Form, Param
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from folkugat_web.api import router from folkugat_web.api import router
from folkugat_web.fragments import tema, temes from folkugat_web.fragments import tema, temes
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.services.temes import write as temes_w from folkugat_web.services.temes import write as temes_w
from folkugat_web.services.temes.links import guess_link_type
from folkugat_web.templates import templates from folkugat_web.templates import templates
@@ -45,23 +46,23 @@ def set_title(
return tema.title(request=request, tema=new_tema, logged_in=logged_in) return tema.title(request=request, tema=new_tema, logged_in=logged_in)
@router.get("/api/tema/{tema_id}/lyric/{lyric_idx}") @router.get("/api/tema/{tema_id}/lyric/{lyric_id}")
def lyric(request: Request, logged_in: auth.LoggedIn, tema_id: int, lyric_idx: int): def lyric(request: Request, logged_in: auth.LoggedIn, tema_id: int, lyric_id: int):
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx) return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_id=lyric_id)
@router.put("/api/tema/{tema_id}/lyric/{lyric_idx}") @router.put("/api/tema/{tema_id}/lyric/{lyric_id}")
def set_lyric( def set_lyric(
request: Request, request: Request,
logged_in: auth.RequireLogin, logged_in: auth.RequireLogin,
tema_id: int, tema_id: int,
lyric_idx: int, lyric_id: int,
title: Annotated[str, Form()], title: Annotated[str, Form()],
lyric: Annotated[str, Form()], lyric: Annotated[str, Form()],
): ):
new_lyric = model.Lyrics(title=title, content=lyric.strip()) new_lyric = model.Lyrics(id=lyric_id, title=title, content=lyric.strip())
temes_w.update_lyric(tema_id=tema_id, lyric_idx=lyric_idx, lyric=new_lyric) temes_w.update_lyric(tema_id=tema_id, lyric_id=lyric_id, lyric=new_lyric)
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx) return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_id=lyric_id)
@router.post("/api/tema/{tema_id}/lyric") @router.post("/api/tema/{tema_id}/lyric")
@@ -71,14 +72,95 @@ def add_lyric(
tema_id: int, tema_id: int,
): ):
new_tema = temes_w.add_lyric(tema_id=tema_id) new_tema = temes_w.add_lyric(tema_id=tema_id)
lyric_idx = len(new_tema.lyrics) - 1 lyric_id = new_tema.lyrics[-1].id
return tema.lyric_editor(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx) if lyric_id is None:
raise RuntimeError("Invalid lyric_id on newly created lyric!")
return tema.lyric_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
lyric_id=lyric_id,
)
@router.delete("/api/tema/{tema_id}/lyric/{lyric_idx}") @router.delete("/api/tema/{tema_id}/lyric/{lyric_id}")
def delete_lyric( def delete_lyric(
tema_id: int, tema_id: int,
lyric_idx: int, lyric_id: int,
): ):
temes_w.delete_lyric(tema_id=tema_id, lyric_idx=lyric_idx) temes_w.delete_lyric(tema_id=tema_id, lyric_id=lyric_id)
return HTMLResponse() return HTMLResponse()
@router.get("/api/tema/{tema_id}/link/{link_id}")
def link(request: Request, logged_in: auth.LoggedIn, tema_id: int, link_id: int):
return tema.link(request=request, logged_in=logged_in, tema_id=tema_id, link_id=link_id)
@router.put("/api/tema/{tema_id}/link/{link_id}")
def set_link(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
content_type: Annotated[model.ContentType, Form()],
url: Annotated[str, Form()] = "",
title: Annotated[str, Form()] = "",
):
link_type = guess_link_type(url)
new_link = model.Link(
id=link_id,
content_type=content_type,
link_type=link_type,
url=url,
title=title,
)
temes_w.update_link(tema_id=tema_id, link_id=link_id, link=new_link)
return tema.link(request=request, logged_in=logged_in, tema_id=tema_id, link_id=link_id)
@router.post("/api/tema/{tema_id}/link")
def add_link(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
):
new_tema = temes_w.add_link(tema_id=tema_id)
link_id = new_tema.links[-1].id
if link_id is None:
raise RuntimeError("Invalid link_id on newly created link!")
return tema.link_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
link_id=link_id,
)
@router.delete("/api/tema/{tema_id}/link/{link_id}")
def delete_link(
_: auth.RequireLogin,
tema_id: int,
link_id: int,
):
temes_w.delete_link(tema_id=tema_id, link_id=link_id)
return HTMLResponse()
@router.get("/api/tema/{tema_id}/link/{link_id}/icon")
def link_icon(
request: Request,
logged_in: auth.LoggedIn,
tema_id: int,
link_id: int,
content_type: Annotated[model.ContentType, Param()],
url: Annotated[str, Param()],
):
return tema.link_icon(
request=request,
logged_in=logged_in,
tema_id=tema_id,
link_id=link_id,
url=url,
content_type=content_type,
)

View File

@@ -0,0 +1,66 @@
<li id="tema-link-{{ link.id }}">
<form class="flex flex-row items-start">
{% include "fragments/tema/link_icon.html" %}
<div class="grow my-2">
<p class="text-sm text-beige">
<select id="tema-link-{{ link.id }}-content-type"
name="content_type"
value="{{ link.content_type.value.capitalize() }}"
class="border border-yellow-50 focus:outline-none
rounded
text-yellow-50 text-center
bg-brown p-0 m-0">
<option value="{{ link.content_type.value }}">
{{ link.content_type.value.capitalize() }}
</option>
{% for ct in ContentType %}
{% if ct != link.content_type %}
<option value="{{ ct.value }}">
{{ ct.value.capitalize() }}
</option>
{% endif %}
{% endfor %}
</select>
</p>
<p>
<input name="title"
placeholder="Títol de l'enllaç"
value="{{ link.title }}"
class="border border-beige focus:outline-none
rounded w-full
bg-brown p-1 my-1"
/>
</p>
<p>
<input name="url"
placeholder="URL de l'enllaç"
value="{{ link.url }}"
class="border border-beige focus:outline-none
rounded w-full
bg-brown p-1 my-1"
hx-get="/api/tema/{{ tema.id }}/link/{{ link_id }}/icon"
hx-trigger="keyup delay:500ms changed"
hx-target="#link-icon-{{ link.id }}"
hx-include="[name='content_type']"
hx-swap="outerHTML"
/>
</p>
</div>
<div class="m-2 text-sm text-beige">
<button title="Desa els canvis"
class="mx-1"
hx-put="/api/tema/{{ tema.id }}/link/{{ link.id }}"
hx-target="#tema-link-{{ link.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/{{ tema.id }}/link/{{ link.id }}"
hx-target="#tema-link-{{ link.id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
</form>
</li>

View File

@@ -1,4 +1,4 @@
<form id="tema-lyric-{{ lyric_idx }}"> <form id="tema-lyric-{{ lyric.id }}">
<h5 class="text-sm text-beige text-right"> <h5 class="text-sm text-beige text-right">
<input name="title" <input name="title"
placeholder="Nom de la lletra" placeholder="Nom de la lletra"
@@ -9,15 +9,15 @@
/> />
<button title="Desa els canvis" <button title="Desa els canvis"
class="mx-1" class="mx-1"
hx-put="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}" hx-put="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric_idx }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-check" aria-hidden="true"></i> <i class="fa fa-check" aria-hidden="true"></i>
</button> </button>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="mx-1" class="mx-1"
hx-get="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}" hx-get="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric_idx }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>
</button> </button>

View File

@@ -1,21 +1,29 @@
<li class="flex flex-row items-start"> <li id="tema-link-{{ link.id }}"
<div class="p-2 m-2 text-beige border border-beige rounded-md"> class="flex flex-row items-start">
<a href="{{ link.url }}" target="_blank"> {% include "fragments/tema/link_icon.html" %}
{% if link.subtype == LinkSubtype.SPOTIFY %}
{% include "icons/spotify.svg" %}
{% elif link.subtype == LinkSubtype.YOUTUBE %}
{% include "icons/youtube.svg" %}
{% elif link.subtype == LinkSubtype.PDF %}
{% include "icons/pdf.svg" %}
{% elif link.type == LinkType.AUDIO %}
{% include "icons/notes.svg" %}
{% else %}
{% include "icons/link.svg" %}
{% endif %}
</a>
</div>
<div class="my-2"> <div class="my-2">
<p class="text-sm text-beige">Partitura</p> <p class="text-sm text-beige">
<p>Hola</p> {{ link.content_type.value.capitalize() }}
</p>
{% if link.title %}
<p>{{ link.title }}</p>
{% endif %}
</div> </div>
{% if logged_in %}
<div class="grow"></div>
<div class="m-2 text-sm text-beige">
<button title="Modifica l'enllaç"
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link.id }}"
hx-target="#tema-link-{{ link.id }}"
hx-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>
<button title="Esborra l'enllaç"
hx-delete="/api/tema/{{ tema.id }}/link/{{ link.id }}"
hx-target="#tema-link-{{ link.id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
{% endif %}
</li> </li>

View File

@@ -0,0 +1,16 @@
<div id="link-icon-{{ link.id }}"
class="p-2 m-2 text-beige border border-beige rounded-md">
<a href="{{ link.url }}" target="_blank">
{% if link.link_type == LinkType.SPOTIFY %}
{% include "icons/spotify.svg" %}
{% elif link.link_type == LinkType.YOUTUBE %}
{% include "icons/youtube.svg" %}
{% elif link.link_type == LinkType.PDF %}
{% include "icons/pdf.svg" %}
{% elif link.content_type == ContentType.AUDIO %}
{% include "icons/notes.svg" %}
{% else %}
{% include "icons/link.svg" %}
{% endif %}
</a>
</div>

View File

@@ -4,13 +4,12 @@
{% endif %} {% endif %}
{% if tema.links %} {% if tema.links %}
{% for link in tema.links %}
<ul class="flex flex-col justify-center" <ul class="flex flex-col justify-center"
id="new-link-target"> id="new-link-target">
{% set link_idx = loop.index0 %} {% for link in tema.links %}
{% include "fragments/tema/link.html" %} {% include "fragments/tema/link.html" %}
</ul>
{% endfor %} {% endfor %}
</ul>
{% endif %} {% endif %}
{% if logged_in %} {% if logged_in %}
@@ -19,7 +18,7 @@
class="text-sm text-beige text-right" class="text-sm text-beige text-right"
hx-post="/api/tema/{{ tema.id }}/link" hx-post="/api/tema/{{ tema.id }}/link"
hx-target="#new-link-target" hx-target="#new-link-target"
hx-swap="beforebegin"> hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i> <i class="fa fa-plus" aria-hidden="true"></i>
Afegeix un enllaç Afegeix un enllaç
</button> </button>

View File

@@ -1,18 +1,18 @@
<div id="tema-lyric-{{ lyric_idx }}"> <div id="tema-lyric-{{ lyric.id }}">
<h5 class="text-sm text-beige text-right"> <h5 class="text-sm text-beige text-right">
{{ lyric.title }} {{ lyric.title }}
{% if logged_in %} {% if logged_in %}
<button title="Modifica la lletra" <button title="Modifica la lletra"
class="mx-1" class="mx-1"
hx-get="/api/tema/{{ tema.id }}/editor/lyric/{{ lyric_idx }}" hx-get="/api/tema/{{ tema.id }}/editor/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric_idx }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i> <i class="fa fa-pencil" aria-hidden="true"></i>
</button> </button>
<button title="Esborra la lletra" <button title="Esborra la lletra"
class="mx-1" class="mx-1"
hx-delete="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}" hx-delete="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric_idx }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>
</button> </button>

View File

@@ -4,7 +4,6 @@
{% if tema.lyrics %} {% if tema.lyrics %}
{% for lyric in tema.lyrics %} {% for lyric in tema.lyrics %}
{% set lyric_idx = loop.index0 %}
{% include "fragments/tema/lyric.html" %} {% include "fragments/tema/lyric.html" %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@@ -4,18 +4,6 @@
{% include "fragments/tema/title.html" %} {% include "fragments/tema/title.html" %}
<div class="text-left"> <div class="text-left">
<!-- {% if tema.scores() %} -->
<!-- temes -->
<!-- <h4 class="text-xl text-beige">Tema</h4> -->
<!-- <hr class="h-px mt-1 mb-3 bg-beige border-0"> -->
<!-- {% for file in tema.files %} -->
<!-- {% if file.type == FileType.PDF %} -->
<!-- {% set pdf_url = file.path %} -->
<!-- {% include "fragments/pdf_viewer.html" %} -->
<!-- {% endif %} -->
<!-- {% endfor %} -->
<!-- {% endif %} -->
{% include "fragments/tema/lyrics.html" %} {% include "fragments/tema/lyrics.html" %}
{% include "fragments/tema/links.html" %} {% include "fragments/tema/links.html" %}

View File

@@ -13,16 +13,16 @@
{% for link in tema.links %} {% for link in tema.links %}
<li class="p-2 m-2 text-beige border border-beige rounded-md"> <li class="p-2 m-2 text-beige border border-beige rounded-md">
<a href="{{ link.url }}" target="_blank"> <a href="{{ link.url }}" target="_blank">
{% if link.type == LinkType.AUDIO %} {% if link.content_type == ContentType.AUDIO %}
{% if link.subtype == LinkSubtype.SPOTIFY %} {% if link.link_type == LinkType.SPOTIFY %}
{% include "icons/spotify.svg" %} {% include "icons/spotify.svg" %}
{% elif link.subtype == LinkSubtype.YOUTUBE %} {% elif link.link_type == LinkType.YOUTUBE %}
{% include "icons/youtube.svg" %} {% include "icons/youtube.svg" %}
{% else %} {% else %}
{% include "icons/notes.svg" %} {% include "icons/notes.svg" %}
{% endif %} {% endif %}
{% elif link.type == LinkType.SCORE %} {% elif link.content_type == ContentType.PARTITURA %}
{% if link.subtype == LinkSubtype.PDF %} {% if link.link_type == LinkType.PDF %}
{% include "icons/pdf.svg" %} {% include "icons/pdf.svg" %}
{% else %} {% else %}
{% include "icons/link.svg" %} {% include "icons/link.svg" %}

View File

@@ -1,6 +1,7 @@
import datetime import datetime
import json import json
from folkugat_web.model import IndexedList
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
@@ -27,9 +28,9 @@ def row_to_tema(row: tuple) -> model.Tema:
return model.Tema( return model.Tema(
id=row[0], id=row[0],
title=row[1], title=row[1],
properties=list(map(model.Property.from_dict, json.loads(row[2]))), properties=IndexedList(map(model.Property.from_dict, json.loads(row[2]))),
links=list(map(model.Link.from_dict, json.loads(row[3]))), links=IndexedList(map(model.Link.from_dict, json.loads(row[3]))),
lyrics=list(map(model.Lyrics.from_dict, json.loads(row[4]))), lyrics=IndexedList(map(model.Lyrics.from_dict, json.loads(row[4]))),
alternatives=json.loads(row[5]), alternatives=json.loads(row[5]),
ngrams=cell_to_ngrams(row[6]), ngrams=cell_to_ngrams(row[6]),
modification_date=datetime.datetime.fromisoformat(row[7]), modification_date=datetime.datetime.fromisoformat(row[7]),

View File

@@ -1,3 +1,4 @@
from folkugat_web.model import IndexedList
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
TEMES = [ TEMES = [
@@ -9,26 +10,29 @@ TEMES = [
# --- # ---
model.Tema( model.Tema(
title="Pasdoble de Muntanya (Cançó amb el nom molt llarg)", title="Pasdoble de Muntanya (Cançó amb el nom molt llarg)",
links=[ links=IndexedList([
model.Link( model.Link(
type=model.LinkType.AUDIO, id=0,
subtype=model.LinkSubtype.SPOTIFY, content_type=model.ContentType.AUDIO,
link_type=model.LinkType.SPOTIFY,
url="https://open.spotify.com/track/4j9Krf19c5USmMvVUCoeWa?si=3023d1d83f814886", url="https://open.spotify.com/track/4j9Krf19c5USmMvVUCoeWa?si=3023d1d83f814886",
title="Versió de l'Orquestrina Trama",
), ),
], ]),
hidden=False, hidden=False,
).with_ngrams(), ).with_ngrams(),
# --- # ---
model.Tema( model.Tema(
title="Astrid Waltz", title="Astrid Waltz",
alternatives=["vals"], alternatives=["vals"],
links=[ links=IndexedList([
model.Link( model.Link(
type=model.LinkType.OTHER, id=0,
subtype=None, content_type=model.ContentType.OTHER,
link_type=None,
url="https://marc.sastre.cat/folkugat", url="https://marc.sastre.cat/folkugat",
) )
], ]),
hidden=False, hidden=False,
).with_ngrams(), ).with_ngrams(),
# --- # ---
@@ -45,13 +49,14 @@ TEMES = [
# --- # ---
model.Tema( model.Tema(
title="El Gitano", title="El Gitano",
links=[ links=IndexedList([
model.Link( model.Link(
type=model.LinkType.SCORE, id=0,
subtype=model.LinkSubtype.PDF, content_type=model.ContentType.PARTITURA,
link_type=model.LinkType.PDF,
url="/db/temes/1/tema.pdf", url="/db/temes/1/tema.pdf",
) )
], ]),
hidden=False, hidden=False,
).with_ngrams(), ).with_ngrams(),
# --- # ---
@@ -68,8 +73,9 @@ TEMES = [
# --- # ---
model.Tema( model.Tema(
title="Malaguenya de Barxeta", title="Malaguenya de Barxeta",
lyrics=[ lyrics=IndexedList([
model.Lyrics( model.Lyrics(
id=0,
title="Malaguenya de Barxeta", title="Malaguenya de Barxeta",
content=""" content="""
Mira si he corregut terres Mira si he corregut terres
@@ -93,13 +99,14 @@ d'allà on renaix de les cendres
el meu País Valencià. el meu País Valencià.
""".strip(), """.strip(),
), ),
], ]),
properties=[ properties=IndexedList([
model.Property( model.Property(
model.PropertyField.AUTOR, id=0,
"Pep Jimeno 'Botifarra'" field=model.PropertyField.AUTOR,
value="Pep Jimeno 'Botifarra'"
) )
], ]),
hidden=False, hidden=False,
).with_ngrams(), ).with_ngrams(),
] ]

View File

@@ -25,11 +25,10 @@ def footer(request, value, logged_in):
def nota(request): def nota(request):
response = templates.TemplateResponse( return templates.TemplateResponse(
"fragments/nota/nota.html", "fragments/nota/nota.html",
{ {
"request": request, "request": request,
} },
headers={"HX-Refresh": "true"},
) )
response.headers["HX-Refresh"] = "true"
return response

View File

@@ -3,6 +3,7 @@ from typing import Optional
from fastapi import Request from fastapi import Request
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
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.links import guess_link_type
from folkugat_web.templates import templates from folkugat_web.templates import templates
@@ -33,13 +34,11 @@ def title_editor(request: Request, logged_in: bool, tema_id: int):
) )
def lyric(request: Request, logged_in: bool, tema_id: int, lyric_idx: int): def lyric(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if tema is None:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise ValueError(f"No tune exists for tema_id: {tema_id}")
if len(tema.lyrics) < lyric_idx: lyric = tema.lyrics.get(lyric_id)
raise ValueError(f'Lyric index out of bounds')
lyric = tema.lyrics[lyric_idx]
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/lyric.html", "fragments/tema/lyric.html",
@@ -47,19 +46,17 @@ def lyric(request: Request, logged_in: bool, tema_id: int, lyric_idx: int):
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
"tema": tema, "tema": tema,
"lyric_idx": lyric_idx, "lyric_id": lyric_id,
"lyric": lyric, "lyric": lyric,
} }
) )
def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_idx: int): def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
tema = temes_q.get_tema_by_id(tema_id) tema = temes_q.get_tema_by_id(tema_id)
if tema is None: if tema is None:
raise ValueError(f"No tune exists for tema_id: {tema_id}") raise ValueError(f"No tune exists for tema_id: {tema_id}")
if len(tema.lyrics) < lyric_idx: lyric = tema.lyrics.get(lyric_id)
raise ValueError(f'Lyric index out of bounds')
lyric = tema.lyrics[lyric_idx]
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/editor/lyric.html", "fragments/tema/editor/lyric.html",
@@ -67,7 +64,75 @@ def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_idx: int
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
"tema": tema, "tema": tema,
"lyric_idx": lyric_idx, "lyric_id": lyric_id,
"lyric": lyric, "lyric": lyric,
} }
) )
def link(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if tema is None:
raise ValueError(f"No tune exists for tema_id: {tema_id}")
link = tema.links.get(link_id)
return templates.TemplateResponse(
"fragments/tema/link.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"link_id": link_id,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def link_editor(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if tema is None:
raise ValueError(f"No tune exists for tema_id: {tema_id}")
link = tema.links.get(link_id)
return templates.TemplateResponse(
"fragments/tema/editor/link.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"link_id": link_id,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def link_icon(
request: Request,
logged_in: bool,
tema_id: int,
link_id: int,
url: str,
content_type: model.ContentType,
):
link = model.Link(
id=link_id,
content_type=content_type,
link_type=guess_link_type(url),
url=url,
)
return templates.TemplateResponse(
"fragments/tema/link_icon.html",
{
"request": request,
"logged_in": logged_in,
"tema_id": tema_id,
"link_id": link_id,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)

View File

@@ -25,8 +25,8 @@ def temes_busca_result(request: Request, tema: model.Tema, logged_in: bool):
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
"tema": tema, "tema": tema,
"LinkSubtype": model.LinkSubtype,
"LinkType": model.LinkType, "LinkType": model.LinkType,
"ContentType": model.ContentType,
} }
).body.decode('utf-8') ).body.decode('utf-8')
@@ -47,8 +47,8 @@ def tema(request: Request, tema_id: int, logged_in: bool):
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
"Pages": Pages, "Pages": Pages,
"LinkSubtype": model.LinkSubtype,
"LinkType": model.LinkType, "LinkType": model.LinkType,
"ContentType": model.ContentType,
"tema": tema, "tema": tema,
} }
) )

View File

@@ -0,0 +1 @@
from ._base import IndexedList

View File

@@ -0,0 +1,36 @@
import dataclasses
from typing import Optional, TypeVar
@dataclasses.dataclass
class WithId:
id: Optional[int]
T = TypeVar("T", bound=WithId)
class IndexedList(list[T]):
def append(self, _item: T) -> None:
if _item.id is None:
_item.id = max((i.id or 0 for i in self), default=0) + 1
return super().append(_item)
def find_idx(self, _id: int) -> int:
try:
i, _ = next(filter(lambda it: it[1].id == _id, enumerate(self)))
except StopIteration:
raise ValueError(f"Could not find item with id {_id}")
return i
def get(self, _id: int) -> T:
i = self.find_idx(_id)
return self.__getitem__(i)
def delete(self, _id: int) -> None:
i = self.find_idx(_id)
return super().__delitem__(i)
def replace(self, _id: int, _obj: T) -> None:
i = self.find_idx(_id)
super().__setitem__(i, _obj)

View File

@@ -5,16 +5,18 @@ from typing import Optional
from folkugat_web.services import ngrams from folkugat_web.services import ngrams
from ._base import IndexedList, WithId
NGrams = dict[int, list[str]] NGrams = dict[int, list[str]]
class ContentType(enum.Enum):
PARTITURA = "partitura"
AUDIO = "àudio"
OTHER = "enllaç"
class LinkType(enum.Enum): class LinkType(enum.Enum):
SCORE = "score"
AUDIO = "audio"
OTHER = "other"
class LinkSubtype(enum.Enum):
# Score # Score
PDF = "pdf" PDF = "pdf"
IMAGE = "image" IMAGE = "image"
@@ -24,16 +26,17 @@ class LinkSubtype(enum.Enum):
@dataclasses.dataclass @dataclasses.dataclass
class Link: class Link(WithId):
type: LinkType content_type: ContentType
subtype: Optional[LinkSubtype] link_type: Optional[LinkType]
url: str url: str
title: str = "" title: str = ""
def to_dict(self): def to_dict(self):
return dict( return dict(
type=self.type.value, id=self.id,
subtype=self.subtype.value if self.subtype else None, content_type=self.content_type.value,
link_type=self.link_type.value if self.link_type else None,
url=self.url, url=self.url,
title=self.title, title=self.title,
) )
@@ -41,8 +44,9 @@ class Link:
@classmethod @classmethod
def from_dict(cls, d): def from_dict(cls, d):
return cls( return cls(
type=LinkType(d["type"]), id=d["id"],
subtype=LinkSubtype(d["subtype"]) if d["subtype"] else None, content_type=ContentType(d["content_type"]),
link_type=LinkType(d["link_type"]) if d["link_type"] else None,
url=d["url"], url=d["url"],
title=d["title"], title=d["title"],
) )
@@ -56,12 +60,13 @@ class PropertyField(enum.Enum):
@dataclasses.dataclass @dataclasses.dataclass
class Property: class Property(WithId):
field: PropertyField field: PropertyField
value: str value: str
def to_dict(self): def to_dict(self):
return dict( return dict(
id=self.id,
field=self.field.value, field=self.field.value,
value=self.value, value=self.value,
) )
@@ -69,18 +74,20 @@ class Property:
@classmethod @classmethod
def from_dict(cls, d): def from_dict(cls, d):
return cls( return cls(
id=d["id"],
field=PropertyField(d["field"]), field=PropertyField(d["field"]),
value=d["value"], value=d["value"],
) )
@dataclasses.dataclass @dataclasses.dataclass
class Lyrics: class Lyrics(WithId):
title: str title: str
content: str content: str
def to_dict(self): def to_dict(self):
return dict( return dict(
id=self.id,
title=self.title, title=self.title,
content=self.content, content=self.content,
) )
@@ -88,6 +95,7 @@ class Lyrics:
@classmethod @classmethod
def from_dict(cls, d): def from_dict(cls, d):
return cls( return cls(
id=d["id"],
title=d["title"], title=d["title"],
content=d["content"], content=d["content"],
) )
@@ -98,9 +106,9 @@ class Tema:
id: Optional[int] = None id: Optional[int] = None
# Info # Info
title: str = "" title: str = ""
properties: list[Property] = dataclasses.field(default_factory=list) properties: IndexedList[Property] = dataclasses.field(default_factory=IndexedList)
links: list[Link] = dataclasses.field(default_factory=list) links: IndexedList[Link] = dataclasses.field(default_factory=IndexedList)
lyrics: list[Lyrics] = dataclasses.field(default_factory=list) lyrics: IndexedList[Lyrics] = dataclasses.field(default_factory=IndexedList)
# Search related # Search related
alternatives: list[str] = dataclasses.field(default_factory=list) alternatives: list[str] = dataclasses.field(default_factory=list)
ngrams: NGrams = dataclasses.field(default_factory=dict) ngrams: NGrams = dataclasses.field(default_factory=dict)
@@ -115,9 +123,3 @@ class Tema:
def with_ngrams(self): def with_ngrams(self):
self.compute_ngrams() self.compute_ngrams()
return self return self
def scores(self):
return [link for link in self.links if link.type is LinkType.SCORE]
def audios(self):
return [link for link in self.links if link.type is LinkType.AUDIO]

View File

@@ -0,0 +1,32 @@
import re
from typing import Optional
from folkugat_web.model import temes as model
IMAGE_FORMATS_RE = "|".join([
'jpg', 'jpeg', 'png'
])
LINK_RES = {
model.LinkType.IMAGE: [
re.compile(rf"^.*\.({IMAGE_FORMATS_RE})$")
],
model.LinkType.PDF: [
re.compile(r"^.*\.pdf$")
],
model.LinkType.SPOTIFY: [
re.compile(r"^.*spotify\.com.*$")
],
model.LinkType.YOUTUBE: [
re.compile(r"^.*youtube\.com.*$"),
re.compile(r"^.*youtu\.be.*$"),
]
}
def guess_link_type(url: str) -> Optional[model.LinkType]:
for link_type, regexes in LINK_RES.items():
for regex in regexes:
if regex.match(url):
return link_type
return None

View File

@@ -20,29 +20,25 @@ def update_title(tema_id: int, title: str) -> model.Tema:
return new_tema return new_tema
def update_lyric(tema_id: int, lyric_idx: int, lyric: model.Lyrics) -> model.Tema: def update_lyric(tema_id: int, lyric_id: int, lyric: model.Lyrics) -> model.Tema:
with get_connection() as con: with get_connection() as con:
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con) tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
if tema is None: if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!") raise ValueError(f"No tune found with tema_id = {tema_id}!")
if lyric_idx > len(tema.lyrics):
raise ValueError(f'Lyric index out of bounds')
tema.lyrics[lyric_idx] = lyric tema.lyrics.replace(lyric_id, lyric)
temes_w.update_tema(tema=tema, con=con) temes_w.update_tema(tema=tema, con=con)
return tema return tema
def delete_lyric(tema_id: int, lyric_idx: int) -> model.Tema: def delete_lyric(tema_id: int, lyric_id: int) -> model.Tema:
with get_connection() as con: with get_connection() as con:
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con) tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
if tema is None: if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!") raise ValueError(f"No tune found with tema_id = {tema_id}!")
if lyric_idx > len(tema.lyrics):
raise ValueError(f'Lyric index out of bounds')
del tema.lyrics[lyric_idx] tema.lyrics.delete(lyric_id)
temes_w.update_tema(tema=tema, con=con) temes_w.update_tema(tema=tema, con=con)
return tema return tema
@@ -54,9 +50,51 @@ def add_lyric(tema_id: int) -> model.Tema:
if tema is None: if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!") raise ValueError(f"No tune found with tema_id = {tema_id}!")
tema.lyrics.append(model.Lyrics( tema.lyrics.append(model.Lyrics(
id=None,
title=tema.title, title=tema.title,
content="", content="",
)) ))
temes_w.update_tema(tema=tema, con=con) temes_w.update_tema(tema=tema, con=con)
return tema return tema
def update_link(tema_id: int, link_id: int, link: model.Link) -> model.Tema:
with get_connection() as con:
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!")
tema.links.replace(link_id, link)
temes_w.update_tema(tema=tema, con=con)
return tema
def delete_link(tema_id: int, link_id: int) -> model.Tema:
with get_connection() as con:
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!")
tema.links.delete(link_id)
temes_w.update_tema(tema=tema, con=con)
return tema
def add_link(tema_id: int) -> model.Tema:
with get_connection() as con:
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!")
tema.links.append(model.Link(
id=None,
title="",
url="",
content_type=model.ContentType.OTHER,
link_type=None,
))
temes_w.update_tema(tema=tema, con=con)
return tema