203 lines
6.9 KiB
Python
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 |