Version 1
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user