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