En del opdateringer
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user