Mappehåndtering
This commit is contained in:
@@ -66,6 +66,8 @@ def scan_library(library_id: int, library_path: str, db_path: str,
|
|||||||
total = len(all_files)
|
total = len(all_files)
|
||||||
done = 0
|
done = 0
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
for fp in all_files:
|
for fp in all_files:
|
||||||
path_str = str(fp)
|
path_str = str(fp)
|
||||||
mtime = get_file_mtime(fp)
|
mtime = get_file_mtime(fp)
|
||||||
@@ -76,6 +78,9 @@ def scan_library(library_id: int, library_path: str, db_path: str,
|
|||||||
# Spring over hvis ikke ændret
|
# Spring over hvis ikke ændret
|
||||||
if path_str in known and known[path_str] == mtime:
|
if path_str in known and known[path_str] == mtime:
|
||||||
done += 1
|
done += 1
|
||||||
|
# Yield hvert 100. fil så andre tråde kan køre
|
||||||
|
if done % 100 == 0:
|
||||||
|
time.sleep(0.005)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -117,6 +122,8 @@ def scan_library(library_id: int, library_path: str, db_path: str,
|
|||||||
logger.warning(f"Scan fejl {fp.name}: {e}")
|
logger.warning(f"Scan fejl {fp.name}: {e}")
|
||||||
|
|
||||||
done += 1
|
done += 1
|
||||||
|
# Lille pause efter hver scannet fil så GUI ikke hænger
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
# Marker manglende filer
|
# Marker manglende filer
|
||||||
for path_str in known:
|
for path_str in known:
|
||||||
|
|||||||
183
linedance-app/local/watchdog_process.py
Normal file
183
linedance-app/local/watchdog_process.py
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
"""
|
||||||
|
watchdog_process.py — Kører som selvstændig subprocess.
|
||||||
|
Overvåger musikmapper og opdaterer SQLite ved fil-ændringer.
|
||||||
|
|
||||||
|
Start: python watchdog_process.py <db_path>
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [watchdog] %(message)s",
|
||||||
|
stream=sys.stderr
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SUPPORTED = {'.mp3', '.flac', '.m4a', '.ogg', '.wav', '.aiff', '.wma'}
|
||||||
|
|
||||||
|
|
||||||
|
def is_supported(path: Path) -> bool:
|
||||||
|
return path.suffix.lower() in SUPPORTED
|
||||||
|
|
||||||
|
|
||||||
|
def get_libraries(db_path: str) -> list[dict]:
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
libs = conn.execute(
|
||||||
|
"SELECT id, path FROM libraries WHERE is_active=1"
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(l) for l in libs]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def process_file(db_path: str, library_id: int, file_path: str,
|
||||||
|
deleted: bool = False):
|
||||||
|
"""Opdater SQLite for én fil."""
|
||||||
|
try:
|
||||||
|
# Tilføj app-mappen til sys.path så tag_reader kan importeres
|
||||||
|
app_dir = str(Path(__file__).parent.parent)
|
||||||
|
if app_dir not in sys.path:
|
||||||
|
sys.path.insert(0, app_dir)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE songs SET file_missing=1 WHERE local_path=?",
|
||||||
|
(file_path,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
from local.tag_reader import read_tags
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
mtime = str(os.path.getmtime(file_path))
|
||||||
|
tags = read_tags(Path(file_path))
|
||||||
|
extra = json.dumps(tags.get("extra_tags", {}), ensure_ascii=False)
|
||||||
|
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id, bpm FROM songs WHERE local_path=?", (file_path,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
bpm = tags.get("bpm", 0) or existing["bpm"] or 0
|
||||||
|
conn.execute("""
|
||||||
|
UPDATE songs SET
|
||||||
|
library_id=?, title=?, artist=?, album=?,
|
||||||
|
bpm=?, duration_sec=?, file_format=?,
|
||||||
|
file_modified_at=?, file_missing=0, extra_tags=?
|
||||||
|
WHERE id=?
|
||||||
|
""", (library_id, tags.get("title",""), tags.get("artist",""),
|
||||||
|
tags.get("album",""), bpm, tags.get("duration_sec",0),
|
||||||
|
tags.get("file_format",""), mtime, extra, existing["id"]))
|
||||||
|
else:
|
||||||
|
conn.execute("""
|
||||||
|
INSERT OR IGNORE INTO songs
|
||||||
|
(id, library_id, local_path, title, artist, album,
|
||||||
|
bpm, duration_sec, file_format, file_modified_at, extra_tags)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
""", (str(uuid.uuid4()), library_id, file_path,
|
||||||
|
tags.get("title",""), tags.get("artist",""),
|
||||||
|
tags.get("album",""), tags.get("bpm",0),
|
||||||
|
tags.get("duration_sec",0), tags.get("file_format",""),
|
||||||
|
mtime, extra))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
logger.info(f"Opdateret: {Path(file_path).name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fejl ved {file_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def run(db_path: str):
|
||||||
|
try:
|
||||||
|
from watchdog.observers.polling import PollingObserver
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
except ImportError:
|
||||||
|
logger.error("watchdog ikke installeret")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
class Handler(FileSystemEventHandler):
|
||||||
|
def __init__(self, library_id: int):
|
||||||
|
self.library_id = library_id
|
||||||
|
|
||||||
|
def on_created(self, event):
|
||||||
|
if not event.is_directory and is_supported(Path(event.src_path)):
|
||||||
|
time.sleep(0.5) # Vent til filen er skrevet færdig
|
||||||
|
process_file(db_path, self.library_id, event.src_path)
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
if not event.is_directory and is_supported(Path(event.src_path)):
|
||||||
|
process_file(db_path, self.library_id, event.src_path)
|
||||||
|
|
||||||
|
def on_deleted(self, event):
|
||||||
|
if not event.is_directory and is_supported(Path(event.src_path)):
|
||||||
|
process_file(db_path, self.library_id, event.src_path,
|
||||||
|
deleted=True)
|
||||||
|
|
||||||
|
def on_moved(self, event):
|
||||||
|
if not event.is_directory:
|
||||||
|
if is_supported(Path(event.src_path)):
|
||||||
|
process_file(db_path, self.library_id, event.src_path,
|
||||||
|
deleted=True)
|
||||||
|
if is_supported(Path(event.dest_path)):
|
||||||
|
process_file(db_path, self.library_id, event.dest_path)
|
||||||
|
|
||||||
|
# Brug 60 sekunders poll-interval — opdager ændringer inden for 1 minut
|
||||||
|
observer = PollingObserver(timeout=60)
|
||||||
|
|
||||||
|
libraries = get_libraries(db_path)
|
||||||
|
if not libraries:
|
||||||
|
logger.info("Ingen biblioteker — venter...")
|
||||||
|
|
||||||
|
for lib in libraries:
|
||||||
|
path = Path(lib["path"])
|
||||||
|
if path.exists():
|
||||||
|
observer.schedule(Handler(lib["id"]), str(path), recursive=True)
|
||||||
|
logger.info(f"Overvåger: {path}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Mappe ikke fundet: {path}")
|
||||||
|
|
||||||
|
observer.start()
|
||||||
|
logger.info("Watchdog kører")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(30)
|
||||||
|
# Tjek om der er kommet nye biblioteker siden start
|
||||||
|
current = get_libraries(db_path)
|
||||||
|
current_paths = {lib["path"] for lib in current}
|
||||||
|
watched_paths = {str(w.path) for w in observer.emitters}
|
||||||
|
for lib in current:
|
||||||
|
if lib["path"] not in watched_paths:
|
||||||
|
path = Path(lib["path"])
|
||||||
|
if path.exists():
|
||||||
|
observer.schedule(
|
||||||
|
Handler(lib["id"]), str(path), recursive=True
|
||||||
|
)
|
||||||
|
logger.info(f"Tilføjet overvågning: {path}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
observer.stop()
|
||||||
|
observer.join()
|
||||||
|
logger.info("Watchdog stoppet")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Brug: python watchdog_process.py <db_path>")
|
||||||
|
sys.exit(1)
|
||||||
|
run(sys.argv[1])
|
||||||
69
linedance-app/ui/bpm_worker.py
Normal file
69
linedance-app/ui/bpm_worker.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
bpm_worker.py — QThread til BPM-analyse i baggrunden.
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class BpmScanWorker(QThread):
|
||||||
|
progress = pyqtSignal(int, int) # done, total
|
||||||
|
finished = pyqtSignal(int) # antal analyseret
|
||||||
|
|
||||||
|
def __init__(self, library_id: int, db_path: str,
|
||||||
|
scan_all: bool = False):
|
||||||
|
super().__init__()
|
||||||
|
self._library_id = library_id
|
||||||
|
self._db_path = db_path
|
||||||
|
self._scan_all = scan_all
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.requestInterruption()
|
||||||
|
# Afbryd hurtigt ved at sætte et flag
|
||||||
|
self._cancelled = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
import time
|
||||||
|
self._cancelled = False
|
||||||
|
try:
|
||||||
|
from local.tag_reader import analyze_bpm
|
||||||
|
conn = sqlite3.connect(self._db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
if self._scan_all:
|
||||||
|
songs = conn.execute(
|
||||||
|
"SELECT id, local_path FROM songs "
|
||||||
|
"WHERE library_id=? AND file_missing=0",
|
||||||
|
(self._library_id,)
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
songs = conn.execute(
|
||||||
|
"SELECT id, local_path FROM songs "
|
||||||
|
"WHERE library_id=? AND file_missing=0 "
|
||||||
|
"AND (bpm IS NULL OR bpm=0)",
|
||||||
|
(self._library_id,)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
total = len(songs)
|
||||||
|
done = 0
|
||||||
|
|
||||||
|
for song in songs:
|
||||||
|
if self._cancelled or self.isInterruptionRequested():
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
bpm = analyze_bpm(song["local_path"])
|
||||||
|
if bpm and bpm > 0:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE songs SET bpm=? WHERE id=?",
|
||||||
|
(int(round(bpm)), song["id"])
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
done += 1
|
||||||
|
self.progress.emit(done, total)
|
||||||
|
time.sleep(0.01) # Yield så GUI ikke hænger
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
self.finished.emit(done)
|
||||||
|
except Exception as e:
|
||||||
|
self.finished.emit(0)
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
library_manager.py — Håndter musikmapper.
|
library_manager.py — Håndter musikmapper.
|
||||||
Tilføj/fjern mapper. Scan KUN ved eksplicit knap-tryk.
|
Tilføj/fjern mapper. BPM-scanning per bibliotek.
|
||||||
|
Fil-scanning starter automatisk når vinduet lukkes.
|
||||||
"""
|
"""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QWidget,
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QPushButton, QFrame, QMessageBox, QProgressBar,
|
QPushButton, QFrame, QMessageBox, QScrollArea, QWidget,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QThread
|
||||||
|
|
||||||
|
|
||||||
class LibraryManagerDialog(QDialog):
|
class LibraryManagerDialog(QDialog):
|
||||||
libraries_changed = pyqtSignal() # signal til main_window om at genindlæse
|
libraries_changed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, db_path: str, parent=None):
|
def __init__(self, db_path: str, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._db_path = db_path
|
self._db_path = db_path
|
||||||
self._workers = {} # library_id → ScanWorker
|
self._workers = {} # library_id → ScanWorker
|
||||||
self._scanning = False
|
|
||||||
|
|
||||||
self.setWindowTitle("Musikmapper")
|
self.setWindowTitle("Musikmapper")
|
||||||
self.setMinimumWidth(600)
|
self.setMinimumWidth(600)
|
||||||
@@ -34,26 +34,24 @@ class LibraryManagerDialog(QDialog):
|
|||||||
layout.setContentsMargins(12, 12, 12, 12)
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
layout.setSpacing(8)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
lbl = QLabel("Tilføj eller fjern musikmapper. Scan starter kun ved klik på knappen.")
|
lbl = QLabel(
|
||||||
|
"Tilføj eller fjern musikmapper. "
|
||||||
|
"Fil-scanning starter automatisk når vinduet lukkes."
|
||||||
|
)
|
||||||
lbl.setObjectName("result_count")
|
lbl.setObjectName("result_count")
|
||||||
lbl.setWordWrap(True)
|
lbl.setWordWrap(True)
|
||||||
layout.addWidget(lbl)
|
layout.addWidget(lbl)
|
||||||
|
|
||||||
self._libs_layout = QVBoxLayout()
|
# Scrollbart område til biblioteksliste
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
self._scroll_content = QWidget()
|
||||||
|
self._libs_layout = QVBoxLayout(self._scroll_content)
|
||||||
self._libs_layout.setSpacing(6)
|
self._libs_layout.setSpacing(6)
|
||||||
layout.addLayout(self._libs_layout)
|
self._libs_layout.addStretch()
|
||||||
|
scroll.setWidget(self._scroll_content)
|
||||||
layout.addStretch()
|
layout.addWidget(scroll, stretch=1)
|
||||||
|
|
||||||
# Global status
|
|
||||||
self._lbl_status = QLabel("")
|
|
||||||
self._lbl_status.setObjectName("result_count")
|
|
||||||
self._lbl_status.hide()
|
|
||||||
layout.addWidget(self._lbl_status)
|
|
||||||
|
|
||||||
self._progress = QProgressBar()
|
|
||||||
self._progress.hide()
|
|
||||||
layout.addWidget(self._progress)
|
|
||||||
|
|
||||||
# Knap-række
|
# Knap-række
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
@@ -62,53 +60,71 @@ class LibraryManagerDialog(QDialog):
|
|||||||
btn_row.addWidget(btn_add)
|
btn_row.addWidget(btn_add)
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
btn_close = QPushButton("Luk")
|
btn_close = QPushButton("Luk")
|
||||||
btn_close.clicked.connect(self._on_close)
|
btn_close.clicked.connect(self.accept)
|
||||||
btn_row.addWidget(btn_close)
|
btn_row.addWidget(btn_close)
|
||||||
layout.addLayout(btn_row)
|
layout.addLayout(btn_row)
|
||||||
|
|
||||||
def _load(self):
|
def _load(self):
|
||||||
"""Indlæs biblioteker fra DB og vis dem — ingen scanning."""
|
"""Indlæs biblioteker fra DB og vis dem."""
|
||||||
while self._libs_layout.count():
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
# Ryd eksisterende rækker (ikke stretch)
|
||||||
|
while self._libs_layout.count() > 1:
|
||||||
item = self._libs_layout.takeAt(0)
|
item = self._libs_layout.takeAt(0)
|
||||||
if item.widget():
|
if item.widget():
|
||||||
item.widget().deleteLater()
|
item.widget().deleteLater()
|
||||||
|
QApplication.processEvents() # Lad Qt rydde op før vi bygger nyt
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(self._db_path)
|
conn = sqlite3.connect(self._db_path)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
libs = conn.execute(
|
libs = conn.execute(
|
||||||
"SELECT id, path, last_full_scan FROM libraries WHERE is_active=1 ORDER BY path"
|
"SELECT id, path, last_full_scan FROM libraries "
|
||||||
|
"WHERE is_active=1 ORDER BY path"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
total_songs = {}
|
|
||||||
|
counts = {}
|
||||||
|
bpm_missing = {}
|
||||||
for lib in libs:
|
for lib in libs:
|
||||||
cnt = conn.execute(
|
counts[lib["id"]] = conn.execute(
|
||||||
"SELECT COUNT(*) FROM songs WHERE library_id=? AND file_missing=0",
|
"SELECT COUNT(*) FROM songs "
|
||||||
|
"WHERE library_id=? AND file_missing=0",
|
||||||
|
(lib["id"],)
|
||||||
|
).fetchone()[0]
|
||||||
|
bpm_missing[lib["id"]] = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM songs "
|
||||||
|
"WHERE library_id=? AND file_missing=0 "
|
||||||
|
"AND (bpm IS NULL OR bpm=0)",
|
||||||
(lib["id"],)
|
(lib["id"],)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
total_songs[lib["id"]] = cnt
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
for lib in libs:
|
|
||||||
self._libs_layout.addWidget(
|
|
||||||
self._make_lib_row(dict(lib), total_songs[lib["id"]])
|
|
||||||
)
|
|
||||||
|
|
||||||
if not libs:
|
if not libs:
|
||||||
lbl = QLabel("Ingen musikmapper tilføjet endnu.")
|
lbl = QLabel("Ingen musikmapper tilføjet endnu.")
|
||||||
lbl.setObjectName("result_count")
|
lbl.setObjectName("result_count")
|
||||||
self._libs_layout.addWidget(lbl)
|
self._libs_layout.insertWidget(0, lbl)
|
||||||
except Exception as e:
|
return
|
||||||
lbl = QLabel(f"Fejl ved indlæsning: {e}")
|
|
||||||
self._libs_layout.addWidget(lbl)
|
|
||||||
|
|
||||||
def _make_lib_row(self, lib: dict, song_count: int) -> QFrame:
|
for i, lib in enumerate(libs):
|
||||||
|
row = self._make_lib_row(
|
||||||
|
dict(lib),
|
||||||
|
counts[lib["id"]],
|
||||||
|
bpm_missing[lib["id"]]
|
||||||
|
)
|
||||||
|
self._libs_layout.insertWidget(i, row)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
lbl = QLabel(f"Fejl: {e}")
|
||||||
|
self._libs_layout.insertWidget(0, lbl)
|
||||||
|
|
||||||
|
def _make_lib_row(self, lib: dict, song_count: int,
|
||||||
|
bpm_missing: int) -> QFrame:
|
||||||
lib_id = lib["id"]
|
lib_id = lib["id"]
|
||||||
path = lib["path"]
|
path = lib["path"]
|
||||||
exists = Path(path).exists()
|
exists = Path(path).exists()
|
||||||
last = lib.get("last_full_scan") or "aldrig"
|
last = lib.get("last_full_scan") or "aldrig"
|
||||||
if isinstance(last, str) and len(last) > 16:
|
if isinstance(last, str) and len(last) > 16:
|
||||||
last = last[:16]
|
last = last[:16]
|
||||||
scanning = lib_id in self._workers
|
|
||||||
|
|
||||||
frame = QFrame()
|
frame = QFrame()
|
||||||
frame.setObjectName("track_display")
|
frame.setObjectName("track_display")
|
||||||
@@ -122,37 +138,43 @@ class LibraryManagerDialog(QDialog):
|
|||||||
vbox.addWidget(lbl_path)
|
vbox.addWidget(lbl_path)
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
lbl_info = QLabel(f" {song_count} sange · senest scannet: {last}")
|
lbl_info = QLabel(
|
||||||
|
f" {song_count} sange · "
|
||||||
|
f"senest scannet: {last} · "
|
||||||
|
f"{bpm_missing} uden BPM"
|
||||||
|
+ (" · ⚠ mappe ikke fundet" if not exists else "")
|
||||||
|
)
|
||||||
lbl_info.setObjectName("result_count")
|
lbl_info.setObjectName("result_count")
|
||||||
vbox.addWidget(lbl_info)
|
vbox.addWidget(lbl_info)
|
||||||
|
|
||||||
# Scan-status label
|
# Status-label til scanning
|
||||||
lbl_scan = QLabel("")
|
lbl_status = QLabel("")
|
||||||
lbl_scan.setObjectName("result_count")
|
lbl_status.setObjectName("result_count")
|
||||||
lbl_scan.hide()
|
lbl_status.hide()
|
||||||
vbox.addWidget(lbl_scan)
|
vbox.addWidget(lbl_status)
|
||||||
|
|
||||||
# Knapper
|
# Knapper
|
||||||
btn_row = QHBoxLayout()
|
btn_row = QHBoxLayout()
|
||||||
btn_row.setSpacing(6)
|
btn_row.setSpacing(6)
|
||||||
|
|
||||||
btn_scan = QPushButton("⟳ Scan nye filer")
|
btn_bpm = QPushButton(f"♩ BPM manglende ({bpm_missing})")
|
||||||
btn_scan.setFixedHeight(30)
|
btn_bpm.setFixedHeight(30)
|
||||||
btn_scan.setEnabled(exists and not scanning)
|
btn_bpm.setEnabled(exists and bpm_missing > 0
|
||||||
btn_scan.clicked.connect(
|
and lib_id not in self._workers)
|
||||||
lambda _, lid=lib_id, p=path, b=btn_scan, s=lbl_scan:
|
btn_bpm.clicked.connect(
|
||||||
self._start_scan(lid, p, False, b, s)
|
lambda _, lid=lib_id, p=path, b=btn_bpm, s=lbl_status:
|
||||||
|
self._start_bpm(lid, p, False, b, s)
|
||||||
)
|
)
|
||||||
btn_row.addWidget(btn_scan)
|
btn_row.addWidget(btn_bpm)
|
||||||
|
|
||||||
btn_scan_all = QPushButton("⟳ Scan alle filer")
|
btn_bpm_all = QPushButton("♩ BPM alle")
|
||||||
btn_scan_all.setFixedHeight(30)
|
btn_bpm_all.setFixedHeight(30)
|
||||||
btn_scan_all.setEnabled(exists and not scanning)
|
btn_bpm_all.setEnabled(exists and lib_id not in self._workers)
|
||||||
btn_scan_all.clicked.connect(
|
btn_bpm_all.clicked.connect(
|
||||||
lambda _, lid=lib_id, p=path, b=btn_scan_all, s=lbl_scan:
|
lambda _, lid=lib_id, p=path, b=btn_bpm_all, s=lbl_status:
|
||||||
self._start_scan(lid, p, True, b, s)
|
self._start_bpm(lid, p, True, b, s)
|
||||||
)
|
)
|
||||||
btn_row.addWidget(btn_scan_all)
|
btn_row.addWidget(btn_bpm_all)
|
||||||
|
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
|
|
||||||
@@ -173,17 +195,31 @@ class LibraryManagerDialog(QDialog):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(self._db_path)
|
conn = sqlite3.connect(self._db_path)
|
||||||
# Tjek om mappen allerede er tilføjet
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
# Tjek om mappen allerede er aktiv
|
||||||
existing = conn.execute(
|
existing = conn.execute(
|
||||||
"SELECT id FROM libraries WHERE path=?", (folder,)
|
"SELECT id, is_active FROM libraries WHERE path=?", (folder,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
QMessageBox.information(self, "Allerede tilføjet",
|
if existing["is_active"]:
|
||||||
"Denne mappe er allerede i listen.")
|
QMessageBox.information(
|
||||||
|
self, "Allerede tilføjet",
|
||||||
|
"Denne mappe er allerede i listen."
|
||||||
|
)
|
||||||
conn.close()
|
conn.close()
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
# Reaktiver en tidligere fjernet mappe
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO libraries (path, is_active) VALUES (?, 1)", (folder,)
|
"UPDATE libraries SET is_active=1 WHERE path=?",
|
||||||
|
(folder,)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO libraries (path, is_active) VALUES (?, 1)",
|
||||||
|
(folder,)
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -196,79 +232,69 @@ class LibraryManagerDialog(QDialog):
|
|||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self, "Fjern mappe",
|
self, "Fjern mappe",
|
||||||
f"Fjern:\n{lib['path']}\n\n"
|
f"Fjern:\n{lib['path']}\n\n"
|
||||||
"Alle sange fra denne mappe slettes også fra databasen.\n"
|
"Alle sange fra denne mappe slettes fra databasen.\n"
|
||||||
"Dans-tags og playlister bevares.",
|
"Dans-tags og playlister bevares.",
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.StandardButton.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Stop evt. scanning på dette bibliotek
|
# Stop evt. BPM-scanning på dette bibliotek
|
||||||
if lib["id"] in self._workers:
|
if lib["id"] in self._workers:
|
||||||
self._workers[lib["id"]].cancel()
|
self._workers[lib["id"]].cancel()
|
||||||
self._workers.pop(lib["id"], None)
|
self._workers.pop(lib["id"], None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(self._db_path)
|
conn = sqlite3.connect(self._db_path)
|
||||||
# Slet sange der tilhører dette bibliotek
|
# Slet sange fra biblioteket
|
||||||
conn.execute("DELETE FROM songs WHERE library_id=?", (lib["id"],))
|
conn.execute(
|
||||||
# Deaktiver biblioteket
|
"DELETE FROM songs WHERE library_id=?", (lib["id"],)
|
||||||
conn.execute("UPDATE libraries SET is_active=0 WHERE id=?", (lib["id"],))
|
)
|
||||||
|
# Slet selve biblioteks-rækken helt
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM libraries WHERE id=?", (lib["id"],)
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
self._load()
|
self._load()
|
||||||
self.libraries_changed.emit()
|
QTimer.singleShot(300, self.libraries_changed.emit)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}")
|
QMessageBox.warning(self, "Fejl", f"Kunne ikke fjerne: {e}")
|
||||||
|
|
||||||
# ── Scanning ──────────────────────────────────────────────────────────────
|
# ── BPM-scanning ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _start_scan(self, library_id: int, path: str,
|
def _start_bpm(self, library_id: int, path: str,
|
||||||
scan_all: bool, btn: QPushButton, lbl: QLabel):
|
scan_all: bool, btn: QPushButton, lbl: QLabel):
|
||||||
if library_id in self._workers:
|
if library_id in self._workers:
|
||||||
return
|
return
|
||||||
|
|
||||||
from local.local_db import DB_PATH
|
from local.local_db import DB_PATH
|
||||||
from ui.scan_worker import ScanWorker
|
from ui.bpm_worker import BpmScanWorker
|
||||||
|
|
||||||
worker = ScanWorker(library_id, path, str(DB_PATH),
|
worker = BpmScanWorker(library_id, str(DB_PATH),
|
||||||
overwrite_bpm=scan_all)
|
scan_all=scan_all)
|
||||||
|
|
||||||
def on_progress(done, total, filename):
|
def on_progress(done, total):
|
||||||
if total > 0:
|
lbl.setText(f"♩ {done}/{total} analyseret...")
|
||||||
lbl.setText(f"⟳ {done}/{total} — {filename}")
|
|
||||||
lbl.show()
|
lbl.show()
|
||||||
btn.setEnabled(False)
|
btn.setEnabled(False)
|
||||||
|
|
||||||
def on_finished(count, lib_path):
|
def on_finished(count):
|
||||||
lbl.setText(f"✓ {count} sange scannet")
|
lbl.setText(f"✓ {count} analyseret")
|
||||||
btn.setEnabled(True)
|
btn.setEnabled(True)
|
||||||
self._workers.pop(library_id, None)
|
self._workers.pop(library_id, None)
|
||||||
QTimer.singleShot(300, self._load)
|
QTimer.singleShot(300, self._load)
|
||||||
self.libraries_changed.emit()
|
|
||||||
|
|
||||||
def on_error(msg):
|
|
||||||
lbl.setText(f"⚠ Fejl: {msg}")
|
|
||||||
lbl.show()
|
|
||||||
btn.setEnabled(True)
|
|
||||||
self._workers.pop(library_id, None)
|
|
||||||
|
|
||||||
worker.progress.connect(on_progress)
|
worker.progress.connect(on_progress)
|
||||||
worker.finished.connect(on_finished)
|
worker.finished.connect(on_finished)
|
||||||
worker.error.connect(on_error)
|
|
||||||
self._workers[library_id] = worker
|
self._workers[library_id] = worker
|
||||||
worker.setPriority(QThread.Priority.LowestPriority)
|
|
||||||
worker.start()
|
worker.start()
|
||||||
|
worker.setPriority(QThread.Priority.LowestPriority)
|
||||||
|
|
||||||
# ── Luk ───────────────────────────────────────────────────────────────────
|
# ── Luk ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _on_close(self):
|
|
||||||
# Stop alle aktive scannere
|
|
||||||
for worker in self._workers.values():
|
|
||||||
worker.cancel()
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
for worker in self._workers.values():
|
for w in list(self._workers.values()):
|
||||||
worker.cancel()
|
w.cancel()
|
||||||
|
w.wait(2000) # Vent max 2 sek på at tråden stopper
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ class LibraryPanel(QWidget):
|
|||||||
header.addWidget(lbl)
|
header.addWidget(lbl)
|
||||||
header.addStretch()
|
header.addStretch()
|
||||||
|
|
||||||
|
btn_refresh = QPushButton("↻ Opdater")
|
||||||
|
btn_refresh.setFixedHeight(28)
|
||||||
|
btn_refresh.setToolTip("Opdater bibliotek fra database")
|
||||||
|
btn_refresh.clicked.connect(self._refresh_library)
|
||||||
|
header.addWidget(btn_refresh)
|
||||||
|
|
||||||
btn_manage = QPushButton("⚙ Mapper")
|
btn_manage = QPushButton("⚙ Mapper")
|
||||||
btn_manage.setFixedHeight(28)
|
btn_manage.setFixedHeight(28)
|
||||||
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
|
btn_manage.setToolTip("Tilføj, fjern og scan musikbiblioteker")
|
||||||
@@ -376,19 +382,31 @@ class LibraryPanel(QWidget):
|
|||||||
self._bpm_worker.done.connect(on_bpm_done)
|
self._bpm_worker.done.connect(on_bpm_done)
|
||||||
self._bpm_worker.start()
|
self._bpm_worker.start()
|
||||||
|
|
||||||
|
def _refresh_library(self):
|
||||||
|
"""Genindlæs bibliotek fra database."""
|
||||||
|
mw = self.window()
|
||||||
|
if hasattr(mw, "_reload_library"):
|
||||||
|
mw._reload_library()
|
||||||
|
|
||||||
def _manage_libraries(self):
|
def _manage_libraries(self):
|
||||||
from ui.library_manager import LibraryManagerDialog
|
from ui.library_manager import LibraryManagerDialog
|
||||||
from local.local_db import DB_PATH
|
from local.local_db import DB_PATH
|
||||||
dialog = LibraryManagerDialog(db_path=str(DB_PATH), parent=self.window())
|
dialog = LibraryManagerDialog(db_path=str(DB_PATH), parent=self.window())
|
||||||
dialog.libraries_changed.connect(self._on_libraries_changed)
|
dialog.libraries_changed.connect(self._on_libraries_changed)
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
# Reload øjeblikkeligt når dialog lukkes
|
||||||
def _on_libraries_changed(self):
|
|
||||||
"""Kald reload på main_window når biblioteker ændres."""
|
|
||||||
mw = self.window()
|
mw = self.window()
|
||||||
if hasattr(mw, "_reload_library"):
|
if hasattr(mw, "_reload_library"):
|
||||||
from PyQt6.QtCore import QTimer
|
mw._reload_library()
|
||||||
QTimer.singleShot(500, mw._reload_library)
|
# Start scanning
|
||||||
|
if hasattr(mw, "start_background_scan"):
|
||||||
|
QTimer.singleShot(1000, mw.start_background_scan)
|
||||||
|
|
||||||
|
def _on_libraries_changed(self):
|
||||||
|
"""Kaldes ved tilføj/fjern — reload øjeblikkeligt."""
|
||||||
|
mw = self.window()
|
||||||
|
if hasattr(mw, "_reload_library"):
|
||||||
|
mw._reload_library()
|
||||||
|
|
||||||
def _add_folder(self):
|
def _add_folder(self):
|
||||||
from PyQt6.QtWidgets import QFileDialog
|
from PyQt6.QtWidgets import QFileDialog
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ from PyQt6.QtWidgets import (
|
|||||||
QSizePolicy, QMenuBar, QMenu, QStatusBar, QFileDialog,
|
QSizePolicy, QMenuBar, QMenu, QStatusBar, QFileDialog,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer, QThread
|
||||||
from PyQt6.QtGui import QAction
|
from PyQt6.QtGui import QAction
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from ui.vu_meter import VUMeter
|
from ui.vu_meter import VUMeter
|
||||||
from ui.playlist_panel import PlaylistPanel
|
from ui.playlist_panel import PlaylistPanel
|
||||||
@@ -79,6 +80,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._song_ended = False
|
self._song_ended = False
|
||||||
self._demo_active = False
|
self._demo_active = False
|
||||||
self._watcher = None
|
self._watcher = None
|
||||||
|
self._scan_workers = [] # Hold referencer til aktive scan-tråde
|
||||||
self._scan_worker = None
|
self._scan_worker = None
|
||||||
self._api_url: str | None = None
|
self._api_url: str | None = None
|
||||||
self._api_token: str | None = None
|
self._api_token: str | None = None
|
||||||
@@ -92,6 +94,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self._connect_player_signals()
|
self._connect_player_signals()
|
||||||
self._library_loaded.connect(self._apply_library)
|
self._library_loaded.connect(self._apply_library)
|
||||||
|
self._db_ready.connect(self._on_db_ready)
|
||||||
self._build_menu()
|
self._build_menu()
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._build_statusbar()
|
self._build_statusbar()
|
||||||
@@ -383,8 +386,7 @@ class MainWindow(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
from local.local_db import init_db
|
from local.local_db import init_db
|
||||||
init_db()
|
init_db()
|
||||||
# Trigger library load via signal
|
self._db_ready.emit()
|
||||||
self._library_loaded.emit([]) # tomt signal = "DB klar, load nu"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -432,6 +434,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Signal til at opdatere biblioteket fra baggrundstråd
|
# Signal til at opdatere biblioteket fra baggrundstråd
|
||||||
_library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list)
|
_library_loaded = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal(list)
|
||||||
|
_db_ready = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
|
||||||
_file_changed_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
|
_file_changed_signal = __import__('PyQt6.QtCore', fromlist=['pyqtSignal']).pyqtSignal()
|
||||||
|
|
||||||
def _reload_library(self):
|
def _reload_library(self):
|
||||||
@@ -483,12 +486,12 @@ class MainWindow(QMainWindow):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _apply_library(self, songs: list):
|
def _on_db_ready(self):
|
||||||
if not songs:
|
"""DB er initialiseret — indlæs bibliotek og start post-init."""
|
||||||
# Tomt signal = DB er klar, start library load og post-init
|
|
||||||
self._reload_library()
|
self._reload_library()
|
||||||
self._post_init()
|
self._post_init()
|
||||||
return
|
|
||||||
|
def _apply_library(self, songs: list):
|
||||||
self._library_panel.load_songs(songs)
|
self._library_panel.load_songs(songs)
|
||||||
count = len(songs)
|
count = len(songs)
|
||||||
self._set_status(
|
self._set_status(
|
||||||
@@ -496,7 +499,7 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _post_init(self):
|
def _post_init(self):
|
||||||
"""Kør efter DB er initialiseret — gendan state."""
|
"""Kør efter DB er initialiseret — gendan state og start scan."""
|
||||||
try:
|
try:
|
||||||
restored = self._playlist_panel.restore_active_playlist()
|
restored = self._playlist_panel.restore_active_playlist()
|
||||||
if restored:
|
if restored:
|
||||||
@@ -513,11 +516,48 @@ class MainWindow(QMainWindow):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Periodisk reload af bibliotek hvert 10. sekund — fanger ny-scannede sange
|
# Scan 30 sek efter opstart — fanger ændringer siden sidst
|
||||||
self._auto_reload_timer = QTimer(self)
|
QTimer.singleShot(30000, self.start_background_scan)
|
||||||
self._auto_reload_timer.setInterval(10000)
|
|
||||||
self._auto_reload_timer.timeout.connect(self._reload_library)
|
def start_background_scan(self):
|
||||||
self._auto_reload_timer.start()
|
"""Start scanning af alle aktive biblioteker i baggrunden."""
|
||||||
|
try:
|
||||||
|
import sqlite3
|
||||||
|
from local.local_db import DB_PATH
|
||||||
|
from ui.scan_worker import ScanWorker
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(DB_PATH))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
libs = conn.execute(
|
||||||
|
"SELECT id, path FROM libraries WHERE is_active=1"
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
pending = [lib for lib in libs if Path(lib["path"]).exists()]
|
||||||
|
if not pending:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_status("Scanner biblioteker i baggrunden...", 4000)
|
||||||
|
self._scan_workers = []
|
||||||
|
finished_count = [0]
|
||||||
|
|
||||||
|
def on_one_finished(count, p):
|
||||||
|
finished_count[0] += 1
|
||||||
|
self._set_status(f"Scanning færdig — {count} filer", 4000)
|
||||||
|
# Ryd færdige workers ud
|
||||||
|
self._scan_workers = [w for w in self._scan_workers
|
||||||
|
if w.isRunning()]
|
||||||
|
|
||||||
|
for lib in pending:
|
||||||
|
worker = ScanWorker(lib["id"], lib["path"], str(DB_PATH),
|
||||||
|
overwrite_bpm=False)
|
||||||
|
worker.finished.connect(on_one_finished)
|
||||||
|
worker.start()
|
||||||
|
worker.setPriority(QThread.Priority.LowestPriority)
|
||||||
|
self._scan_workers.append(worker)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def add_library_path(self, path: str):
|
def add_library_path(self, path: str):
|
||||||
try:
|
try:
|
||||||
@@ -984,9 +1024,15 @@ class MainWindow(QMainWindow):
|
|||||||
if self._scan_worker and self._scan_worker.isRunning():
|
if self._scan_worker and self._scan_worker.isRunning():
|
||||||
self._scan_worker.quit()
|
self._scan_worker.quit()
|
||||||
self._scan_worker.wait(2000)
|
self._scan_worker.wait(2000)
|
||||||
|
# Stop scan workers
|
||||||
|
if hasattr(self, "_scan_workers"):
|
||||||
|
for w in self._scan_workers:
|
||||||
|
if w.isRunning():
|
||||||
|
w.cancel()
|
||||||
|
# Stop watchdog subprocess
|
||||||
|
if hasattr(self, "_watchdog_proc") and self._watchdog_proc:
|
||||||
try:
|
try:
|
||||||
if self._watcher:
|
self._watchdog_proc.terminate()
|
||||||
self._watcher.stop()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|||||||
Reference in New Issue
Block a user