Escollir partitura del tema en un set
This commit is contained in:
@@ -6,7 +6,10 @@
|
|||||||
outputs = { self, nixpkgs, flake-utils }:
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
flake-utils.lib.eachDefaultSystem (system: let
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
|
||||||
python = pkgs.python311;
|
python = pkgs.python311;
|
||||||
pythonPackages = pkgs.python311Packages;
|
pythonPackages = pkgs.python311Packages;
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
sqlite
|
sqlite
|
||||||
lazysql
|
lazysql
|
||||||
opencode
|
opencode
|
||||||
|
claude-code
|
||||||
# Local https
|
# Local https
|
||||||
mkcert
|
mkcert
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ def set_tema(
|
|||||||
set_id: int,
|
set_id: int,
|
||||||
entry_id: int,
|
entry_id: int,
|
||||||
tema_id: Annotated[int, Form()],
|
tema_id: Annotated[int, Form()],
|
||||||
|
score_id: Annotated[int | None, Form()] = None,
|
||||||
):
|
):
|
||||||
return playlist.set_tema(
|
return playlist.set_tema(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -208,6 +209,28 @@ def set_tema(
|
|||||||
set_id=set_id,
|
set_id=set_id,
|
||||||
entry_id=entry_id,
|
entry_id=entry_id,
|
||||||
tema_id=tema_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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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">
|
<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"
|
<img class="pb-2"
|
||||||
src="{{ tema_entry.tema.main_score().preview_url }}" />
|
src="{{ effective_score.preview_url }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import TypedDict
|
|||||||
from folkugat_web.model import playlists as model
|
from folkugat_web.model import playlists as model
|
||||||
|
|
||||||
PlaylistRowTuple = tuple[int, str | None, int]
|
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):
|
class PlaylistRowDict(TypedDict):
|
||||||
@@ -11,6 +11,7 @@ class PlaylistRowDict(TypedDict):
|
|||||||
playlist_id: int
|
playlist_id: int
|
||||||
set_id: int
|
set_id: int
|
||||||
tema_id: int | None
|
tema_id: int | None
|
||||||
|
score_id: int | None
|
||||||
|
|
||||||
|
|
||||||
def playlist_entry_to_row(tema_in_set: model.PlaylistEntry) -> PlaylistRowDict:
|
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,
|
'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,
|
||||||
|
'score_id': tema_in_set.score_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ def row_to_playlist_entry(row: PlaylistEntryRowTuple) -> model.PlaylistEntry:
|
|||||||
playlist_id=row[1],
|
playlist_id=row[1],
|
||||||
set_id=row[2],
|
set_id=row[2],
|
||||||
tema_id=row[3],
|
tema_id=row[3],
|
||||||
|
score_id=row[4],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ def create_playlist_entries_table(con: Connection):
|
|||||||
playlist_id INTEGER NOT NULL,
|
playlist_id INTEGER NOT NULL,
|
||||||
set_id INTEGER NOT NULL,
|
set_id INTEGER NOT NULL,
|
||||||
tema_id INTEGER,
|
tema_id INTEGER,
|
||||||
|
score_id INTEGER,
|
||||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE,
|
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()
|
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)
|
filter_clause, data = _filter_clause(entry_id=entry_id, set_id=set_id, playlist_id=playlist_id)
|
||||||
query = f"""
|
query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
id, playlist_id, set_id, tema_id
|
id, playlist_id, set_id, tema_id, score_id
|
||||||
FROM playlist_entries
|
FROM playlist_entries
|
||||||
WHERE {filter_clause}
|
WHERE {filter_clause}
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ def insert_playlist_entry(
|
|||||||
) -> model.PlaylistEntry:
|
) -> model.PlaylistEntry:
|
||||||
query = """
|
query = """
|
||||||
INSERT INTO playlist_entries
|
INSERT INTO playlist_entries
|
||||||
(id, playlist_id, set_id, tema_id)
|
(id, playlist_id, set_id, tema_id, score_id)
|
||||||
VALUES
|
VALUES
|
||||||
(:id, :playlist_id, :set_id, :tema_id)
|
(:id, :playlist_id, :set_id, :tema_id, :score_id)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
"""
|
"""
|
||||||
data = conversion.playlist_entry_to_row(pl_entry)
|
data = conversion.playlist_entry_to_row(pl_entry)
|
||||||
@@ -63,7 +63,7 @@ def update_playlist_entry(
|
|||||||
query = """
|
query = """
|
||||||
UPDATE playlist_entries
|
UPDATE playlist_entries
|
||||||
SET
|
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
|
WHERE
|
||||||
id = :id
|
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):
|
def set_tema(
|
||||||
playlists_service.set_tema(playlist_id=playlist_id, set_id=set_id, entry_id=entry_id, tema_id=tema_id)
|
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.get_tema(entry_id=entry_id)
|
||||||
tema_entry = playlists_service.add_tema_to_tema_in_set(tema_entry)
|
tema_entry = playlists_service.add_tema_to_tema_in_set(tema_entry)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class PlaylistEntry:
|
|||||||
playlist_id: int
|
playlist_id: int
|
||||||
set_id: int
|
set_id: int
|
||||||
tema_id: int | None
|
tema_id: int | None
|
||||||
|
score_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
@@ -27,6 +28,7 @@ class TemaInSet:
|
|||||||
id: int | None
|
id: int | None
|
||||||
tema_id: int | None
|
tema_id: int | None
|
||||||
tema: Tema | None
|
tema: Tema | None
|
||||||
|
score_id: int | None = None
|
||||||
|
|
||||||
def to_playlist_entry(self, playlist_id: int, set_id: int) -> PlaylistEntry:
|
def to_playlist_entry(self, playlist_id: int, set_id: int) -> PlaylistEntry:
|
||||||
return PlaylistEntry(
|
return PlaylistEntry(
|
||||||
@@ -34,6 +36,7 @@ class TemaInSet:
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
set_id=set_id,
|
set_id=set_id,
|
||||||
tema_id=self.tema_id,
|
tema_id=self.tema_id,
|
||||||
|
score_id=self.score_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -42,8 +45,19 @@ class TemaInSet:
|
|||||||
id=entry.id,
|
id=entry.id,
|
||||||
tema_id=entry.tema_id,
|
tema_id=entry.tema_id,
|
||||||
tema=None,
|
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
|
@dataclasses.dataclass
|
||||||
class SetScore:
|
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)
|
The given `tema` is assumed to have properties, lyrics and scores (if source is None)
|
||||||
"""
|
"""
|
||||||
if tema is None:
|
if tema is None:
|
||||||
return unknown_tune()
|
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(
|
return lilypond_model.LilypondTune(
|
||||||
header=lilypond_model.HeaderData.from_tema(tema=tema),
|
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,
|
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
|
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 = {
|
temes_by_id = {
|
||||||
tema_in_set.tema_id: tema_in_set.tema
|
tema_in_set.tema_id: tema_in_set.tema
|
||||||
for tema_in_set in set_entry.temes
|
for tema_in_set in set_entry.temes
|
||||||
if tema_in_set.id is not None and tema_in_set.tema
|
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)
|
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(
|
return lilypond_model.LilypondSet(
|
||||||
title=set_title,
|
title=set_title,
|
||||||
tunes=tunes
|
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,
|
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:
|
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)
|
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:
|
if not tune_set.temes:
|
||||||
return None
|
return None
|
||||||
if len(tune_set.temes) == 1:
|
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 None
|
||||||
return playlists.SetScore(
|
return playlists.SetScore(
|
||||||
pdf_url=main_score.pdf_url,
|
pdf_url=effective_score.pdf_url,
|
||||||
img_url=main_score.img_url,
|
img_url=effective_score.img_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
lilypond_set = lilypond_build.set_from_set(set_entry=tune_set)
|
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