Session editor and live sessions!

This commit is contained in:
marc
2025-03-21 19:42:43 +01:00
parent 6c83d11e5b
commit ac54453b7f
54 changed files with 1247 additions and 173 deletions

View File

@@ -1,6 +1,7 @@
from ._router import router from ._router import router
from .auth import * from .auth import *
from .index import * from .index import *
from .sessio import *
from .sessions import * from .sessions import *
from .tema import * from .tema import *
from .temes import * from .temes import *

View File

@@ -0,0 +1 @@
from . import index, live, set_page

View File

@@ -0,0 +1,199 @@
from typing import Annotated
from fastapi import Form, Request
from folkugat_web.api import router
from folkugat_web.fragments import live, sessio
from folkugat_web.services import auth
from folkugat_web.services.temes import write as temes_service
from folkugat_web.templates import templates
@router.get("/sessio/{session_id}")
def page(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"page_title": "Folkugat",
"content": f"/api/content/sessio/{session_id}",
"logged_in": logged_in,
}
)
@router.get("/api/content/sessio/{session_id}")
def contingut(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
):
return sessio.pagina(request, session_id, logged_in)
@router.put("/api/sessio/{session_id}/live")
def set_live(
request: Request,
_: auth.RequireLogin,
session_id: int,
):
return live.start_live_session(request=request, session_id=session_id)
@router.delete("/api/sessio/{session_id}/live")
def stop_live(
request: Request,
_: auth.RequireLogin,
session_id: int,
):
return live.stop_live_session(request=request, session_id=session_id)
@router.post("/api/sessio/{session_id}/set")
def add_set(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
):
return sessio.add_set(request=request, session_id=session_id, logged_in=logged_in)
@router.get("/api/sessio/{session_id}/set/{set_id}")
def get_set(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return sessio.get_set(request=request, session_id=session_id, set_id=set_id, logged_in=logged_in)
@router.delete("/api/sessio/{session_id}/set/{set_id}")
def delete_set(
_: auth.RequireLogin,
session_id: int,
set_id: int,
):
return sessio.delete_set(session_id=session_id, set_id=set_id)
@router.post("/api/sessio/{session_id}/set/{set_id}")
def add_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
):
return sessio.add_tema(request=request, session_id=session_id, set_id=set_id, logged_in=logged_in)
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def get_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.get_tema(
request=request, session_id=session_id, set_id=set_id, entry_id=entry_id, logged_in=logged_in)
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/editor")
def get_tema_editor(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.get_tema_editor(
request=request, session_id=session_id, set_id=set_id, entry_id=entry_id, logged_in=logged_in)
@router.delete("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def delete_tema(
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.delete_tema(session_id=session_id, set_id=set_id, entry_id=entry_id)
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/busca")
def busca_tema(
request: Request,
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
query: str,
):
return sessio.busca_tema(
request=request,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
query=query,
)
@router.put("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def set_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
tema_id: Annotated[int, Form()],
):
return sessio.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=tema_id,
)
@router.put("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/unknown")
def set_tema_unknown(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=None,
)
@router.post("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def set_tema_new(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
title: Annotated[str, Form()],
):
new_tema = temes_service.create_tema(title=title)
return sessio.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=new_tema.id,
)

View File

@@ -0,0 +1,37 @@
from fastapi import Request
from folkugat_web.api import router
from folkugat_web.fragments import set_page
from folkugat_web.services import auth
from folkugat_web.templates import templates
@router.get("/live")
def page(
request: Request,
logged_in: auth.LoggedIn,
):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"page_title": "Folkugat",
"content": f"/api/content/live",
"logged_in": logged_in,
}
)
@router.get("/api/content/live")
def contingut(
request: Request,
logged_in: auth.LoggedIn,
):
return set_page.live(request, logged_in)
@router.get("/api/content/live/set")
def get_set_page(
request: Request,
logged_in: auth.LoggedIn,
):
return set_page.live_set(request, logged_in)

View File

@@ -0,0 +1,33 @@
from fastapi import Request
from folkugat_web.api import router
from folkugat_web.fragments import set_page
from folkugat_web.services import auth
from folkugat_web.templates import templates
@router.get("/sessio/{session_id}/set/{set_id}")
def page(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"page_title": "Folkugat",
"content": f"/api/content/sessio/{session_id}/set/{set_id}",
"logged_in": logged_in,
}
)
@router.get("/api/content/sessio/{session_id}/set/{set_id}")
def contingut(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return set_page.pagina(request, session_id, set_id, logged_in)

View File

@@ -1 +1 @@
from . import editor, index, sessio from . import editor, index

View File

@@ -1,41 +0,0 @@
from fastapi import Request
from folkugat_web.api import router
from folkugat_web.fragments import live, sessions
from folkugat_web.services import auth
from folkugat_web.templates import templates
@router.get("/sessio/{session_id}")
def page(
request: Request,
session_id: int,
logged_in: auth.LoggedIn,
):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"page_title": "Folkugat",
"content": f"/api/content/sessio/{session_id}",
"logged_in": logged_in,
}
)
@router.get("/api/content/sessio/{session_id}")
def contingut(
request: Request,
session_id: int,
logged_in: auth.LoggedIn,
):
return sessions.sessio(request, session_id, logged_in)
@router.put("/api/sessio/{session_id}/live")
def set_live(request: Request, session_id: int, _: auth.RequireLogin):
return live.start_live_session(request=request, session_id=session_id)
@router.delete("/api/sessio/{session_id}/live")
def stop_live(request: Request, session_id: int, _: auth.RequireLogin):
return live.stop_live_session(request=request, session_id=session_id)

View File

