Offline check ser ok ud.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user