En del opdateringer

This commit is contained in:
2026-04-19 00:58:48 +02:00
parent efe3739626
commit e4ab9caab6
14 changed files with 3412 additions and 189 deletions

View File

@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
# AcoustID API nøgle — kan overskrives i Indstillinger → Afspilning
# Registrér din egen på https://acoustid.org/new-application
ACOUSTID_API_KEY = "71W9SJdajAI"
ACOUSTID_API_KEY = "6fd9DGNDqG"
ACOUSTID_API_URL = "https://api.acoustid.org/v2/lookup"
# Pause mellem API-kald — rolig baggrundskørsel
@@ -154,7 +154,8 @@ def run_acoustid_scan(db_path: str, api_key: str = "", on_progress=None, stop_ev
logger.info("AcoustID: stoppet af bruger")
break
conn = sqlite3.connect(db_path)
conn = sqlite3.connect(db_path, timeout=10)
conn.execute("PRAGMA journal_mode=WAL")
conn.row_factory = sqlite3.Row
rows = conn.execute("""
@@ -181,7 +182,8 @@ def run_acoustid_scan(db_path: str, api_key: str = "", on_progress=None, stop_ev
found = 0
logger.info(f"AcoustID: batch {batch_num}{total} sange")
conn = sqlite3.connect(db_path)
conn = sqlite3.connect(db_path, timeout=10)
conn.execute("PRAGMA journal_mode=WAL")
conn.row_factory = sqlite3.Row
for row in rows:

View File

@@ -35,9 +35,10 @@ def _get_conn() -> sqlite3.Connection:
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 = sqlite3.connect(str(DB_PATH), check_same_thread=False, timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys=OFF") # FK checker forhindrer level_id gem
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=OFF")
return conn
@@ -186,11 +187,16 @@ def init_db():
count = conn.execute("SELECT COUNT(*) FROM dance_levels").fetchone()[0]
if count == 0:
defaults = [
(1, "Begynder", "Passer til alle"),
(2, "Let øvet", "Lidt erfaring kræves"),
(3, "Øvet", "Kræver regelmæssig træning"),
(4, "Erfaren", "For dedikerede dansere"),
(5, "Ekspert", "Konkurrenceniveau"),
(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"),
]
for row in defaults:
conn.execute(
@@ -261,6 +267,40 @@ MIGRATIONS: dict[int, list[str]] = {
"""ALTER TABLE songs ADD COLUMN mbid TEXT""",
"""ALTER TABLE songs ADD COLUMN acoustid TEXT""",
],
9: [
# Opdater niveau-navne til korrekte betegnelser i rigtig rækkefølge
"DELETE FROM dance_levels",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (10, 'Absolute Beginner', 'Ingen tidligere danse-erfaring kræves')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (20, 'Beginner', 'Lidt tidligere erfaring')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (30, 'High Beginner', 'God begynder, klar til mere')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (40, 'Low Improver', 'Begyndende øvet')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (50, 'Improver', 'Grundlæggende færdigheder på plads')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (60, 'High Improver', 'Stærk øvet, næsten intermediate')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (70, 'Low Intermediate', 'Begyndende intermediate')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (80, 'Intermediate', 'Erfaren danser')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (90, 'High Intermediate', 'Stærk intermediate')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (99, 'Advanced', 'Fuld beherskelse af trin og teknik')",
],
10: [
# Ret stavefejl i eksisterende data
"UPDATE dance_levels SET name='Low Intermediate' WHERE name='Low Intermidiate' OR name='Low Intermidiat'",
"UPDATE dance_levels SET name='Intermediate' WHERE name='Intermidiate' OR name='Intermidate'",
"UPDATE dance_levels SET name='High Intermediate' WHERE name='High Intermidiate' OR name='High Intermidiat'",
],
11: [
# Genopret dance_levels med korrekte navne og rækkefølge
"DELETE FROM dance_levels",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (10, 'Absolute Beginner', 'Ingen tidligere danse-erfaring kræves')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (20, 'Beginner', 'Lidt tidligere erfaring')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (30, 'High Beginner', 'God begynder, klar til mere')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (40, 'Low Improver', 'Begyndende øvet')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (50, 'Improver', 'Grundlæggende færdigheder på plads')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (60, 'High Improver', 'Stærk øvet, næsten intermediate')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (70, 'Low Intermediate', 'Begyndende intermediate')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (80, 'Intermediate', 'Erfaren danser')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (90, 'High Intermediate', 'Stærk intermediate')",
"INSERT INTO dance_levels (sort_order, name, description) VALUES (99, 'Advanced', 'Fuld beherskelse af trin og teknik')",
],
}
@@ -442,24 +482,31 @@ def get_song_by_path(local_path: str) -> sqlite3.Row | None:
def search_songs(query: str, limit: int = 50) -> list[sqlite3.Row]:
"""Søg i alle tags — titel, artist, album, danse og alle øvrige tags."""
"""Søg i titel, artist, album, dans, koreograf, niveau og øvrige tags."""
import logging as _log
_log.getLogger(__name__).info(f"search_songs: '{query}'")
pattern = f"%{query}%"
with get_db() as conn:
return conn.execute("""
rows = conn.execute("""
SELECT DISTINCT s.* FROM songs s
LEFT JOIN song_dances sd ON sd.song_id = s.id
LEFT JOIN dances d ON d.id = sd.dance_id
LEFT JOIN dance_levels dl ON dl.id = d.level_id
WHERE s.file_missing = 0
AND (
s.title LIKE ? OR
s.artist LIKE ? OR
s.album LIKE ? OR
d.name LIKE ? OR
s.extra_tags LIKE ?
s.title LIKE ? OR
s.artist LIKE ? OR
s.album LIKE ? OR
d.name LIKE ? OR
d.choreographer LIKE ? OR
dl.name LIKE ? OR
s.extra_tags LIKE ?
)
ORDER BY s.artist, s.title
LIMIT ?
""", (pattern, pattern, pattern, pattern, pattern, limit)).fetchall()
""", (pattern,)*7 + (limit,)).fetchall()
_log.getLogger(__name__).info(f"search_songs: '{query}'{len(rows)} resultater")
return rows
def get_songs_for_library(library_id: int) -> list[sqlite3.Row]:
@@ -672,10 +719,11 @@ def update_dance_info(dance_id: int, choreographer: str = "",
def get_or_create_dance(name: str, level_id: int | None,
conn=None) -> int:
conn=None, choreographer: str = "") -> int:
"""Find eller opret en dans (name + level_id kombination).
Returnerer dance_id. conn er valgfri — bruges ved nested kald."""
name = name.strip()
name = name.strip()
choreo = choreographer.strip()
close = False
if conn is None:
conn = new_conn()
@@ -687,13 +735,15 @@ def get_or_create_dance(name: str, level_id: int | None,
).fetchone()
if existing:
conn.execute(
"UPDATE dances SET use_count=use_count+1 WHERE id=?",
(existing["id"],)
"UPDATE dances SET use_count=use_count+1"
+ (", choreographer=?" if choreo else "") +
" WHERE id=?",
((choreo, existing["id"]) if choreo else (existing["id"],))
)
return existing["id"]
conn.execute(
"INSERT INTO dances (name, level_id, use_count, source) VALUES (?,?,1,'local')",
(name, level_id)
"INSERT INTO dances (name, level_id, choreographer, use_count, source) VALUES (?,?,?,1,'local')",
(name, level_id, choreo)
)
return conn.execute(
"SELECT id FROM dances WHERE name=? COLLATE NOCASE AND level_id IS ?",
@@ -705,19 +755,34 @@ def get_or_create_dance(name: str, level_id: int | None,
conn.close()
def get_choreographer_suggestions(prefix: str, limit: int = 20) -> list[str]:
"""Returnerer koreografer der starter med prefix, sorteret alfabetisk."""
with get_db() as conn:
rows = conn.execute("""
SELECT DISTINCT choreographer
FROM dances
WHERE choreographer LIKE ? COLLATE NOCASE
AND choreographer != ''
ORDER BY choreographer
LIMIT ?
""", (f"{prefix}%", limit)).fetchall()
return [r["choreographer"] for r in rows]
def get_dance_suggestions(prefix: str, limit: int = 20) -> list[dict]:
"""Returnerer danse der starter med prefix som {id, name, level_id, level_name}.
"""Returnerer danse der matcher prefix i navn ELLER koreograf.
Sorteret efter popularitet — bruges til autoudfyld."""
with get_db() as conn:
rows = conn.execute("""
SELECT d.id, d.name, d.level_id, d.use_count,
SELECT d.id, d.name, d.level_id, d.use_count, d.choreographer,
dl.name as level_name, dl.sort_order
FROM dances d
LEFT JOIN dance_levels dl ON dl.id = d.level_id
WHERE d.name LIKE ? COLLATE NOCASE
OR d.choreographer LIKE ? COLLATE NOCASE
ORDER BY d.use_count DESC, dl.sort_order, d.name
LIMIT ?
""", (f"{prefix}%", limit)).fetchall()
""", (f"%{prefix}%", f"%{prefix}%", limit)).fetchall()
return [dict(r) for r in rows]
@@ -725,7 +790,7 @@ def get_dances_for_song(song_id: str) -> list[dict]:
"""Hent hoveddanse for en sang med niveau-info og workshop-flag."""
with get_db() as conn:
rows = conn.execute("""
SELECT d.id as dance_id, d.name, d.level_id,
SELECT d.id as dance_id, d.name, d.level_id, d.choreographer,
dl.name as level_name, sd.dance_order,
sd.id as song_dance_id, sd.is_workshop
FROM song_dances sd

View File

@@ -224,7 +224,7 @@ def _read_vorbis(audio, result: dict):
result["dances"] = [d.strip() for d in tags[VORBIS_DANCE_KEY][0].split(",") if d.strip()]
else:
result["dances"] = [dances[k] for k in sorted(dances.keys())]
# Alle øvrige tags som extra_tags
# Øvrige tags
skip = {"title", "artist", "album", "bpm", VORBIS_DANCE_KEY}
extra = {}
for key, values in tags.items():
@@ -243,6 +243,10 @@ def _read_vorbis(audio, result: dict):
except Exception:
pass
break
def _read_m4a(audio, result: dict):
"""M4A/AAC/MP4 — iTunes atoms."""
tags = audio.tags
if not tags:
return
@@ -262,7 +266,6 @@ def _read_vorbis(audio, result: dict):
v.decode("utf-8") if isinstance(v, (bytes, MP4FreeForm)) else str(v)
for v in tags[M4A_DANCE_FREEFORM]
]
# Menneskelige navne til M4A-nøgler
M4A_NAMES = {
"\xa9nam": "titel", "\xa9ART": "artist", "\xa9alb": "album",
"\xa9day": "år", "\xa9gen": "genre", "\xa9wrt": "komponist",
@@ -284,7 +287,7 @@ def _read_vorbis(audio, result: dict):
except Exception:
pass
result["extra_tags"] = extra
# MBID — gemmes som freeform atom ----:com.apple.iTunes:MusicBrainz Recording Id
# MBID
for key in tags:
if "musicbrainz" in key.lower() and "recording" in key.lower():
try:
@@ -295,14 +298,35 @@ def _read_vorbis(audio, result: dict):
except Exception:
pass
break
def _read_generic(audio, result: dict):
"""Generisk læsning for WMA, AIFF og andre formater via easy tags."""
try:
easy = MutagenFile(result["local_path"], easy=True)
if easy and easy.tags:
result["title"] = easy.tags.get("title", [result["title"]])[0]
result["artist"] = easy.tags.get("artist", [""])[0]
result["album"] = easy.tags.get("album", [""])[0]
from mutagen import File as MutagenFileEasy
local_path = result.get("local_path", "")
if local_path:
easy = MutagenFileEasy(local_path, easy=True)
if easy and easy.tags:
result["title"] = easy.tags.get("title", [result["title"]])[0]
result["artist"] = easy.tags.get("artist", [""])[0]
result["album"] = easy.tags.get("album", [""])[0]
except Exception:
pass
# Fallback: læs direkte fra audio-objektet
try:
tags = audio.tags
if hasattr(tags, "items"):
for key, val in tags.items():
k = str(key).lower()
v = str(val[0]) if hasattr(val, "__iter__") and not isinstance(val, str) else str(val)
if "title" in k and not result["title"]:
result["title"] = v
elif "artist" in k and not result["artist"]:
result["artist"] = v
elif "album" in k and not result["album"]:
result["album"] = v
except Exception:
pass
# ── Skrivning ─────────────────────────────────────────────────────────────────