""" 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'} 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 from local.local_db import find_or_create_song, upsert_file 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()) extra_tags = tags.get("extra_tags", "{}") # Find eller opret sang i global katalog song_id = find_or_create_song( title=title, artist=artist, album=album, bpm=bpm, duration_sec=duration_sec, mbid=mbid, acoustid=acoustid, ) # Opdater BPM på sangen hvis vi har bedre data 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( song_id=song_id, local_path=path_str, file_format=file_format, file_modified_at=mtime, extra_tags=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