Offline check ser ok ud.

This commit is contained in:
2026-04-14 15:05:54 +02:00
parent 66804681da
commit d4356e7337
4 changed files with 301 additions and 9 deletions

View File

@@ -50,10 +50,12 @@ def scan_library(library_id: int, library_path: str, db_path: str,
# Byg indeks over kendte filer
known = {}
for row in conn.execute(
"SELECT local_path, file_modified_at FROM songs WHERE library_id=?",
"SELECT local_path, file_modified_at, file_missing FROM songs WHERE library_id=?",
(library_id,)
).fetchall():
known[row["local_path"]] = row["file_modified_at"]
# Sange markeret som manglende medtages ikke i known — de skal altid genscanes
if not row["file_missing"]:
known[row["local_path"]] = row["file_modified_at"]
# Find alle musikfiler
all_files = []
@@ -87,10 +89,44 @@ def scan_library(library_id: int, library_path: str, db_path: str,
tags = read_tags(fp)
extra = json.dumps(tags.get("extra_tags", {}), ensure_ascii=False)
# Match 1: præcis sti-match
existing = conn.execute(
"SELECT id, bpm FROM songs WHERE local_path=?", (path_str,)
).fetchone()
# Match 2: titel+artist match — fil er flyttet eller var missing
if not existing:
title = tags.get("title", "")
artist = tags.get("artist", "")
if title:
# Prioritér file_missing=1 sange, men tag også sange med ugyldig sti
existing = conn.execute("""
SELECT id, bpm FROM songs
WHERE title=? AND artist=? AND file_missing=1
LIMIT 1
""", (title, artist)).fetchone()
if not existing:
# Tjek om der er en sang med samme titel+artist men ugyldig sti
existing = conn.execute("""
SELECT id, bpm, local_path FROM songs
WHERE title=? AND artist=? AND file_missing=0
LIMIT 1
""", (title, artist)).fetchone()
if existing:
from pathlib import Path as _Path
old_path = existing["local_path"] or ""
if old_path and not _Path(old_path).exists():
pass # Sti er ugyldig — brug dette match
else:
existing = None # Sti er valid — det er en anden fil
if existing:
# Opdater stien så den peger på den nye placering
conn.execute(
"UPDATE songs SET local_path=? WHERE id=?",
(path_str, existing["id"])
)
if existing:
bpm = tags.get("bpm", 0)
if not overwrite_bpm and existing["bpm"] and existing["bpm"] > 0:

View File

@@ -99,6 +99,7 @@ class LibraryPanel(QWidget):
song_selected = pyqtSignal(dict)
add_to_playlist = pyqtSignal(dict)
scan_requested = pyqtSignal()
sync_requested = pyqtSignal()
edit_tags_requested = pyqtSignal(dict)
send_mail_requested = pyqtSignal(dict)
@@ -248,6 +249,15 @@ class LibraryPanel(QWidget):
def _on_scan_clicked(self):
self.scan_requested.emit()
def _on_sync_clicked(self):
self._btn_sync.setText("⇅ ...")
self._btn_sync.setEnabled(False)
self.sync_requested.emit()
QTimer.singleShot(3000, lambda: (
self._btn_sync.setText("⇅ Sync"),
self._btn_sync.setEnabled(True),
))
def set_scanning(self, scanning: bool, status_text: str = ""):
pass # Status vises i statuslinjen

View File

@@ -364,6 +364,7 @@ class MainWindow(QMainWindow):
self._library_panel.song_selected.connect(self._on_library_song_selected)
self._library_panel.add_to_playlist.connect(self._add_song_to_playlist)
self._library_panel.scan_requested.connect(self.start_scan)
self._library_panel.sync_requested.connect(self._manual_sync)
self._library_panel.edit_tags_requested.connect(self._open_tag_editor)
self._library_panel.send_mail_requested.connect(self._send_mail)
@@ -563,7 +564,7 @@ class MainWindow(QMainWindow):
except Exception:
pass
QTimer.singleShot(30000, self.start_background_scan)
QTimer.singleShot(5000, self.start_background_scan)
def start_background_scan(self):
"""Start scanning af alle aktive biblioteker i baggrunden."""

View File

@@ -5,13 +5,31 @@ playlist_panel.py — Danseliste med Ny/Gem/Hent knapper, autogem og event-overb
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
QLabel, QHBoxLayout, QPushButton, QMenu, QAbstractItemView,
QMessageBox,
QMessageBox, QStyledItemDelegate,
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QByteArray
from PyQt6.QtGui import QColor, QDragEnterEvent, QDropEvent
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QByteArray, QRect
from PyQt6.QtGui import QColor, QDragEnterEvent, QDropEvent, QPainter
ACTIVE_PLAYLIST_NAME = "__aktiv__" # fast navn til autogem-listen
ACTIVE_PLAYLIST_NAME = "__aktiv__"
class AvailabilityDelegate(QStyledItemDelegate):
"""Tegner en farvet dot til højre for hvert playlist-item."""
def paint(self, painter, option, index):
super().paint(painter, option, index)
color = index.data(Qt.ItemDataRole.UserRole + 1)
if not color:
return
painter.save()
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QColor(color))
painter.setPen(Qt.PenStyle.NoPen)
r = option.rect
cx = r.right() - 10
cy = r.center().y()
painter.drawEllipse(cx - 5, cy - 5, 10, 10)
painter.restore()
class PlaylistPanel(QWidget):
@@ -89,6 +107,12 @@ class PlaylistPanel(QWidget):
btn_load.clicked.connect(self._load_dialog)
toolbar.addWidget(btn_load)
self._btn_offline_check = QPushButton("⬤ Offline tjek")
self._btn_offline_check.setFixedHeight(26)
self._btn_offline_check.setToolTip("Kontrollér om alle sange er tilgængelige på denne maskine")
self._btn_offline_check.clicked.connect(self._check_offline)
toolbar.addWidget(self._btn_offline_check)
toolbar.addStretch()
self._lbl_autosave = QLabel("")
@@ -133,6 +157,7 @@ class PlaylistPanel(QWidget):
# ── Liste ─────────────────────────────────────────────────────────────
self._list = QListWidget()
self._list.setItemDelegate(AvailabilityDelegate(self._list))
self._list.setObjectName("playlist_list")
self._list.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop)
self._list.setDefaultDropAction(Qt.DropAction.MoveAction)
@@ -176,6 +201,7 @@ class PlaylistPanel(QWidget):
self._song_ended = False
if name:
self._title_label.setText(f"DANSELISTE — {name.upper()}")
self._check_availability(self._songs)
self._refresh()
self._trigger_autosave()
@@ -444,6 +470,7 @@ class PlaylistPanel(QWidget):
self._btn_save_current.setToolTip(f"Gem ændringer til '{pl['name']}'")
self._title_label.setText(f"DANSELISTE — {pl['name'].upper()}")
self._lbl_autosave.setText("✓ gendannet")
self._check_availability(self._songs)
# Find næste uafspillede
ni = self.next_playable_idx()
@@ -875,6 +902,98 @@ class PlaylistPanel(QWidget):
self.next_song_ready.emit(self._songs[ni])
self.event_started.emit()
# ── Tilgængelighed ────────────────────────────────────────────────────────
def _check_availability(self, songs: list[dict]) -> list[dict]:
"""Tjek om sange er tilgængelige lokalt — forsøger auto-match ved gule."""
from pathlib import Path
from local.local_db import get_db
try:
with get_db() as conn:
for song in songs:
path = song.get("local_path", "")
if path and Path(path).exists():
song["availability"] = "green"
continue
# Forsøg auto-match via titel+artist
title = song.get("title", "")
artist = song.get("artist", "")
try:
match = conn.execute("""
SELECT id, local_path FROM songs
WHERE title=? AND artist=? AND file_missing=0
AND local_path IS NOT NULL AND local_path != ''
LIMIT 1
""", (title, artist)).fetchone()
if match and Path(match["local_path"]).exists():
song["local_path"] = match["local_path"]
song["id"] = match["id"]
song["file_missing"] = False
song["availability"] = "green"
continue
except Exception:
pass
song["availability"] = "yellow" if not path else "red"
except Exception:
# DB fejl — sæt alle til yellow
for song in songs:
song.setdefault("availability", "yellow")
return songs
def check_offline_availability(self):
"""Gennemgå alle sange og forsøg auto-match for gule/røde."""
from pathlib import Path
from local.local_db import get_db
with get_db() as conn:
for song in self._songs:
path = song.get("local_path", "")
# Grøn — eksisterer og tilgængeligt
if path and Path(path).exists():
song["availability"] = "green"
song["file_missing"] = False
# Opdater songs tabellen
conn.execute(
"UPDATE songs SET file_missing=0, local_path=? WHERE id=?",
(path, song["id"])
)
continue
# Forsøg auto-match via titel+artist
title = song.get("title", "")
artist = song.get("artist", "")
match = conn.execute("""
SELECT id, local_path FROM songs
WHERE title=? AND artist=? AND file_missing=0
AND local_path IS NOT NULL AND local_path != ''
LIMIT 1
""", (title, artist)).fetchone()
if match and Path(match["local_path"]).exists():
song["local_path"] = match["local_path"]
song["id"] = match["id"]
song["availability"] = "green"
song["file_missing"] = False
# Opdater playlist_songs til at pege på den fundne sang
if self._named_playlist_id:
conn.execute("""
UPDATE playlist_songs SET song_id=?
WHERE playlist_id=? AND song_id=(
SELECT id FROM songs
WHERE title=? AND artist=?
LIMIT 1
)
""", (match["id"], self._named_playlist_id, title, artist))
else:
song["availability"] = "red"
self._refresh()
green_count = sum(1 for s in self._songs if s.get("availability") == "green")
return green_count, len(self._songs)
# ── Højreklik ─────────────────────────────────────────────────────────────
def _show_context_menu(self, pos):
@@ -885,6 +1004,9 @@ class PlaylistPanel(QWidget):
if idx is None:
return
song = self._songs[idx] if 0 <= idx < len(self._songs) else None
avail = song.get("availability", "yellow") if song else "yellow"
all_green = all(s.get("availability", "yellow") == "green" for s in self._songs)
menu = QMenu(self)
act_play = menu.addAction("▶ Afspil denne")
menu.addSeparator()
@@ -892,15 +1014,20 @@ class PlaylistPanel(QWidget):
act_unplay = menu.addAction("↺ Sæt til ikke afspillet")
act_played = menu.addAction("✓ Sæt til afspillet")
menu.addSeparator()
# Dans-valg
act_dance = menu.addAction("💃 Vælg dans...")
# Workshop toggle
is_ws = song.get("is_workshop", False) if song else False
act_ws = menu.addAction("🎓 Fjern workshop" if is_ws else "🎓 Markér som workshop")
menu.addSeparator()
act_dance_info = menu.addAction(" Dans-info...")
menu.addSeparator()
# Offline tilgængelighed
act_check = menu.addAction("🔍 Kontrollér offline afvikling")
act_check.setEnabled(not all_green)
act_manual = menu.addAction("📂 Manuel match af fil...")
act_manual.setEnabled(avail != "green")
menu.addSeparator()
act_remove = menu.addAction("✕ Fjern fra liste")
action = menu.exec(self._list.mapToGlobal(pos))
if action == act_play:
self.song_selected.emit(idx)
@@ -928,6 +1055,10 @@ class PlaylistPanel(QWidget):
from ui.dance_info_dialog import DanceInfoDialog
dlg = DanceInfoDialog(song, parent=self.window())
dlg.exec()
elif action == act_check:
self._check_offline()
elif action == act_manual and song:
self._manual_match(idx, song)
elif action == act_remove:
self._songs.pop(idx)
self._statuses.pop(idx)
@@ -935,6 +1066,111 @@ class PlaylistPanel(QWidget):
self._current_idx = max(-1, self._current_idx - 1)
self._refresh(); self._trigger_autosave()
def _check_offline(self):
"""Kør offline tjek og vis resultat."""
green, total = self.check_offline_availability()
red = sum(1 for s in self._songs if s.get("availability") == "red")
if red == 0:
self._set_status_msg(f"✓ Alle {total} sange er tilgængelige lokalt", 4000)
else:
self._set_status_msg(
f"{green}/{total} tilgængelige — {red} ikke fundet (rød markering)", 6000
)
def _set_status_msg(self, msg: str, ms: int = 3000):
"""Vis besked i playlist-titlen kortvarigt."""
old = self._title_label.text()
self._title_label.setText(msg)
QTimer.singleShot(ms, lambda: self._title_label.setText(old))
def _manual_match(self, idx: int, song: dict):
"""Søg i biblioteket og lad brugeren vælge den rigtige sang."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit,
QListWidget, QListWidgetItem, QPushButton, QLabel
)
from local.local_db import get_db
from pathlib import Path
dlg = QDialog(self.window())
dlg.setWindowTitle(f"Manuel match: {song.get('title','?')}")
dlg.setMinimumSize(500, 400)
layout = QVBoxLayout(dlg)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
lbl = QLabel(f"Søg i biblioteket efter en sang til: <b>{song.get('title','')} {song.get('artist','')}</b>")
lbl.setWordWrap(True)
layout.addWidget(lbl)
search = QLineEdit()
search.setPlaceholderText("Søg på titel eller artist...")
search.setText(song.get("title", ""))
layout.addWidget(search)
lst = QListWidget()
layout.addWidget(lst)
def _load_results(text):
lst.clear()
try:
with get_db() as conn:
rows = conn.execute("""
SELECT id, title, artist, local_path, file_format
FROM songs
WHERE file_missing=0
AND (title LIKE ? OR artist LIKE ?)
ORDER BY title LIMIT 100
""", (f"%{text}%", f"%{text}%")).fetchall()
for row in rows:
label = f"{row['title']} · {row['artist']} [{row['file_format']}]"
item = QListWidgetItem(label)
item.setData(Qt.ItemDataRole.UserRole, dict(row))
lst.addItem(item)
except Exception:
pass
search.textChanged.connect(_load_results)
_load_results(search.text())
btn_row = QHBoxLayout()
btn_ok = QPushButton("✓ Brug valgt sang")
btn_ok.setEnabled(False)
btn_cancel = QPushButton("Annuller")
btn_row.addWidget(btn_ok)
btn_row.addWidget(btn_cancel)
layout.addLayout(btn_row)
lst.itemSelectionChanged.connect(lambda: btn_ok.setEnabled(bool(lst.currentItem())))
btn_cancel.clicked.connect(dlg.reject)
def _accept():
item = lst.currentItem()
if not item:
return
chosen = item.data(Qt.ItemDataRole.UserRole)
path = chosen.get("local_path", "")
if not path or not Path(path).exists():
return
song["local_path"] = path
song["availability"] = "green"
song["file_missing"] = False
# Opdater DB
try:
with get_db() as conn:
conn.execute(
"UPDATE songs SET local_path=?, file_missing=0 WHERE id=?",
(path, song.get("id", ""))
)
except Exception:
pass
self._refresh()
dlg.accept()
btn_ok.clicked.connect(_accept)
lst.itemDoubleClicked.connect(lambda _: _accept())
dlg.exec()
# ── Render ────────────────────────────────────────────────────────────────
def _refresh(self):
@@ -969,10 +1205,19 @@ class PlaylistPanel(QWidget):
active = dances[0] if dances else "ingen dans tagget"
ws_tag = " 🎓" if song.get("is_workshop") else ""
# Tilgængeligheds-dot til højre — kun hvis tjekket (ikke yellow)
avail = song.get("availability", "yellow")
avail_color = {"green": "#27ae60", "red": "#e74c3c"}.get(avail, None)
avail_tip = {"green": "Tilgængelig lokalt", "red": "Ikke fundet lokalt"}.get(avail, "")
text = (f"{i+1:>2}. {song.get('title','')}{ws_tag}\n"
f" {song.get('artist','')} · {active}")
item = QListWidgetItem(f"{icon} {text}")
item.setData(Qt.ItemDataRole.UserRole, i)
item.setData(Qt.ItemDataRole.UserRole + 1, avail_color)
if avail_tip:
item.setToolTip(avail_tip)
if status == "playing":
item.setForeground(QColor(self.STATUS_COLOR["playing"]))
f = item.font(); f.setBold(True); item.setFont(f)