Compare commits

...

11 Commits

Author SHA1 Message Date
marc
443c1b1391 Canviar el preview de la pàgina de tema 2026-01-05 10:40:43 +01:00
marc
4e7a6b18d6 Afegir control visual de les lletres per a partitures 2026-01-05 10:19:25 +01:00
marc
c0624d1e56 Edició de llistes (afegir i esborrar llistes) 2025-12-21 17:30:19 +01:00
marc
fdcda1b566 Nova pàgina de llistes 2025-12-21 17:09:59 +01:00
marc
ac79785cf0 Afegir partitures de llistes (cançoners) 2025-12-21 15:40:21 +01:00
marc
089e61fb0b Canvi de nom a les URLs playlist -> llista 2025-12-20 23:48:54 +01:00
marc
56ab91bd42 Pàgines llista de reproducció 2025-12-20 23:29:11 +01:00
marc
5428d49e89 Crea i esborra playlists quan es creen i esborren sessions 2025-12-20 21:20:58 +01:00
marc
43706af7c4 Afegir funcions especials de lilypond 2025-12-20 20:31:54 +01:00
marc
c1ec268823 Afegir metadades per quan es comparteixen enllaços 2025-11-02 14:22:22 +01:00
marc
2fbdbbf290 Desacoblar playlists de sessions i afegir slow jams 2025-11-01 19:55:24 +01:00
88 changed files with 2232 additions and 1778 deletions

2
.gitignore vendored
View File

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

24
AGENTS.md Normal file
View 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
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1753432016, "lastModified": 1766125104,
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=", "narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6027c30c8e9810896b92429f0092f624f7b1aace", "rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -60,7 +60,10 @@
lilypond-with-fonts lilypond-with-fonts
# Project tools # Project tools
sqlite sqlite
lazysql
opencode opencode
# Local https
mkcert
]; ];
shellHook = '' shellHook = ''

View File

@@ -1,14 +1,16 @@
from folkugat_web.api.router import get_router from folkugat_web.api.router import get_router
from . import auth, index, sessio, sessions, tema, temes from . import auth, index, llista, llistes, sessio, sessions, tema, temes
router = get_router() router = get_router()
router.include_router(auth.router)
router.include_router(index.router)
router.include_router(llista.router)
router.include_router(llistes.router)
router.include_router(sessio.router) router.include_router(sessio.router)
router.include_router(sessions.router) router.include_router(sessions.router)
router.include_router(tema.router) router.include_router(tema.router)
router.include_router(temes.router) router.include_router(temes.router)
router.include_router(auth.router)
router.include_router(index.router)
__all__ = ["router"] __all__ = ["router"]

View File

