This commit is contained in:
2026-04-10 15:06:59 +02:00
parent 3031b7153b
commit e5a4711004
7806 changed files with 1918528 additions and 335 deletions

View File

@@ -65,7 +65,8 @@ def read_tags(path: str | Path) -> dict:
"""
Læser metadata og danse fra en lydfil.
Returnerer dict med: title, artist, album, bpm, duration_sec,
file_format, file_modified_at, dances, can_write_dances.
file_format, file_modified_at, dances, can_write_dances,
extra_tags (dict med alle øvrige tags som {navn: værdi}).
"""
path = Path(path)
result = {
@@ -79,6 +80,7 @@ def read_tags(path: str | Path) -> dict:
"file_modified_at": get_file_modified_at(path),
"dances": [],
"can_write_dances": can_write_dances(path),
"extra_tags": {},
}
if not MUTAGEN_AVAILABLE:
@@ -127,6 +129,17 @@ def _read_mp3(audio, result: dict):
except (ValueError, TypeError):
pass
dances = {}
extra = {}
# Kendte ID3-felt-navne til menneskelige navne
ID3_NAMES = {
"TIT2": "titel", "TPE1": "artist", "TALB": "album", "TBPM": "bpm",
"TYER": "år", "TDRC": "dato", "TCON": "genre", "TPE2": "albumartist",
"TPOS": "disknummer", "TRCK": "spornummer", "TCOM": "komponist",
"TLYR": "sangtekst", "TCOP": "copyright", "TPUB": "udgiver",
"TENC": "kodet_af", "TLAN": "sprog", "TMOO": "stemning",
"TPE3": "dirigent", "TPE4": "fortolket_af", "TOAL": "original_album",
"TOPE": "original_artist", "TORY": "original_år",
}
for key, frame in tags.items():
if key.startswith("TXXX:") and TXXX_DANCE_PREFIX in key:
try:
@@ -134,7 +147,31 @@ def _read_mp3(audio, result: dict):
dances[num] = str(frame.text[0])
except (ValueError, IndexError):
pass
elif key.startswith("TXXX:"):
# Custom TXXX-felt — gem under dets beskrivelse
desc = key[5:] # fjern "TXXX:"
try:
extra[desc] = str(frame.text[0])
except Exception:
pass
elif key in ID3_NAMES and key not in ("TIT2","TPE1","TALB","TBPM"):
# Standardfelt vi ikke allerede har gemt
try:
val = str(frame.text[0]) if hasattr(frame, "text") else str(frame)
if val:
extra[ID3_NAMES[key]] = val
except Exception:
pass
elif hasattr(frame, "text") and key not in ("TIT2","TPE1","TALB","TBPM"):
# Alle andre tekstfelter
try:
val = str(frame.text[0])
if val and not key.startswith("APIC"): # spring albumcover over
extra[key] = val
except Exception:
pass
result["dances"] = [dances[k] for k in sorted(dances.keys())]
result["extra_tags"] = extra
def _read_vorbis(audio, result: dict):
@@ -149,7 +186,7 @@ def _read_vorbis(audio, result: dict):
result["bpm"] = int(tags.get("bpm", [0])[0])
except (ValueError, TypeError):
pass
# Danse gemmes som linedance_dance.1, linedance_dance.2 ...
# Danse
dances = {}
for key, values in tags.items():
if key.lower().startswith(f"{VORBIS_DANCE_KEY}."):
@@ -158,11 +195,21 @@ def _read_vorbis(audio, result: dict):
dances[num] = values[0]
except (ValueError, IndexError):
pass
# Fallback: enkelt felt linedance_dance med komma-separeret liste
if not dances and VORBIS_DANCE_KEY in tags:
result["dances"] = [d.strip() for d in tags[VORBIS_DANCE_KEY][0].split(",") if d.strip()]
return
result["dances"] = [dances[k] for k in sorted(dances.keys())]
else:
result["dances"] = [dances[k] for k in sorted(dances.keys())]
# Alle øvrige tags som extra_tags
skip = {"title", "artist", "album", "bpm", VORBIS_DANCE_KEY}
extra = {}
for key, values in tags.items():
k = key.lower()
if k not in skip and not k.startswith(VORBIS_DANCE_KEY):
try:
extra[k] = str(values[0])
except Exception:
pass
result["extra_tags"] = extra
def _read_m4a(audio, result: dict):
@@ -180,12 +227,33 @@ def _read_m4a(audio, result: dict):
result["bpm"] = int(tags["tmpo"][0])
except (ValueError, TypeError):
pass
# Danse gemmes som ----:LINEDANCE:DANCE — én værdi per dans
if M4A_DANCE_FREEFORM in tags:
result["dances"] = [
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",
"\xa9cmt": "kommentar", "aART": "albumartist", "trkn": "spornummer",
"disk": "disknummer", "cprt": "copyright", "\xa9lyr": "sangtekst",
"tmpo": "bpm",
}
skip_keys = {"\xa9nam", "\xa9ART", "\xa9alb", "tmpo", M4A_DANCE_FREEFORM, "covr"}
extra = {}
for key, values in tags.items():
if key in skip_keys:
continue
label = M4A_NAMES.get(key, key)
try:
val = values[0]
if isinstance(val, (bytes, MP4FreeForm)):
val = val.decode("utf-8", errors="replace")
extra[label] = str(val)
except Exception:
pass
result["extra_tags"] = extra
def _read_generic(audio, result: dict):
@@ -278,3 +346,46 @@ def read_dances_from_file(path: str | Path) -> list[str]:
"""Læser kun danse fra en fil — hurtigere end fuld read_tags()."""
tags = read_tags(path)
return tags.get("dances", [])
# ── BPM-analyse ───────────────────────────────────────────────────────────────
def analyze_bpm(path: str | Path) -> float | None:
"""
Analysér BPM fra lydfilen ved hjælp af librosa.
Returnerer BPM som float eller None ved fejl.
Tager 2-5 sekunder per sang — kør i baggrundstråd.
"""
try:
import librosa
# Indlæs kun de første 60 sekunder for hastighed
y, sr = librosa.load(str(path), duration=60.0, mono=True)
tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
# librosa returnerer array i nyere versioner
if hasattr(tempo, "__len__"):
bpm = float(tempo[0]) if len(tempo) > 0 else 0.0
else:
bpm = float(tempo)
return round(bpm, 1) if bpm > 0 else None
except ImportError:
print("librosa ikke installeret — installer med: pip install librosa")
return None
except Exception as e:
print(f"BPM-analyse fejl for {path}: {e}")
return None
def analyze_and_save_bpm(path: str | Path, song_id: str) -> float | None:
"""Analysér BPM og gem i SQLite. Returnerer målt BPM."""
bpm = analyze_bpm(path)
if bpm and bpm > 0:
try:
from local.local_db import get_db
with get_db() as conn:
conn.execute(
"UPDATE songs SET bpm=? WHERE id=? AND (bpm IS NULL OR bpm=0)",
(int(round(bpm)), song_id)
)
except Exception as e:
print(f"BPM gem fejl: {e}")
return bpm