""" 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