@@ -13,6 +13,7 @@ def index(request: Request, logged_in: auth.LoggedIn):
{ {
"request": request, "request": request,
"page_title": "Folkugat", "page_title": "Folkugat",
"page_description": "Sessions de folk a Sant Cugat",
"content": "/api/content/sessions", "content": "/api/content/sessions",
"logged_in": logged_in, "logged_in": logged_in,
"animate": True, "animate": True,

View File

@@ -0,0 +1,9 @@
from folkugat_web.api.router import get_router
from . import playlist, set_page
router = get_router()
router.include_router(set_page.router)
router.include_router(playlist.router)
__all__ = ["router"]

View File

@@ -0,0 +1,269 @@
from typing import Annotated
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.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,
playlist_id: int,
):
return playlist.add_set(
request=request,
playlist_id=playlist_id,
logged_in=logged_in,
)
@router.get("/api/llista/{playlist_id}/set/{set_id}")
def get_set(
request: Request,
logged_in: auth.LoggedIn,
playlist_id: int,
set_id: int,
):
return playlist.get_set(
request=request,
playlist_id=playlist_id,
set_id=set_id,
logged_in=logged_in,
)
@router.delete("/api/llista/{playlist_id}/set/{set_id}")
def delete_set(
_: auth.RequireLogin,
playlist_id: int,
set_id: int,
):
return playlist.delete_set(
playlist_id=playlist_id,
set_id=set_id,
)
@router.post("/api/llista/{playlist_id}/set/{set_id}")
def add_tema(
request: Request,
logged_in: auth.RequireLogin,
playlist_id: int,
set_id: int,
):
return playlist.add_tema(
request=request,
playlist_id=playlist_id,
set_id=set_id,
logged_in=logged_in,
)
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
def get_tema(
request: Request,
logged_in: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
):
return playlist.get_tema(
request=request,
playlist_id=playlist_id,
set_id=set_id,
entry_id=entry_id,
logged_in=logged_in,
)
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/editor")
def get_tema_editor(
request: Request,
logged_in: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
):
return playlist.get_tema_editor(
request=request,
playlist_id=playlist_id,
set_id=set_id,
entry_id=entry_id,
logged_in=logged_in,
)
@router.delete("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
def delete_tema(
_: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
):
return playlist.delete_tema(
playlist_id=playlist_id,
set_id=set_id,
entry_id=entry_id,
)
@router.get("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/busca")
def busca_tema(
request: Request,
_: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
query: str,
):
return playlist.busca_tema(
request=request,
playlist_id=playlist_id,
set_id=set_id,
entry_id=entry_id,
query=query,
)
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
def set_tema(
request: Request,
logged_in: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
tema_id: Annotated[int, Form()],
):
return playlist.set_tema(
request=request,
logged_in=logged_in,
playlist_id=playlist_id,
set_id=set_id,
entry_id=entry_id,
tema_id=tema_id,
)
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/unknown")
def set_tema_unknown(
request: Request,
logged_in: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
):
return playlist.set_tema(
request=request,
logged_in=logged_in,
playlist_id=playlist_id,
set_id=set_id,
entry_id=entry_id,
tema_id=None,
)
@router.post("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}")
def set_tema_new(
request: Request,
logged_in: auth.RequireLogin,
playlist_id: int,
set_id: int,
entry_id: int,
title: Annotated[str, Form()],
):
new_tema = temes_service.create_tema(title=title)
return playlist.set_tema(
request=request,
logged_in=logged_in,
playlist_id=playlist_id,
set_id=set_id,
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,
)

View File

@@ -0,0 +1,55 @@
from fastapi import HTTPException, Request
from folkugat_web.api.router import get_router
from folkugat_web.fragments import set_page
from folkugat_web.model import playlists
from folkugat_web.services import auth
from folkugat_web.services import playlists as playlists_service
from folkugat_web.templates import templates
router = get_router()
def set_description(set_: playlists.Set) -> str:
tema_names = [tis.tema.title if tis.tema else "Desconegut" for tis in set_.temes]
return " i ".join(filter(bool, [
", ".join(tema_names[:-1]),
tema_names[-1]
]))
@router.get("/llista/{playlist_id}/set/{set_id}")
def page(
request: Request,
logged_in: auth.LoggedIn,
playlist_id: int,
set_id: int,
):
set_ = playlists_service.get_set(playlist_id=playlist_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(
"index.html",
{
"request": request,
"page_title": "Folkugat - Set",
"page_description": set_description(set_=set_),
"content": f"/api/content/llista/{playlist_id}/set/{set_id}",
"logged_in": logged_in,
}
)
@router.get("/api/content/llista/{playlist_id}/set/{set_id}")
async def contingut(
request: Request,
logged_in: auth.LoggedIn,
playlist_id: int,
set_id: int,
):
return await set_page.pagina(
request=request,
playlist_id=playlist_id,
set_id=set_id,
logged_in=logged_in,
)

View File

@@ -0,0 +1,5 @@
from folkugat_web.api.router import get_router
from . import index
router = get_router()
router.include_router(index.router)

View 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)

View File

@@ -1,12 +1,11 @@
from folkugat_web.api.router import get_router from folkugat_web.api.router import get_router
from . import cartell, index, live, playlist, set_page from . import cartell, index, live, notes
router = get_router() router = get_router()
router.include_router(cartell.router) router.include_router(cartell.router)
router.include_router(index.router) router.include_router(index.router)
router.include_router(live.router) router.include_router(live.router)
router.include_router(set_page.router) router.include_router(notes.router)
router.include_router(playlist.router)
__all__ = ["router"] __all__ = ["router"]

View File

@@ -1,27 +1,47 @@
from typing import Annotated from fastapi import HTTPException, Request
from fastapi import Form, Request
from folkugat_web.api.router import get_router from folkugat_web.api.router import get_router
from folkugat_web.fragments import live from folkugat_web.fragments import live
from folkugat_web.fragments.sessio import page as sessio from folkugat_web.fragments.sessio import page as sessio
from folkugat_web.model.sessions import Session
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.services.temes import write as temes_service from folkugat_web.services import sessions as sessions_service
from folkugat_web.templates import templates from folkugat_web.templates import templates
router = get_router() router = get_router()
def session_description(session: Session) -> str:
date_names = sessions_service.get_date_names(session.date)
date_str = f"{date_names.day_name} {date_names.day} {date_names.month_name} de {date_names.year}"
location_str = f"A {session.venue.name}" if session.venue else ""
time_str = f"de {session.start_time.strftime('%H:%M')} a {session.end_time.strftime('%H:%M')}"
location_time_str = " ".join([location_str, time_str])
if location_time_str:
location_time_str = location_time_str[0].upper() + location_time_str[1:]
notes_str = session.notes or ""
return "\n".join(filter(bool, [
date_str,
location_time_str,
notes_str,
]))
@router.get("/sessio/{session_id}") @router.get("/sessio/{session_id}")
def page( def page(
request: Request, request: Request,
logged_in: auth.LoggedIn, logged_in: auth.LoggedIn,
session_id: int, session_id: int,
): ):
session = sessions_service.get_session(session_id=session_id)
if not session:
raise HTTPException(status_code=404, detail="Could not find session")
return templates.TemplateResponse( return templates.TemplateResponse(
"index.html", "index.html",
{ {
"request": request, "request": request,
"page_title": "Folkugat", "page_title": "Folkugat - Sessió",
"page_description": session_description(session),
"page_card": session.cartell_url,
"content": f"/api/content/sessio/{session_id}", "content": f"/api/content/sessio/{session_id}",
"logged_in": logged_in, "logged_in": logged_in,
} }

View File

@@ -0,0 +1,47 @@
import dataclasses
from typing import Annotated
from fastapi import HTTPException, Request
from fastapi.params import Form
from folkugat_web.api.router import get_router
from folkugat_web.fragments.sessio import notes
from folkugat_web.services import auth
from folkugat_web.services import sessions as sessions_service
router = get_router()
@router.get("/api/sessio/{session_id}/notes")
def get_notes(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
):
return notes.notes(request, session_id, logged_in)
@router.get("/api/sessio/{session_id}/notes/editor")
def get_notes_editor(
request: Request,
_: auth.RequireLogin,
session_id: int,
):
return notes.notes_editor(request, session_id)
@router.put("/api/sessio/{session_id}/notes")
async def set_notes(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
content: Annotated[str, Form()],
):
session = sessions_service.get_session(session_id=session_id)
if not session:
raise HTTPException(status_code=404, detail="Could not find session")
new_session = dataclasses.replace(
session,
notes=content.strip(),
)
sessions_service.set_session(new_session)
return notes.notes(request, session_id, logged_in)

View File

@@ -1,189 +0,0 @@
from typing import Annotated
from fastapi import Form, Request
from folkugat_web.api.router import get_router
from folkugat_web.fragments import live
from folkugat_web.fragments.sessio import playlist
from folkugat_web.services import auth
from folkugat_web.services.temes import write as temes_service
from folkugat_web.templates import templates
router = get_router()
@router.post("/api/sessio/{session_id}/playlist/set")
def add_set(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
):
return playlist.add_set(
request=request,
session_id=session_id,
logged_in=logged_in,
)
@router.get("/api/sessio/{session_id}/playlist/set/{set_id}")
def get_set(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return playlist.get_set(
request=request,
session_id=session_id,
set_id=set_id,
logged_in=logged_in,
)
@router.delete("/api/sessio/{session_id}/playlist/set/{set_id}")
def delete_set(
_: auth.RequireLogin,
session_id: int,
set_id: int,
):
return playlist.delete_set(
session_id=session_id,
set_id=set_id,
)
@router.post("/api/sessio/{session_id}/playlist/set/{set_id}")
def add_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
):
return playlist.add_tema(
request=request,
session_id=session_id,
set_id=set_id,
logged_in=logged_in,
)
@router.get("/api/sessio/{session_id}/playlist/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 playlist.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}/playlist/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 playlist.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}/playlist/set/{set_id}/tema/{entry_id}")
def delete_tema(
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return playlist.delete_tema(
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
)
@router.get("/api/sessio/{session_id}/playlist/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 playlist.busca_tema(
request=request,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
query=query,
)
@router.put("/api/sessio/{session_id}/playlist/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 playlist.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}/playlist/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 playlist.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}/playlist/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 playlist.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

@@ -1,35 +0,0 @@
from fastapi import Request
from folkugat_web.api.router import get_router
from folkugat_web.fragments import set_page
from folkugat_web.services import auth
from folkugat_web.templates import templates
router = get_router()
@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}")
async def contingut(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return await set_page.pagina(request, session_id, set_id, logged_in)

View File

@@ -21,13 +21,32 @@ from folkugat_web.utils import FnChain
router = get_router() 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}") @router.get("/tema/{tema_id}")
def page(request: Request, logged_in: auth.LoggedIn, tema_id: int): 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 = (
FnChain.transform(tema) |
scores_service.add_scores_to_tema |
properties_service.add_properties_to_tema
).result()
return templates.TemplateResponse( return templates.TemplateResponse(
"index.html", "index.html",
{ {
"request": request, "request": request,
"page_title": "Folkugat", "page_title": f"Folkugat - {tema.title}",
"page_description": _build_page_description(tema),
"page_card": "",
"content": f"/api/tema/{tema_id}", "content": f"/api/tema/{tema_id}",
"logged_in": logged_in, "logged_in": logged_in,
} }

View File

@@ -33,11 +33,25 @@ def set_lyric(
lyric_id: int, lyric_id: int,
title: Annotated[str, Form()], title: Annotated[str, Form()],
content: 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) lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
if not lyric: if not lyric:
raise HTTPException(status_code=404, detail="Could not find 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) return lyrics_fragments.lyric(request=request, logged_in=logged_in, lyric=new_lyric)

View File

@@ -29,11 +29,16 @@ def page(
order_params=order_params, order_params=order_params,
) )
content_url = f"/api/content/temes?{temes_params}" content_url = f"/api/content/temes?{temes_params}"
if query:
additional_description = f" - Resultats per a '{query}'"
else:
additional_description = ""
return templates.TemplateResponse( return templates.TemplateResponse(
"index.html", "index.html",
{ {
"request": request, "request": request,
"page_title": "Folkugat", "page_title": "Folkugat - Temes",
"page_description": f"Buscador de temes{additional_description}",
"content": content_url, "content": content_url,
"logged_in": logged_in, "logged_in": logged_in,
"animate": False, "animate": False,

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

View File

@@ -1,8 +1,10 @@
<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">
<a href="/">
<img src="{{ url_for(request, 'static', path='img/folkugat.svg') }}" <img src="{{ url_for(request, 'static', path='img/folkugat.svg') }}"
class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3" class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3"
width="100" width="100"
alt="Folkugat"/> 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> </a>
<h1 class="text-3xl sm:text-8xl text-beige m-3 {% if animate %} opacity-0 animate-fade-in-one {% endif %}">Folkugat</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

@@ -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>

View 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>

View 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>

View File

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

View File

@@ -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 %}

View File

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

View File

@@ -13,14 +13,14 @@
{% for tema_in_set in set.temes %} {% for tema_in_set in set.temes %}
{% if tema_in_set.tema is not none %} {% if tema_in_set.tema is not none %}
{% set tema = tema_in_set.tema %} {% set tema = tema_in_set.tema %}
{% include "fragments/sessio/set/tema_title.html" %} {% include "fragments/llista/set/tema_title.html" %}
{% else %} {% else %}
<h3 class="text-center text-3xl p-4"> <i>Desconegut</i> </h3> <h3 class="text-center text-3xl p-4"> <i>Desconegut</i> </h3>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="mx-12"> <div class="mx-12">
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
{% include "fragments/sessio/set/set_score.html"%} {% include "fragments/llista/set/set_score.html"%}
</div> </div>
{% else %} {% else %}
{% for tema_in_set in set.temes %} {% for tema_in_set in set.temes %}
@@ -31,7 +31,7 @@
{% endif %} {% endif %}
{% if tema_in_set.tema is not none %} {% if tema_in_set.tema is not none %}
{% set tema = tema_in_set.tema %} {% set tema = tema_in_set.tema %}
{% include "fragments/sessio/set/tema.html"%} {% include "fragments/llista/set/tema.html"%}
{% else %} {% else %}
<h3 class="text-center text-3xl p-4"> <h3 class="text-center text-3xl p-4">
<i>Desconegut</i> <i>Desconegut</i>

View File

@@ -1,4 +1,4 @@
{% include "fragments/sessio/set/tema_title.html" %} {% include "fragments/llista/set/tema_title.html" %}
<div class="mx-12 text-left"> <div class="mx-12 text-left">
{% if tema.main_score() is not none %} {% if tema.main_score() is not none %}

View File

@@ -2,12 +2,12 @@
m-4 rounded-lg bg-white m-4 rounded-lg bg-white
px-2 py-1 my-1" px-2 py-1 my-1"
id="set-entry-{{ set_id }}" id="set-entry-{{ set_id }}"
hx-get="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}" hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}" hx-target="#set-entry-{{ set_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-trigger="reload-set-{{ set_id }}"> hx-trigger="reload-set-{{ set_id }}">
{% if set_entry.temes | length > 1 or not set_entry.temes[0].tema %} {% if set_entry.temes | length > 1 or not set_entry.temes[0].tema %}
{% set set_url = "/sessio/%d/set/%d" | format(session_id, set_id) %} {% set set_url = "/llista/%d/set/%d" | format(playlist_id, set_id) %}
{% else %} {% else %}
{% set set_url = "/tema/%d" | format(set_entry.temes[0].tema_id) %} {% set set_url = "/tema/%d" | format(set_entry.temes[0].tema_id) %}
{% endif %} {% endif %}
@@ -24,27 +24,27 @@
py-2 mx-2 py-2 mx-2
w-full max-w-[655px]"> w-full max-w-[655px]">
{% if logged_in %} {% if logged_in %}
<button class="text-beige w-full" <button class="text-beige w-full"
hx-delete="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}" hx-delete="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}" hx-target="#set-entry-{{ set_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>
Esborra el set Esborra el set
</button> </button>
{% endif %} {% endif %}
<ol id="set-entry-{{ set_id }}-list" <ol id="set-entry-{{ set_id }}-list"
class="flex flex-col items-start w-full"> class="flex flex-col items-start w-full">
{% for tema_entry in set_entry.temes %} {% for tema_entry in set_entry.temes %}
{% if new_entry %} {% if new_entry %}
{% include "fragments/sessio/tema_editor.html" %} {% include "fragments/llista/tema_editor.html" %}
{% else %} {% else %}
{% include "fragments/sessio/tema_entry.html" %} {% include "fragments/llista/tema_entry.html" %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ol> </ol>
{% if logged_in %} {% if logged_in %}
<button class="text-beige mt-2 w-full" <button class="text-beige mt-2 w-full"
hx-post="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}" hx-post="/api/llista/{{ playlist_id }}/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}-list" hx-target="#set-entry-{{ set_id }}-list"
hx-swap="beforeend transition:true"> hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i> <i class="fa fa-plus" aria-hidden="true"></i>

View File

@@ -12,13 +12,13 @@
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
rounded text-center text-black rounded text-center text-black
p-1 m-1" p-1 m-1"
hx-get="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca" hx-get="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca"
hx-trigger="revealed, keyup delay:500ms changed" hx-trigger="revealed, keyup delay:500ms changed"
hx-target="#tune-entry-{{ tema_entry.id }}-search-results" hx-target="#tune-entry-{{ tema_entry.id }}-search-results"
hx-swap="outerHTML"/> hx-swap="outerHTML"/>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="text-beige mx-1" class="text-beige mx-1"
hx-get="/api/sessio/{{ session_id }}/playlist/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-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -15,14 +15,14 @@
<div class="flex-none flex flex-row shrink-0"> <div class="flex-none flex flex-row shrink-0">
<button title="Edita el tema" <button title="Edita el tema"
class="text-beige mx-1" class="text-beige mx-1"
hx-get="/api/sessio/{{ session_id }}/playlist/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-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i> <i class="fa fa-pencil" aria-hidden="true"></i>
</button> </button>
<button title="Esborra el tema" <button title="Esborra el tema"
class="text-beige mx-1" class="text-beige mx-1"
hx-delete="/api/sessio/{{ session_id }}/playlist/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-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -4,7 +4,7 @@
<li> <li>
<button class="bg-beige text-brown rounded <button class="bg-beige text-brown rounded
m-1 px-2" m-1 px-2"
hx-put="/api/sessio/{{ session_id }}/playlist/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-vals='{"tema_id": "{{ tema.id }}"}'
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
@@ -15,7 +15,7 @@
<li> <li>
<button class="border border-beige text-beige rounded <button class="border border-beige text-beige rounded
m-1 px-2" m-1 px-2"
hx-put="/api/sessio/{{ session_id }}/playlist/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-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-question" aria-hidden="true"></i> <i class="fa fa-question" aria-hidden="true"></i>
@@ -25,7 +25,7 @@
<li> <li>
<button class="border border-beige text-beige rounded <button class="border border-beige text-beige rounded
m-1 px-2" m-1 px-2"
hx-post="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ entry_id }}" hx-post="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ entry_id }}"
hx-vals='{"title": "{{ query }}"}' hx-vals='{"title": "{{ query }}"}'
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">

View File

@@ -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 %}

View 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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -27,6 +27,19 @@
</button> </button>
</li> </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> </ul>
<hr class="h-px bg-beige border-0"> <hr class="h-px bg-beige border-0">
<div hx-get="/api/sessions/live" <div hx-get="/api/sessions/live"

View File

@@ -0,0 +1,13 @@
<h4 class="py-4 text-xl text-beige">
Notes
{% if logged_in %}
<button title="Edita les notes"
class="mx-1"
hx-get="/api/sessio/{{ session_id }}/notes/editor"
hx-target="#notes"
hx-swap="innerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>
{% endif %}
</h4>
{{ notes | safe }}

View File

@@ -0,0 +1,28 @@
<h4 class="py-4 text-xl text-beige">Notes</h4>
<form id="notes-form"
class="w-full">
<h5 class="text-sm text-beige text-right">
<button title="Desa els canvis"
class="mx-1"
hx-put="/api/sessio/{{session_id}}/notes"
hx-target="#notes"
hx-swap="innerHTML">
<i class="fa fa-check" aria-hidden="true"></i>
</button>
<button title="Descarta els canvis"
class="mx-1"
hx-get="/api/sessio/{{session_id}}/notes"
hx-target="#notes"
hx-swap="innerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</h5>
<hr class="h-px mt-1 mb-3 bg-beige border-0">
<textarea name="content"
placeholder="Notes"
rows="{{ max(notes.count('\n') + 1, 5) }}"
class="border border-beige focus:outline-none
w-full text-left
rounded
bg-brown p-2 m-0">{{notes}}</textarea>
</form>

View File

@@ -31,20 +31,34 @@
{% endif %} {% endif %}
</div> </div>
{% if logged_in or session.notes %} {% if logged_in or session.notes %}
<div class="text-left"> <div id="notes"
<h4 class="py-4 text-xl text-beige">Notes</h4> class="text-left">
{% if session.notes %}{{ session.notes }}{% endif %} {% set notes = session.notes if session.notes else "" %}
{% include "fragments/sessio/notes.html" %}
</div> </div>
{% endif %} {% endif %}
{% if logged_in or False %} {% if logged_in or (session.slowjam and session.slowjam.sets) %}
<div class="text-left"> <div class="text-left">
<h4 class="py-4 text-xl text-beige">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/llista/playlist.html" %}
</div> </div>
{% endif %} {% endif %}
{% if logged_in or playlist.sets %} {% if logged_in or (session.setlist and session.setlist.sets) %}
<div class="text-left"> <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">
{% include "fragments/sessio/playlist.html" %} <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/llista/playlist.html" %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

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

View File

@@ -7,6 +7,16 @@
rounded rounded
bg-brown px-2 " 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" <button title="Desa els canvis"
class="mx-1" class="mx-1"
hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}" hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"

View File

@@ -1,6 +1,9 @@
<div id="tema-lyric-{{ lyric.id }}"> <div id="tema-lyric-{{ lyric.id }}">
<h5 class="text-sm text-beige text-right"> <h5 class="text-sm text-beige text-right">
{{ lyric.title }} {{ lyric.title }}
{% if logged_in and lyric.max_columns %}
<span class="text-xs ml-2">{{ lyric.max_columns }} columnes</span>
{% endif %}
{% if logged_in %} {% if logged_in %}
<button title="Modifica la lletra" <button title="Modifica la lletra"
class="mx-1" class="mx-1"
@@ -20,6 +23,6 @@
</h5> </h5>
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
<div class="text-center"> <div class="text-center">
{{ lyric.content.replace('\n', '<br>') | safe }} {{ (lyric.content.replace('~', '')).replace('\n', '<br>') | safe }}
</div> </div>
</div> </div>

View File

@@ -4,6 +4,16 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<!-- Essential Open Graph (Facebook, WhatsApp, etc.) -->
<meta property="og:title" content="{{ page_title }}">
<meta property="og:description" content="{{ page_description }}">
<meta property="og:image" content="{{ page_card or url_for(request, 'static', path='img/folkugat_card.jpg') }}">
<!-- Twitter Card (optional but recommended) -->
<meta name="twitter:title" content="{{ page_title }}">
<meta name="twitter:description" content="{{ page_description }}">
<meta name="twitter:image" content="{{ page_card or url_for(request, 'static', path='img/folkugat_card.jpg') }}">
<title>{{ page_title }}</title> <title>{{ page_title }}</title>
<meta name="description" content="{{ page_description }}"/> <meta name="description" content="{{ page_description }}"/>

View File

@@ -0,0 +1,84 @@
% -------------------------------------------------------------------------
% Extra custom functions
% -------------------------------------------------------------------------
% Alternative chords
#(define (pitches-bass-inversion-context-list music context)
"Creates a list containing relevant pitches, bass, inversion and context to
serve as argument for `ignatzek-chord-names', derived from @var{music}
"
(let* (;; work on a copy of `music', warranting not to change it
(m-add (ly:music-deep-copy music))
(ev-notes-orig (event-chord-notes m-add))
;; if chord is an inversion some pitches are below root, in this case
;; 'octavation-property is set.
;; reset pitch of them
;; TODO this is an ugly hack, find better method
(ev-notes
(map
(lambda (m)
(if (and (not (null? (ly:music-property m 'octavation)))
(= (ly:music-property m 'octavation) -1))
(begin
(ly:music-set-property! m 'pitch
(ly:pitch-transpose
(ly:music-property m 'pitch)
(ly:make-pitch 1 0 0)))
m)
m))
ev-notes-orig))
;; sort the ptches ascending as expected by `ignatzek-chord-names'
(ev-pitches
(sort
(map (lambda (m) (ly:music-property m 'pitch)) ev-notes)
ly:pitch<?))
;; get bass
(bass-note
(filter
(lambda (m)
(not (null? (ly:music-property m 'bass))))
ev-notes))
(bass-pitch
(if (not (null? bass-note))
(ly:music-property (car bass-note) 'pitch)
'()))
;; get inversion
(inversion-note
(filter
(lambda (m)
(not (null? (ly:music-property m 'inversion))))
ev-notes))
(inversion-pitch
(if (not (null? inversion-note))
(ly:music-property (car inversion-note) 'pitch)
'()))
;; TODO why is this needed?
(in-pitches
(if (null? inversion-pitch)
(delq bass-pitch ev-pitches)
ev-pitches)))
(list in-pitches bass-pitch inversion-pitch context)))
altChords =
#(define-music-function (m1 m2)(ly:music? ly:music?)
"Return the default ChordName of @var{m1}, with an added parenthesized ChordName
derived from @var{m2}"
#{
\applyOutput ChordNames.ChordName
#(lambda (g ctx p)
(let ((main-text (ly:grob-property g 'text))
(alt-text
(apply
ignatzek-chord-names
(pitches-bass-inversion-context-list m2 ctx))))
(ly:grob-set-property! g 'text
#{
\markup
{ $main-text \hspace #0.4 \fontsize #-3 \parenthesize $alt-text }
#})))
$m1
#})
% -------------------------------------------------------------------------

View 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 %}
}

View File

@@ -1,4 +1,5 @@
\version "2.24.4" \version "2.24.4"
{% include "lilypond/lib.ly" %}
{% if tune.score_source is not none %} {% if tune.score_source is not none %}
\score { \score {
\language "english" \language "english"

View File

@@ -7,13 +7,25 @@
% ----------------------------------------------------- % -----------------------------------------------------
% A % A
\repeat volta 2 {
\alternative {
\volta 1 { }
\volta 2 { }
}
}
% B % B
\repeat volta 2 {
\alternative {
\volta 1 { }
\volta 2 { }
}
}
} }
} }
\new Staff { \new Staff {
\relative { \relative c' {
% ----------------------------------------------------- % -----------------------------------------------------
% Compàs % Compàs
% ----------------------------------------------------- % -----------------------------------------------------

View File

@@ -1,4 +1,7 @@
\version "2.24.4" \version "2.24.4"
{% include "lilypond/lib.ly" %}
\paper { \paper {
top-margin = 10 top-margin = 10
left-margin = 15 left-margin = 15
@@ -35,7 +38,7 @@
\hspace #1 \hspace #1
\center-column { \center-column {
{% for line in paragraph.lines %} {% for line in paragraph.lines %}
\line { {{ line | safe }} } {% if line %} \line { {{ line | safe }} } {% else %} \vspace #1 {% endif %}
{% endfor %} {% endfor %}
} }
{% endfor %} {% endfor %}

View File

@@ -9,6 +9,7 @@
} }
} }
} }
\noPageBreak
{% if tune.score_source is not none %} {% if tune.score_source is not none %}
\score { \score {
\language "english" \language "english"

View File

@@ -1,5 +1,8 @@
{{ score_beginning }} {{ score_beginning }}
\version "2.24.4" \version "2.24.4"
{% include "lilypond/lib.ly" %}
\book { \book {
\paper { \paper {
top-margin = 10 top-margin = 10

View File

@@ -14,12 +14,14 @@ DB_FILES_DIR = DB_DIR / "fitxer"
DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema" DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema"
DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio" DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio"
DB_FILES_SET_DIR = DB_FILES_DIR / "set" DB_FILES_SET_DIR = DB_FILES_DIR / "set"
DB_FILES_PLAYLIST_DIR = DB_FILES_DIR / "llistes"
DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp" DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp"
for path in [ for path in [
DB_FILES_DIR, DB_FILES_DIR,
DB_FILES_TEMA_DIR, DB_FILES_TEMA_DIR,
DB_FILES_SET_DIR, DB_FILES_SET_DIR,
DB_FILES_PLAYLIST_DIR,
DB_FILES_SET_DIR, DB_FILES_SET_DIR,
]: ]:
path.mkdir(exist_ok=True) path.mkdir(exist_ok=True)

View File

@@ -2,12 +2,13 @@ from typing import TypedDict
from folkugat_web.model import playlists as model from folkugat_web.model import playlists as model
PlaylistRowTuple = tuple[int, int, int, int | None] PlaylistRowTuple = tuple[int, str | None, int]
PlaylistEntryRowTuple = tuple[int, int, int, int | None]
class PlaylistRowDict(TypedDict): class PlaylistRowDict(TypedDict):
id: int | None id: int | None
session_id: int playlist_id: int
set_id: int set_id: int
tema_id: int | None tema_id: int | None
@@ -15,16 +16,25 @@ class PlaylistRowDict(TypedDict):
def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict: def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict:
return { return {
'id': tema_in_set.id, 'id': tema_in_set.id,
'session_id': tema_in_set.session_id, 'playlist_id': tema_in_set.playlist_id,
'set_id': tema_in_set.set_id, 'set_id': tema_in_set.set_id,
'tema_id': tema_in_set.tema_id, 'tema_id': tema_in_set.tema_id,
} }
def row_to_playlist_entry(row: PlaylistRowTuple) -> model.PlaylistEntry: def row_to_playlist_entry(row: PlaylistEntryRowTuple) -> model.PlaylistEntry:
return model.PlaylistEntry( return model.PlaylistEntry(
id=row[0], id=row[0],
session_id=row[1], playlist_id=row[1],
set_id=row[2], set_id=row[2],
tema_id=row[3], 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]),
)

View File

@@ -4,15 +4,30 @@ from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Connection | None = None): def create_db(con: Connection | None = None):
with get_connection(con) as con: with get_connection(con) as con:
create_playlists_table(con) create_playlists_table(con)
create_playlist_entries_table(con)
def create_playlists_table(con: Connection): def create_playlists_table(con: Connection):
query = """ query = """
CREATE TABLE IF NOT EXISTS playlists ( CREATE TABLE IF NOT EXISTS playlists (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
session_id INTEGER NOT NULL, name TEXT,
hidden INTEGER NOT NULL DEFAULT 1
)
"""
cur = con.cursor()
_ = cur.execute(query)
def create_playlist_entries_table(con: Connection):
query = """
CREATE TABLE IF NOT EXISTS playlist_entries (
id INTEGER PRIMARY KEY,
playlist_id INTEGER NOT NULL,
set_id INTEGER NOT NULL, set_id INTEGER NOT NULL,
tema_id INTEGER tema_id INTEGER,
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
FOREIGN KEY (tema_id) REFERENCES temes(id) ON DELETE CASCADE
) )
""" """
cur = con.cursor() cur = con.cursor()

View File

@@ -1,11 +1,9 @@
from collections.abc import Iterable, Iterator from collections.abc import Iterator
from typing import TypedDict from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.dal.sql.sessions import conversion as sessions_conversion from folkugat_web.dal.sql.sessions import conversion as sessions_conversion
from folkugat_web.dal.sql.temes.conversion import row_to_tema
from folkugat_web.model import playlists as model from folkugat_web.model import playlists as model
from folkugat_web.model import temes as temes_model
from folkugat_web.model.sessions import Session from folkugat_web.model.sessions import Session
from folkugat_web.utils import groupby from folkugat_web.utils import groupby
@@ -15,13 +13,13 @@ from . import conversion
class QueryData(TypedDict, total=False): class QueryData(TypedDict, total=False):
id: int id: int
set_id: int set_id: int
session_id: int playlist_id: int
def _filter_clause( def _filter_clause(
entry_id: int | None = None, entry_id: int | None = None,
set_id: int | None = None, set_id: int | None = None,
session_id: int | None = None, playlist_id: int | None = None,
) -> tuple[str, QueryData]: ) -> tuple[str, QueryData]:
filter_clauses: list[str] = [] filter_clauses: list[str] = []
query_data: QueryData = {} query_data: QueryData = {}
@@ -32,9 +30,9 @@ def _filter_clause(
if set_id is not None: if set_id is not None:
filter_clauses.append("set_id = :set_id") filter_clauses.append("set_id = :set_id")
query_data["set_id"] = set_id query_data["set_id"] = set_id
if session_id is not None: if playlist_id is not None:
filter_clauses.append("session_id = :session_id") filter_clauses.append("playlist_id = :playlist_id")
query_data["session_id"] = session_id query_data["playlist_id"] = playlist_id
return " AND ".join(filter_clauses), query_data return " AND ".join(filter_clauses), query_data
@@ -42,14 +40,14 @@ def _filter_clause(
def get_playlist_entries( def get_playlist_entries(
entry_id: int | None = None, entry_id: int | None = None,
set_id: int | None = None, set_id: int | None = None,
session_id: int | None = None, playlist_id: int | None = None,
con: Connection | None = None, con: Connection | None = None,
) -> Iterator[model.PlaylistEntry]: ) -> Iterator[model.PlaylistEntry]:
filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, session_id=session_id) filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, playlist_id=playlist_id)
query = f""" query = f"""
SELECT SELECT
id, session_id, set_id, tema_id id, playlist_id, set_id, tema_id
FROM playlists FROM playlist_entries
WHERE {filter_clause} WHERE {filter_clause}
ORDER BY id ASC ORDER BY id ASC
""" """
@@ -59,57 +57,54 @@ def get_playlist_entries(
return map(conversion.row_to_playlist_entry, cur.fetchall()) return map(conversion.row_to_playlist_entry, cur.fetchall())
GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, str | None, str | None, bool] def get_playlist_name(playlist_id: int, con: Connection | None = None) -> str | None:
def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> dict[int, list[Session]]:
placeholders = ", ".join(["?" for _ in tema_ids])
query = f"""
SELECT
p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name,
s.venue_url, s.notes, s.cartell_url, s.is_live
FROM playlists p JOIN sessions s ON p.session_id = s.id
WHERE p.tema_id IN ({placeholders})
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, tema_ids)
result_rows: Iterable[GetTuneSessionsRow] = cur.fetchall()
return dict(groupby(
result_rows,
key_fn=lambda row: row[0],
group_fn=lambda rows: list(sessions_conversion.row_to_session(row[1:]) for row in rows)
))
CommonlyPlayedTuneRow = tuple[int, str, str, str, str, int, int]
def get_commonly_played_tunes(
tema_id: int,
con: Connection | None = None,
) -> list[temes_model.CommonlyPlayedTema]:
query = """ query = """
SELECT SELECT name
id, title, alternatives, creation_date, modification_date, hidden, count FROM playlists
FROM ( WHERE id = :playlist_id
SELECT tema_id, count(*) count FROM playlists p JOIN (
SELECT session_id, set_id
FROM playlists
WHERE tema_id = ?
) s
ON p.session_id == s.session_id AND p.set_id == s.set_id
WHERE tema_id != ?
GROUP BY tema_id
) common JOIN temes t ON common.tema_id == t.id
""" """
data = dict(playlist_id=playlist_id)
with get_connection(con) as con: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, [tema_id, tema_id]) _ = cur.execute(query, data)
result_rows: Iterable[CommonlyPlayedTuneRow] = cur.fetchall() row = cur.fetchone()
return [ return row[0] if row else None
temes_model.CommonlyPlayedTema(
tema=row_to_tema(row[:6]),
count=row[6], def get_all_playlists(logged_in: bool = False, con: Connection | None = None) -> Iterator[model.Playlist]:
) for row in result_rows 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])
)

View File

@@ -4,27 +4,66 @@ from folkugat_web.model import playlists as model
from . import conversion from . import conversion
def insert_playlist_entry(pl_entry: model.PlaylistEntry, con: Connection | None = None) -> model.PlaylistEntry: def create_playlist(
name: str | None = None,
con: Connection | None = None,
) -> int:
query = """ query = """
INSERT INTO playlists INSERT INTO playlists
(id, session_id, set_id, tema_id) (name)
VALUES VALUES
(:id, :session_id, :set_id, :tema_id) (:name)
RETURNING *
"""
data = dict(name=name)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
row: conversion.PlaylistRowTuple = cur.fetchone()
return row[0]
def delete_playlist(
playlist_id: int,
con: Connection | None = None,
) -> None:
query = """
DELETE FROM playlists
WHERE id = :id
"""
data = dict(id=playlist_id)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
def insert_playlist_entry(
pl_entry: model.PlaylistEntry,
con: Connection | None = None,
) -> model.PlaylistEntry:
query = """
INSERT INTO playlist_entries
(id, playlist_id, set_id, tema_id)
VALUES
(:id, :playlist_id, :set_id, :tema_id)
RETURNING * RETURNING *
""" """
data = conversion.playlist_entry_to_row(pl_entry) data = conversion.playlist_entry_to_row(pl_entry)
with get_connection(con) as con: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, data) _ = cur.execute(query, data)
row: conversion.PlaylistRowTuple = cur.fetchone() row: conversion.PlaylistEntryRowTuple = cur.fetchone()
return conversion.row_to_playlist_entry(row) return conversion.row_to_playlist_entry(row)
def update_playlist_entry(entry: model.PlaylistEntry, con: Connection | None = None): def update_playlist_entry(
entry: model.PlaylistEntry,
con: Connection | None = None,
):
query = """ query = """
UPDATE playlists UPDATE playlist_entries
SET SET
id = :id, session_id = :session_id, set_id = :set_id, tema_id = :tema_id id = :id, playlist_id = :playlist_id, set_id = :set_id, tema_id = :tema_id
WHERE WHERE
id = :id id = :id
""" """
@@ -35,9 +74,12 @@ def update_playlist_entry(entry: model.PlaylistEntry, con: Connection | None = N
return return
def delete_playlist_entry(entry_id: int, con: Connection | None = None): def delete_playlist_entry(
entry_id: int,
con: Connection | None = None,
):
query = """ query = """
DELETE FROM playlists DELETE FROM playlist_entries
WHERE id = :id WHERE id = :id
""" """
data = dict(id=entry_id) data = dict(id=entry_id)
@@ -47,12 +89,50 @@ def delete_playlist_entry(entry_id: int, con: Connection | None = None):
return return
def delete_playlist_set(session_id: int, set_id: int, con: Connection | None = None): def delete_playlist_set(
playlist_id: int,
set_id: int,
con: Connection | None = None,
):
query = """ query = """
DELETE FROM playlists DELETE FROM playlist_entries
WHERE session_id = :session_id AND set_id = :set_id WHERE playlist_id = :playlist_id AND set_id = :set_id
""" """
data = dict(session_id=session_id, set_id=set_id) data = dict(playlist_id=playlist_id, set_id=set_id)
with get_connection(con) as con:
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: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, data) _ = cur.execute(query, data)

View File

@@ -4,6 +4,7 @@ from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Connection | None = None): def create_db(con: Connection | None = None):
with get_connection(con) as con: with get_connection(con) as con:
create_sessions_table(con) create_sessions_table(con)
create_session_playlists_table(con)
def create_sessions_table(con: Connection): def create_sessions_table(con: Connection):
@@ -22,3 +23,18 @@ def create_sessions_table(con: Connection):
""" """
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query) _ = cur.execute(query)
def create_session_playlists_table(con: Connection):
query = """
CREATE TABLE IF NOT EXISTS session_playlists (
session_id INTEGER,
playlist_type TEXT,
playlist_id INTEGER NOT NULL,
PRIMARY KEY (session_id, playlist_type),
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
)
"""
cur = con.cursor()
_ = cur.execute(query)

View File

@@ -0,0 +1,138 @@
from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.dal.sql.temes.conversion import row_to_tema
from folkugat_web.model import sessions as sessions_model
from folkugat_web.model import temes as temes_model
from folkugat_web.model.playlists import PlaylistType
from folkugat_web.utils import groupby
from . import conversion
def get_playlist_id(
session_id: int,
playlist_type: PlaylistType,
con: Connection | None = None,
) -> int | None:
query = f"""
SELECT playlist_id
FROM session_playlists
WHERE
session_id = :session_id AND
playlist_type = :playlist_type
"""
data = dict(
session_id=session_id,
playlist_type=playlist_type.value,
)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
if row := cur.fetchone():
return row[0]
return None
def insert_playlist(
session_id: int,
playlist_type: PlaylistType,
playlist_id: int,
con: Connection | None = None,
):
query = f"""
INSERT INTO session_playlists
(session_id, playlist_type, playlist_id)
VALUES
(:session_id, :playlist_type, :playlist_id)
"""
data = dict(
session_id=session_id,
playlist_type=playlist_type.value,
playlist_id=playlist_id,
)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
def delete_playlist(
session_id: int,
playlist_type: PlaylistType,
con: Connection | None = None,
):
query = f"""
DELETE FROM session_playlists
WHERE
session_id = :session_id AND
playlist_type = :playlist_type
"""
data = dict(
session_id=session_id,
playlist_type=playlist_type.value,
)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, str | None, str | None, bool]
def get_tune_sessions(
tema_ids: list[int],
con: Connection | None = None,
) -> dict[int, list[sessions_model.Session]]:
placeholders = ", ".join(["?" for _ in tema_ids])
query = f"""
SELECT
p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name,
s.venue_url, s.notes, s.cartell_url, s.is_live
FROM playlist_entries p
JOIN session_playlists sp ON p.playlist_id = sp.playlist_id
JOIN sessions s ON sp.session_id = s.id
WHERE
p.tema_id IN ({placeholders}) AND
sp.playlist_type = "{PlaylistType.SESSION_SETLIST.value}"
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, tema_ids)
result_rows: list[GetTuneSessionsRow] = cur.fetchall()
return dict(groupby(
result_rows,
key_fn=lambda row: row[0],
group_fn=lambda rows: list(conversion.row_to_session(row[1:]) for row in rows)
))
CommonlyPlayedTuneRow = tuple[int, str, str, str, str, int, int]
def get_commonly_played_tunes(
tema_id: int,
con: Connection | None = None,
) -> list[temes_model.CommonlyPlayedTema]:
query = f"""
SELECT
id, title, alternatives, creation_date, modification_date, hidden, count
FROM (
SELECT tema_id, count(*) count FROM playlist_entries p JOIN (
SELECT pe.playlist_id, pe.set_id
FROM playlist_entries pe JOIN session_playlists sp USING (playlist_id)
WHERE tema_id = :tema_id AND playlist_type = :playlist_type
) s
ON p.playlist_id == s.playlist_id AND p.set_id == s.set_id
WHERE tema_id != :tema_id
GROUP BY tema_id
) common JOIN temes t ON common.tema_id == t.id
"""
data = dict(tema_id=tema_id, playlist_type=PlaylistType.SESSION_SETLIST.value)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
result_rows: list[CommonlyPlayedTuneRow] = cur.fetchall()
return [
temes_model.CommonlyPlayedTema(
tema=row_to_tema(row[:6]),
count=row[6],
) for row in result_rows
]

View File

@@ -4,7 +4,7 @@ from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import temes as model 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): class LyricRowDict(TypedDict):
@@ -12,6 +12,7 @@ class LyricRowDict(TypedDict):
tema_id: int tema_id: int
title: str title: str
content: str content: str
max_columns: int | None
def lyric_to_row(lyric: model.Lyrics) -> LyricRowDict: 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, "tema_id": lyric.tema_id,
"title": lyric.title, "title": lyric.title,
"content": lyric.content, "content": lyric.content,
"max_columns": lyric.max_columns,
} }
@@ -29,6 +31,7 @@ def row_to_lyric(row: LyricRowTuple) -> model.Lyrics:
tema_id=row[1], tema_id=row[1],
title=row[2], title=row[2],
content=row[3], 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""" query = f"""
SELECT SELECT
id, tema_id, title, content id, tema_id, title, content, max_columns
FROM tema_lyrics FROM tema_lyrics
{filter_clause} {filter_clause}
""" """
@@ -77,9 +80,9 @@ def insert_lyric(lyric: model.Lyrics, con: Connection | None = None) -> model.Ly
data = lyric_to_row(lyric) data = lyric_to_row(lyric)
query = f""" query = f"""
INSERT INTO tema_lyrics INSERT INTO tema_lyrics
(id, tema_id, title, content) (id, tema_id, title, content, max_columns)
VALUES VALUES
(:id, :tema_id, :title, :content) (:id, :tema_id, :title, :content, :max_columns)
RETURNING * RETURNING *
""" """
with get_connection(con) as con: 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, tema_id=tema_id,
title=title or "", title=title or "",
content="", content="",
max_columns=None,
) )
return insert_lyric(new_lyric, con=con) return insert_lyric(new_lyric, con=con)
@@ -104,7 +108,7 @@ def update_lyric(lyric: model.Lyrics, con: Connection | None = None):
query = """ query = """
UPDATE tema_lyrics UPDATE tema_lyrics
SET SET
tema_id = :tema_id, title = :title, content = :content tema_id = :tema_id, title = :title, content = :content, max_columns = :max_columns
WHERE WHERE
id = :id id = :id
""" """

View File

@@ -0,0 +1 @@
from folkugat_web.fragments import llistes

View File

@@ -14,7 +14,7 @@ def sessio_en_directe(request: Request):
raise RuntimeError("Got a session without id!") raise RuntimeError("Got a session without id!")
current_set = None current_set = None
if playlist := playlists_service.get_playlist(session_id=session.id): if playlist := service.get_session_setlist(session_id=session.id):
if playlist.sets: if playlist.sets:
current_set = playlists_service.add_temes_to_set(playlist.sets[-1]) current_set = playlists_service.add_temes_to_set(playlist.sets[-1])

View 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()

View File

@@ -0,0 +1,237 @@
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
from folkugat_web.services.temes import search as search_service
from folkugat_web.templates import templates
def add_set(
request: Request,
playlist_id: int,
logged_in: bool,
):
new_set = playlists_service.add_set(playlist_id=playlist_id)
return templates.TemplateResponse(
"fragments/llista/set_entry.html",
{
"request": request,
"logged_in": logged_in,
"new_entry": True,
"playlist_id": playlist_id,
"set_id": new_set.id,
"set_entry": new_set,
}
)
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/llista/set_entry.html",
{
"request": request,
"logged_in": logged_in,
"new_entry": True,
"playlist_id": playlist_id,
"set_id": set_id,
"set_entry": set_entry,
}
)
else:
return HTMLResponse()
def delete_set(playlist_id: int, set_id: int):
playlists_service.delete_set(playlist_id=playlist_id, set_id=set_id)
return HTMLResponse()
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/llista/tema_editor.html",
{
"request": request,
"logged_in": logged_in,
"playlist_id": playlist_id,
"set_id": set_id,
"tema_entry": new_tema,
}
)
def get_tema(request: Request, playlist_id: int, set_id: int, entry_id: int, logged_in: bool):
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/llista/tema_entry.html",
{
"request": request,
"logged_in": logged_in,
"playlist_id": playlist_id,
"set_id": set_id,
"tema_entry": tema_entry,
}
)
def get_tema_editor(request: Request, playlist_id: int, set_id: int, entry_id: int, logged_in: bool):
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/llista/tema_editor.html",
{
"request": request,
"logged_in": logged_in,
"playlist_id": playlist_id,
"set_id": set_id,
"tema_entry": tema_entry,
}
)
def delete_tema(playlist_id: int, set_id: int, entry_id: int):
playlists_service.delete_tema(entry_id=entry_id)
if not playlists_service.get_set(playlist_id=playlist_id, set_id=set_id):
headers = {
"HX-Trigger": f"reload-set-{set_id}"
}
else:
headers = {}
return HTMLResponse(headers=headers)
def busca_tema(
request: Request,
playlist_id: int,
set_id: int,
entry_id: int,
query: str,
):
n_results = 4
suggestions = []
if not query:
# If there is no query, suggest tunes commonly played together
set_entry = playlists_service.get_set(
playlist_id=playlist_id,
set_id=set_id,
)
if set_entry:
tema_ids = {tema_in_set.tema_id
for tema_in_set in set_entry.temes
if tema_in_set and tema_in_set.tema_id is not None}
commonly_played_tema_ids = {
cpt.tema.id
for tema_id in tema_ids
for cpt in sessions_service.get_commonly_played_temes(tema_id)
if cpt.tema.id is not None
} - tema_ids
suggestions = query_service.get_temes_by_ids(
tema_ids=list(commonly_played_tema_ids)
)
if len(suggestions) >= n_results:
suggestions = suggestions[:n_results]
elif not suggestions:
suggestions = search_service.busca_temes(
query=query,
properties=[],
hidden=True,
limit=n_results,
offset=0,
)
return templates.TemplateResponse(
"fragments/llista/tema_results.html",
{
"request": request,
"playlist_id": playlist_id,
"set_id": set_id,
"entry_id": entry_id,
"results": suggestions,
"query": query,
}
)
def set_tema(request: Request, logged_in: bool, playlist_id: int, set_id: int, entry_id: int, tema_id: int | None):
playlists_service.set_tema(playlist_id=playlist_id, set_id=set_id, entry_id=entry_id, tema_id=tema_id)
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/llista/tema_entry.html",
{
"request": request,
"logged_in": logged_in,
"playlist_id": playlist_id,
"set_id": set_id,
"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,
}
)

View File

@@ -1,10 +1,5 @@
from fastapi import Request 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 import sessions as sessions_service
from folkugat_web.services.temes import query as query_service
from folkugat_web.services.temes import search as search_service
from folkugat_web.templates import templates from folkugat_web.templates import templates

View File

@@ -0,0 +1,30 @@
from fastapi import Request
from folkugat_web.services import sessions as sessions_service
from folkugat_web.templates import templates
def notes(request: Request, session_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id)
return templates.TemplateResponse(
"fragments/sessio/notes.html",
{
"request": request,
"logged_in": logged_in,
"session_id": session_id,
"session": session,
"notes": session.notes if session and session.notes else "",
}
)
def notes_editor(request: Request, session_id: int):
session = sessions_service.get_session(session_id=session_id)
return templates.TemplateResponse(
"fragments/sessio/notes_editor.html",
{
"request": request,
"session_id": session_id,
"notes": session.notes if session and session.notes else "",
"max": max,
}
)

View File

@@ -1,6 +1,5 @@
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from folkugat_web.model.pagines import Pages 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 import sessions as sessions_service
from folkugat_web.templates import templates from folkugat_web.templates import templates
@@ -9,8 +8,7 @@ def pagina(request: Request, session_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id) session = sessions_service.get_session(session_id=session_id)
if not session: if not session:
raise HTTPException(status_code=404, detail="Could not find session") raise HTTPException(status_code=404, detail="Could not find session")
playlist = playlists_service.get_playlist(session_id=session_id) session = sessions_service.add_playlists_to_session(session=session)
playlist = playlists_service.add_temes_to_playlist(playlist)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/sessio/pagina.html", "fragments/sessio/pagina.html",
{ {
@@ -19,7 +17,6 @@ def pagina(request: Request, session_id: int, logged_in: bool):
"Pages": Pages, "Pages": Pages,
"session_id": session_id, "session_id": session_id,
"session": session, "session": session,
"playlist": playlist,
"date_names": sessions_service.get_date_names, "date_names": sessions_service.get_date_names,
} }
) )

View File

@@ -1,169 +0,0 @@
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 query as query_service
from folkugat_web.services.temes import search as search_service
from folkugat_web.templates import templates
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,
):
n_results = 4
suggestions = []
if not query:
# If there is no query, suggest tunes commonly played together
set_entry = playlists_service.get_set(
session_id=session_id,
set_id=set_id,
)
if set_entry:
tema_ids = {tema_in_set.tema_id
for tema_in_set in set_entry.temes
if tema_in_set and tema_in_set.tema_id is not None}
commonly_played_tema_ids = {
cpt.tema.id
for tema_id in tema_ids
for cpt in playlists_service.get_commonly_played_temes(tema_id)
if cpt.tema.id is not None
} - tema_ids
suggestions = query_service.get_temes_by_ids(
tema_ids=list(commonly_played_tema_ids)
)
if len(suggestions) >= n_results:
suggestions = suggestions[:n_results]
elif not suggestions:
suggestions = search_service.busca_temes(
query=query,
properties=[],
hidden=True,
limit=n_results,
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": suggestions,
"query": query,
}
)
def set_tema(request: Request, logged_in: bool, session_id: int, set_id: int, entry_id: int, tema_id: int | None):
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

@@ -6,22 +6,20 @@ from folkugat_web.services import sessions as sessions_service
from folkugat_web.templates import templates from folkugat_web.templates import templates
async def pagina(request: Request, session_id: int, set_id: int, logged_in: bool): async def pagina(request: Request, playlist_id: int, set_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id) set_ = playlists_service.get_set(playlist_id=playlist_id, set_id=set_id)
set_ = playlists_service.get_set(session_id=session_id, set_id=set_id)
if not set_: if not set_:
raise HTTPException(status_code=404, detail="Set not found") raise HTTPException(status_code=404, detail="Set not found")
set_ = playlists_service.add_temes_to_set(set_) set_ = playlists_service.add_temes_to_set(set_)
set_ = await playlists_service.add_set_score_to_set(set_) set_ = await playlists_service.add_set_score_to_set(set_)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/sessio/set/pagina.html", "fragments/llista/set/pagina.html",
{ {
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
"Pages": Pages, "Pages": Pages,
"session_id": session_id,
"session": session,
"set": set_, "set": set_,
"session": None,
"date_names": sessions_service.get_date_names, "date_names": sessions_service.get_date_names,
"LinkType": LinkType, "LinkType": LinkType,
"ContentType": ContentType, "ContentType": ContentType,
@@ -33,12 +31,12 @@ async def live(request: Request, logged_in: bool):
session = sessions_service.get_live_session() session = sessions_service.get_live_session()
set_ = None set_ = None
if session and session.id: if session and session.id:
playlist = playlists_service.get_playlist(session_id=session.id) playlist = sessions_service.get_session_setlist(session_id=session.id)
if playlist.sets: if playlist and playlist.sets:
set_ = playlists_service.add_temes_to_set(playlist.sets[-1]) set_ = playlists_service.add_temes_to_set(playlist.sets[-1])
set_ = await playlists_service.add_set_score_to_set(set_) set_ = await playlists_service.add_set_score_to_set(set_)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/sessio/set/pagina.html", "fragments/llista/set/pagina.html",
{ {
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,
@@ -56,12 +54,12 @@ async def live_set(request: Request, logged_in: bool):
session = sessions_service.get_live_session() session = sessions_service.get_live_session()
set_ = None set_ = None
if session and session.id: if session and session.id:
playlist = playlists_service.get_playlist(session_id=session.id) playlist = sessions_service.get_session_setlist(session_id=session.id)
if playlist.sets: if playlist and playlist.sets:
set_ = playlists_service.add_temes_to_set(playlist.sets[-1]) set_ = playlists_service.add_temes_to_set(playlist.sets[-1])
set_ = await playlists_service.add_set_score_to_set(set_) set_ = await playlists_service.add_set_score_to_set(set_)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/sessio/set/set_page.html", "fragments/llista/set/set_page.html",
{ {
"request": request, "request": request,
"logged_in": logged_in, "logged_in": logged_in,

View File

@@ -32,13 +32,16 @@ class LyricsText:
@classmethod @classmethod
def from_lyrics(cls, lyrics: temes.Lyrics, max_cols: int = 3) -> Self: 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 = [ 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 for par_str in lyrics.content.split("\n\n") if par_str
] ]
return cls(lines=[ return cls(lines=[
LyricsLine(paragraphs=list(line_pars)) 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: def hash(self) -> bytes:
@@ -89,3 +92,15 @@ class LilypondSet:
self.title.encode(), self.title.encode(),
*(tune.hash() for tune in self.tunes), *(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),
)

View File

@@ -4,3 +4,4 @@ import enum
class Pages(enum.IntEnum): class Pages(enum.IntEnum):
Sessions = 0 Sessions = 0
Temes = 1 Temes = 1
Llistes = 2

View File

@@ -1,15 +1,23 @@
import dataclasses import dataclasses
import enum
from collections.abc import Iterator from collections.abc import Iterator
from typing import Self from typing import Self
from folkugat_web.model.temes import Tema from folkugat_web.model.temes import Tema
from folkugat_web.utils import groupby from folkugat_web.utils import groupby
DEFAULT_PLAYLIST_NAME = "Llista de temes"
class PlaylistType(enum.Enum):
SESSION_SETLIST = "session_setlist"
SESSION_SLOWJAM = "session_slowjam"
@dataclasses.dataclass @dataclasses.dataclass
class PlaylistEntry: class PlaylistEntry:
id: int | None id: int | None
session_id: int playlist_id: int
set_id: int set_id: int
tema_id: int | None tema_id: int | None
@@ -20,17 +28,21 @@ class TemaInSet:
tema_id: int | None tema_id: int | None
tema: Tema | None tema: Tema | None
def to_playlist_entry(self, session_id: int, set_id: int) -> PlaylistEntry: def to_playlist_entry(self, playlist_id: int, set_id: int) -> PlaylistEntry:
return PlaylistEntry( return PlaylistEntry(
id=self.id, id=self.id,
session_id=session_id, playlist_id=playlist_id,
set_id=set_id, set_id=set_id,
tema_id=self.tema_id, tema_id=self.tema_id,
) )
@classmethod @classmethod
def from_playlist_entry(cls, entry: PlaylistEntry) -> Self: def from_playlist_entry(cls, entry: PlaylistEntry) -> Self:
return cls(id=entry.id, tema_id=entry.tema_id, tema=None) return cls(
id=entry.id,
tema_id=entry.tema_id,
tema=None,
)
@dataclasses.dataclass @dataclasses.dataclass
@@ -39,16 +51,22 @@ class SetScore:
pdf_url: str | None pdf_url: str | None
@dataclasses.dataclass
class PlaylistScore:
img_url: str | None
pdf_url: str | None
@dataclasses.dataclass @dataclasses.dataclass
class Set: class Set:
id: int id: int
temes: list[TemaInSet] temes: list[TemaInSet]
score: SetScore | None score: SetScore | None
def to_playlist_entries(self, session_id: int) -> Iterator[PlaylistEntry]: def to_playlist_entries(self, playlist_id: int) -> Iterator[PlaylistEntry]:
for tema_in_set in self.temes: for tema_in_set in self.temes:
yield tema_in_set.to_playlist_entry( yield tema_in_set.to_playlist_entry(
session_id=session_id, playlist_id=playlist_id,
set_id=self.id, set_id=self.id,
) )
@@ -58,26 +76,33 @@ class Set:
raise ValueError("All PlaylistEntries must have the same session_id") raise ValueError("All PlaylistEntries must have the same session_id")
return cls( return cls(
id=set_id, id=set_id,
temes=[TemaInSet.from_playlist_entry(entry) for entry in sorted(entries, key=lambda e: e.id or 0)], temes=[
TemaInSet.from_playlist_entry(entry)
for entry in sorted(entries, key=lambda e: e.id or 0)
],
score=None, score=None,
) )
@dataclasses.dataclass @dataclasses.dataclass
class Playlist: class Playlist:
session_id: int id: int
name: str | None
sets: list[Set] sets: list[Set]
hidden: bool = True
playlist_score: PlaylistScore | None = None
def to_playlist_entries(self) -> Iterator[PlaylistEntry]: def to_playlist_entries(self) -> Iterator[PlaylistEntry]:
for set_entry in self.sets: for set_entry in self.sets:
yield from set_entry.to_playlist_entries(session_id=self.session_id) yield from set_entry.to_playlist_entries(playlist_id=self.id)
@classmethod @classmethod
def from_playlist_entries(cls, session_id: int, entries: list[PlaylistEntry]) -> Self: def from_playlist_entries(cls, playlist_id: int, name: str | None, entries: list[PlaylistEntry]) -> Self:
if any(entry.session_id != session_id for entry in entries): if any(entry.playlist_id != playlist_id for entry in entries):
raise ValueError("All PlaylistEntries must have the same session_id") raise ValueError("All PlaylistEntries must have the same playlist_id")
return cls( return cls(
session_id=session_id, id=playlist_id,
name=name,
sets=[ sets=[
Set.from_playlist_entries(set_id, set_entries) 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) for set_id, set_entries in groupby(entries, key_fn=lambda e: e.set_id, group_fn=list)

View File

@@ -2,6 +2,8 @@ import dataclasses
import datetime import datetime
import enum import enum
from folkugat_web.model import playlists
DEFAULT_START_TIME = datetime.time(20, 30) DEFAULT_START_TIME = datetime.time(20, 30)
DEFAULT_END_TIME = datetime.time(22, 30) DEFAULT_END_TIME = datetime.time(22, 30)
@@ -21,6 +23,8 @@ class Session:
venue: SessionVenue = dataclasses.field(default_factory=SessionVenue) venue: SessionVenue = dataclasses.field(default_factory=SessionVenue)
notes: str | None = None notes: str | None = None
cartell_url: str | None = None cartell_url: str | None = None
slowjam: playlists.Playlist | None = None
setlist: playlists.Playlist | None = None
is_live: bool = False is_live: bool = False

View File

@@ -4,13 +4,15 @@ import dataclasses
import datetime import datetime
import enum import enum
import itertools import itertools
from typing import Self from typing import TYPE_CHECKING, Self
from folkugat_web.model.lilypond.processing import RenderError from folkugat_web.model.lilypond.processing import RenderError
from folkugat_web.model.search import NGrams from folkugat_web.model.search import NGrams
from folkugat_web.model.sessions import Session
from folkugat_web.services import ngrams from folkugat_web.services import ngrams
if TYPE_CHECKING:
from folkugat_web.model.sessions import Session
class ContentType(enum.Enum): class ContentType(enum.Enum):
PARTITURA = "partitura" PARTITURA = "partitura"
@@ -58,6 +60,7 @@ class Lyrics:
tema_id: int tema_id: int
title: str title: str
content: str content: str
max_columns: int | None = None
@dataclasses.dataclass @dataclasses.dataclass

View File

@@ -100,6 +100,10 @@ def get_set_filename(filename: str) -> Path:
return db.DB_FILES_SET_DIR / filename return db.DB_FILES_SET_DIR / filename
def get_playlist_filename(filename: str) -> Path:
return db.DB_FILES_PLAYLIST_DIR / filename
@asynccontextmanager @asynccontextmanager
async def tmp_file(content: str): async def tmp_file(content: str):
input_filename = create_tmp_filename(extension=".ly") input_filename = create_tmp_filename(extension=".ly")

View File

@@ -4,6 +4,7 @@ from fastapi import HTTPException
from folkugat_web.model import playlists as playlists_model from folkugat_web.model import playlists as playlists_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.model.lilypond import score as lilypond_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 lyrics as lyrics_service
from folkugat_web.services.temes import properties as properties_service from folkugat_web.services.temes import properties as properties_service
from folkugat_web.services.temes import query as temes_q from folkugat_web.services.temes import query as temes_q
@@ -96,3 +97,20 @@ def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet:
title=set_title, title=set_title,
tunes=tunes 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
)

View File

@@ -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 from folkugat_web.templates import templates
SCORE_BEGINNING = "% --- SCORE BEGINNING --- %" SCORE_BEGINNING = "% --- SCORE BEGINNING --- %"
@@ -23,3 +23,10 @@ def set_source(tune_set: LilypondSet) -> str:
score_beginning=SCORE_BEGINNING, score_beginning=SCORE_BEGINNING,
tune_set=tune_set, tune_set=tune_set,
) )
def playlist_source(playlist: LilypondPlaylist) -> str:
return templates.get_template("lilypond/playlist.ly").render(
score_beginning=SCORE_BEGINNING,
playlist=playlist,
)

View File

@@ -5,7 +5,6 @@ from folkugat_web.dal.sql._connection import get_connection
from folkugat_web.dal.sql.playlists import query, write from folkugat_web.dal.sql.playlists import query, write
from folkugat_web.log import logger from folkugat_web.log import logger
from folkugat_web.model import playlists from folkugat_web.model import playlists
from folkugat_web.model import temes as temes_model
from folkugat_web.model.lilypond import score as lilypond_model from folkugat_web.model.lilypond import score as lilypond_model
from folkugat_web.services import files as files_service from folkugat_web.services import files as files_service
from folkugat_web.services.lilypond import build as lilypond_build from folkugat_web.services.lilypond import build as lilypond_build
@@ -41,37 +40,45 @@ def add_tema_to_tema_in_set(tema_in_set: playlists.TemaInSet) -> playlists.TemaI
return tema_in_set return tema_in_set
def get_playlist(session_id: int, con: Connection | None = None) -> playlists.Playlist: def get_playlist(playlist_id: int, con: Connection | None = None) -> playlists.Playlist:
return playlists.Playlist.from_playlist_entries( with get_connection(con) as playlist_con:
session_id=session_id, playlist_name = query.get_playlist_name(playlist_id=playlist_id, con=playlist_con)
entries=list(query.get_playlist_entries(session_id=session_id, con=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 add_set(session_id: int, con: Connection | None = None) -> playlists.Set: 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:
with get_connection(con) as con: with get_connection(con) as con:
curr_playlist = get_playlist(session_id=session_id, con=con) curr_playlist = get_playlist(playlist_id=playlist_id, con=con)
new_set_id = max([set_entry.id for set_entry in curr_playlist.sets], default=0) + 1 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) new_entry = playlists.PlaylistEntry(id=None, playlist_id=playlist_id, set_id=new_set_id, tema_id=None)
inserted_entry = write.insert_playlist_entry(new_entry) inserted_entry = write.insert_playlist_entry(new_entry)
return playlists.Set.from_playlist_entries(set_id=inserted_entry.set_id, entries=[inserted_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: Connection | None = None) -> playlists.Set | None: def get_set(playlist_id: int, set_id: int, con: Connection | None = None) -> playlists.Set | None:
entries = list(query.get_playlist_entries(session_id=session_id, set_id=set_id, con=con)) entries = list(query.get_playlist_entries(playlist_id=playlist_id, set_id=set_id, con=con))
if entries: if entries:
return playlists.Set.from_playlist_entries(set_id=set_id, entries=entries) return playlists.Set.from_playlist_entries(set_id=set_id, entries=entries)
else: else:
return None return None
def delete_set(session_id: int, set_id: int, con: Connection | None = None): def delete_set(playlist_id: int, set_id: int, con: Connection | None = None):
write.delete_playlist_set(session_id=session_id, set_id=set_id, con=con) write.delete_playlist_set(playlist_id=playlist_id, set_id=set_id, con=con)
def add_tema(session_id: int, set_id: int, con: Connection | None = None) -> playlists.TemaInSet: def add_tema(playlist_id: int, set_id: int, con: Connection | None = None) -> playlists.TemaInSet:
with get_connection(con) as con: with get_connection(con) as con:
new_entry = playlists.PlaylistEntry(id=None, session_id=session_id, set_id=set_id, tema_id=None) new_entry = playlists.PlaylistEntry(id=None, playlist_id=playlist_id, set_id=set_id, tema_id=None)
inserted_entry = write.insert_playlist_entry(new_entry) inserted_entry = write.insert_playlist_entry(new_entry)
return playlists.TemaInSet.from_playlist_entry(inserted_entry) return playlists.TemaInSet.from_playlist_entry(inserted_entry)
@@ -87,10 +94,10 @@ def delete_tema(entry_id: int, con: Connection | None = None):
write.delete_playlist_entry(entry_id=entry_id, con=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: int | None, def set_tema(playlist_id: int, set_id: int, entry_id: int, tema_id: int | None,
con: Connection | None = None): con: Connection | None = None):
with get_connection(con) as con: 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) new_entry = playlists.PlaylistEntry(id=entry_id, playlist_id=playlist_id, set_id=set_id, tema_id=tema_id)
write.update_playlist_entry(entry=new_entry, con=con) write.update_playlist_entry(entry=new_entry, con=con)
@@ -165,7 +172,73 @@ async def add_set_score_to_set(tune_set: playlists.Set) -> playlists.Set:
return tune_set return tune_set
def get_commonly_played_temes( async def get_or_create_playlist_score(playlist: playlists.Playlist) -> playlists.PlaylistScore | None:
tema_id: int, # The playlist is assumed to be enriched with tunes
) -> list[temes_model.CommonlyPlayedTema]: if not playlist.sets:
return query.get_commonly_played_tunes(tema_id=tema_id) 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)

View File

@@ -1,10 +1,17 @@
import dataclasses
import datetime import datetime
from datetime import date as Date from datetime import date as Date
from folkugat_web.config import date as config 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.dal.sql.sessions import query, write
from folkugat_web.model import sessions as model from folkugat_web.model import sessions as model
from folkugat_web.model import temes as temes_model
from folkugat_web.model.playlists import Playlist, PlaylistType
from folkugat_web.model.sql import Order, OrderCol, Range from folkugat_web.model.sql import Order, OrderCol, Range
from folkugat_web.services import playlists as playlists_service
def get_date_names(date: Date) -> model.DateNames: def get_date_names(date: Date) -> model.DateNames:
@@ -60,14 +67,19 @@ def get_session(session_id: int) -> model.Session | None:
def insert_session(session: model.Session): 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): def set_session(session: model.Session):
write.update_session(session) write.update_session(session)
_update_session_playlist_names(session)
def delete_session(session_id: int): def delete_session(session_id: int):
_delete_session_playlists(session_id)
write.delete_session_by_id(session_id) write.delete_session_by_id(session_id)
@@ -77,3 +89,181 @@ def stop_live_sessions():
def set_live_session(session_id: int): def set_live_session(session_id: int):
write.set_live_session(session_id=session_id) write.set_live_session(session_id=session_id)
def get_session_playlist_id(
session_id: int,
playlist_type: PlaylistType,
con: Connection | None = None,
) -> int | None:
with get_connection(con=con) as con:
return session_playlists.get_playlist_id(
session_id=session_id,
playlist_type=playlist_type,
con=con,
)
def get_session_playlist(
session_id: int,
playlist_type: PlaylistType,
con: Connection | None = None,
) -> Playlist | None:
with get_connection(con=con) as con:
playlist_id = get_session_playlist_id(
session_id=session_id,
playlist_type=playlist_type,
con=con,
)
if playlist_id is None:
return None
return playlists_service.get_playlist(
playlist_id=playlist_id,
con=con,
)
def get_session_setlist(
session_id: int,
con: Connection | None = None,
) -> Playlist | None:
return get_session_playlist(
session_id=session_id,
playlist_type=PlaylistType.SESSION_SETLIST,
con=con,
)
def get_session_slowjam(
session_id: int,
con: Connection | None = None,
) -> Playlist | None:
return get_session_playlist(
session_id=session_id,
playlist_type=PlaylistType.SESSION_SLOWJAM,
con=con,
)
def add_playlists_to_session(session: model.Session) -> model.Session:
if session.id is not None:
with get_connection() as con:
setlist = get_session_setlist(session_id=session.id, con=con)
if setlist:
setlist = playlists_service.add_temes_to_playlist(setlist)
slowjam = get_session_slowjam(session_id=session.id, con=con)
if slowjam:
slowjam = playlists_service.add_temes_to_playlist(slowjam)
session = dataclasses.replace(
session,
setlist=setlist,
slowjam=slowjam,
)
return session
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,
)

View File

@@ -17,6 +17,7 @@ def _sub(pattern: str, sub: str) -> Callable[[str], str]:
def _clean_string(lyrics_str: str) -> str: def _clean_string(lyrics_str: str) -> str:
return ( return (
FnChain.transform(lyrics_str) | FnChain.transform(lyrics_str) |
# _sub(r"~", "") |
_sub(r"\t", " ") | _sub(r"\t", " ") |
_sub(r" +", " ") | _sub(r" +", " ") |
_sub(r" \n", "\n") | _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) 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): def update_lyric(lyric: model.Lyrics, title: str, content: str, max_columns: int | None = None):
lyric = dataclasses.replace(lyric, title=title.strip(), content=_clean_string(content)) # 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) lyrics_dal.update_lyric(lyric=lyric)
return lyric return lyric

View File

@@ -1,6 +1,6 @@
from collections.abc import Iterable from collections.abc import Iterable
from folkugat_web.dal.sql.playlists import query as playlists_q from folkugat_web.dal.sql.sessions import playlists as session_playlists
from folkugat_web.dal.sql.temes import query as temes_q from folkugat_web.dal.sql.temes import query as temes_q
from folkugat_web.model import sessions as sessions_model from folkugat_web.model import sessions as sessions_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
@@ -20,7 +20,7 @@ def tema_compute_stats(
) -> model.Tema: ) -> model.Tema:
if tema.id: if tema.id:
if tune_sessions_dict is None: if tune_sessions_dict is None:
tune_sessions_dict = playlists_q.get_tune_sessions(tema_ids=[tema.id]) tune_sessions_dict = session_playlists.get_tune_sessions(tema_ids=[tema.id])
if tema.id and (tune_sessions := tune_sessions_dict.get(tema.id)): if tema.id and (tune_sessions := tune_sessions_dict.get(tema.id)):
unique_tune_sessions = set(tune_sessions) unique_tune_sessions = set(tune_sessions)
tema.stats = model.Stats( tema.stats = model.Stats(
@@ -33,7 +33,7 @@ def tema_compute_stats(
def temes_compute_stats(temes: Iterable[model.Tema]) -> list[model.Tema]: def temes_compute_stats(temes: Iterable[model.Tema]) -> list[model.Tema]:
temes = list(temes) temes = list(temes)
tema_ids = [tema.id for tema in temes if tema.id is not None] tema_ids = [tema.id for tema in temes if tema.id is not None]
tune_sessions_dict = playlists_q.get_tune_sessions(tema_ids=tema_ids) tune_sessions_dict = session_playlists.get_tune_sessions(tema_ids=tema_ids)
return [tema_compute_stats(tema=tema, tune_sessions_dict=tune_sessions_dict) for tema in temes] return [tema_compute_stats(tema=tema, tune_sessions_dict=tune_sessions_dict) for tema in temes]
@@ -41,5 +41,5 @@ def tema_compute_played_with(
tema: model.Tema, tema: model.Tema,
) -> model.Tema: ) -> model.Tema:
if tema.id: if tema.id:
tema.played_with = playlists_q.get_commonly_played_tunes(tema_id=tema.id) tema.played_with = session_playlists.get_commonly_played_tunes(tema_id=tema.id)
return tema return tema

View File

@@ -0,0 +1,57 @@
from folkugat_web.dal.sql import get_connection
from folkugat_web.dal.sql.playlists import write as playlists_w
from folkugat_web.dal.sql.playlists.ddl import (create_playlist_entries_table,
create_playlists_table)
from folkugat_web.dal.sql.sessions import playlists as session_playlists
from folkugat_web.dal.sql.sessions import query as sessions_q
from folkugat_web.dal.sql.sessions.ddl import create_session_playlists_table
from folkugat_web.model.playlists import PlaylistType
with get_connection() as con:
cur = con.cursor()
drop_query = """DROP TABLE rehearse_data"""
_ = cur.execute(drop_query)
alter_query = """ALTER TABLE playlists RENAME TO playlists_old"""
_ = cur.execute(alter_query)
# Create new tables
create_playlist_entries_table(con=con)
create_playlists_table(con=con)
create_session_playlists_table(con=con)
# Create new playlists
sessions = sessions_q.get_sessions(con=con)
for session in sessions:
if session.id is None:
raise ValueError("Session without id!")
setlist_id = playlists_w.create_playlist(con=con)
session_playlists.insert_playlist(
session_id=session.id,
playlist_type=PlaylistType.SESSION_SETLIST,
playlist_id=setlist_id,
con=con,
)
slowjam_id = playlists_w.create_playlist(con=con)
session_playlists.insert_playlist(
session_id=session.id,
playlist_type=PlaylistType.SESSION_SLOWJAM,
playlist_id=slowjam_id,
con=con,
)
# Migrate
migrate_query = """
INSERT INTO playlist_entries
(id, playlist_id, set_id, tema_id)
SELECT
NULL, sp.playlist_id, pl_old.set_id, pl_old.tema_id
FROM playlists_old pl_old JOIN session_playlists sp USING (session_id)
WHERE sp.playlist_type = "session_setlist"
"""
_ = cur.execute(migrate_query)
print("DONE!")

View 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()

View 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()

View 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!")