Escollir partitura del tema en un set
This commit is contained in:
@@ -6,7 +6,10 @@
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
|
||||
python = pkgs.python311;
|
||||
pythonPackages = pkgs.python311Packages;
|
||||
@@ -62,6 +65,7 @@
|
||||
sqlite
|
||||
lazysql
|
||||
opencode
|
||||
claude-code
|
||||
# Local https
|
||||
mkcert
|
||||
];
|
||||
|
||||
@@ -200,6 +200,7 @@ def set_tema(
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
tema_id: Annotated[int, Form()],
|
||||
score_id: Annotated[int | None, Form()] = None,
|
||||
):
|
||||
return playlist.set_tema(
|
||||
request=request,
|
||||
@@ -208,6 +209,28 @@ def set_tema(
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=tema_id,
|
||||
score_id=score_id,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/llista/{playlist_id}/set/{set_id}/tema/{entry_id}/score")
|
||||
def set_tema_score(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
score_id: Annotated[int | None, Form()] = None,
|
||||
):
|
||||
if score_id is not None and score_id < 0:
|
||||
score_id = None
|
||||
return playlist.set_tema_score(
|
||||
request=request,
|
||||
logged_in=logged_in,
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
score_id=score_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,10 +30,34 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if logged_in and tema_entry.tema %}
|
||||
{% set available_scores = tema_entry.tema.scores | selectattr('hidden', 'equalto', false) | list %}
|
||||
{% if available_scores | length > 1 %}
|
||||
<div class="flex flex-row items-center my-1 w-full text-sm">
|
||||
<span class="text-gray-600 mr-2">Partitura:</span>
|
||||
<select class="border border-beige bg-beige rounded p-1 text-sm"
|
||||
hx-put="/api/llista/{{ playlist_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/score"
|
||||
hx-target="#tune-entry-{{ tema_entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
name="score_id">
|
||||
<option value="-1" {% if not tema_entry.score_id %}selected{% endif %}>
|
||||
(Per defecte)
|
||||
</option>
|
||||
{% for score in available_scores %}
|
||||
<option value="{{ score.id }}"
|
||||
{% if tema_entry.score_id == score.id %}selected{% endif %}>
|
||||
{{ score.title or 'Partitura ' ~ loop.index }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="my-1 w-full">
|
||||
{% if tema_entry.tema and tema_entry.tema.main_score() and tema_entry.tema.main_score().preview_url %}
|
||||
{% set effective_score = tema_entry.get_effective_score() %}
|
||||
{% if effective_score and effective_score.preview_url %}
|
||||
<img class="pb-2"
|
||||
src="{{ tema_entry.tema.main_score().preview_url }}" />
|
||||
src="{{ effective_score.preview_url }}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TypedDict
|
||||
from folkugat_web.model import playlists as model
|
||||
|
||||
PlaylistRowTuple = tuple[int, str | None, int]
|
||||
PlaylistEntryRowTuple = tuple[int, int, int, int | None]
|
||||
PlaylistEntryRowTuple = tuple[int, int, int, int | None, int | None]
|
||||
|
||||
|
||||
class PlaylistRowDict(TypedDict):
|
||||
@@ -11,6 +11,7 @@ class PlaylistRowDict(TypedDict):
|
||||
playlist_id: int
|
||||
set_id: int
|
||||
tema_id: int | None
|
||||
score_id: int | None
|
||||
|
||||
|
||||
def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict:
|
||||
@@ -19,6 +20,7 @@ def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict:
|
||||
'playlist_id': tema_in_set.playlist_id,
|
||||
'set_id': tema_in_set.set_id,
|
||||
'tema_id': tema_in_set.tema_id,
|
||||
'score_id': tema_in_set.score_id,
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +30,7 @@ def row_to_playlist_entry(row: PlaylistEntryRowTuple) -> model.PlaylistEntry:
|
||||
playlist_id=row[1],
|
||||
set_id=row[2],
|
||||
tema_id=row[3],
|
||||
score_id=row[4],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,10 @@ def create_playlist_entries_table(con: Connection):
|
||||
playlist_id INTEGER NOT NULL,
|
||||
set_id INTEGER NOT NULL,
|
||||
tema_id INTEGER,
|
||||
score_id INTEGER,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tema_id) REFERENCES temes(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (tema_id) REFERENCES temes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (score_id) REFERENCES tema_scores(id) ON DELETE SET NULL
|
||||
)
|
||||
"""
|
||||
cur = con.cursor()
|
||||
|
||||
@@ -46,7 +46,7 @@ def get_playlist_entries(
|
||||
filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, playlist_id=playlist_id)
|
||||
query = f"""
|
||||
SELECT
|
||||
id, playlist_id, set_id, tema_id
|
||||
id, playlist_id, set_id, tema_id, score_id
|
||||
FROM playlist_entries
|
||||
WHERE {filter_clause}
|
||||
ORDER BY id ASC
|
||||
|
||||
@@ -43,9 +43,9 @@ def insert_playlist_entry(
|
||||
) -> model.PlaylistEntry:
|
||||
query = """
|
||||
INSERT INTO playlist_entries
|
||||
(id, playlist_id, set_id, tema_id)
|
||||
(id, playlist_id, set_id, tema_id, score_id)
|
||||
VALUES
|
||||
(:id, :playlist_id, :set_id, :tema_id)
|
||||
(:id, :playlist_id, :set_id, :tema_id, :score_id)
|
||||
RETURNING *
|
||||
"""
|
||||
data = conversion.playlist_entry_to_row(pl_entry)
|
||||
@@ -63,7 +63,7 @@ def update_playlist_entry(
|
||||
query = """
|
||||
UPDATE playlist_entries
|
||||
SET
|
||||
id = :id, playlist_id = :playlist_id, set_id = :set_id, tema_id = :tema_id
|
||||
id = :id, playlist_id = :playlist_id, set_id = :set_id, tema_id = :tema_id, score_id = :score_id
|
||||
WHERE
|
||||
id = :id
|
||||
"""
|
||||
|
||||
@@ -158,8 +158,50 @@ def busca_tema(
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
def set_tema(
|
||||
request: Request,
|
||||
logged_in: bool,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
tema_id: int | None,
|
||||
score_id: int | None = None,
|
||||
):
|
||||
playlists_service.set_tema(
|
||||
playlist_id=playlist_id, set_id=set_id, entry_id=entry_id, tema_id=tema_id, score_id=score_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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def set_tema_score(
|
||||
request: Request,
|
||||
logged_in: bool,
|
||||
playlist_id: int,
|
||||
set_id: int,
|
||||
entry_id: int,
|
||||
score_id: int | None,
|
||||
):
|
||||
"""Update only the score selection for an existing playlist entry."""
|
||||
# Get current entry to preserve tema_id
|
||||
current_entry = playlists_service.get_tema(entry_id=entry_id)
|
||||
playlists_service.set_tema(
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
entry_id=entry_id,
|
||||
tema_id=current_entry.tema_id,
|
||||
score_id=score_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(
|
||||
|
||||
@@ -20,6 +20,7 @@ class PlaylistEntry:
|
||||
playlist_id: int
|
||||
set_id: int
|
||||
tema_id: int | None
|
||||
score_id: int | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -27,6 +28,7 @@ class TemaInSet:
|
||||
id: int | None
|
||||
tema_id: int | None
|
||||
tema: Tema | None
|
||||
score_id: int | None = None
|
||||
|
||||
def to_playlist_entry(self, playlist_id: int, set_id: int) -> PlaylistEntry:
|
||||
return PlaylistEntry(
|
||||
@@ -34,6 +36,7 @@ class TemaInSet:
|
||||
playlist_id=playlist_id,
|
||||
set_id=set_id,
|
||||
tema_id=self.tema_id,
|
||||
score_id=self.score_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -42,8 +45,19 @@ class TemaInSet:
|
||||
id=entry.id,
|
||||
tema_id=entry.tema_id,
|
||||
tema=None,
|
||||
score_id=entry.score_id,
|
||||
)
|
||||
|
||||
def get_effective_score(self):
|
||||
"""Returns the specified score if set, otherwise falls back to main_score()."""
|
||||
if self.tema is None:
|
||||
return None
|
||||
if self.score_id is not None:
|
||||
for score in self.tema.scores:
|
||||
if score.id == self.score_id:
|
||||
return score
|
||||
return self.tema.main_score()
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SetScore:
|
||||
|
||||
@@ -26,15 +26,34 @@ def unknown_tune() -> lilypond_model.LilypondTune:
|
||||
)
|
||||
|
||||
|
||||
def tune_from_tema(tema: model.Tema | None, score_source: str | None = None) -> lilypond_model.LilypondTune:
|
||||
def tune_from_tema(
|
||||
tema: model.Tema | None,
|
||||
score_source: str | None = None,
|
||||
score_id: int | None = None,
|
||||
) -> lilypond_model.LilypondTune:
|
||||
"""
|
||||
The given `tema` is assumed to have properties, lyrics and scores (if source is None)
|
||||
"""
|
||||
if tema is None:
|
||||
return unknown_tune()
|
||||
|
||||
# Determine the score source to use
|
||||
effective_source = score_source
|
||||
if effective_source is None and tema.scores:
|
||||
if score_id is not None:
|
||||
# Find the specific score by ID
|
||||
for score in tema.scores:
|
||||
if score.id == score_id:
|
||||
effective_source = score.source
|
||||
break
|
||||
if effective_source is None:
|
||||
# Fallback to main_score or first score
|
||||
main = tema.main_score()
|
||||
effective_source = main.source if main else tema.scores[0].source
|
||||
|
||||
return lilypond_model.LilypondTune(
|
||||
header=lilypond_model.HeaderData.from_tema(tema=tema),
|
||||
score_source=score_source or (tema.scores[0].source if tema.scores else None),
|
||||
score_source=effective_source,
|
||||
lyrics=lilypond_model.LyricsText.from_lyrics(lyrics=tema.lyrics[0]) if tema.lyrics else None,
|
||||
)
|
||||
|
||||
@@ -84,15 +103,23 @@ def set_from_set(set_entry: playlists_model.Set) -> lilypond_model.LilypondSet:
|
||||
"""
|
||||
The tune_set is assumed to be enriched with tunes
|
||||
"""
|
||||
tema_ids = [tema_in_set.tema_id for tema_in_set in set_entry.temes]
|
||||
temes_by_id = {
|
||||
tema_in_set.tema_id: tema_in_set.tema
|
||||
for tema_in_set in set_entry.temes
|
||||
if tema_in_set.id is not None and tema_in_set.tema
|
||||
}
|
||||
temes = [temes_by_id[tema_id] if tema_id is not None else None for tema_id in tema_ids]
|
||||
temes = [temes_by_id.get(t.tema_id) for t in set_entry.temes]
|
||||
set_title = build_set_title(temes=temes)
|
||||
tunes = tunes_from_temes(temes)
|
||||
|
||||
# Build tunes with score_id consideration
|
||||
tunes = [
|
||||
tune_from_tema(
|
||||
tema=temes_by_id.get(tema_in_set.tema_id),
|
||||
score_id=tema_in_set.score_id,
|
||||
)
|
||||
for tema_in_set in set_entry.temes
|
||||
]
|
||||
|
||||
return lilypond_model.LilypondSet(
|
||||
title=set_title,
|
||||
tunes=tunes
|
||||
|
||||
@@ -95,9 +95,11 @@ def delete_tema(entry_id: int, con: Connection | None = None):
|
||||
|
||||
|
||||
def set_tema(playlist_id: int, set_id: int, entry_id: int, tema_id: int | None,
|
||||
con: Connection | None = None):
|
||||
score_id: int | None = None, con: Connection | None = None):
|
||||
with get_connection(con) as con:
|
||||
new_entry = playlists.PlaylistEntry(id=entry_id, playlist_id=playlist_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, score_id=score_id
|
||||
)
|
||||
write.update_playlist_entry(entry=new_entry, con=con)
|
||||
|
||||
|
||||
@@ -111,11 +113,13 @@ async def get_or_create_set_score(tune_set: playlists.Set) -> playlists.SetScore
|
||||
if not tune_set.temes:
|
||||
return None
|
||||
if len(tune_set.temes) == 1:
|
||||
if (tema := tune_set.temes[0].tema) is None or (main_score := tema.main_score()) is None:
|
||||
tema_in_set = tune_set.temes[0]
|
||||
effective_score = tema_in_set.get_effective_score()
|
||||
if effective_score is None:
|
||||
return None
|
||||
return playlists.SetScore(
|
||||
pdf_url=main_score.pdf_url,
|
||||
img_url=main_score.img_url,
|
||||
pdf_url=effective_score.pdf_url,
|
||||
img_url=effective_score.img_url,
|
||||
)
|
||||
|
||||
lilypond_set = lilypond_build.set_from_set(set_entry=tune_set)
|
||||
|
||||
41
scripts/09_add_playlist_entry_score_id.py
Normal file
41
scripts/09_add_playlist_entry_score_id.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add score_id column to playlist_entries table.
|
||||
This allows playlist entries to optionally specify which score to use for a tune.
|
||||
"""
|
||||
|
||||
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 score_id column already exists
|
||||
cur.execute("PRAGMA table_info(playlist_entries)")
|
||||
columns = [column[1] for column in cur.fetchall()]
|
||||
|
||||
if 'score_id' not in columns:
|
||||
print("Adding score_id column to playlist_entries table...")
|
||||
cur.execute("ALTER TABLE playlist_entries ADD COLUMN score_id INTEGER")
|
||||
print("Column added successfully.")
|
||||
else:
|
||||
print("score_id column already exists.")
|
||||
|
||||
con.commit()
|
||||
|
||||
print("Migration completed successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user