@@ -32,7 +32,7 @@ def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
@router.delete("/api/tema/{tema_id}") @router.delete("/api/tema/{tema_id}")
def delete_tema(request: Request, _: auth.RequireLogin, tema_id: int): def delete_tema(_: auth.RequireLogin, tema_id: int):
temes_w.delete_tema(tema_id=tema_id) temes_w.delete_tema(tema_id=tema_id)
return HTMLResponse(headers={ return HTMLResponse(headers={
'HX-Redirect': '/temes' 'HX-Redirect': '/temes'
@@ -40,7 +40,7 @@ def delete_tema(request: Request, _: auth.RequireLogin, tema_id: int):
@router.post("/api/tema") @router.post("/api/tema")
def create_tema(request: Request, _: auth.RequireLogin, title: Annotated[str, Form()] = ""): def create_tema(_: auth.RequireLogin, title: Annotated[str, Form()] = ""):
new_tema = temes_w.create_tema(title=title) new_tema = temes_w.create_tema(title=title)
return HTMLResponse(headers={ return HTMLResponse(headers={
'HX-Redirect': f'/tema/{new_tema.id}' 'HX-Redirect': f'/tema/{new_tema.id}'

View File

@@ -36,5 +36,5 @@ def content(
@router.get("/api/temes/busca") @router.get("/api/temes/busca")
def busca(request: Request, query: str, logged_in: auth.LoggedIn): def busca(request: Request, query: str, logged_in: auth.LoggedIn, limit: int = 10, offset: int = 0):
return temes.temes_busca(request, query, logged_in) return temes.temes_busca(request, query=query, limit=limit, offset=offset, logged_in=logged_in)

View File

@@ -606,11 +606,20 @@ video {
margin: 1.5rem; margin: 1.5rem;
} }
.m-8 {
margin: 2rem;
}
.mx-1 { .mx-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mx-12 {
margin-left: 3rem;
margin-right: 3rem;
}
.mx-2 { .mx-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
@@ -661,6 +670,14 @@ video {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
.mt-4 {
margin-top: 1rem;
}
.mt-8 {
margin-top: 2rem;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }
@@ -936,14 +953,14 @@ video {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.pl-10 {
padding-left: 2.5rem;
}
.pl-5 { .pl-5 {
padding-left: 1.25rem; padding-left: 1.25rem;
} }
.pt-4 {
padding-top: 1rem;
}
.text-left { .text-left {
text-align: left; text-align: left;
} }
@@ -994,6 +1011,11 @@ video {
color: rgb(178 124 9 / var(--tw-text-opacity, 1)); color: rgb(178 124 9 / var(--tw-text-opacity, 1));
} }
.text-brown {
--tw-text-opacity: 1;
color: rgb(62 56 52 / var(--tw-text-opacity, 1));
}
.text-white { .text-white {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1)); color: rgb(255 255 255 / var(--tw-text-opacity, 1));

View File

@@ -1,5 +1,8 @@
<div class="h-4/5 min-h-[400px] flex flex-col items-center justify-center"> <div class="h-4/5 min-h-[400px] flex flex-col items-center justify-center">
<img src = "{{ url_for('static', path='img/folkugat.svg') }}" class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3" width="100" alt="Folkugat"/> <img src="{{ url_for('static', path='img/folkugat.svg') }}"
class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3"
width="100"
alt="Folkugat"/>
<h1 class="text-3xl sm:text-8xl text-beige m-3 {% if animate %} opacity-0 animate-fade-in-one {% endif %}">{{ page_title }}</h1> <h1 class="text-3xl sm:text-8xl text-beige m-3 {% if animate %} opacity-0 animate-fade-in-one {% endif %}">{{ page_title }}</h1>
<h2 class="text-center sm:text-3xl m-6 {% if animate %} opacity-0 animate-fade-in-two {% endif %}">Sessions de folk a Sant Cugat</h2> <h2 class="text-center sm:text-3xl m-6 {% if animate %} opacity-0 animate-fade-in-two {% endif %}">Sessions de folk a Sant Cugat</h2>
</div> </div>

View File

@@ -1,6 +1,9 @@
{% if session %}
<div class="bg-beige text-white mt-2 py-2 text-xl overflow-hidden"> <div class="bg-beige text-white mt-2 py-2 text-xl overflow-hidden">
{% if current_set is not none %}
<a href="/live">
{% else %}
<a href="/sessio/{{ session.id }}"> <a href="/sessio/{{ session.id }}">
{% endif %}
<div class="animate-marquee whitespace-nowrap"> <div class="animate-marquee whitespace-nowrap">
{% for _ in (1, 2, 3) %} {% for _ in (1, 2, 3) %}
<span class="inline-block mx-4"> <span class="inline-block mx-4">
@@ -9,14 +12,25 @@
<span class="inline-block mx-4"> <span class="inline-block mx-4">
</span> </span>
{% if current_set is not none %}
<span class="inline-block mx-4"> <span class="inline-block mx-4">
Està sonant <i>Pasdoble de Muntanya</i> Estem tocant
{% for tema_in_set in current_set.temes %}
{% if loop.index != 1 %}
+
{% endif %}
{% if tema_in_set.tema is not none %}
<i>{{ tema_in_set.tema.title }}</i>
{% else %}
<i> ??? </i>
{% endif %}
{% endfor %}
</span> </span>
<span class="inline-block mx-4"> <span class="inline-block mx-4">
</span> </span>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</a> </a>
</div> </div>
{% endif %}

View File

@@ -0,0 +1,2 @@
{% include "fragments/menu.html" %}
{% include "fragments/sessio/live/set_page.html" %}

View File

@@ -0,0 +1,5 @@
<div hx-get="/api/content/live/set"
hx-trigger="every 20s"
hx-swap="outerHTML">
{% include "fragments/sessio/set/set_page.html" %}
</div>"

View File

@@ -1,5 +1,5 @@
<button title="Inicia una sessió" <button title="Inicia una sessió"
class="text-beige mx-1" class="text-beige m-2"
hx-put="/api/sessio/{{ session.id }}/live" hx-put="/api/sessio/{{ session.id }}/live"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-play" aria-hidden="true"></i> <i class="fa fa-play" aria-hidden="true"></i>

View File

@@ -1,5 +1,5 @@
<button title="Atura una sessió" <button title="Atura una sessió"
class="text-beige mx-1" class="text-beige m-2"
hx-delete="/api/sessio/{{ session.id }}/live" hx-delete="/api/sessio/{{ session.id }}/live"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-stop" aria-hidden="true"></i> <i class="fa fa-stop" aria-hidden="true"></i>

View File

@@ -1,19 +1,19 @@
{% include "fragments/menu.html" %} {% include "fragments/menu.html" %}
<div class="flex justify-center"> <div class="flex justify-center">
<div class="m-12 grow max-w-4xl text-center"> <div class="m-12 grow max-w-4xl">
<h3 class="text-3xl p-4"> <h3 class="text-center text-3xl p-4">
{% set dn = date_names(session.date) %} {% set dn = date_names(session.date) %}
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} de {{ dn.year }} {{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} de {{ dn.year }}
</h3>
{% if logged_in %} {% if logged_in %}
{% if session.is_live %} {% if session.is_live %}
{% include "fragments/sessions/live/stop.html" %} {% include "fragments/sessio/live/stop.html" %}
{% else %} {% else %}
{% include "fragments/sessions/live/start.html" %} {% include "fragments/sessio/live/start.html" %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</h3>
<div class="text-left"> <div class="text-left">
<h4 class="text-xl text-beige">Horari i lloc</h4> <h4 class="pt-4 text-xl text-beige">Horari i lloc</h4>
De {{ session.start_time.strftime("%H:%M") }} De {{ session.start_time.strftime("%H:%M") }}
a {{ session.end_time.strftime("%H:%M") }} a {{ session.end_time.strftime("%H:%M") }}
{% if session.venue.name %} {% if session.venue.name %}
@@ -29,5 +29,11 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% if logged_in or playlist.sets %}
<div class="text-left">
<h4 class="pt-4 text-xl text-beige mt-2">Temes tocats</h4>
{% include "fragments/sessio/playlist.html" %}
</div>
{% endif %}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,17 @@
<ul id="playlist-{{ session.id }}" class="">
{% for set_entry in playlist.sets %}
{% set set_id = set_entry.id %}
{% include "fragments/sessio/set_entry.html" %}
{% endfor %}
</ul>
{% if logged_in %}
<div class="flex flex-col items-center">
<button class="text-beige mt-2"
hx-post="/api/sessio/{{ session.id }}/set"
hx-target="#playlist-{{ session.id }}"
hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i>
Afegeix set
</button>
</div>
{% endif %}

View File

@@ -0,0 +1,2 @@
{% include "fragments/menu.html" %}
{% include "fragments/sessio/set/set_page.html" %}

View File

@@ -0,0 +1,30 @@
{% if session is not none %}
<div class="text-beige m-3">
{% set dn = date_names(session.date) %}
<a href="/sessio/{{ session.id }}">
<i class="fa fa-chevron-left px-2" aria-hidden="true"></i>
<i>
Tocat el {{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} de {{ dn.year }}
</i>
</a>
</div>
{% endif %}
{% for tema_in_set in set.temes %}
{% if loop.index != 1 %}
<div class="mx-12">
<hr class="h-px mt-1 mb-3 bg-beige border-0">
</div>
{% endif %}
{% if tema_in_set.tema is not none %}
{% set tema = tema_in_set.tema %}
{% include "fragments/sessio/set/tema.html"%}
{% else %}
<h3 class="text-center text-3xl p-4">
<i>Desconegut</i>
</h3>
<div class="text-center">
<i>No sabem quin tema és aquest</i>
</div>
{% endif %}
<div class="m-8"> </div>
{% endfor %}

View File

@@ -0,0 +1,25 @@
<h3 class="text-center text-3xl p-4">
{{ tema.title }}
<a class="text-beige"
º href="/tema/{{ tema.id }}">
<i class="fa fa-chevron-right px-2" aria-hidden="true"></i>
</a>
</h3>
<div class="mx-12 text-left">
{% if tema.score() is not none %}
{% include "fragments/tema/score.html" %}
{% endif %}
{% if tema.lyrics %}
{% set lyric = tema.lyrics[0] %}
<div>
<h4 class="mt-4 text-xl text-beige">Lletra</h4>
<hr class="h-px mt-1 mb-3 bg-beige border-0">
<div class="text-center">
{{ lyric.content.replace('\n', '<br>') | safe }}
</div>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,46 @@
<li class="flex flex-row grow items-center
border border-beige rounded
px-2 py-1 my-1"
id="set-entry-{{ set_id }}"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}"
hx-swap="outerHTML"
hx-trigger="reload-set-{{ set_id }}">
<div class="flex-1 flex flex-col items-center">
{% if logged_in %}
<button class="text-beige mt-2"
hx-delete="/api/sessio/{{ session_id }}/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
Esborra el set
</button>
{% endif %}
<ol id="set-entry-{{ set_id }}-list"
class="flex flex-col items-center">
{% for tema_entry in set_entry.temes %}
{% if new_entry %}
{% include "fragments/sessio/tema_editor.html" %}
{% else %}
{% include "fragments/sessio/tema_entry.html" %}
{% endif %}
{% endfor %}
</ol>
{% if logged_in %}
<button class="text-beige mt-2"
hx-post="/api/sessio/{{ session_id }}/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}-list"
hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i>
Afegeix un tema
</button>
{% endif %}
</div>
<div class="ml-auto">
<a title="Mostra els temes"
class="text-beige"
href="/sessio/{{ session_id }}/set/{{ set_id }}">
<i class="fa fa-chevron-right" aria-hidden="true"></i>
</a>
</div>
</li>

View File

@@ -0,0 +1,30 @@
<li id="tune-entry-{{ tema_entry.id }}"
class="flex flex-col items-center my-1">
<div class="flex flex-row">
<input
name="query"
{% if tema_entry.tema %}
value="{{ tema_entry.tema.title }}"
{% else %}
value=""
{% endif %}
placeholder="Busca un tema..."
class="border border-beige focus:outline-none
rounded text-center
bg-brown p-1 m-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca"
hx-trigger="revealed, keyup delay:500ms changed"
hx-target="#tune-entry-{{ tema_entry.id }}-search-results"
hx-swap="outerHTML"/>
<button title="Descarta els canvis"
class="text-beige mx-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
hx-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
<ul id="tune-entry-{{ tema_entry.id }}-search-results"
class="flex flex-wrap">
</ul>
</li>

View File

@@ -0,0 +1,29 @@
<li id="tune-entry-{{ tema_entry.id }}"
class="flex flex-row my-1">
<div class="mx-1 text-center">
{% if tema_entry.tema is none %}
<i class="fa fa-question" aria-hidden="true"></i>
<i>(Desconegut)</i>
{% else %}
<a href="/sessio/{{ session_id }}/set/{{ set_id }}">
{{ tema_entry.tema.title }}
</a>
{% endif %}
</div>
{% if logged_in %}
<button title="Edita el tema"
class="text-beige mx-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/editor"
hx-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>
<button title="Esborra el tema"
class="text-beige mx-1"
hx-delete="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
hx-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
{% endif %}
</li>

View File

@@ -0,0 +1,36 @@
<ul id="tune-entry-{{ entry_id }}-search-results"
class="flex flex-wrap items-center justify-center">
{% for tema in results %}
<li>
<button class="bg-beige text-brown rounded
m-1 px-2"
hx-put="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
hx-vals='{"tema_id": "{{ tema.id }}"}'
hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML">
{{ tema.title }}
</button>
</li>
{% endfor %}
<li>
<button class="border border-beige text-beige rounded
m-1 px-2"
hx-put="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}/unknown"
hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML">
<i class="fa fa-question" aria-hidden="true"></i>
<i>(Desconegut)</i>
</button>
</li>
<li>
<button class="border border-beige text-beige rounded
m-1 px-2"
hx-post="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
hx-vals='{"title": "{{ query }}"}'
hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML">
<i class="fa fa-plus" aria-hidden="true"></i>
<i>{{ query }}</i>
</button>
</li>
</ul>

View File

@@ -1,24 +1,11 @@
<p> {% if session == None %}
{% if session == None %} L'últim Dimecres de cada mes
<a> L'últim Dimecres de cada mes </a> {% else %}
{% else %}
<a>
{% set dn = date_names(session.date) %} {% set dn = date_names(session.date) %}
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} {{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
</a>
de {{ session.start_time.strftime("%H:%M") }} de {{ session.start_time.strftime("%H:%M") }}
a {{ session.end_time.strftime("%H:%M") }} a {{ session.end_time.strftime("%H:%M") }}
{% if session.venue.name %} {% if session.venue.name %}
a {{ session.venue.name }} a {{ session.venue.name }}
<!-- {% if session.venue.url %} -->
<!-- <a href="{{ session.venue.url }}" -->
<!-- class="text-beige" -->
<!-- target="_blank"> -->
<!-- {{ session.venue.name }} -->
<!-- </a> -->
<!-- {% else %} -->
<!-- {{ session.venue.name }} -->
<!-- {% endif %} -->
{% endif %} {% endif %}
{% endif %} {% endif %}
</p>

View File

@@ -4,15 +4,17 @@
relative" relative"
id="session-row-{{session.id}}"> id="session-row-{{session.id}}">
<a href="/session/{{session.id}}"> <a href="/session/{{session.id}}">
<div class="flex flex-row grow justify-center pl-10"> <div class="flex flex-row grow items-center">
<div class="flex-1"> <div class="flex-1">
<a href="/sessio/{{session.id}}/">
{% include "fragments/sessions/session_date.html" %} {% include "fragments/sessions/session_date.html" %}
</a>
</div> </div>
<div class="ml-auto"> <div class="ml-auto">
<a title="Més informació" <a title="Més informació"
class="text-beige mx-1" class="text-beige mx-1"
href="/sessio/{{session.id}}/"> href="/sessio/{{session.id}}/">
<i class="fa fa-info-circle" aria-hidden="true"></i> <i class="fa fa-chevron-right" aria-hidden="true"></i>
</a> </a>
{% if logged_in %} {% if logged_in %}
<button title="Edita la sessió" <button title="Edita la sessió"

View File

@@ -9,6 +9,8 @@
{% include "icons/pdf.svg" %} {% include "icons/pdf.svg" %}
{% elif link.content_type == ContentType.AUDIO %} {% elif link.content_type == ContentType.AUDIO %}
{% include "icons/notes.svg" %} {% include "icons/notes.svg" %}
{% elif link.content_type == ContentType.PARTITURA %}
{% include "icons/notes.svg" %}
{% else %} {% else %}
{% include "icons/link.svg" %} {% include "icons/link.svg" %}
{% endif %} {% endif %}

View File

@@ -1,5 +1,5 @@
{% if logged_in or tema.links %} {% if logged_in or tema.links %}
<h4 class="text-xl text-beige">Enllaços</h4> <h4 class="pt-4 text-xl text-beige">Enllaços</h4>
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
<ul class="flex flex-col justify-center" <ul class="flex flex-col justify-center"
id="new-link-target"> id="new-link-target">

View File

@@ -1,5 +1,5 @@
{% if logged_in or tema.lyrics %} {% if logged_in or tema.lyrics %}
<h4 class="text-xl text-beige">Lletra</h4> <h4 class="pt-4 text-xl text-beige">Lletra</h4>
{% endif %} {% endif %}
{% if tema.lyrics %} {% if tema.lyrics %}

View File

@@ -1,20 +1,17 @@
{% include "fragments/menu.html" %} {% include "fragments/menu.html" %}
<div class="flex justify-center"> <div class="flex flex-col justify-center">
<div class="m-12 grow max-w-4xl text-center"> <div class="mt-8">
{% include "fragments/tema/title.html" %} {% include "fragments/tema/title.html" %}
<div class="text-left"> </div>
<div class="p-12 text-left">
<div id="tema-{{ tema.id }}-score" <div id="tema-{{ tema.id }}-score"
hx-get="/api/tema/{{ tema.id }}/score" hx-get="/api/tema/{{ tema.id }}/score"
hx-trigger="load, reload-tema-{{ tema.id }}-score from:body" hx-trigger="load, reload-tema-{{ tema.id }}-score from:body"
hx-swap="innerHTML" hx-swap="innerHTML"
> >
</div> </div>
{% include "fragments/tema/lyrics.html" %} {% include "fragments/tema/lyrics.html" %}
{% include "fragments/tema/links.html" %} {% include "fragments/tema/links.html" %}
{% include "fragments/tema/properties.html" %} {% include "fragments/tema/properties.html" %}
</div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{% if logged_in or tema.properties %} {% if logged_in or tema.properties %}
<h4 class="text-xl mt-3 text-beige">Informació</h4> <h4 class="pt-4 text-xl mt-3 text-beige">Informació</h4>
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
<ul class="flex flex-col" <ul class="flex flex-col"

View File

@@ -1,15 +1,15 @@
{% set score = tema.score() %} {% set score = tema.score() %}
{% if score %} {% if score %}
<h4 class="text-xl text-beige">Partitura</h4> <h4 class="mp-4 text-xl text-beige">Partitura</h4>
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
{% if score.link_type == LinkType.PDF %} {% if score.link_type == LinkType.PDF %}
{% set pdf_url = score.url %} {% set pdf_url = score.url %}
{% include "fragments/pdf_viewer.html" %} {% include "fragments/pdf_viewer.html" %}
{% elif score.link_type == LinkType.IMAGE %} {% elif score.link_type == LinkType.IMAGE %}
<div class="flex justify-center"> <div class="flex justify-center">
<img class="m-2" <a href="{{ score.url }}" target="_blank">
src="{{ score.url }}" <img class="m-2" src="{{ score.url }}" />
/> </a>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -1,6 +1,6 @@
<div class="flex flex-row flex-wrap justify-center" <div class="flex flex-row flex-wrap justify-center"
id="tema-title"> id="tema-title">
<h3 class="text-3xl p-4">{{ tema.title }}</h3> <h3 class="text-3xl text-center p-4">{{ tema.title }}</h3>
{% if logged_in %} {% if logged_in %}
<button title="Canvia el títol" <button title="Canvia el títol"
class="text-beige text-2xl mx-2" class="text-beige text-2xl mx-2"

View File

@@ -10,7 +10,8 @@
p-2 m-2" p-2 m-2"
hx-get="/api/temes/busca" hx-get="/api/temes/busca"
hx-trigger="revealed, keyup delay:500ms changed" hx-trigger="revealed, keyup delay:500ms changed"
hx-target="#search-results"> hx-target="#search-results"
hx-swap="outerHTML">
{% if logged_in %} {% if logged_in %}
<button title="Afegeix un tema" <button title="Afegeix un tema"
class="text-beige m-2" class="text-beige m-2"

View File

@@ -4,19 +4,9 @@
<li class="px-2 text-beige"> <li class="px-2 text-beige">
<a href="{{ link.url }}" target="_blank"> <a href="{{ link.url }}" target="_blank">
{% if link.content_type == ContentType.AUDIO %} {% if link.content_type == ContentType.AUDIO %}
{% if link.link_type == LinkType.SPOTIFY %} <i class="fa fa-volume-up" aria-hidden="true"></i>
<i class="fa fa-music" aria-hidden="true"></i>
{% elif link.link_type == LinkType.YOUTUBE %}
<i class="fa fa-play" aria-hidden="true"></i>
{% else %}
<i class="fa fa-music" aria-hidden="true"></i>
{% endif %}
{% elif link.content_type == ContentType.PARTITURA %} {% elif link.content_type == ContentType.PARTITURA %}
{% if link.link_type == LinkType.PDF %} <i class="fa fa-music" aria-hidden="true"></i>
<i class="fa fa-file" aria-hidden="true"></i>
{% else %}
<i class="fa fa-link" aria-hidden="true"></i>
{% endif %}
{% else %} {% else %}
<i class="fa fa-link" aria-hidden="true"></i> <i class="fa fa-link" aria-hidden="true"></i>
{% endif %} {% endif %}

View File

@@ -1,5 +1,6 @@
<table id="search-results" <div id="search-results"
class="text-left min-w-full w-full"> class="min-w-full w-full">
<table class="text-left min-w-full w-full">
<tr class="border-b border-beige"> <tr class="border-b border-beige">
<td class="font-bold py-2 px-4">Nom</td> <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">Enllaços</td>
@@ -16,4 +17,25 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% if prev_offset is not none or next_offset is not none %}
<div class="py-2">
{% if prev_offset is not none %}
<button class="text-beige mx-2"
hx-get="/api/temes/busca?query={{ query }}&offset={{ prev_offset }}"
hx-target="#search-results"
hx-swap="outerHTML">
<i class="fa fa-chevron-left" aria-hidden="true"></i>
</button>
{% endif %}
{% if next_offset is not none %}
<button class="text-beige mx-2"
hx-get="/api/temes/busca?query={{ query }}&offset={{ next_offset }}"
hx-target="#search-results"
hx-swap="outerHTML">
<i class="fa fa-chevron-right" aria-hidden="true"></i>
</button>
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -1,4 +1,5 @@
from folkugat_web.dal.sql import get_connection, sessions from folkugat_web.dal.sql import get_connection, sessions
from folkugat_web.dal.sql.playlists import ddl as playlists_ddl
from folkugat_web.dal.sql.temes import ddl as temes_ddl from folkugat_web.dal.sql.temes import ddl as temes_ddl
@@ -6,3 +7,4 @@ def create_db():
with get_connection() as con: with get_connection() as con:
sessions.create_db(con) sessions.create_db(con)
temes_ddl.create_db(con) temes_ddl.create_db(con)
playlists_ddl.create_db(con)

View File

@@ -0,0 +1,19 @@
from folkugat_web.model import playlists as model
def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> dict:
return {
'id': tema_in_set.id,
'session_id': tema_in_set.session_id,
'set_id': tema_in_set.set_id,
'tema_id': tema_in_set.tema_id,
}
def row_to_playlist_entry(row: tuple) -> model.PlaylistEntry:
return model.PlaylistEntry(
id=row[0],
session_id=row[1],
set_id=row[2],
tema_id=row[3],
)

View File

@@ -0,0 +1,27 @@
from typing import Optional
from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Optional[Connection] = None):
with get_connection(con) as con:
create_playlists_table(con)
def drop_playlists_table(con: Connection):
query = "DROP TABLE IF EXISTS playlists"
cur = con.cursor()
cur.execute(query)
def create_playlists_table(con: Connection):
query = """
CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY,
session_id INTEGER NOT NULL,
set_id INTEGER NOT NULL,
tema_id INTEGER
)
"""
cur = con.cursor()
cur.execute(query)

View File

@@ -0,0 +1,47 @@
from collections.abc import Iterator
from typing import Optional
from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import playlists as model
from ._conversion import row_to_playlist_entry
def _filter_clause(
entry_id: Optional[int] = None,
set_id: Optional[int] = None,
session_id: Optional[int] = None,
) -> tuple[str, dict]:
filter_clauses = []
query_data = {}
if entry_id is not None:
filter_clauses.append("id = :id")
query_data["id"] = entry_id
if set_id is not None:
filter_clauses.append("set_id = :set_id")
query_data["set_id"] = set_id
if session_id is not None:
filter_clauses.append("session_id = :session_id")
query_data["session_id"] = session_id
return " AND ".join(filter_clauses), query_data
def get_playlist_entries(
entry_id: Optional[int] = None,
set_id: Optional[int] = None,
session_id: Optional[int] = None,
con: Optional[Connection] = None,
) -> Iterator[model.PlaylistEntry]:
filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, session_id=session_id)
query = f"""
SELECT
id, session_id, set_id, tema_id
FROM playlists
WHERE {filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
cur.execute(query, data)
return map(row_to_playlist_entry, cur.fetchall())

View File

@@ -0,0 +1,61 @@
from typing import Optional
from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import playlists as model
from ._conversion import playlist_entry_to_row, row_to_playlist_entry
def insert_playlist_entry(pl_entry: model.PlaylistEntry, con: Optional[Connection] = None) -> model.PlaylistEntry:
query = """
INSERT INTO playlists
(id, session_id, set_id, tema_id)
VALUES
(:id, :session_id, :set_id, :tema_id)
RETURNING *
"""
data = playlist_entry_to_row(pl_entry)
with get_connection(con) as con:
cur = con.cursor()
cur.execute(query, data)
row = cur.fetchone()
return row_to_playlist_entry(row)
def update_playlist_entry(entry: model.PlaylistEntry, con: Optional[Connection] = None):
query = """
UPDATE playlists
SET
id = :id, session_id = :session_id, set_id = :set_id, tema_id = :tema_id
WHERE
id = :id
"""
data = playlist_entry_to_row(entry)
with get_connection(con) as con:
cur = con.cursor()
cur.execute(query, data)
return
def delete_playlist_entry(entry_id: int, con: Optional[Connection] = None):
query = """
DELETE FROM playlists
WHERE id = :id
"""
data = dict(id=entry_id)
with get_connection(con) as con:
cur = con.cursor()
cur.execute(query, data)
return
def delete_playlist_set(session_id: int, set_id: int, con: Optional[Connection] = None):
query = """
DELETE FROM playlists
WHERE session_id = :session_id AND set_id = :set_id
"""
data = dict(session_id=session_id, set_id=set_id)
with get_connection(con) as con:
cur = con.cursor()
cur.execute(query, data)
return

View File

@@ -1,4 +1,4 @@
from folkugat_web.model import IndexedList from folkugat_web.model import IndexedList, playlists
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
TEMES = [ TEMES = [
@@ -110,3 +110,19 @@ el meu País Valencià.
hidden=False, hidden=False,
).with_ngrams(), ).with_ngrams(),
] ]
PLAYLIST_ENTRIES = [
playlists.PlaylistEntry(0, 2, 0, 1),
playlists.PlaylistEntry(1, 2, 0, 2),
playlists.PlaylistEntry(2, 2, 1, 3),
playlists.PlaylistEntry(3, 2, 2, 4),
playlists.PlaylistEntry(4, 2, 2, 5),
playlists.PlaylistEntry(5, 2, 2, 6),
playlists.PlaylistEntry(6, 2, 3, 7),
playlists.PlaylistEntry(7, 2, 4, 8),
]
PLAYLIST = playlists.Playlist.from_playlist_entries(
session_id=2,
entries=PLAYLIST_ENTRIES,
)

View File

@@ -1,16 +1,29 @@
from fastapi import Request from fastapi import Request
from fastapi.responses import HTMLResponse
from folkugat_web.model import sessions as model from folkugat_web.model import sessions as model
from folkugat_web.services import playlists as playlists_service
from folkugat_web.services import sessions as service from folkugat_web.services import sessions as service
from folkugat_web.templates import templates from folkugat_web.templates import templates
def sessio_en_directe(request: Request): def sessio_en_directe(request: Request):
session = service.get_live_session() session = service.get_live_session()
if not session:
return HTMLResponse()
if not session.id:
raise RuntimeError("Got a session without id!")
current_set = None
if playlist := playlists_service.get_playlist(session_id=session.id):
if playlist.sets:
current_set = playlists_service.add_temes_to_set(playlist.sets[-1])
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/marquee.html", "fragments/marquee.html",
{ {
"request": request, "request": request,
"session": session, "session": session,
"current_set": current_set,
} }
) )
@@ -19,7 +32,7 @@ def start_live_session(request: Request, session_id: int):
service.set_live_session(session_id=session_id) service.set_live_session(session_id=session_id)
session = model.Session(id=session_id) session = model.Session(id=session_id)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/sessions/live/stop.html", "fragments/sessio/live/stop.html",
{ {
"request": request, "request": request,
"session": session, "session": session,
@@ -34,7 +47,7 @@ def stop_live_session(request: Request, session_id: int):
service.stop_live_sessions() service.stop_live_sessions()
session = model.Session(id=session_id) session = model.Session(id=session_id)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/sessions/live/start.html", "fragments/sessio/live/start.html",
{ {
"request": request, "request": request,
"session": session, "session": session,

View File

@@ -0,0 +1,163 @@
from typing import Optional
from fastapi import Request
from fastapi.responses import HTMLResponse
from folkugat_web.model.pagines import Pages
from folkugat_web.services import playlists as playlists_service
from folkugat_web.services import sessions as sessions_service
from folkugat_web.services.temes import search as search_service
from folkugat_web.templates import templates
def pagina(request: Request, session_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id)
playlist = playlists_service.get_playlist(session_id=session_id)
playlist = playlists_service.add_temes_to_playlist(playlist)
return templates.TemplateResponse(
"fragments/sessio/pagina.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session_id": session_id,
"session": session,
"playlist": playlist,
"date_names": sessions_service.get_date_names,
}
)
def add_set(request: Request, session_id: int, logged_in: bool):
new_set = playlists_service.add_set(session_id=session_id)
return templates.TemplateResponse(
"fragments/sessio/set_entry.html",
{
"request": request,
"logged_in": logged_in,
"new_entry": True,
"session_id": session_id,
"set_id": new_set.id,
"set_entry": new_set,
}
)
def get_set(request: Request, session_id: int, set_id: int, logged_in: bool):
set_entry = playlists_service.get_set(session_id=session_id, set_id=set_id)
if set_entry:
return templates.TemplateResponse(
"fragments/sessio/set_entry.html",
{
"request": request,
"logged_in": logged_in,
"new_entry": True,
"session_id": session_id,
"set_id": set_id,
"set_entry": set_entry,
}
)
else:
return HTMLResponse()
def delete_set(session_id: int, set_id: int):
playlists_service.delete_set(session_id=session_id, set_id=set_id)
return HTMLResponse()
def add_tema(request: Request, session_id: int, set_id: int, logged_in: bool):
new_tema = playlists_service.add_tema(session_id=session_id, set_id=set_id)
playlists_service.add_tema_to_tema_in_set(new_tema)
return templates.TemplateResponse(
"fragments/sessio/tema_editor.html",
{
"request": request,
"logged_in": logged_in,
"session_id": session_id,
"set_id": set_id,
"tema_entry": new_tema,
}
)
def get_tema(request: Request, session_id: int, set_id: int, entry_id: int, logged_in: bool):
tema_entry = playlists_service.get_tema(entry_id=entry_id)
playlists_service.add_tema_to_tema_in_set(tema_entry)
return templates.TemplateResponse(
"fragments/sessio/tema_entry.html",
{
"request": request,
"logged_in": logged_in,
"session_id": session_id,
"set_id": set_id,
"tema_entry": tema_entry,
}
)
def get_tema_editor(request: Request, session_id: int, set_id: int, entry_id: int, logged_in: bool):
tema_entry = playlists_service.get_tema(entry_id=entry_id)
playlists_service.add_tema_to_tema_in_set(tema_entry)
return templates.TemplateResponse(
"fragments/sessio/tema_editor.html",
{
"request": request,
"logged_in": logged_in,
"session_id": session_id,
"set_id": set_id,
"tema_entry": tema_entry,
}
)
def delete_tema(session_id: int, set_id: int, entry_id: int):
playlists_service.delete_tema(entry_id=entry_id)
if not playlists_service.get_set(session_id=session_id, set_id=set_id):
headers = {
"HX-Trigger": f"reload-set-{set_id}"
}
else:
headers = {}
return HTMLResponse(headers=headers)
def busca_tema(
request: Request,
session_id: int,
set_id: int,
entry_id: int,
query: str,
):
results = search_service.busca_temes(
query=query,
hidden=True,
limit=4,
offset=0,
)
return templates.TemplateResponse(
"fragments/sessio/tema_results.html",
{
"request": request,
"session_id": session_id,
"set_id": set_id,
"entry_id": entry_id,
"results": results,
"query": query,
}
)
def set_tema(request: Request, logged_in: bool, session_id: int, set_id: int, entry_id: int, tema_id: Optional[int]):
playlists_service.set_tema(session_id=session_id, set_id=set_id, entry_id=entry_id, tema_id=tema_id)
tema_entry = playlists_service.get_tema(entry_id=entry_id)
playlists_service.add_tema_to_tema_in_set(tema_entry)
return templates.TemplateResponse(
"fragments/sessio/tema_entry.html",
{
"request": request,
"logged_in": logged_in,
"session_id": session_id,
"set_id": set_id,
"tema_entry": tema_entry,
}
)

View File

@@ -87,20 +87,6 @@ def _sessions_list(request: Request, sessions: list[model.Session], has_more_ses
) )
def sessio(request: Request, session_id: int, logged_in: bool):
session = service.get_session(session_id=session_id)
return templates.TemplateResponse(
"fragments/sessions/sessio.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session": session,
"date_names": service.get_date_names,
}
)
def sessions_editor_row(request: Request, session_date: Optional[model.Session] = None, session_id: Optional[int] = None): def sessions_editor_row(request: Request, session_date: Optional[model.Session] = None, session_id: Optional[int] = None):
if session_date is None: if session_date is None:
if session_id is None: if session_id is None:

View File

@@ -0,0 +1,72 @@
from fastapi import HTTPException, Request
from folkugat_web.model.pagines import Pages
from folkugat_web.model.temes import ContentType, LinkType
from folkugat_web.services import playlists as playlists_service
from folkugat_web.services import sessions as sessions_service
from folkugat_web.templates import templates
def pagina(request: Request, session_id: int, set_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id)
set_ = playlists_service.get_set(session_id=session_id, set_id=set_id)
if not set_:
raise HTTPException(status_code=404, detail="Set not found")
set_ = playlists_service.add_temes_to_set(set_)
return templates.TemplateResponse(
"fragments/sessio/set/pagina.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session_id": session_id,
"session": session,
"set": set_,
"date_names": sessions_service.get_date_names,
"LinkType": LinkType,
"ContentType": ContentType,
}
)
def live(request: Request, logged_in: bool):
session = sessions_service.get_live_session()
set_ = None
if session and session.id:
playlist = playlists_service.get_playlist(session_id=session.id)
if playlist.sets:
set_ = playlists_service.add_temes_to_set(playlist.sets[-1])
return templates.TemplateResponse(
"fragments/sessio/set/pagina.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session": session,
"set": set_,
"date_names": sessions_service.get_date_names,
"LinkType": LinkType,
"ContentType": ContentType,
}
)
def live_set(request: Request, logged_in: bool):
session = sessions_service.get_live_session()
set_ = None
if session and session.id:
playlist = playlists_service.get_playlist(session_id=session.id)
if playlist.sets:
set_ = playlists_service.add_temes_to_set(playlist.sets[-1])
return templates.TemplateResponse(
"fragments/sessio/set/set_page.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session": session,
"set": set_,
"date_names": sessions_service.get_date_names,
"LinkType": LinkType,
"ContentType": ContentType,
}
)

View File

@@ -19,14 +19,30 @@ def temes_pagina(request: Request, logged_in: bool, query: str):
) )
def temes_busca(request: Request, query: str, logged_in: bool): def temes_busca(request: Request, logged_in: bool, query: str, offset: int = 0, limit: int = 10):
temes = temes_s.busca_temes(query=query, hidden=logged_in) temes = temes_s.busca_temes(
query=query,
hidden=logged_in,
limit=limit + 1,
offset=offset,
)
next_offset, prev_offset = None, None
if len(temes) > limit:
next_offset = offset + limit
temes = temes[:limit]
if offset > 0:
prev_offset = max(offset - limit, 0)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/temes/results.html", "fragments/temes/results.html",
{ {
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
"temes": temes, "temes": temes,
"query": query,
"prev_offset": prev_offset,
"next_offset": next_offset,
"LinkType": model.LinkType, "LinkType": model.LinkType,
"ContentType": model.ContentType, "ContentType": model.ContentType,
} }

View File

@@ -0,0 +1,77 @@
import dataclasses
from collections.abc import Iterator
from typing import Optional, Self
from folkugat_web.model.temes import Tema
from folkugat_web.utils import groupby
@dataclasses.dataclass
class PlaylistEntry:
id: Optional[int]
session_id: int
set_id: int
tema_id: Optional[int]
@dataclasses.dataclass
class TemaInSet:
id: Optional[int]
tema_id: Optional[int]
tema: Optional[Tema]
def to_playlist_entry(self, session_id: int, set_id: int) -> PlaylistEntry:
return PlaylistEntry(
id=self.id,
session_id=session_id,
set_id=set_id,
tema_id=self.tema_id,
)
@classmethod
def from_playlist_entry(cls, entry: PlaylistEntry) -> Self:
return cls(id=entry.id, tema_id=entry.tema_id, tema=None)
@dataclasses.dataclass
class Set:
id: int
temes: list[TemaInSet]
def to_playlist_entries(self, session_id) -> Iterator[PlaylistEntry]:
for tema_in_set in self.temes:
yield tema_in_set.to_playlist_entry(
session_id=session_id,
set_id=self.id,
)
@classmethod
def from_playlist_entries(cls, set_id: int, entries: list[PlaylistEntry]) -> Self:
if any(entry.set_id != set_id for entry in entries):
raise ValueError("All PlaylistEntries must have the same session_id")
return cls(
id=set_id,
temes=[TemaInSet.from_playlist_entry(entry) for entry in entries],
)
@dataclasses.dataclass
class Playlist:
session_id: int
sets: list[Set]
def to_playlist_entries(self) -> Iterator[PlaylistEntry]:
for set_entry in self.sets:
yield from set_entry.to_playlist_entries(session_id=self.session_id)
@classmethod
def from_playlist_entries(cls, session_id: int, entries: list[PlaylistEntry]) -> Self:
if any(entry.session_id != session_id for entry in entries):
raise ValueError("All PlaylistEntries must have the same session_id")
return cls(
session_id=session_id,
sets=[
Set.from_playlist_entries(set_id, set_entries)
for set_id, set_entries in groupby(entries, key_fn=lambda e: e.set_id, group_fn=list)
],
)

View File

@@ -125,4 +125,4 @@ class Tema:
return self return self
def score(self) -> Optional[Link]: def score(self) -> Optional[Link]:
return next(filter(lambda l: l.content_type is ContentType.PARTITURA, self.links), None) return next(filter(lambda l: l.content_type is ContentType.PARTITURA and l.url.startswith('/'), self.links), None)

View File

@@ -0,0 +1,78 @@
from typing import Optional
from folkugat_web.dal.sql import Connection
from folkugat_web.dal.sql._connection import get_connection
from folkugat_web.dal.sql.playlists import query, write
from folkugat_web.model import playlists
from folkugat_web.services.temes import query as temes_query
def add_temes_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist:
for set_ in playlist.sets:
add_temes_to_set(set_)
return playlist
def add_temes_to_set(set_: playlists.Set) -> playlists.Set:
for tema_in_set in set_.temes:
add_tema_to_tema_in_set(tema_in_set)
return set_
def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaInSet:
if tema_in_set.tema_id is not None:
tema_in_set.tema = temes_query.get_tema_by_id(tema_in_set.tema_id)
return tema_in_set
def get_playlist(session_id: int, con: Optional[Connection] = None) -> playlists.Playlist:
return playlists.Playlist.from_playlist_entries(
session_id=session_id,
entries=list(query.get_playlist_entries(session_id=session_id, con=con))
)
def add_set(session_id: int, con: Optional[Connection] = None) -> playlists.Set:
with get_connection(con) as con:
curr_playlist = get_playlist(session_id=session_id, con=con)
new_set_id = max([set_entry.id for set_entry in curr_playlist.sets], default=0) + 1
new_entry = playlists.PlaylistEntry(id=None, session_id=session_id, set_id=new_set_id, tema_id=None)
inserted_entry = write.insert_playlist_entry(new_entry)
return playlists.Set.from_playlist_entries(set_id=inserted_entry.set_id, entries=[inserted_entry])
def get_set(session_id: int, set_id: int, con: Optional[Connection] = None) -> Optional[playlists.Set]:
entries = list(query.get_playlist_entries(session_id=session_id, set_id=set_id, con=con))
if entries:
return playlists.Set.from_playlist_entries(set_id=set_id, entries=entries)
else:
return None
def delete_set(session_id: int, set_id: int, con: Optional[Connection] = None):
write.delete_playlist_set(session_id=session_id, set_id=set_id, con=con)
def add_tema(session_id: int, set_id: int, con: Optional[Connection] = None) -> playlists.TemaInSet:
with get_connection(con) as con:
new_entry = playlists.PlaylistEntry(id=None, session_id=session_id, set_id=set_id, tema_id=None)
inserted_entry = write.insert_playlist_entry(new_entry)
return playlists.TemaInSet.from_playlist_entry(inserted_entry)
def get_tema(entry_id: int, con: Optional[Connection] = None) -> playlists.TemaInSet:
with get_connection(con) as con:
entry = next(query.get_playlist_entries(entry_id=entry_id))
return playlists.TemaInSet.from_playlist_entry(entry)
def delete_tema(entry_id: int, con: Optional[Connection] = None):
with get_connection(con) as con:
write.delete_playlist_entry(entry_id=entry_id, con=con)
def set_tema(session_id: int, set_id: int, entry_id: int, tema_id: Optional[int],
con: Optional[Connection] = None):
with get_connection(con) as con:
new_entry = playlists.PlaylistEntry(id=entry_id, session_id=session_id, set_id=set_id, tema_id=tema_id)
write.update_playlist_entry(entry=new_entry, con=con)

View File

@@ -55,7 +55,7 @@ def _thread(it: Iterable[T], *funcs: Callable[[Iterable], Iterable]) -> Iterable
return functools.reduce(lambda i, fn: fn(i), funcs, it) return functools.reduce(lambda i, fn: fn(i), funcs, it)
def busca_temes(query: str, hidden: bool = False, limit: int = 20, offset: int = 0) -> list[model.Tema]: def busca_temes(query: str, hidden: bool = False, limit: int = 10, offset: int = 0) -> list[model.Tema]:
t0 = time.time() t0 = time.time()
with get_connection() as con: with get_connection() as con:
result = _thread( result = _thread(

View File

@@ -7,11 +7,12 @@ from folkugat_web.model import temes as model
def create_tema(title: str = "") -> model.Tema: def create_tema(title: str = "") -> model.Tema:
return temes_w.insert_tema(tema=model.Tema(title=title)) new_tema = model.Tema(title=title, hidden=False).with_ngrams()
return temes_w.insert_tema(tema=new_tema)
def delete_tema(tema_id: int) -> None: def delete_tema(tema_id: int) -> None:
tema = temes_w.delete_tema(tema_id=tema_id) temes_w.delete_tema(tema_id=tema_id)
def update_title(tema_id: int, title: str) -> model.Tema: def update_title(tema_id: int, title: str) -> model.Tema:
@@ -20,9 +21,7 @@ def update_title(tema_id: int, title: str) -> 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}!")
new_tema = dataclasses.replace(tema, title=title) new_tema = dataclasses.replace(tema, title=title).with_ngrams()
new_tema.compute_ngrams()
temes_w.update_tema(tema=new_tema, con=con) temes_w.update_tema(tema=new_tema, con=con)
return new_tema return new_tema