Version 1

This commit is contained in:
2026-04-10 23:59:23 +02:00
parent 9d7adf42c1
commit d55859c593
17 changed files with 743 additions and 490 deletions

View File

@@ -17,36 +17,51 @@ from pathlib import Path
DB_PATH = Path.home() / ".linedance" / "local.db"
_local = threading.local()
_global_conn: sqlite3.Connection | None = None
def _get_conn() -> sqlite3.Connection:
"""Returnerer en thread-lokal forbindelse."""
if not hasattr(_local, "conn") or _local.conn is None:
"""Returnerer en global forbindelse i autocommit mode."""
global _global_conn
if _global_conn is None:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL") # bedre concurrent adgang
conn.execute("PRAGMA foreign_keys=ON")
_local.conn = conn
return _local.conn
_global_conn = sqlite3.connect(str(DB_PATH), check_same_thread=False,
isolation_level=None) # autocommit
_global_conn.row_factory = sqlite3.Row
_global_conn.execute("PRAGMA journal_mode=WAL")
_global_conn.execute("PRAGMA foreign_keys=ON")
return _global_conn
def new_conn() -> sqlite3.Connection:
"""Åbn en frisk forbindelse til brug i tag_editor og dialogs."""
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys=OFF") # FK checker forhindrer level_id gem
return conn
@contextmanager
def get_db():
"""Context manager der bruger app-forbindelsen i autocommit mode.
Hver statement committer med det samme — ingen eksplicit transaktion."""
conn = _get_conn()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
def get_db_raw() -> sqlite3.Connection:
return _get_conn()
def init_db():
"""Opret alle tabeller hvis de ikke findes."""
conn = _get_conn()
# Brug executescript direkte (ikke via context manager) da det auto-committer
# executescript committer automatisk og nulstiller isolation_level
# Kør det direkte på den underliggende connection
conn.executescript("""
CREATE TABLE IF NOT EXISTS libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -148,23 +163,20 @@ def init_db():
CREATE INDEX IF NOT EXISTS idx_song_dances ON song_dances(song_id);
""")
# Kør migrations for ældre databaser (each separately)
migrations = [
"ALTER TABLE songs ADD COLUMN extra_tags TEXT NOT NULL DEFAULT '{}'",
"ALTER TABLE song_dances ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
"ALTER TABLE dance_alternatives ADD COLUMN alt_dance_name TEXT NOT NULL DEFAULT ''",
"ALTER TABLE dance_alternatives ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
"ALTER TABLE dance_alternatives ADD COLUMN source TEXT NOT NULL DEFAULT 'local'",
"ALTER TABLE dance_alternatives ADD COLUMN created_by TEXT NOT NULL DEFAULT ''",
]
for sql in migrations:
try:
conn.execute(sql)
conn.commit()
except Exception:
pass
# executescript slår foreign_keys fra — genaktiver
conn.execute("PRAGMA foreign_keys=ON")
# Seed standard-niveauer — KUN hvis tabellen er tom
# Tilføj db_version tabel hvis den ikke findes
conn.execute("""
CREATE TABLE IF NOT EXISTS db_version (
version INTEGER PRIMARY KEY
)
""")
# Kør versionsbaserede migrationer
_run_versioned_migrations(conn)
# Seed standard-niveauer
count = conn.execute("SELECT COUNT(*) FROM dance_levels").fetchone()[0]
if count == 0:
defaults = [
@@ -174,14 +186,49 @@ def init_db():
(4, "Erfaren", "For dedikerede dansere"),
(5, "Ekspert", "Konkurrenceniveau"),
]
conn.executemany(
"INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)",
defaults
for row in defaults:
conn.execute(
"INSERT OR IGNORE INTO dance_levels (sort_order, name, description) VALUES (?,?,?)",
row
)
# ── Versionsbaserede migrationer ──────────────────────────────────────────────
# Tilføj aldrig gamle — tilføj kun nye versioner nederst.
MIGRATIONS: dict[int, list[str]] = {
1: [
"ALTER TABLE songs ADD COLUMN extra_tags TEXT NOT NULL DEFAULT '{}'",
"ALTER TABLE song_dances ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
"ALTER TABLE dance_alternatives ADD COLUMN alt_dance_name TEXT NOT NULL DEFAULT ''",
"ALTER TABLE dance_alternatives ADD COLUMN level_id INTEGER REFERENCES dance_levels(id)",
"ALTER TABLE dance_alternatives ADD COLUMN source TEXT NOT NULL DEFAULT 'local'",
"ALTER TABLE dance_alternatives ADD COLUMN created_by TEXT NOT NULL DEFAULT ''",
],
# Eksempel på fremtidig migration:
# 2: ["ALTER TABLE songs ADD COLUMN mbid TEXT"],
}
def _run_versioned_migrations(conn):
"""Kør kun migrationer der ikke allerede er kørt vha. db_version tabel."""
row = conn.execute("SELECT version FROM db_version").fetchone()
current_version = row["version"] if row else 0
for version in sorted(MIGRATIONS.keys()):
if version <= current_version:
continue
for sql in MIGRATIONS[version]:
try:
conn.execute(sql)
except Exception:
pass # kolonnen eksisterer allerede
conn.execute(
"INSERT OR REPLACE INTO db_version (version) VALUES (?)", (version,)
)
conn.commit()
print(f"Dans-niveauer seedet: {len(defaults)} niveauer")
else:
print(f"Dans-niveauer: {count} niveauer i databasen")
@@ -282,30 +329,49 @@ def upsert_song(song_data: dict) -> str:
extra_tags_json,
))
# Opdater danse hvis de er med i data
# Opdater danse hvis de er med i data — bevar level_id og alternativer
if "dances" in song_data:
conn.execute("DELETE FROM song_dances WHERE song_id=?", (song_id,))
for i, dance in enumerate(song_data["dances"], start=1):
# dance kan være str eller dict med {name, level_id}
file_dances = []
for dance in song_data["dances"]:
if isinstance(dance, dict):
name = dance.get("name", "")
level_id = dance.get("level_id")
file_dances.append(dance.get("name", ""))
else:
name = dance
level_id = None
conn.execute(
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) VALUES (?,?,?,?)",
(song_id, name, i, level_id),
)
# Registrer navne i ordbogen
try:
from local.local_db import register_dance_name as _reg
for dance in song_data["dances"]:
nm = dance.get("name", dance) if isinstance(dance, dict) else dance
if nm:
_reg(nm)
except Exception:
pass
file_dances.append(dance)
file_dances = [d for d in file_dances if d]
# Hent eksisterende danse med level_id og alternativer
existing = conn.execute(
"SELECT id, dance_name, dance_order, level_id FROM song_dances "
"WHERE song_id=? ORDER BY dance_order",
(song_id,)
).fetchall()
existing_map = {r["dance_name"].lower(): r for r in existing}
# Slet danse der ikke længere er i filen
file_lower = [d.lower() for d in file_dances]
for row in existing:
if row["dance_name"].lower() not in file_lower:
conn.execute(
"DELETE FROM dance_alternatives WHERE song_dance_id=?", (row["id"],)
)
conn.execute("DELETE FROM song_dances WHERE id=?", (row["id"],))
# Tilføj eller opdater danse fra filen
for i, name in enumerate(file_dances, start=1):
ex = existing_map.get(name.lower())
if ex:
# Bevar level_id — opdater kun dance_order
conn.execute(
"UPDATE song_dances SET dance_order=? WHERE id=?",
(i, ex["id"])
)
else:
# Ny dans — ingen level_id endnu
conn.execute(
"INSERT INTO song_dances (song_id, dance_name, dance_order, level_id) "
"VALUES (?,?,?,NULL)",
(song_id, name, i)
)
return song_id
@@ -465,15 +531,41 @@ def clear_event_state():
# ── Dans-navne ordbog ─────────────────────────────────────────────────────────
def get_dance_name_suggestions(prefix: str, limit: int = 20) -> list[str]:
"""Returnerer danse-navne der starter med prefix, sorteret efter popularitet."""
"""Returnerer dans-navne der starter med prefix fra alle kendte sources,
sorteret efter popularitet. Inkluderer navne fra song_dances og dance_alternatives."""
with get_db() as conn:
# Hent fra dance_names ordbog (primær kilde)
rows = conn.execute("""
SELECT name FROM dance_names
SELECT name, use_count FROM dance_names
WHERE name LIKE ? COLLATE NOCASE
ORDER BY use_count DESC, name
LIMIT ?
""", (f"{prefix}%", limit)).fetchall()
return [r["name"] for r in rows]
names = {r["name"]: r["use_count"] for r in rows}
# Supplér med navne direkte fra song_dances der ikke er i ordbogen
extra = conn.execute("""
SELECT DISTINCT dance_name as name FROM song_dances
WHERE dance_name LIKE ? COLLATE NOCASE
LIMIT ?
""", (f"{prefix}%", limit)).fetchall()
for r in extra:
if r["name"] not in names:
names[r["name"]] = 0
# Supplér med alternativ-danse
extra2 = conn.execute("""
SELECT DISTINCT alt_dance_name as name FROM dance_alternatives
WHERE alt_dance_name LIKE ? COLLATE NOCASE
LIMIT ?
""", (f"{prefix}%", limit)).fetchall()
for r in extra2:
if r["name"] not in names:
names[r["name"]] = 0
# Sorter: kendte navne med høj use_count først, derefter alfabetisk
return sorted(names.keys(),
key=lambda n: (-names[n], n.lower()))[:limit]
def register_dance_name(name: str, source: str = "local"):