Compare commits
8 Commits
43706af7c4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
443c1b1391 | ||
|
|
4e7a6b18d6 | ||
|
|
c0624d1e56 | ||
|
|
fdcda1b566 | ||
|
|
ac79785cf0 | ||
|
|
089e61fb0b | ||
|
|
56ab91bd42 | ||
|
|
5428d49e89 |
24
AGENTS.md
Normal file
24
AGENTS.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Development Commands
|
||||
|
||||
## Build/Lint/Test
|
||||
- **Run development server**: `JWT_SECRET=12345 uvicorn folkugat_web.main:app --reload --reload-include "*.html" --host 0.0.0.0`
|
||||
- **Build for production**: `./build.sh`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Python
|
||||
- **Type hints**: Use strict typing with `from typing import` and custom types from `folkugat_web.typing`
|
||||
- **Imports**: Standard library first, third-party, then local imports. Use absolute imports for local modules.
|
||||
- **Naming**: snake_case for variables/functions, PascalCase for classes, UPPER_CASE for constants
|
||||
- **Error handling**: Use Result[ResultT, ErrorT] pattern from utils for service functions
|
||||
- **Async**: Use async/await for I/O operations, FastAPI endpoints are async
|
||||
|
||||
### Frontend
|
||||
- **CSS**: Use Tailwind CSS classes with custom brown/beige theme
|
||||
- **Templates**: Jinja2 templates in folkugat_web/assets/templates/
|
||||
- **Static files**: Organized in folkugat_web/assets/static/
|
||||
|
||||
### Architecture
|
||||
- **Structure**: api/routes/, config/, dal/, fragments/, model/, services/
|
||||
- **Database**: SQLite with custom DAL layer in dal/sql/
|
||||
- **Authentication**: JWT-based with cookie storage
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1753432016,
|
||||
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
|
||||
"lastModified": 1766125104,
|
||||
"narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6027c30c8e9810896b92429f0092f624f7b1aace",
|
||||
"rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
|
||||
from . import auth, index, playlist, sessio, sessions, tema, temes
|
||||
from . import auth, index, llista, llistes, sessio, sessions, tema, temes
|
||||
|
||||
router = get_router()
|
||||
|
||||
router.include_router(auth.router)
|
||||
router.include_router(index.router)
|
||||
router.include_router(playlist.router)
|
||||
router.include_router(llista.router)
|
||||
router.include_router(llistes.router)
|
||||
router.include_router(sessio.router)
|
||||
router.include_router(sessions.router)
|
||||
router.include_router(tema.router)
|
||||
|
||||
@@ -1,15 +1,77 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Form, Request
|
||||
from fastapi import Form, HTTPException, Request
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import playlist
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services import playlists as playlists_service
|
||||
from folkugat_web.services.temes import write as temes_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.post("/api/playlist/{playlist_id}/set")
|
||||
@router.get("/llista/{playlist_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
):
|
||||
playlist = playlists_service.get_playlist(playlist_id=playlist_id)
|
||||
if not playlist:
|
||||
raise HTTPException(status_code=404, detail="Could not find playlist")
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat - Setlist",
|
||||
"page_description": playlist.name or "Llista de temes",
|
||||
"page_card": None,
|
||||
"content": f"/api/content/llista/{playlist_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/llista/{playlist_id}")
|
||||
async def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
):
|
||||
return await playlist.pagina(request, playlist_id, logged_in)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/name")
|
||||
def get_name(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
playlist_id: int,
|
||||
):
|
||||
return playlist.name(request=request, playlist_id=playlist_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.get("/api/llista/{playlist_id}/editor/name")
|
||||
def name_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
):
|
||||
return playlist.name_editor(request=request, playlist_id=playlist_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/name")
|
||||
def set_name(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
name: Annotated[str, Form()],
|
||||
):
|
||||
_ = playlists_service.update_name(playlist_id=playlist_id, name=name)
|
||||
return playlist.name(request=request, playlist_id=playlist_id, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.post("/api/llista/{playlist_id}/set")
|
||||
def add_set(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -22,7 +84,7 @@ def add_set(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/playlist/{playlist_id}/set/{set_id}")
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}")
|
||||
def get_set(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
@@ -37,7 +99,7 @@ def get_set(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/api/playlist/{playlist_id}/set/{set_id}")
|
||||
@router.delete("/api/llista/{playlist_id}/set/{set_id}")
|
||||
def delete_set(
|
||||
_: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
@@ -49,7 +111,7 @@ def delete_set(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/playlist/{playlist_id}/set/{set_id}")
|
||||
@router.post("/api/llista/{playlist_id}/set/{set_id}")
|
||||
def add_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -64,7 +126,7 @@ def add_tema(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def get_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -81,7 +143,7 @@ def get_tema(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}/editor")
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/editor")
|
||||
def get_tema_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -98,7 +160,7 @@ def get_tema_editor(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
@router.delete("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def delete_tema(
|
||||
_: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
@@ -112,7 +174,7 @@ def delete_tema(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}/busca")
|
||||
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/busca")
|
||||
def busca_tema(
|
||||
request: Request,
|
||||
_: auth.RequireLogin,
|
||||
@@ -130,7 +192,7 @@ def busca_tema(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def set_tema(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -149,7 +211,7 @@ def set_tema(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}/unknown")
|
||||
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/unknown")
|
||||
def set_tema_unknown(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -167,7 +229,7 @@ def set_tema_unknown(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/playlist/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
@router.post("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
|
||||
def set_tema_new(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
@@ -185,3 +247,23 @@ def set_tema_new(
|
||||
entry_id=entry_id,
|
||||
tema_id=new_tema.id,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/visible")
|
||||
def set_visible(request: Request, logged_in: auth.RequireLogin, playlist_id: int):
|
||||
new_playlist = playlists_service.set_visibility(playlist_id=playlist_id, hidden=False)
|
||||
return playlist.visibility(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist=new_playlist,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/invisible")
|
||||
def set_invisible(request: Request, logged_in: auth.RequireLogin, playlist_id: int):
|
||||
new_playlist = playlists_service.set_visibility(playlist_id=playlist_id, hidden=True)
|
||||
return playlist.visibility(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist=new_playlist,
|
||||
)
|
||||
@@ -17,7 +17,7 @@ def set_description(set_: playlists.Set) -> str:
|
||||
]))
|
||||
|
||||
|
||||
@router.get("/playlist/{playlist_id}/set/{set_id}")
|
||||
@router.get("/llista/{playlist_id}/set/{set_id}")
|
||||
def page(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
@@ -34,13 +34,13 @@ def page(
|
||||
"request": request,
|
||||
"page_title": "Folkugat - Set",
|
||||
"page_description": set_description(set_=set_),
|
||||
"content": f"/api/content/playlist/{playlist_id}/set/{set_id}",
|
||||
"content": f"/api/content/llista/{playlist_id}/set/{set_id}",
|
||||
"logged_in": logged_in,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/playlist/{playlist_id}/set/{set_id}")
|
||||
@router.get("/api/content/llista/{playlist_id}/set/{set_id}")
|
||||
async def contingut(
|
||||
request: Request,
|
||||
logged_in: auth.LoggedIn,
|
||||
5
folkugat_web/api/routes/llistes/__init__.py
Normal file
5
folkugat_web/api/routes/llistes/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from folkugat_web.api.router import get_router
|
||||
from . import index
|
||||
|
||||
router = get_router()
|
||||
router.include_router(index.router)
|
||||
36
folkugat_web/api/routes/llistes/index.py
Normal file
36
folkugat_web/api/routes/llistes/index.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.api.router import get_router
|
||||
from folkugat_web.fragments import llistes
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
router = get_router()
|
||||
|
||||
|
||||
@router.get("/llistes")
|
||||
def page(request: Request, logged_in: auth.LoggedIn):
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat",
|
||||
"content": "/api/content/llistes",
|
||||
"logged_in": logged_in,
|
||||
"animate": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/content/llistes")
|
||||
def content(request: Request, logged_in: auth.LoggedIn):
|
||||
return llistes.llistes_pagina(request, logged_in)
|
||||
|
||||
|
||||
@router.post("/api/llistes/editor/")
|
||||
def insert_row(request: Request, _: auth.RequireLogin):
|
||||
return llistes.llistes_editor_insert_row(request)
|
||||
|
||||
|
||||
@router.delete("/api/llistes/{playlist_id}")
|
||||
def delete_playlist(playlist_id: int, _: auth.RequireLogin):
|
||||
return llistes.llistes_editor_delete_row(playlist_id)
|
||||
@@ -21,18 +21,31 @@ from folkugat_web.utils import FnChain
|
||||
router = get_router()
|
||||
|
||||
|
||||
def _build_page_description(tema: Tema) -> str:
|
||||
info_lines = []
|
||||
if composer := tema.composer():
|
||||
info_lines.append(f"Autor: {composer}")
|
||||
if origin := tema.origin():
|
||||
info_lines.append(f"Orígen: {origin}")
|
||||
return "\n".join(info_lines)
|
||||
|
||||
|
||||
@router.get("/tema/{tema_id}")
|
||||
def page(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
if not tema:
|
||||
raise HTTPException(status_code=404, detail="Could not find tune")
|
||||
tema = scores_service.add_scores_to_tema(tema)
|
||||
tema = (
|
||||
FnChain.transform(tema) |
|
||||
scores_service.add_scores_to_tema |
|
||||
properties_service.add_properties_to_tema
|
||||
).result()
|
||||
return templates.TemplateResponse(
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"page_title": "Folkugat - Tema",
|
||||
"page_description": tema.title,
|
||||
"page_title": f"Folkugat - {tema.title}",
|
||||
"page_description": _build_page_description(tema),
|
||||
"page_card": "",
|
||||
"content": f"/api/tema/{tema_id}",
|
||||
"logged_in": logged_in,
|
||||
|
||||
@@ -33,11 +33,25 @@ def set_lyric(
|
||||
lyric_id: int,
|
||||
title: Annotated[str, Form()],
|
||||
content: Annotated[str, Form()],
|
||||
max_columns: Annotated[str | None, Form()] = None,
|
||||
):
|
||||
lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
|
||||
if not lyric:
|
||||
raise HTTPException(status_code=404, detail="Could not find lyric!")
|
||||
new_lyric = lyrics_service.update_lyric(lyric=lyric, title=title, content=content)
|
||||
|
||||
# Parse max_columns from string to int if provided
|
||||
max_columns_int = None
|
||||
if max_columns is not None and max_columns.strip():
|
||||
try:
|
||||
max_columns_int = int(max_columns.strip())
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="max_columns must be a valid integer")
|
||||
|
||||
try:
|
||||
new_lyric = lyrics_service.update_lyric(lyric=lyric, title=title, content=content, max_columns=max_columns_int)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
return lyrics_fragments.lyric(request=request, logged_in=logged_in, lyric=new_lyric)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
||||
<div class="flex flex-row flex-wrap justify-center"
|
||||
id="llista-name">
|
||||
<form>
|
||||
<input name="name"
|
||||
placeholder="Nom de la playlist"
|
||||
value="{{ playlist.name if playlist.name else '' }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded text-3xl
|
||||
bg-brown p-2 m-0"
|
||||
/>
|
||||
<button title="Desa"
|
||||
class="text-beige text-3xl mx-1"
|
||||
hx-put="/api/llista/{{ playlist.id }}/name"
|
||||
hx-target="#llista-name"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button title="Descarta"
|
||||
class="text-beige text-3xl mx-1"
|
||||
hx-get="/api/llista/{{ playlist.id }}/name"
|
||||
hx-target="#llista-name"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
20
folkugat_web/assets/templates/fragments/llista/name.html
Normal file
20
folkugat_web/assets/templates/fragments/llista/name.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="flex flex-row flex-wrap justify-center"
|
||||
id="llista-name">
|
||||
<h3 class="text-3xl text-center p-4">
|
||||
{% if playlist.name %}
|
||||
{{ playlist.name }}
|
||||
{% else %}
|
||||
Llista de temes
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% if logged_in %}
|
||||
<button title="Canvia el nom"
|
||||
class="text-beige text-2xl mx-2"
|
||||
hx-get="/api/llista/{{ playlist.id }}/editor/name"
|
||||
hx-target="#llista-name"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% include "fragments/llista/visibility.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
14
folkugat_web/assets/templates/fragments/llista/pagina.html
Normal file
14
folkugat_web/assets/templates/fragments/llista/pagina.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
<div class="flex justify-center">
|
||||
<div class="m-12 grow max-w-4xl">
|
||||
<div id="playlist-name">
|
||||
{% include "fragments/llista/name.html" %}
|
||||
</div>
|
||||
<div class="text-left">
|
||||
{% set playlist_id = playlist.id %}
|
||||
{% set playlist = playlist %}
|
||||
{% include "fragments/llista/playlist.html" %}
|
||||
</div>
|
||||
{% include "fragments/llista/score.html" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,13 @@
|
||||
<ul id="playlist-{{ playlist_id }}" class="">
|
||||
<ul id="llista-{{ playlist_id }}" class="">
|
||||
{% for set_entry in playlist.sets %}
|
||||
{% set set_id = set_entry.id %}
|
||||
{% include "fragments/playlist/set_entry.html" %}
|
||||
{% endfor %}
|
||||
{% include "fragments/llista/set_entry.html" %} {% endfor %}
|
||||
</ul>
|
||||
{% if logged_in %}
|
||||
<div class="flex flex-col items-center">
|
||||
<button class="text-beige mt-2"
|
||||
hx-post="/api/playlist/{{ playlist_id }}/set"
|
||||
hx-target="#playlist-{{ playlist_id }}"
|
||||
hx-post="/api/llista/{{ playlist_id }}/set"
|
||||
hx-target="#llista-{{ playlist_id }}"
|
||||
hx-swap="beforeend transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix set
|
||||
@@ -0,0 +1,9 @@
|
||||
{% if playlist.playlist_score is not none and playlist.playlist_score.pdf_url is not none %}
|
||||
<div class="flex flex-col items-center mt-4 mb-6">
|
||||
<div class="bg-beige border rounded border-beige m-2 p-2">
|
||||
<a href="{{ playlist.playlist_score.pdf_url }}" target="_blank" class="text-white">
|
||||
Obre en PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
{% include "fragments/llista/set/set_page.html" %}
|
||||
@@ -13,14 +13,14 @@
|
||||
{% for tema_in_set in set.temes %}
|
||||
{% if tema_in_set.tema is not none %}
|
||||
{% set tema = tema_in_set.tema %}
|
||||
{% include "fragments/playlist/set/tema_title.html" %}
|
||||
{% include "fragments/llista/set/tema_title.html" %}
|
||||
{% else %}
|
||||
<h3 class="text-center text-3xl p-4"> <i>Desconegut</i> </h3>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="mx-12">
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
{% include "fragments/playlist/set/set_score.html"%}
|
||||
{% include "fragments/llista/set/set_score.html"%}
|
||||
</div>
|
||||
{% else %}
|
||||
{% for tema_in_set in set.temes %}
|
||||
@@ -31,7 +31,7 @@
|
||||
{% endif %}
|
||||
{% if tema_in_set.tema is not none %}
|
||||
{% set tema = tema_in_set.tema %}
|
||||
{% include "fragments/playlist/set/tema.html"%}
|
||||
{% include "fragments/llista/set/tema.html"%}
|
||||
{% else %}
|
||||
<h3 class="text-center text-3xl p-4">
|
||||
<i>Desconegut</i>
|
||||
@@ -1,4 +1,4 @@
|
||||
{% include "fragments/playlist/set/tema_title.html" %}
|
||||
{% include "fragments/llista/set/tema_title.html" %}
|
||||
<div class="mx-12 text-left">
|
||||
|
||||
{% if tema.main_score() is not none %}
|
||||
@@ -2,12 +2,12 @@
|
||||
m-4 rounded-lg bg-white
|
||||
px-2 py-1 my-1"
|
||||
id="set-entry-{{ set_id }}"
|
||||
hx-get="/api/playlist/{{ playlist_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="reload-set-{{ set_id }}">
|
||||
hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
|
||||
hx-target="#set-entry-{{ set_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="reload-set-{{ set_id }}">
|
||||
{% if set_entry.temes | length > 1 or not set_entry.temes[0].tema %}
|
||||
{% set set_url = "/playlist/%d/set/%d" | format(playlist_id, set_id) %}
|
||||
{% set set_url = "/llista/%d/set/%d" | format(playlist_id, set_id) %}
|
||||
{% else %}
|
||||
{% set set_url = "/tema/%d" | format(set_entry.temes[0].tema_id) %}
|
||||
{% endif %}
|
||||
@@ -24,27 +24,27 @@
|
||||
py-2 mx-2
|
||||
w-full max-w-[655px]">
|
||||
{% if logged_in %}
|
||||
<button class="text-beige w-full"
|
||||
hx-delete="/api/playlist/{{ playlist_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-start w-full">
|
||||
{% for tema_entry in set_entry.temes %}
|
||||
{% if new_entry %}
|
||||
{% include "fragments/playlist/tema_editor.html" %}
|
||||
{% else %}
|
||||
{% include "fragments/playlist/tema_entry.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% if logged_in %}
|
||||
<button class="text-beige mt-2 w-full"
|
||||
hx-post="/api/playlist/{{ playlist_id }}/set/{{ set_id }}"
|
||||
<button class="text-beige w-full"
|
||||
hx-delete="/api/llista/{{ playlist_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-start w-full">
|
||||
{% for tema_entry in set_entry.temes %}
|
||||
{% if new_entry %}
|
||||
{% include "fragments/llista/tema_editor.html" %}
|
||||
{% else %}
|
||||
{% include "fragments/llista/tema_entry.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% if logged_in %}
|
||||
<button class="text-beige mt-2 w-full"
|
||||
hx-post="/api/llista/{{ playlist_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>
|
||||
@@ -12,13 +12,13 @@
|
||||
class="border border-beige focus:outline-none
|
||||
rounded text-center text-black
|
||||
p-1 m-1"
|
||||
hx-get="/api/playlist/{{ playlist_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"/>
|
||||
hx-get="/api/llista/{{ playlist_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/playlist/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
|
||||
hx-get="/api/llista/{{ playlist_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>
|
||||
@@ -15,14 +15,14 @@
|
||||
<div class="flex-none flex flex-row shrink-0">
|
||||
<button title="Edita el tema"
|
||||
class="text-beige mx-1"
|
||||
hx-get="/api/playlist/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/editor"
|
||||
hx-get="/api/llista/{{ playlist_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/playlist/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}"
|
||||
hx-delete="/api/llista/{{ playlist_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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<li>
|
||||
<button class="bg-beige text-brown rounded
|
||||
m-1 px-2"
|
||||
hx-put="/api/playlist/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-put="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-vals='{"tema_id": "{{ tema.id }}"}'
|
||||
hx-target="#tune-entry-{{ entry_id }}"
|
||||
hx-swap="outerHTML">
|
||||
@@ -15,7 +15,7 @@
|
||||
<li>
|
||||
<button class="border border-beige text-beige rounded
|
||||
m-1 px-2"
|
||||
hx-put="/api/playlist/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}/unknown"
|
||||
hx-put="/api/llista/{{ playlist_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>
|
||||
@@ -25,7 +25,7 @@
|
||||
<li>
|
||||
<button class="border border-beige text-beige rounded
|
||||
m-1 px-2"
|
||||
hx-post="/api/playlist/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-post="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
|
||||
hx-vals='{"title": "{{ query }}"}'
|
||||
hx-target="#tune-entry-{{ entry_id }}"
|
||||
hx-swap="outerHTML">
|
||||
@@ -0,0 +1,15 @@
|
||||
{% if not playlist.hidden %}
|
||||
<button title="Llista visible (amaga-la)"
|
||||
class="text-beige text-2xl mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/invisible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button title="Llista invisible (mostra-la)"
|
||||
class="text-beige text-2xl mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/visible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye-slash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
26
folkugat_web/assets/templates/fragments/llistes/pagina.html
Normal file
26
folkugat_web/assets/templates/fragments/llistes/pagina.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
<div class="p-12 text-center">
|
||||
<h3 class="text-3xl text-beige p-4">Llistes de Repertori</h3>
|
||||
{% if logged_in %}
|
||||
<button title="Afegeix una llista"
|
||||
class="text-beige m-2"
|
||||
hx-post="/api/llistes/editor/"
|
||||
hx-target="#{{ playlist_list_id }}"
|
||||
hx-swap="afterbegin transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix una llista
|
||||
</button>
|
||||
{% endif %}
|
||||
<div id="{{ playlist_list_id }}"
|
||||
class="flex flex-col items-center">
|
||||
{% if playlists %}
|
||||
{% for playlist in playlists %}
|
||||
{% include "fragments/llistes/playlist_entry.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<p class="text-beige/70">No hi ha llistes disponibles.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="
|
||||
border rounded border-beige
|
||||
p-2 m-2 w-full max-w-xl
|
||||
relative"
|
||||
id="playlist-entry-{{ playlist.id }}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<p class="text-xl text-white">
|
||||
<a href="/llista/{{ playlist.id }}">
|
||||
{% if playlist.name %}
|
||||
{{ playlist.name }}
|
||||
{% else %}
|
||||
Llista de temes
|
||||
{% endif %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if logged_in %}
|
||||
<div class="ml-auto">
|
||||
{% include "fragments/llistes/playlist_visibility.html" %}
|
||||
<button title="Esborra la llista"
|
||||
class="text-beige mx-1"
|
||||
hx-delete="/api/llistes/{{ playlist.id }}"
|
||||
hx-target="#playlist-entry-{{ playlist.id }}"
|
||||
hx-swap="outerHTML swap:0.5s">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
{% if not playlist.hidden %}
|
||||
<button title="Llista visible (amaga-la)"
|
||||
class="text-beige mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/invisible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button title="Llista invisible (mostra-la)"
|
||||
class="text-beige mx-2"
|
||||
hx-put="/api/llista/{{ playlist.id }}/visible"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-eye-slash" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -27,6 +27,19 @@
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{% if menu_selected_id == Pages.Llistes %}
|
||||
<li class="p-2 text-beige">
|
||||
<button>
|
||||
{% else %}
|
||||
<li class="p-2">
|
||||
<button hx-get="/api/content/llistes"
|
||||
hx-replace-url="/llistes"
|
||||
hx-target="#content">
|
||||
{% endif %}
|
||||
Llistes
|
||||
</button>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<hr class="h-px bg-beige border-0">
|
||||
<div hx-get="/api/sessions/live"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{% include "fragments/menu.html" %}
|
||||
{% include "fragments/playlist/set/set_page.html" %}
|
||||
@@ -39,18 +39,26 @@
|
||||
{% endif %}
|
||||
{% if logged_in or (session.slowjam and session.slowjam.sets) %}
|
||||
<div class="text-left">
|
||||
<h4 class="py-4 text-xl text-beige mt-2">Slow Jam</h4>
|
||||
<h4 class="py-4 text-xl text-beige mt-2">
|
||||
<a href="/llista/{{ session.slowjam.id }}" class="text-beige">
|
||||
Slow Jam
|
||||
</a>
|
||||
</h4>
|
||||
{% set playlist_id = session.slowjam.id %}
|
||||
{% set playlist = session.slowjam %}
|
||||
{% include "fragments/playlist/playlist.html" %}
|
||||
{% include "fragments/llista/playlist.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if logged_in or (session.setlist and session.setlist.sets) %}
|
||||
<div class="text-left">
|
||||
<h4 class="py-4 text-xl text-beige mt-2">Temes tocats</h4>
|
||||
<h4 class="py-4 text-xl text-beige mt-2">
|
||||
<a href="/llista/{{ session.setlist.id }}" class="text-beige">
|
||||
Temes tocats
|
||||
</a>
|
||||
</h4>
|
||||
{% set playlist_id = session.setlist.id %}
|
||||
{% set playlist = session.setlist %}
|
||||
{% include "fragments/playlist/playlist.html" %}
|
||||
{% include "fragments/llista/playlist.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
rounded
|
||||
bg-brown px-2 "
|
||||
/>
|
||||
<input name="max_columns"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
placeholder="3 (default)"
|
||||
value="{{ lyric.max_columns or '' }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded
|
||||
bg-brown px-2 ml-2 w-16 "
|
||||
/>
|
||||
<button title="Desa els canvis"
|
||||
class="mx-1"
|
||||
hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<div id="tema-lyric-{{ lyric.id }}">
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
{{ lyric.title }}
|
||||
{% if logged_in and lyric.max_columns %}
|
||||
<span class="text-xs ml-2">{{ lyric.max_columns }} columnes</span>
|
||||
{% endif %}
|
||||
{% if logged_in %}
|
||||
<button title="Modifica la lletra"
|
||||
class="mx-1"
|
||||
@@ -20,6 +23,6 @@
|
||||
</h5>
|
||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||
<div class="text-center">
|
||||
{{ lyric.content.replace('\n', '<br>') | safe }}
|
||||
{{ (lyric.content.replace('~', '')).replace('\n', '<br>') | safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
49
folkugat_web/assets/templates/lilypond/playlist.ly
Normal file
49
folkugat_web/assets/templates/lilypond/playlist.ly
Normal file
@@ -0,0 +1,49 @@
|
||||
{{ score_beginning }}
|
||||
\version "2.24.4"
|
||||
|
||||
{% include "lilypond/lib.ly" %}
|
||||
|
||||
\book {
|
||||
\paper {
|
||||
top-margin = 10
|
||||
left-margin = 15
|
||||
right-margin = 15
|
||||
ragged-bottom = ##f
|
||||
|
||||
scoreTitleMarkup = \markup {
|
||||
\center-column {
|
||||
\fontsize #3 \bold \fromproperty #'header:piece
|
||||
\fill-line {
|
||||
\null
|
||||
\right-column {
|
||||
\fromproperty #'header:composer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\header {
|
||||
title = \markup { "{{ playlist.title | safe }}" }
|
||||
tagline = "Partitura generada amb LilyPond"
|
||||
copyright = "Folkugat"
|
||||
}
|
||||
|
||||
{% for set in playlist.sets %}
|
||||
{% if set.tunes|length > 1 %}
|
||||
\markup {
|
||||
\vspace #2
|
||||
\fill-line {
|
||||
\center-column {
|
||||
\fontsize #2 \bold "{{ set.title | safe }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
\noPageBreak
|
||||
{% endif %}
|
||||
|
||||
{% for tune in set.tunes %}
|
||||
{% include "lilypond/tune_in_set.ly" %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
\hspace #1
|
||||
\center-column {
|
||||
{% for line in paragraph.lines %}
|
||||
\line { {{ line | safe }} }
|
||||
{% if line %} \line { {{ line | safe }} } {% else %} \vspace #1 {% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
\noPageBreak
|
||||
{% if tune.score_source is not none %}
|
||||
\score {
|
||||
\language "english"
|
||||
|
||||
@@ -14,12 +14,14 @@ DB_FILES_DIR = DB_DIR / "fitxer"
|
||||
DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema"
|
||||
DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio"
|
||||
DB_FILES_SET_DIR = DB_FILES_DIR / "set"
|
||||
DB_FILES_PLAYLIST_DIR = DB_FILES_DIR / "llistes"
|
||||
DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp"
|
||||
|
||||
for path in [
|
||||
DB_FILES_DIR,
|
||||
DB_FILES_TEMA_DIR,
|
||||
DB_FILES_SET_DIR,
|
||||
DB_FILES_PLAYLIST_DIR,
|
||||
DB_FILES_SET_DIR,
|
||||
]:
|
||||
path.mkdir(exist_ok=True)
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import TypedDict
|
||||
|
||||
from folkugat_web.model import playlists as model
|
||||
|
||||
PlaylistRowTuple = tuple[int, str | None]
|
||||
PlaylistRowTuple = tuple[int, str | None, int]
|
||||
PlaylistEntryRowTuple = tuple[int, int, int, int | None]
|
||||
|
||||
|
||||
@@ -29,3 +29,12 @@ def row_to_playlist_entry(row: PlaylistEntryRowTuple) -> model.PlaylistEntry:
|
||||
set_id=row[2],
|
||||
tema_id=row[3],
|
||||
)
|
||||
|
||||
|
||||
def row_to_playlist(row: PlaylistRowTuple) -> model.Playlist:
|
||||
return model.Playlist(
|
||||
id=row[0],
|
||||
name=row[1],
|
||||
sets=[], # Empty for list view
|
||||
hidden=bool(row[2]),
|
||||
)
|
||||
|
||||
@@ -11,8 +11,9 @@ def create_playlists_table(con: Connection):
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS playlists (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT
|
||||
)
|
||||
name TEXT,
|
||||
hidden INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
|
||||
@@ -55,3 +55,56 @@ def get_playlist_entries(
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return map(conversion.row_to_playlist_entry, cur.fetchall())
|
||||
|
||||
|
||||
def get_playlist_name(playlist_id: int, con: Connection | None = None) -> str | None:
|
||||
query = """
|
||||
SELECT name
|
||||
FROM playlists
|
||||
WHERE id = :playlist_id
|
||||
"""
|
||||
data = dict(playlist_id=playlist_id)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def get_all_playlists(logged_in: bool = False, con: Connection | None = None) -> Iterator[model.Playlist]:
|
||||
if logged_in:
|
||||
# Show all playlists for logged in users, except session-associated ones
|
||||
query = """
|
||||
SELECT id, name, hidden
|
||||
FROM playlists
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT playlist_id
|
||||
FROM session_playlists
|
||||
)
|
||||
ORDER BY id DESC
|
||||
"""
|
||||
else:
|
||||
# Show only visible playlists for non-logged in users, except session-associated ones
|
||||
query = """
|
||||
SELECT id, name, hidden
|
||||
FROM playlists
|
||||
WHERE hidden = 0 AND id NOT IN (
|
||||
SELECT DISTINCT playlist_id
|
||||
FROM session_playlists
|
||||
)
|
||||
ORDER BY id DESC
|
||||
"""
|
||||
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
|
||||
for row in rows:
|
||||
# Convert to Playlist model without sets for list view
|
||||
yield model.Playlist(
|
||||
id=row[0],
|
||||
name=row[1],
|
||||
sets=[], # Empty sets list for now
|
||||
hidden=bool(row[2])
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ def delete_playlist(
|
||||
DELETE FROM playlists
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(playlist_id=playlist_id)
|
||||
data = dict(id=playlist_id)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
@@ -103,3 +103,37 @@ def delete_playlist_set(
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return
|
||||
|
||||
|
||||
def update_playlist_name(
|
||||
playlist_id: int,
|
||||
name: str | None,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
UPDATE playlists SET
|
||||
name = :name
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(id=playlist_id, name=name)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return
|
||||
|
||||
|
||||
def update_playlist_visibility(
|
||||
playlist_id: int,
|
||||
hidden: bool,
|
||||
con: Connection | None = None,
|
||||
):
|
||||
query = """
|
||||
UPDATE playlists SET
|
||||
hidden = :hidden
|
||||
WHERE id = :id
|
||||
"""
|
||||
data = dict(id=playlist_id, hidden=1 if hidden else 0)
|
||||
with get_connection(con) as con:
|
||||
cur = con.cursor()
|
||||
_ = cur.execute(query, data)
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TypedDict
|
||||
from folkugat_web.dal.sql import Connection, get_connection
|
||||
from folkugat_web.model import temes as model
|
||||
|
||||
LyricRowTuple = tuple[int, int, str, str]
|
||||
LyricRowTuple = tuple[int, int, str, str, int | None]
|
||||
|
||||
|
||||
class LyricRowDict(TypedDict):
|
||||
@@ -12,6 +12,7 @@ class LyricRowDict(TypedDict):
|
||||
tema_id: int
|
||||
title: str
|
||||
content: str
|
||||
max_columns: int | None
|
||||
|
||||
|
||||
def lyric_to_row(lyric: model.Lyrics) -> LyricRowDict:
|
||||
@@ -20,6 +21,7 @@ def lyric_to_row(lyric: model.Lyrics) -> LyricRowDict:
|
||||
"tema_id": lyric.tema_id,
|
||||
"title": lyric.title,
|
||||
"content": lyric.content,
|
||||
"max_columns": lyric.max_columns,
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ def row_to_lyric(row: LyricRowTuple) -> model.Lyrics:
|
||||
tema_id=row[1],
|
||||
title=row[2],
|
||||
content=row[3],
|
||||
max_columns=row[4],
|
||||
)
|
||||
|
||||
|
||||
@@ -63,7 +66,7 @@ def get_lyrics(lyric_id: int | None = None, tema_id: int | None = None, con: Con
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
id, tema_id, title, content
|
||||
id, tema_id, title, content, max_columns
|
||||
FROM tema_lyrics
|
||||
{filter_clause}
|
||||
"""
|
||||
@@ -77,9 +80,9 @@ def insert_lyric(lyric: model.Lyrics, con: Connection | None = None) -> model.Ly
|
||||
data = lyric_to_row(lyric)
|
||||
query = f"""
|
||||
INSERT INTO tema_lyrics
|
||||
(id, tema_id, title, content)
|
||||
(id, tema_id, title, content, max_columns)
|
||||
VALUES
|
||||
(:id, :tema_id, :title, :content)
|
||||
(:id, :tema_id, :title, :content, :max_columns)
|
||||
RETURNING *
|
||||
"""
|
||||
with get_connection(con) as con:
|
||||
@@ -95,6 +98,7 @@ def create_lyric(tema_id: int, title: str | None = None, con: Connection | None
|
||||
tema_id=tema_id,
|
||||
title=title or "",
|
||||
content="",
|
||||
max_columns=None,
|
||||
)
|
||||
return insert_lyric(new_lyric, con=con)
|
||||
|
||||
@@ -104,7 +108,7 @@ def update_lyric(lyric: model.Lyrics, con: Connection | None = None):
|
||||
query = """
|
||||
UPDATE tema_lyrics
|
||||
SET
|
||||
tema_id = :tema_id, title = :title, content = :content
|
||||
tema_id = :tema_id, title = :title, content = :content, max_columns = :max_columns
|
||||
WHERE
|
||||
id = :id
|
||||
"""
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from folkugat_web.fragments import llistes
|
||||
42
folkugat_web/fragments/llistes.py
Normal file
42
folkugat_web/fragments/llistes.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import Request
|
||||
from folkugat_web.model import playlists as playlists_model
|
||||
from folkugat_web.model.pagines import Pages
|
||||
from folkugat_web.services import playlists as playlists_service
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
|
||||
def llistes_pagina(request: Request, logged_in: bool):
|
||||
playlists = playlists_service.get_all_playlists(logged_in=logged_in)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/llistes/pagina.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"playlists": playlists,
|
||||
"Pages": Pages,
|
||||
"menu_selected_id": Pages.Llistes,
|
||||
"playlist_list_id": "playlist-list",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def llistes_editor_insert_row(request: Request):
|
||||
new_playlist = playlists_service.create_playlist(name=playlists_model.DEFAULT_PLAYLIST_NAME)
|
||||
return llistes_editor_row(request, new_playlist)
|
||||
|
||||
|
||||
def llistes_editor_row(request: Request, playlist: playlists_model.Playlist):
|
||||
return templates.TemplateResponse(
|
||||
"fragments/llistes/playlist_entry.html",
|
||||
{
|
||||
"request": request,
|
||||
"playlist": playlist,
|
||||
"logged_in": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def llistes_editor_delete_row(playlist_id: int):
|
||||
playlists_service.delete_playlist(playlist_id)
|
||||
from fastapi.responses import HTMLResponse
|
||||
return HTMLResponse()
|
||||
@@ -1,5 +1,7 @@
|
||||
from fastapi import Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.model.pagines import Pages
|
||||
from folkugat_web.model.playlists import PlaylistType
|
||||
from folkugat_web.services import playlists as playlists_service
|
||||
from folkugat_web.services import sessions as sessions_service
|
||||
from folkugat_web.services.temes import query as query_service
|
||||
@@ -14,7 +16,7 @@ def add_set(
|
||||
):
|
||||
new_set = playlists_service.add_set(playlist_id=playlist_id)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/set_entry.html",
|
||||
"fragments/llista/set_entry.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -30,7 +32,7 @@ def get_set(request: Request, playlist_id: int, set_id: int, logged_in: bool):
|
||||
set_entry = playlists_service.get_set(playlist_id=playlist_id, set_id=set_id)
|
||||
if set_entry:
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/set_entry.html",
|
||||
"fragments/llista/set_entry.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -53,7 +55,7 @@ def add_tema(request: Request, playlist_id: int, set_id: int, logged_in: bool):
|
||||
new_tema = playlists_service.add_tema(playlist_id=playlist_id, set_id=set_id)
|
||||
new_tema = playlists_service.add_tema_to_tema_in_set(new_tema)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/tema_editor.html",
|
||||
"fragments/llista/tema_editor.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -68,7 +70,7 @@ def get_tema(request: Request, playlist_id: int, set_id: int, entry_id: int, log
|
||||
tema_entry = playlists_service.get_tema(entry_id=entry_id)
|
||||
tema_entry = playlists_service.add_tema_to_tema_in_set(tema_entry)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/tema_entry.html",
|
||||
"fragments/llista/tema_entry.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -83,7 +85,7 @@ def get_tema_editor(request: Request, playlist_id: int, set_id: int, entry_id: i
|
||||
tema_entry = playlists_service.get_tema(entry_id=entry_id)
|
||||
tema_entry = playlists_service.add_tema_to_tema_in_set(tema_entry)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/tema_editor.html",
|
||||
"fragments/llista/tema_editor.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -144,7 +146,7 @@ def busca_tema(
|
||||
offset=0,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/tema_results.html",
|
||||
"fragments/llista/tema_results.html",
|
||||
{
|
||||
"request": request,
|
||||
"playlist_id": playlist_id,
|
||||
@@ -161,7 +163,7 @@ def set_tema(request: Request, logged_in: bool, playlist_id: int, set_id: int, e
|
||||
tema_entry = playlists_service.get_tema(entry_id=entry_id)
|
||||
tema_entry = playlists_service.add_tema_to_tema_in_set(tema_entry)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/tema_entry.html",
|
||||
"fragments/llista/tema_entry.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -170,3 +172,66 @@ def set_tema(request: Request, logged_in: bool, playlist_id: int, set_id: int, e
|
||||
"tema_entry": tema_entry,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def pagina(request: Request, playlist_id: int, logged_in: bool):
|
||||
playlist = playlists_service.get_playlist(playlist_id=playlist_id)
|
||||
if not playlist:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="Could not find playlist")
|
||||
playlist = playlists_service.add_temes_to_playlist(playlist)
|
||||
playlist = await playlists_service.add_playlist_score_to_playlist(playlist)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/llista/pagina.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"playlist_id": playlist_id,
|
||||
"playlist": playlist,
|
||||
"Pages": Pages,
|
||||
"PlaylistType": PlaylistType
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def name(request: Request, playlist_id: int, logged_in: bool):
|
||||
playlist = playlists_service.get_playlist(playlist_id=playlist_id)
|
||||
if not playlist:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="Could not find playlist")
|
||||
return templates.TemplateResponse(
|
||||
"fragments/llista/name.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"playlist_id": playlist_id,
|
||||
"playlist": playlist,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def name_editor(request: Request, playlist_id: int, logged_in: bool):
|
||||
playlist = playlists_service.get_playlist(playlist_id=playlist_id)
|
||||
if not playlist:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=404, detail="Could not find playlist")
|
||||
return templates.TemplateResponse(
|
||||
"fragments/llista/editor/name.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"playlist_id": playlist_id,
|
||||
"playlist": playlist,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def visibility(request: Request, logged_in: bool, playlist):
|
||||
return templates.TemplateResponse(
|
||||
"fragments/llista/visibility.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"playlist": playlist,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ async def pagina(request: Request, playlist_id: int, set_id: int, logged_in: boo
|
||||
set_ = playlists_service.add_temes_to_set(set_)
|
||||
set_ = await playlists_service.add_set_score_to_set(set_)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/set/pagina.html",
|
||||
"fragments/llista/set/pagina.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -36,7 +36,7 @@ async def live(request: Request, logged_in: bool):
|
||||
set_ = playlists_service.add_temes_to_set(playlist.sets[-1])
|
||||
set_ = await playlists_service.add_set_score_to_set(set_)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/set/pagina.html",
|
||||
"fragments/llista/set/pagina.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
@@ -59,7 +59,7 @@ async def live_set(request: Request, logged_in: bool):
|
||||
set_ = playlists_service.add_temes_to_set(playlist.sets[-1])
|
||||
set_ = await playlists_service.add_set_score_to_set(set_)
|
||||
return templates.TemplateResponse(
|
||||
"fragments/playlist/set/set_page.html",
|
||||
"fragments/llista/set/set_page.html",
|
||||
{
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
|
||||
@@ -32,13 +32,16 @@ class LyricsText:
|
||||
|
||||
@classmethod
|
||||
def from_lyrics(cls, lyrics: temes.Lyrics, max_cols: int = 3) -> Self:
|
||||
# Use stored max_columns if available, otherwise use the provided max_cols
|
||||
effective_max_cols = lyrics.max_columns if lyrics.max_columns is not None else max_cols
|
||||
# Remove ~ characters before processing
|
||||
paragraphs = [
|
||||
LyricsParagraph(lines=par_str.splitlines())
|
||||
LyricsParagraph(lines=[line.replace("~", "") for line in par_str.splitlines()])
|
||||
for par_str in lyrics.content.split("\n\n") if par_str
|
||||
]
|
||||
return cls(lines=[
|
||||
LyricsLine(paragraphs=list(line_pars))
|
||||
for line_pars in batched(paragraphs, max_cols)
|
||||
for line_pars in batched(paragraphs, effective_max_cols)
|
||||
])
|
||||
|
||||
def hash(self) -> bytes:
|
||||
@@ -89,3 +92,15 @@ class LilypondSet:
|
||||
self.title.encode(),
|
||||
*(tune.hash() for tune in self.tunes),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LilypondPlaylist:
|
||||
title: str
|
||||
sets: list[LilypondSet]
|
||||
|
||||
def hash(self) -> bytes:
|
||||
return get_hash(
|
||||
self.title.encode(),
|
||||
*(lilypond_set.hash() for lilypond_set in self.sets),
|
||||
)
|
||||
|
||||
@@ -4,3 +4,4 @@ import enum
|
||||
class Pages(enum.IntEnum):
|
||||
Sessions = 0
|
||||
Temes = 1
|
||||
Llistes = 2
|
||||
|
||||
@@ -6,6 +6,8 @@ from typing import Self
|
||||
from folkugat_web.model.temes import Tema
|
||||
from folkugat_web.utils import groupby
|
||||
|
||||
DEFAULT_PLAYLIST_NAME = "Llista de temes"
|
||||
|
||||
|
||||
class PlaylistType(enum.Enum):
|
||||
SESSION_SETLIST = "session_setlist"
|
||||
@@ -49,6 +51,12 @@ class SetScore:
|
||||
pdf_url: str | None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PlaylistScore:
|
||||
img_url: str | None
|
||||
pdf_url: str | None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Set:
|
||||
id: int
|
||||
@@ -79,18 +87,22 @@ class Set:
|
||||
@dataclasses.dataclass
|
||||
class Playlist:
|
||||
id: int
|
||||
name: str | None
|
||||
sets: list[Set]
|
||||
hidden: bool = True
|
||||
playlist_score: PlaylistScore | None = None
|
||||
|
||||
def to_playlist_entries(self) -> Iterator[PlaylistEntry]:
|
||||
for set_entry in self.sets:
|
||||
yield from set_entry.to_playlist_entries(playlist_id=self.id)
|
||||
|
||||
@classmethod
|
||||
def from_playlist_entries(cls, playlist_id: int, entries: list[PlaylistEntry]) -> Self:
|
||||
def from_playlist_entries(cls, playlist_id: int, name: str | None, entries: list[PlaylistEntry]) -> Self:
|
||||
if any(entry.playlist_id != playlist_id for entry in entries):
|
||||
raise ValueError("All PlaylistEntries must have the same playlist_id")
|
||||
return cls(
|
||||
id=playlist_id,
|
||||
name=name,
|
||||
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)
|
||||
|
||||
@@ -60,6 +60,7 @@ class Lyrics:
|
||||
tema_id: int
|
||||
title: str
|
||||
content: str
|
||||
max_columns: int | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
||||
@@ -100,6 +100,10 @@ def get_set_filename(filename: str) -> Path:
|
||||
return db.DB_FILES_SET_DIR / filename
|
||||
|
||||
|
||||
def get_playlist_filename(filename: str) -> Path:
|
||||
return db.DB_FILES_PLAYLIST_DIR / filename
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def tmp_file(content: str):
|
||||
input_filename = create_tmp_filename(extension=".ly")
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi import HTTPException
|
||||
from folkugat_web.model import playlists as playlists_model
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.model.lilypond import score as lilypond_model
|
||||
from folkugat_web.services import playlists as playlists_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
|
||||
@@ -96,3 +97,20 @@ def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet:
|
||||
title=set_title,
|
||||
tunes=tunes
|
||||
)
|
||||
|
||||
|
||||
def playlist_from_playlist(playlist: playlists_model.Playlist) -> lilypond_model.LilypondPlaylist:
|
||||
"""
|
||||
The playlist is assumed to be enriched with tunes
|
||||
"""
|
||||
lilypond_sets = []
|
||||
for set_entry in playlist.sets:
|
||||
lilypond_set = set_from_set(set_entry)
|
||||
if lilypond_set.tunes and all(map(playlists_service._elegible_for_set_score, lilypond_set.tunes)):
|
||||
lilypond_sets.append(lilypond_set)
|
||||
|
||||
playlist_title = playlist.name or "Llista"
|
||||
return lilypond_model.LilypondPlaylist(
|
||||
title=playlist_title,
|
||||
sets=lilypond_sets
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from folkugat_web.model.lilypond.score import LilypondSet, LilypondTune
|
||||
from folkugat_web.model.lilypond.score import LilypondPlaylist, LilypondSet, LilypondTune
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
SCORE_BEGINNING = "% --- SCORE BEGINNING --- %"
|
||||
@@ -23,3 +23,10 @@ def set_source(tune_set: LilypondSet) -> str:
|
||||
score_beginning=SCORE_BEGINNING,
|
||||
tune_set=tune_set,
|
||||
)
|
||||
|
||||
|
||||
def playlist_source(playlist: LilypondPlaylist) -> str:
|
||||
return templates.get_template("lilypond/playlist.ly").render(
|
||||
score_beginning=SCORE_BEGINNING,
|
||||
playlist=playlist,
|
||||
)
|
||||
|
||||
@@ -41,10 +41,18 @@ def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaI
|
||||
|
||||
|
||||
def get_playlist(playlist_id: int, con: Connection | None = None) -> playlists.Playlist:
|
||||
return playlists.Playlist.from_playlist_entries(
|
||||
playlist_id=playlist_id,
|
||||
entries=list(query.get_playlist_entries(playlist_id=playlist_id, con=con))
|
||||
)
|
||||
with get_connection(con) as playlist_con:
|
||||
playlist_name = query.get_playlist_name(playlist_id=playlist_id, con=playlist_con)
|
||||
return playlists.Playlist.from_playlist_entries(
|
||||
playlist_id=playlist_id,
|
||||
name=playlist_name,
|
||||
entries=list(query.get_playlist_entries(playlist_id=playlist_id, con=playlist_con))
|
||||
)
|
||||
|
||||
|
||||
def update_name(playlist_id: int, name: str | None) -> playlists.Playlist:
|
||||
write.update_playlist_name(playlist_id=playlist_id, name=name)
|
||||
return get_playlist(playlist_id=playlist_id)
|
||||
|
||||
|
||||
def add_set(playlist_id: int, con: Connection | None = None) -> playlists.Set:
|
||||
@@ -162,3 +170,75 @@ async def add_set_score_to_set(tune_set: playlists.Set) -> playlists.Set:
|
||||
)
|
||||
else:
|
||||
return tune_set
|
||||
|
||||
|
||||
async def get_or_create_playlist_score(playlist: playlists.Playlist) -> playlists.PlaylistScore | None:
|
||||
# The playlist is assumed to be enriched with tunes
|
||||
if not playlist.sets:
|
||||
return None
|
||||
|
||||
lilypond_playlist = lilypond_build.playlist_from_playlist(playlist)
|
||||
if not lilypond_playlist.sets:
|
||||
return None
|
||||
|
||||
playlist_score_hash = lilypond_playlist.hash().hex()
|
||||
|
||||
pdf_filepath = files_service.get_playlist_filename(f"{playlist_score_hash}.pdf")
|
||||
|
||||
if not pdf_filepath.exists():
|
||||
# No score exists, so we need to create it
|
||||
playlist_source = lilypond_source.playlist_source(playlist=lilypond_playlist)
|
||||
out_filepath = files_service.get_playlist_filename(playlist_score_hash)
|
||||
async with files_service.tmp_file(content=playlist_source) as source_filepath:
|
||||
if not pdf_filepath.exists():
|
||||
pdf_result = await lilypond_render.render_file(
|
||||
input_file=source_filepath,
|
||||
output=lilypond_render.RenderOutput.PDF,
|
||||
output_file=out_filepath,
|
||||
)
|
||||
if pdf_result.error is not None:
|
||||
return None
|
||||
if pdf_result.result is None:
|
||||
raise RuntimeError("This shouldn't happen")
|
||||
pdf_filepath = pdf_result.result
|
||||
|
||||
return playlists.PlaylistScore(
|
||||
img_url=None, # Only PDF generation for now
|
||||
pdf_url=files_service.get_db_file_path(pdf_filepath),
|
||||
)
|
||||
|
||||
|
||||
async def add_playlist_score_to_playlist(playlist: playlists.Playlist) -> playlists.Playlist:
|
||||
if score := await get_or_create_playlist_score(playlist=playlist):
|
||||
return dataclasses.replace(
|
||||
playlist,
|
||||
playlist_score=score,
|
||||
)
|
||||
else:
|
||||
return playlist
|
||||
|
||||
|
||||
def get_all_playlists(logged_in: bool = False) -> list[playlists.Playlist]:
|
||||
return list(query.get_all_playlists(logged_in=logged_in))
|
||||
|
||||
|
||||
def set_visibility(playlist_id: int, hidden: bool) -> playlists.Playlist:
|
||||
with get_connection() as con:
|
||||
playlist = get_playlist(playlist_id=playlist_id, con=con)
|
||||
if playlist is None:
|
||||
raise ValueError(f"No playlist found with playlist_id = {playlist_id}!")
|
||||
|
||||
# Update hidden status in database
|
||||
write.update_playlist_visibility(playlist_id=playlist_id, hidden=hidden, con=con)
|
||||
|
||||
# Return updated playlist
|
||||
return dataclasses.replace(playlist, hidden=hidden)
|
||||
|
||||
|
||||
def create_playlist(name: str | None = None) -> playlists.Playlist:
|
||||
playlist_id = write.create_playlist(name=name)
|
||||
return get_playlist(playlist_id=playlist_id)
|
||||
|
||||
|
||||
def delete_playlist(playlist_id: int):
|
||||
write.delete_playlist(playlist_id=playlist_id)
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import date as Date
|
||||
|
||||
from folkugat_web.config import date as config
|
||||
from folkugat_web.dal.sql import Connection, get_connection
|
||||
from folkugat_web.dal.sql.playlists import write as playlists_write
|
||||
from folkugat_web.dal.sql.sessions import playlists as session_playlists
|
||||
from folkugat_web.dal.sql.sessions import query, write
|
||||
from folkugat_web.model import sessions as model
|
||||
@@ -66,14 +67,19 @@ def get_session(session_id: int) -> model.Session | None:
|
||||
|
||||
|
||||
def insert_session(session: model.Session):
|
||||
return write.insert_session(session)
|
||||
created_session = write.insert_session(session)
|
||||
if created_session.id is not None:
|
||||
_create_session_playlists(created_session.id)
|
||||
return created_session
|
||||
|
||||
|
||||
def set_session(session: model.Session):
|
||||
write.update_session(session)
|
||||
_update_session_playlist_names(session)
|
||||
|
||||
|
||||
def delete_session(session_id: int):
|
||||
_delete_session_playlists(session_id)
|
||||
write.delete_session_by_id(session_id)
|
||||
|
||||
|
||||
@@ -160,3 +166,104 @@ def get_commonly_played_temes(
|
||||
tema_id: int,
|
||||
) -> list[temes_model.CommonlyPlayedTema]:
|
||||
return session_playlists.get_commonly_played_tunes(tema_id=tema_id)
|
||||
|
||||
|
||||
def _get_playlist_names(session: model.Session) -> tuple[str, str]:
|
||||
date_names = get_date_names(session.date)
|
||||
|
||||
setlist_name = f"Sessió del {date_names.day} {date_names.month_name} de {date_names.year}"
|
||||
slowjam_name = f"Slow Jam del {date_names.day} {date_names.month_name} de {date_names.year}"
|
||||
|
||||
return setlist_name, slowjam_name
|
||||
|
||||
|
||||
def _create_session_playlists(session_id: int):
|
||||
session = get_session(session_id=session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
setlist_name, slowjam_name = _get_playlist_names(session=session)
|
||||
|
||||
setlist_playlist_id = playlists_write.create_playlist(name=setlist_name)
|
||||
slowjam_playlist_id = playlists_write.create_playlist(name=slowjam_name)
|
||||
|
||||
with get_connection() as con:
|
||||
session_playlists.insert_playlist(
|
||||
session_id=session_id,
|
||||
playlist_type=PlaylistType.SESSION_SETLIST,
|
||||
playlist_id=setlist_playlist_id,
|
||||
con=con
|
||||
)
|
||||
session_playlists.insert_playlist(
|
||||
session_id=session_id,
|
||||
playlist_type=PlaylistType.SESSION_SLOWJAM,
|
||||
playlist_id=slowjam_playlist_id,
|
||||
con=con
|
||||
)
|
||||
|
||||
|
||||
def _update_session_playlist_names(session: model.Session):
|
||||
if session.id is None:
|
||||
return
|
||||
|
||||
setlist_name, slowjam_name = _get_playlist_names(session=session)
|
||||
|
||||
with get_connection() as con:
|
||||
setlist_playlist_id = session_playlists.get_playlist_id(
|
||||
session_id=session.id,
|
||||
playlist_type=PlaylistType.SESSION_SETLIST,
|
||||
con=con
|
||||
)
|
||||
slowjam_playlist_id = session_playlists.get_playlist_id(
|
||||
session_id=session.id,
|
||||
playlist_type=PlaylistType.SESSION_SLOWJAM,
|
||||
con=con
|
||||
)
|
||||
|
||||
if setlist_playlist_id is not None:
|
||||
playlists_write.update_playlist_name(
|
||||
playlist_id=setlist_playlist_id,
|
||||
name=setlist_name,
|
||||
con=con
|
||||
)
|
||||
if slowjam_playlist_id is not None:
|
||||
playlists_write.update_playlist_name(
|
||||
playlist_id=slowjam_playlist_id,
|
||||
name=slowjam_name,
|
||||
con=con
|
||||
)
|
||||
|
||||
|
||||
def _delete_session_playlists(session_id: int):
|
||||
with get_connection() as con:
|
||||
setlist_playlist_id = session_playlists.get_playlist_id(
|
||||
session_id=session_id,
|
||||
playlist_type=PlaylistType.SESSION_SETLIST,
|
||||
con=con
|
||||
)
|
||||
slowjam_playlist_id = session_playlists.get_playlist_id(
|
||||
session_id=session_id,
|
||||
playlist_type=PlaylistType.SESSION_SLOWJAM,
|
||||
con=con
|
||||
)
|
||||
|
||||
if setlist_playlist_id is not None:
|
||||
session_playlists.delete_playlist(
|
||||
session_id=session_id,
|
||||
playlist_type=PlaylistType.SESSION_SETLIST,
|
||||
con=con,
|
||||
)
|
||||
playlists_write.delete_playlist(
|
||||
playlist_id=setlist_playlist_id,
|
||||
con=con,
|
||||
)
|
||||
if slowjam_playlist_id is not None:
|
||||
session_playlists.delete_playlist(
|
||||
session_id=session_id,
|
||||
playlist_type=PlaylistType.SESSION_SLOWJAM,
|
||||
con=con,
|
||||
)
|
||||
playlists_write.delete_playlist(
|
||||
playlist_id=slowjam_playlist_id,
|
||||
con=con,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ def _sub(pattern: str, sub: str) -> Callable[[str], str]:
|
||||
def _clean_string(lyrics_str: str) -> str:
|
||||
return (
|
||||
FnChain.transform(lyrics_str) |
|
||||
# _sub(r"~", "") |
|
||||
_sub(r"\t", " ") |
|
||||
_sub(r" +", " ") |
|
||||
_sub(r" \n", "\n") |
|
||||
@@ -39,8 +40,13 @@ def get_lyric_by_id(lyric_id: int, tema_id: int | None = None) -> model.Lyrics |
|
||||
return next(iter(lyrics_dal.get_lyrics(lyric_id=lyric_id, tema_id=tema_id)), None)
|
||||
|
||||
|
||||
def update_lyric(lyric: model.Lyrics, title: str, content: str):
|
||||
lyric = dataclasses.replace(lyric, title=title.strip(), content=_clean_string(content))
|
||||
def update_lyric(lyric: model.Lyrics, title: str, content: str, max_columns: int | None = None):
|
||||
# Validate max_columns if provided
|
||||
if max_columns is not None:
|
||||
if not isinstance(max_columns, int) or max_columns < 1 or max_columns > 10:
|
||||
raise ValueError("max_columns must be an integer between 1 and 10")
|
||||
|
||||
lyric = dataclasses.replace(lyric, title=title.strip(), content=_clean_string(content), max_columns=max_columns)
|
||||
lyrics_dal.update_lyric(lyric=lyric)
|
||||
return lyric
|
||||
|
||||
|
||||
71
scripts/06_migrate_session_playlist_names.py
Executable file
71
scripts/06_migrate_session_playlist_names.py
Executable file
@@ -0,0 +1,71 @@
|
||||
import datetime
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
|
||||
from folkugat_web.dal.sql import get_connection
|
||||
from folkugat_web.dal.sql.sessions import playlists as session_playlists
|
||||
from folkugat_web.model.playlists import PlaylistType
|
||||
from folkugat_web.services import sessions as sessions_service
|
||||
from folkugat_web.dal.sql.playlists import write as playlists_write
|
||||
|
||||
def main():
|
||||
"""Main migration function."""
|
||||
with get_connection() as con:
|
||||
cur = con.cursor()
|
||||
query = """ SELECT
|
||||
sp.playlist_id,
|
||||
sp.session_id,
|
||||
s.date,
|
||||
sp.playlist_type,
|
||||
p.name as current_name
|
||||
FROM session_playlists sp
|
||||
JOIN sessions s ON sp.session_id = s.id
|
||||
JOIN playlists p ON sp.playlist_id = p.id
|
||||
WHERE p.name IS NULL OR p.name = ''
|
||||
ORDER BY sp.session_id, sp.playlist_type """
|
||||
_ = cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if not rows:
|
||||
print("No session playlists need renaming.")
|
||||
return
|
||||
|
||||
updated_count = 0
|
||||
for row in rows:
|
||||
playlist_id, session_id, session_date, playlist_type, current_name = row
|
||||
|
||||
# Parse session date properly
|
||||
session_date = sessions_service.get_date_names(
|
||||
datetime.date.fromisoformat(session_date)
|
||||
)
|
||||
|
||||
# Generate proper name based on playlist type
|
||||
if playlist_type == PlaylistType.SESSION_SETLIST.value:
|
||||
new_name = f"Sessió del {session_date.day} de {session_date.month_name} de {session_date.year}"
|
||||
elif playlist_type == PlaylistType.SESSION_SLOWJAM.value:
|
||||
new_name = f"Slow Jam del {session_date.day} de {session_date.month_name} de {session_date.year}"
|
||||
else:
|
||||
print(f"Unknown playlist type {playlist_type} for playlist {playlist_id}")
|
||||
continue
|
||||
|
||||
# Update the playlist name using existing function
|
||||
playlists_write.update_playlist_name(
|
||||
playlist_id=playlist_id,
|
||||
name=new_name,
|
||||
con=con
|
||||
)
|
||||
|
||||
current_name_display = current_name or "None"
|
||||
print(
|
||||
f"Updated playlist {playlist_id} (session {session_id}) "
|
||||
f"from '{current_name_display}' to '{new_name}'"
|
||||
)
|
||||
updated_count += 1
|
||||
|
||||
con.commit()
|
||||
print(f"Migration completed! Updated {updated_count} playlist names.")
|
||||
print("DONE!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
46
scripts/07_add_playlist_hidden.py
Normal file
46
scripts/07_add_playlist_hidden.py
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add hidden column to playlists table and set all existing playlists to hidden.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root to the path so we can import project modules
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from folkugat_web.config.db import DB_FILE
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Connecting to database at: {DB_FILE}")
|
||||
|
||||
with sqlite3.connect(DB_FILE) as con:
|
||||
cur = con.cursor()
|
||||
|
||||
# Check if hidden column already exists
|
||||
cur.execute("PRAGMA table_info(playlists)")
|
||||
columns = [column[1] for column in cur.fetchall()]
|
||||
|
||||
if 'hidden' not in columns:
|
||||
print("Adding hidden column to playlists table...")
|
||||
cur.execute("ALTER TABLE playlists ADD COLUMN hidden INTEGER NOT NULL DEFAULT 1")
|
||||
print("Column added successfully.")
|
||||
else:
|
||||
print("Hidden column already exists.")
|
||||
|
||||
# Set all existing playlists to hidden (1)
|
||||
print("Setting all existing playlists to hidden...")
|
||||
cur.execute("UPDATE playlists SET hidden = 1")
|
||||
updated_count = cur.rowcount
|
||||
print(f"Updated {updated_count} playlists to hidden.")
|
||||
|
||||
con.commit()
|
||||
|
||||
print("Migration completed successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
scripts/08_add_lyrics_max_columns.py
Normal file
8
scripts/08_add_lyrics_max_columns.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from folkugat_web.dal.sql import get_connection
|
||||
|
||||
with get_connection() as con:
|
||||
cur = con.cursor()
|
||||
alter_query = """ ALTER TABLE tema_lyrics ADD COLUMN max_columns INTEGER"""
|
||||
_ = cur.execute(alter_query)
|
||||
|
||||
print("DONE!")
|
||||
Reference in New Issue
Block a user