Files
LinedanceAfspiller/linedance-app/local/scanner.py
2026-04-20 00:01:41 +02:00

203 lines
6.9 KiB
Python

"""
scanner.py — Scanning af musikbiblioteker i baggrunden. v0.9
Skriver til files-tabellen og finder/opretter sange i songs-tabellen.
"""
import os
import logging
import time
from pathlib import Path
logger = logging.getLogger(__name__)
SUPPORTED = {'.mp3', '.flac', '.m4a', '.ogg', '.wav', '.aiff', '.wma'}
import uuid as _uuid_module
def _find_or_create_song_conn(conn, title, artist, album, bpm,
duration_sec, mbid, acoustid) -> str:
"""Find eller opret sang via eksisterende forbindelse."""
if mbid:
row = conn.execute("SELECT id FROM songs WHERE mbid=?", (mbid,)).fetchone()
if row:
return row["id"]
if acoustid:
row = conn.execute("SELECT id FROM songs WHERE acoustid=?", (acoustid,)).fetchone()
if row:
if mbid:
conn.execute("UPDATE songs SET mbid=? WHERE id=? AND mbid IS NULL", (mbid, row["id"]))
return row["id"]
if title:
row = conn.execute(
"SELECT id FROM songs WHERE title=? AND artist=?", (title, artist)
).fetchone()
if row:
if mbid:
conn.execute("UPDATE songs SET mbid=? WHERE id=? AND mbid IS NULL", (mbid, row["id"]))
return row["id"]
new_id = str(_uuid_module.uuid4())
conn.execute(
"INSERT INTO songs (id, title, artist, album, bpm, duration_sec, mbid, acoustid) "
"VALUES (?,?,?,?,?,?,?,?)",
(new_id, title, artist, album, bpm, duration_sec, mbid or None, acoustid or None)
)
return new_id
def _upsert_file_conn(conn, song_id, local_path, file_format,
file_modified_at, extra_tags) -> str:
"""Opret eller opdater fil-post via eksisterende forbindelse."""
existing = conn.execute(
"SELECT id FROM files WHERE local_path=?", (local_path,)
).fetchone()
if existing:
conn.execute("""
UPDATE files SET song_id=?, file_missing=0,
file_format=?, file_modified_at=?, extra_tags=?
WHERE id=?
""", (song_id, file_format, file_modified_at, extra_tags, existing["id"]))
return existing["id"]
else:
file_id = str(_uuid_module.uuid4())
conn.execute(
"INSERT INTO files (id, song_id, local_path, file_format, file_modified_at, extra_tags) "
"VALUES (?,?,?,?,?,?)",
(file_id, song_id, local_path, file_format, file_modified_at, extra_tags)
)
return file_id
def is_supported(path) -> bool:
return Path(path).suffix.lower() in SUPPORTED
def get_file_mtime(path: Path) -> str:
try:
return str(os.path.getmtime(str(path)))
except Exception:
return ""
def scan_library(library_id: int, library_path: str, db_path: str,
overwrite_bpm: bool = False,
progress_callback=None) -> int:
"""
Scan ét bibliotek og upsert til files + songs tabellerne.
Returnerer antal scannede filer.
"""
import sqlite3
from local.tag_reader import read_tags
base = Path(library_path)
if not base.exists():
return 0
# Byg indeks over kendte filer (path → mtime)
conn = sqlite3.connect(db_path, timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
known = {}
for row in conn.execute(
"SELECT local_path, file_modified_at FROM files WHERE file_missing=0"
).fetchall():
known[row["local_path"]] = row["file_modified_at"]
# Find alle musikfiler
all_files = []
for dirpath, _, filenames in os.walk(str(base), followlinks=False):
for fn in filenames:
fp = Path(dirpath) / fn
if is_supported(fp):
all_files.append(fp)
total = len(all_files)
done = 0
for fp in all_files:
path_str = str(fp)
mtime = get_file_mtime(fp)
if progress_callback:
progress_callback(done, total, fp.name)
# Spring over uændrede filer
if path_str in known and known[path_str] == mtime:
done += 1
time.sleep(0.005)
continue
try:
tags = read_tags(str(fp))
title = tags.get("title", "") or fp.stem
artist = tags.get("artist", "")
album = tags.get("album", "")
bpm = tags.get("bpm", 0)
mbid = tags.get("mbid", "")
acoustid = tags.get("acoustid", "")
duration_sec = tags.get("duration_sec", 0)
file_format = tags.get("file_format", fp.suffix.lstrip(".").lower())
import json as _json
_extra = tags.get("extra_tags", {})
extra_tags = _json.dumps(_extra) if isinstance(_extra, dict) else (_extra or "{}")
# Find eller opret sang — alt via samme conn
song_id = _find_or_create_song_conn(
conn, title, artist, album, bpm, duration_sec, mbid, acoustid
)
# Opdater BPM
if bpm and bpm > 0:
conn.execute(
"UPDATE songs SET bpm=? WHERE id=? AND (bpm=0 OR bpm IS NULL)",
(bpm, song_id)
)
# Opret eller opdater fil-post
_upsert_file_conn(conn, song_id, path_str, file_format, mtime, extra_tags)
# Dans-tags fra fil
file_dances = tags.get("dances", [])
if file_dances:
existing_count = conn.execute(
"SELECT COUNT(*) FROM song_dances WHERE song_id=?", (song_id,)
).fetchone()[0]
if existing_count == 0:
import uuid
for order, dance_name in enumerate(file_dances, start=1):
dance_row = conn.execute(
"SELECT id FROM dances WHERE name=? COLLATE NOCASE LIMIT 1",
(dance_name,)
).fetchone()
if not dance_row:
cur = conn.execute(
"INSERT INTO dances (name) VALUES (?)", (dance_name,)
)
dance_id = cur.lastrowid
else:
dance_id = dance_row["id"]
conn.execute(
"INSERT OR IGNORE INTO song_dances (id, song_id, dance_id, dance_order) VALUES (?,?,?,?)",
(str(uuid.uuid4()), song_id, dance_id, order)
)
conn.commit()
except Exception as e:
logger.warning(f"Scan fejl {fp.name}: {e}")
done += 1
time.sleep(0.02)
# Marker manglende filer
for path_str in known:
if not Path(path_str).exists():
conn.execute(
"UPDATE files SET file_missing=1 WHERE local_path=?", (path_str,)
)
conn.commit()
conn.close()
logger.info(f"Scan færdig: {done} filer i {library_path}")
return done