695 lines
26 KiB
Python
695 lines
26 KiB
Python
"""
|
|
local_db.py — Lokal SQLite database for LineDance Player v0.9
|
|
|
|
Ny arkitektur:
|
|
songs — global katalog (synkroniseret med server, server-UUID som ID)
|
|
files — lokalt fil-index (kun denne maskine)
|
|
playlist_songs — refererer til song_id + valgfri file_id
|
|
"""
|
|
|
|
import sqlite3
|
|
import logging
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DB_PATH = Path.home() / ".linedance" / "local.db"
|
|
|
|
|
|
def get_db_path() -> Path:
|
|
return DB_PATH
|
|
|
|
|
|
@contextmanager
|
|
def get_db():
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(DB_PATH, timeout=10)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
except Exception:
|
|
conn.rollback()
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ── Schema ────────────────────────────────────────────────────────────────────
|
|
|
|
SCHEMA = """
|
|
-- Musik-biblioteker (mapper brugeren har tilføjet)
|
|
CREATE TABLE IF NOT EXISTS libraries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
path TEXT NOT NULL UNIQUE,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Global sang-katalog (synkroniseret med server)
|
|
-- ID er server-UUID. Sange uden server-ID har et lokalt UUID.
|
|
CREATE TABLE IF NOT EXISTS songs (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL DEFAULT '',
|
|
artist TEXT NOT NULL DEFAULT '',
|
|
album TEXT NOT NULL DEFAULT '',
|
|
bpm INTEGER NOT NULL DEFAULT 0,
|
|
duration_sec INTEGER NOT NULL DEFAULT 0,
|
|
mbid TEXT UNIQUE,
|
|
acoustid TEXT,
|
|
server_synced INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Lokalt fil-index (kun denne maskine)
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id TEXT PRIMARY KEY,
|
|
song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE,
|
|
local_path TEXT NOT NULL UNIQUE,
|
|
file_missing INTEGER NOT NULL DEFAULT 0,
|
|
file_format TEXT NOT NULL DEFAULT '',
|
|
file_modified_at TEXT NOT NULL DEFAULT '',
|
|
extra_tags TEXT NOT NULL DEFAULT '{}',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_files_song_id ON files(song_id);
|
|
CREATE INDEX IF NOT EXISTS idx_files_missing ON files(file_missing);
|
|
|
|
-- Dans-niveauer
|
|
CREATE TABLE IF NOT EXISTS dance_levels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sort_order INTEGER NOT NULL,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
-- Danse
|
|
-- Dans + niveau + koreograf er unik kombination
|
|
CREATE TABLE IF NOT EXISTS dances (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
level_id INTEGER REFERENCES dance_levels(id),
|
|
choreographer TEXT NOT NULL DEFAULT '',
|
|
video_url TEXT NOT NULL DEFAULT '',
|
|
stepsheet_url TEXT NOT NULL DEFAULT '',
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
use_count INTEGER NOT NULL DEFAULT 1,
|
|
source TEXT NOT NULL DEFAULT 'local',
|
|
UNIQUE(name, level_id, choreographer)
|
|
);
|
|
|
|
-- Sang-dans tags
|
|
CREATE TABLE IF NOT EXISTS song_dances (
|
|
id TEXT PRIMARY KEY,
|
|
song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE,
|
|
dance_id INTEGER NOT NULL REFERENCES dances(id),
|
|
dance_order INTEGER NOT NULL DEFAULT 1,
|
|
UNIQUE(song_id, dance_id)
|
|
);
|
|
|
|
-- Alternativ-dans tags
|
|
CREATE TABLE IF NOT EXISTS song_alt_dances (
|
|
id TEXT PRIMARY KEY,
|
|
song_id TEXT NOT NULL REFERENCES songs(id) ON DELETE CASCADE,
|
|
dance_id INTEGER NOT NULL REFERENCES dances(id),
|
|
note TEXT NOT NULL DEFAULT '',
|
|
source TEXT NOT NULL DEFAULT 'local',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(song_id, dance_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_song_dances ON song_dances(song_id);
|
|
CREATE INDEX IF NOT EXISTS idx_song_alt_dances ON song_alt_dances(song_id);
|
|
|
|
-- Playlister
|
|
CREATE TABLE IF NOT EXISTS playlists (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
tags TEXT NOT NULL DEFAULT '',
|
|
api_project_id TEXT UNIQUE,
|
|
is_linked INTEGER NOT NULL DEFAULT 0,
|
|
server_permission TEXT NOT NULL DEFAULT 'edit',
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- Playliste-sange
|
|
CREATE TABLE IF NOT EXISTS playlist_songs (
|
|
id TEXT PRIMARY KEY,
|
|
playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
|
|
song_id TEXT NOT NULL REFERENCES songs(id),
|
|
file_id TEXT REFERENCES files(id),
|
|
position INTEGER NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
is_workshop INTEGER NOT NULL DEFAULT 0,
|
|
dance_override TEXT NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_playlist_songs_playlist ON playlist_songs(playlist_id);
|
|
CREATE INDEX IF NOT EXISTS idx_playlist_songs_song ON playlist_songs(song_id);
|
|
|
|
-- Event-state (gemmes løbende)
|
|
CREATE TABLE IF NOT EXISTS event_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
-- Dans-navne til autoudfyld
|
|
CREATE TABLE IF NOT EXISTS dance_names (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
use_count INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
"""
|
|
|
|
DEFAULT_DANCE_LEVELS = [
|
|
(10, "Absolute Beginner", "Ingen tidligere danse-erfaring kræves"),
|
|
(20, "Beginner", "Lidt tidligere erfaring"),
|
|
(30, "High Beginner", "God begynder, klar til mere"),
|
|
(40, "Low Improver", "Begyndende øvet"),
|
|
(50, "Improver", "Grundlæggende færdigheder på plads"),
|
|
(60, "High Improver", "Stærk øvet, næsten intermediate"),
|
|
(70, "Low Intermediate", "Begyndende intermediate"),
|
|
(80, "Intermediate", "Erfaren danser"),
|
|
(90, "High Intermediate", "Stærk intermediate"),
|
|
(99, "Advanced", "Fuld beherskelse af trin og teknik"),
|
|
]
|
|
|
|
|
|
def init_db():
|
|
"""Opret tabeller og seed dance_levels hvis de mangler."""
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
with get_db() as conn:
|
|
conn.executescript(SCHEMA)
|
|
# Seed dans-niveauer
|
|
for sort_order, name, desc in DEFAULT_DANCE_LEVELS:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)",
|
|
(sort_order, name, desc)
|
|
)
|
|
logger.info("Database initialiseret")
|
|
|
|
|
|
# ── Biblioteker ───────────────────────────────────────────────────────────────
|
|
|
|
def add_library(path: str) -> int:
|
|
with get_db() as conn:
|
|
cur = conn.execute(
|
|
"INSERT OR IGNORE INTO libraries (path) VALUES (?)", (path,)
|
|
)
|
|
return cur.lastrowid
|
|
|
|
|
|
def get_libraries() -> list:
|
|
with get_db() as conn:
|
|
return conn.execute(
|
|
"SELECT * FROM libraries WHERE is_active=1 ORDER BY path"
|
|
).fetchall()
|
|
|
|
|
|
def remove_library(library_id: int):
|
|
with get_db() as conn:
|
|
row = conn.execute(
|
|
"SELECT path FROM libraries WHERE id=?", (library_id,)
|
|
).fetchone()
|
|
if row:
|
|
# Marker filer fra denne mappe som missing
|
|
conn.execute(
|
|
"UPDATE files SET file_missing=1 WHERE local_path LIKE ?",
|
|
(row["path"] + "%",)
|
|
)
|
|
conn.execute("DELETE FROM libraries WHERE id=?", (library_id,))
|
|
|
|
|
|
# ── Sange ─────────────────────────────────────────────────────────────────────
|
|
|
|
def find_or_create_song(title: str, artist: str = "", album: str = "",
|
|
bpm: int = 0, duration_sec: int = 0,
|
|
mbid: str = "", acoustid: str = "",
|
|
song_id: str = None) -> str:
|
|
"""
|
|
Find eksisterende sang eller opret ny. Returnerer song_id.
|
|
Match-hierarki: server_id → mbid → acoustid → titel+artist → opret ny
|
|
"""
|
|
import uuid as _uuid
|
|
with get_db() as conn:
|
|
# Match på server-ID
|
|
if song_id:
|
|
row = conn.execute(
|
|
"SELECT id FROM songs WHERE id=?", (song_id,)
|
|
).fetchone()
|
|
if row:
|
|
# Opdater data hvis bedre info tilgængeligt
|
|
conn.execute("""
|
|
UPDATE songs SET
|
|
title = CASE WHEN title='' THEN ? ELSE title END,
|
|
artist = CASE WHEN artist='' THEN ? ELSE artist END,
|
|
bpm = CASE WHEN bpm=0 THEN ? ELSE bpm END,
|
|
mbid = CASE WHEN mbid IS NULL AND ? != '' THEN ? ELSE mbid END,
|
|
acoustid = CASE WHEN acoustid IS NULL AND ? != '' THEN ? ELSE acoustid END
|
|
WHERE id=?
|
|
""", (title, artist, bpm, mbid, mbid, acoustid, acoustid, song_id))
|
|
return song_id
|
|
|
|
# Match på MBID
|
|
if mbid:
|
|
row = conn.execute(
|
|
"SELECT id FROM songs WHERE mbid=?", (mbid,)
|
|
).fetchone()
|
|
if row:
|
|
return row["id"]
|
|
|
|
# Match på AcoustID
|
|
if acoustid:
|
|
row = conn.execute(
|
|
"SELECT id FROM songs WHERE acoustid=?", (acoustid,)
|
|
).fetchone()
|
|
if row:
|
|
if mbid:
|
|
conn.execute(
|
|
"UPDATE songs SET mbid=? WHERE id=? AND mbid IS NULL",
|
|
(mbid, row["id"])
|
|
)
|
|
return row["id"]
|
|
|
|
# Match på titel + artist
|
|
if title:
|
|
row = conn.execute(
|
|
"SELECT id FROM songs WHERE title=? AND artist=?",
|
|
(title, artist)
|
|
).fetchone()
|
|
if row:
|
|
if mbid:
|
|
conn.execute(
|
|
"UPDATE songs SET mbid=? WHERE id=? AND mbid IS NULL",
|
|
(mbid, row["id"])
|
|
)
|
|
return row["id"]
|
|
|
|
# Opret ny sang
|
|
new_id = song_id or str(_uuid.uuid4())
|
|
conn.execute(
|
|
"INSERT INTO songs (id, title, artist, album, bpm, duration_sec, mbid, acoustid) "
|
|
"VALUES (?,?,?,?,?,?,?,?)",
|
|
(new_id, title, artist, album, bpm, duration_sec,
|
|
mbid or None, acoustid or None)
|
|
)
|
|
return new_id
|
|
|
|
|
|
def get_song(song_id: str) -> sqlite3.Row | None:
|
|
with get_db() as conn:
|
|
return conn.execute(
|
|
"SELECT * FROM songs WHERE id=?", (song_id,)
|
|
).fetchone()
|
|
|
|
|
|
def update_song_bpm(song_id: str, bpm: int):
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"UPDATE songs SET bpm=? WHERE id=? AND (bpm=0 OR bpm IS NULL)",
|
|
(bpm, song_id)
|
|
)
|
|
|
|
|
|
def update_song_mbid(song_id: str, mbid: str, acoustid: str = ""):
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"UPDATE songs SET mbid=?, acoustid=? WHERE id=?",
|
|
(mbid or None, acoustid or None, song_id)
|
|
)
|
|
|
|
|
|
# ── Filer ─────────────────────────────────────────────────────────────────────
|
|
|
|
def upsert_file(song_id: str, local_path: str, file_format: str = "",
|
|
file_modified_at: str = "", extra_tags: str = "{}") -> str:
|
|
"""Opret eller opdater en fil-post. Returnerer file_id."""
|
|
import uuid as _uuid
|
|
with get_db() as conn:
|
|
existing = conn.execute(
|
|
"SELECT id FROM files WHERE local_path=?", (local_path,)
|
|
).fetchone()
|
|
if existing:
|
|
conn.execute("""
|
|
UPDATE files SET
|
|
song_id=?, file_missing=0,
|
|
file_format=?, file_modified_at=?, extra_tags=?
|
|
WHERE id=?
|
|
""", (song_id, file_format, file_modified_at, extra_tags, existing["id"]))
|
|
return existing["id"]
|
|
else:
|
|
file_id = str(_uuid.uuid4())
|
|
conn.execute(
|
|
"INSERT INTO files (id, song_id, local_path, file_format, file_modified_at, extra_tags) "
|
|
"VALUES (?,?,?,?,?,?)",
|
|
(file_id, song_id, local_path, file_format, file_modified_at, extra_tags)
|
|
)
|
|
return file_id
|
|
|
|
|
|
def get_file_for_song(song_id: str) -> sqlite3.Row | None:
|
|
"""Find bedste tilgængelige fil for en sang."""
|
|
with get_db() as conn:
|
|
return conn.execute(
|
|
"SELECT * FROM files WHERE song_id=? AND file_missing=0 LIMIT 1",
|
|
(song_id,)
|
|
).fetchone()
|
|
|
|
|
|
def get_file(file_id: str) -> sqlite3.Row | None:
|
|
with get_db() as conn:
|
|
return conn.execute(
|
|
"SELECT * FROM files WHERE id=?", (file_id,)
|
|
).fetchone()
|
|
|
|
|
|
def mark_file_missing(local_path: str):
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"UPDATE files SET file_missing=1 WHERE local_path=?", (local_path,)
|
|
)
|
|
|
|
|
|
def get_all_known_paths() -> set[str]:
|
|
with get_db() as conn:
|
|
rows = conn.execute("SELECT local_path FROM files").fetchall()
|
|
return {r["local_path"] for r in rows}
|
|
|
|
|
|
# ── Søgning i bibliotek ───────────────────────────────────────────────────────
|
|
|
|
def search_songs(query: str, limit: int = 200) -> list:
|
|
"""Søg i sange der har en tilgængelig fil."""
|
|
with get_db() as conn:
|
|
pattern = f"%{query}%"
|
|
return conn.execute("""
|
|
SELECT s.*, f.id as file_id, f.local_path, f.file_format, f.file_missing,
|
|
GROUP_CONCAT(d.name, ', ') as dance_names
|
|
FROM songs s
|
|
JOIN files f ON f.song_id = s.id AND f.file_missing = 0
|
|
LEFT JOIN song_dances sd ON sd.song_id = s.id
|
|
LEFT JOIN dances d ON d.id = sd.dance_id
|
|
WHERE s.title LIKE ? OR s.artist LIKE ? OR s.album LIKE ?
|
|
GROUP BY s.id
|
|
ORDER BY s.title
|
|
LIMIT ?
|
|
""", (pattern, pattern, pattern, limit)).fetchall()
|
|
|
|
|
|
def get_all_songs_with_files(limit: int = 5000) -> list:
|
|
"""Hent alle sange med tilgængelige filer — til biblioteksvisning."""
|
|
with get_db() as conn:
|
|
return conn.execute("""
|
|
SELECT s.*, f.id as file_id, f.local_path, f.file_format, f.file_missing,
|
|
GROUP_CONCAT(d.name ORDER BY sd.dance_order, ', ') as dance_names
|
|
FROM songs s
|
|
JOIN files f ON f.song_id = s.id AND f.file_missing = 0
|
|
LEFT JOIN song_dances sd ON sd.song_id = s.id
|
|
LEFT JOIN dances d ON d.id = sd.dance_id
|
|
GROUP BY s.id
|
|
ORDER BY s.title
|
|
LIMIT ?
|
|
""", (limit,)).fetchall()
|
|
|
|
|
|
# ── Playlister ────────────────────────────────────────────────────────────────
|
|
|
|
def create_playlist(name: str, description: str = "", tags: str = "") -> str:
|
|
import uuid as _uuid
|
|
pl_id = str(_uuid.uuid4())
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"INSERT INTO playlists (id, name, description, tags) VALUES (?,?,?,?)",
|
|
(pl_id, name, description, tags)
|
|
)
|
|
return pl_id
|
|
|
|
|
|
def get_playlists(tag_filter: str | None = None) -> list:
|
|
with get_db() as conn:
|
|
if tag_filter:
|
|
return conn.execute("""
|
|
SELECT p.id, p.name, p.description, p.tags, p.api_project_id,
|
|
p.is_linked, p.server_permission, p.is_deleted, p.created_at,
|
|
COUNT(ps.position) as song_count
|
|
FROM playlists p
|
|
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
|
|
WHERE p.name != '__aktiv__' AND p.is_deleted = 0
|
|
AND (p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR p.tags = ?)
|
|
GROUP BY p.id ORDER BY p.created_at DESC
|
|
""", (f"{tag_filter},%", f"%, {tag_filter},%",
|
|
f"%, {tag_filter}", tag_filter)).fetchall()
|
|
else:
|
|
return conn.execute("""
|
|
SELECT p.id, p.name, p.description, p.tags, p.api_project_id,
|
|
p.is_linked, p.server_permission, p.is_deleted, p.created_at,
|
|
COUNT(ps.position) as song_count
|
|
FROM playlists p
|
|
LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id
|
|
WHERE p.name != '__aktiv__' AND p.is_deleted = 0
|
|
GROUP BY p.id ORDER BY p.created_at DESC
|
|
""").fetchall()
|
|
|
|
|
|
def delete_playlist(playlist_id: str):
|
|
"""Soft-slet — sæt is_deleted=1."""
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"UPDATE playlists SET is_deleted=1 WHERE id=?", (playlist_id,)
|
|
)
|
|
|
|
|
|
def get_playlist_with_songs(playlist_id: str) -> dict:
|
|
with get_db() as conn:
|
|
playlist = conn.execute(
|
|
"SELECT * FROM playlists WHERE id=?", (playlist_id,)
|
|
).fetchone()
|
|
if not playlist:
|
|
return {}
|
|
|
|
songs = conn.execute("""
|
|
SELECT ps.id as ps_id, ps.position, ps.status,
|
|
ps.is_workshop, ps.dance_override,
|
|
ps.song_id, ps.file_id,
|
|
s.title, s.artist, s.album, s.bpm, s.duration_sec,
|
|
f.local_path, f.file_format, f.file_missing
|
|
FROM playlist_songs ps
|
|
JOIN songs s ON s.id = ps.song_id
|
|
LEFT JOIN files f ON f.id = ps.file_id
|
|
WHERE ps.playlist_id = ?
|
|
ORDER BY ps.position
|
|
""", (playlist_id,)).fetchall()
|
|
|
|
return {"playlist": dict(playlist), "songs": [dict(s) for s in songs]}
|
|
|
|
|
|
def add_song_to_playlist(playlist_id: str, song_id: str,
|
|
file_id: str | None = None,
|
|
position: int | None = None) -> str:
|
|
import uuid as _uuid
|
|
with get_db() as conn:
|
|
if position is None:
|
|
row = conn.execute(
|
|
"SELECT MAX(position) as max_pos FROM playlist_songs WHERE playlist_id=?",
|
|
(playlist_id,)
|
|
).fetchone()
|
|
position = (row["max_pos"] or 0) + 1
|
|
ps_id = str(_uuid.uuid4())
|
|
conn.execute(
|
|
"INSERT INTO playlist_songs (id, playlist_id, song_id, file_id, position) "
|
|
"VALUES (?,?,?,?,?)",
|
|
(ps_id, playlist_id, song_id, file_id, position)
|
|
)
|
|
return ps_id
|
|
|
|
|
|
# ── Dans-tags ─────────────────────────────────────────────────────────────────
|
|
|
|
def get_all_playlist_tags() -> list[str]:
|
|
return get_playlist_tags()
|
|
|
|
|
|
def get_playlist_tags() -> list[str]:
|
|
with get_db() as conn:
|
|
rows = conn.execute(
|
|
"SELECT tags FROM playlists WHERE tags != '' AND name != '__aktiv__' AND is_deleted=0"
|
|
).fetchall()
|
|
tags = set()
|
|
for row in rows:
|
|
for tag in row["tags"].split(","):
|
|
t = tag.strip().lower()
|
|
if t:
|
|
tags.add(t)
|
|
return sorted(tags)
|
|
|
|
|
|
def update_playlist_tags(playlist_id: str, tags: str):
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"UPDATE playlists SET tags=? WHERE id=?", (tags, playlist_id)
|
|
)
|
|
|
|
|
|
# ── Event-state ───────────────────────────────────────────────────────────────
|
|
|
|
def save_event_state(current_idx: int, statuses: list[str]):
|
|
import json
|
|
with get_db() as conn:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO event_state (key,value) VALUES ('current_idx',?)",
|
|
(str(current_idx),)
|
|
)
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO event_state (key,value) VALUES ('statuses',?)",
|
|
(json.dumps(statuses),)
|
|
)
|
|
|
|
|
|
def load_event_state() -> tuple | None:
|
|
import json
|
|
with get_db() as conn:
|
|
idx_row = conn.execute(
|
|
"SELECT value FROM event_state WHERE key='current_idx'"
|
|
).fetchone()
|
|
stat_row = conn.execute(
|
|
"SELECT value FROM event_state WHERE key='statuses'"
|
|
).fetchone()
|
|
if not idx_row or not stat_row:
|
|
return None
|
|
return int(idx_row["value"]), json.loads(stat_row["value"])
|
|
|
|
|
|
# ── Dans-niveauer ─────────────────────────────────────────────────────────────
|
|
|
|
def get_dance_levels() -> list:
|
|
with get_db() as conn:
|
|
return conn.execute(
|
|
"SELECT * FROM dance_levels ORDER BY sort_order"
|
|
).fetchall()
|
|
|
|
|
|
def upsert_dance_levels(levels: list[dict]):
|
|
with get_db() as conn:
|
|
for lvl in levels:
|
|
conn.execute("""
|
|
INSERT INTO dance_levels (id, sort_order, name, description)
|
|
VALUES (:id, :sort_order, :name, :description)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
sort_order=excluded.sort_order,
|
|
description=excluded.description
|
|
""", lvl)
|
|
|
|
# ── Dans-søgning (til DancePickerDialog) ─────────────────────────────────────
|
|
|
|
def get_dance_suggestions(prefix: str = "", limit: int = 20) -> list:
|
|
"""Hent dans-forslag med niveau og koreograf til autoudfyld."""
|
|
with get_db() as conn:
|
|
pattern = f"{prefix}%"
|
|
return conn.execute("""
|
|
SELECT d.id, d.name, d.level_id, dl.name as level_name,
|
|
d.choreographer, d.use_count
|
|
FROM dances d
|
|
LEFT JOIN dance_levels dl ON dl.id = d.level_id
|
|
WHERE d.name LIKE ? COLLATE NOCASE
|
|
ORDER BY d.use_count DESC, d.name
|
|
LIMIT ?
|
|
""", (pattern, limit)).fetchall()
|
|
|
|
|
|
def get_choreographer_suggestions(prefix: str = "", limit: int = 15) -> list[str]:
|
|
"""Hent koreograf-navne til autoudfyld."""
|
|
with get_db() as conn:
|
|
pattern = f"{prefix}%"
|
|
rows = conn.execute("""
|
|
SELECT DISTINCT choreographer FROM dances
|
|
WHERE choreographer != '' AND choreographer LIKE ? COLLATE NOCASE
|
|
ORDER BY choreographer
|
|
LIMIT ?
|
|
""", (pattern, limit)).fetchall()
|
|
return [r["choreographer"] for r in rows]
|
|
|
|
# ── Dans-søgning (til DancePickerDialog og DanceInfoDialog) ──────────────────
|
|
|
|
def get_dance_suggestions(prefix: str = "", limit: int = 20) -> list:
|
|
"""Hent dans-forslag med niveau og koreograf til autoudfyld."""
|
|
with get_db() as conn:
|
|
pattern = f"{prefix}%"
|
|
return conn.execute("""
|
|
SELECT d.id, d.name, d.level_id, dl.name as level_name,
|
|
d.choreographer, d.use_count
|
|
FROM dances d
|
|
LEFT JOIN dance_levels dl ON dl.id = d.level_id
|
|
WHERE d.name LIKE ? COLLATE NOCASE
|
|
ORDER BY d.use_count DESC, d.name
|
|
LIMIT ?
|
|
""", (pattern, limit)).fetchall()
|
|
|
|
|
|
def get_choreographer_suggestions(prefix: str = "", limit: int = 15) -> list[str]:
|
|
"""Hent koreograf-navne til autoudfyld."""
|
|
with get_db() as conn:
|
|
pattern = f"{prefix}%"
|
|
rows = conn.execute("""
|
|
SELECT DISTINCT choreographer FROM dances
|
|
WHERE choreographer != '' AND choreographer LIKE ? COLLATE NOCASE
|
|
ORDER BY choreographer
|
|
LIMIT ?
|
|
""", (pattern, limit)).fetchall()
|
|
return [r["choreographer"] for r in rows]
|
|
|
|
|
|
def get_dances_for_song(song_id: str) -> list:
|
|
"""Hent alle danse tagget på en sang med niveau og koreograf."""
|
|
with get_db() as conn:
|
|
rows = conn.execute("""
|
|
SELECT d.id, d.name, d.level_id, dl.name as level_name, d.choreographer,
|
|
d.video_url, d.stepsheet_url, d.notes, sd.dance_order
|
|
FROM song_dances sd
|
|
JOIN dances d ON d.id = sd.dance_id
|
|
LEFT JOIN dance_levels dl ON dl.id = d.level_id
|
|
WHERE sd.song_id = ?
|
|
ORDER BY sd.dance_order
|
|
""", (song_id,)).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_alt_dances_for_song(song_id: str) -> list:
|
|
"""Hent alle alternativ-danse tagget på en sang."""
|
|
with get_db() as conn:
|
|
rows = conn.execute("""
|
|
SELECT d.id, d.name, d.level_id, dl.name as level_name, d.choreographer,
|
|
d.video_url, d.stepsheet_url, sad.note
|
|
FROM song_alt_dances sad
|
|
JOIN dances d ON d.id = sad.dance_id
|
|
LEFT JOIN dance_levels dl ON dl.id = d.level_id
|
|
WHERE sad.song_id = ?
|
|
ORDER BY d.name
|
|
""", (song_id,)).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
def get_or_create_dance(name: str, level_id: int | None, conn,
|
|
choreographer: str = "") -> int:
|
|
"""
|
|
Find eller opret dans. Returnerer dance_id.
|
|
Dans + niveau + koreograf er unik kombination.
|
|
"""
|
|
choreo = choreographer or ""
|
|
existing = conn.execute(
|
|
"SELECT id FROM dances WHERE name=? "
|
|
"AND (level_id=? OR (level_id IS NULL AND ? IS NULL)) "
|
|
"AND choreographer=?",
|
|
(name, level_id, level_id, choreo)
|
|
).fetchone()
|
|
if existing:
|
|
return existing["id"]
|
|
cur = conn.execute(
|
|
"INSERT INTO dances (name, level_id, choreographer) VALUES (?,?,?)",
|
|
(name, level_id, choreo)
|
|
)
|
|
return cur.lastrowid |