# Standard library imports from typing import Optional # PyQt imports from PyQt6.QtCore import QEvent, Qt from PyQt6.QtGui import QKeyEvent from PyQt6.QtWidgets import ( QDialog, QListWidgetItem, QMainWindow, ) # Third party imports from sqlalchemy.orm.session import Session # App imports from classes import MusicMusterSignals from helpers import ( ask_yes_no, get_relative_date, ms_to_mmss, ) from log import log from models import Settings, Tracks from playlistmodel import PlaylistModel from ui import dlg_TrackSelect_ui class TrackSelectDialog(QDialog): """Select track from database""" def __init__( self, parent: QMainWindow, session: Session, new_row_number: int, base_model: PlaylistModel, add_to_header: Optional[bool] = False, *args: Qt.WindowType, **kwargs: Qt.WindowType, ) -> None: """ Subclassed QDialog to manage track selection """ super().__init__(parent, *args, **kwargs) self.session = session self.new_row_number = new_row_number self.base_model = base_model self.add_to_header = add_to_header self.ui = dlg_TrackSelect_ui.Ui_Dialog() self.ui.setupUi(self) self.ui.btnAdd.clicked.connect(self.add_selected) self.ui.btnAddClose.clicked.connect(self.add_selected_and_close) self.ui.btnClose.clicked.connect(self.close) self.ui.matchList.itemDoubleClicked.connect(self.add_selected) self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) self.ui.radioTitle.toggled.connect(self.title_artist_toggle) self.ui.searchString.textEdited.connect(self.chars_typed) self.track: Optional[Tracks] = None self.signals = MusicMusterSignals() record = Settings.get_setting(self.session, "dbdialog_width") width = record.f_int or 800 record = Settings.get_setting(self.session, "dbdialog_height") height = record.f_int or 600 self.resize(width, height) if add_to_header: self.ui.lblNote.setVisible(False) self.ui.txtNote.setVisible(False) def add_selected(self) -> None: """Handle Add button""" track = None if self.ui.matchList.selectedItems(): item = self.ui.matchList.currentItem() if item: track = item.data(Qt.ItemDataRole.UserRole) note = self.ui.txtNote.text() if not (track or note): return track_id = None if track: track_id = track.id if note and not track_id: self.base_model.insert_row(self.new_row_number, track_id, note) self.ui.txtNote.clear() self.new_row_number += 1 return self.ui.txtNote.clear() self.select_searchtext() if track_id is None: log.error("track_id is None and should not be") return # Check whether track is already in playlist move_existing = False existing_prd = self.base_model.is_track_in_playlist(track_id) if existing_prd is not None: if ask_yes_no( "Duplicate row", "Track already in playlist. " "Move to new location?", default_yes=True, ): move_existing = True if self.add_to_header: if move_existing and existing_prd: # "and existing_prd" for mypy's benefit self.base_model.move_track_to_header( self.new_row_number, existing_prd, note ) else: self.base_model.add_track_to_header(self.new_row_number, track_id) # Close dialog - we can only add one track to a header self.accept() else: # Adding a new track row if move_existing and existing_prd: # "and existing_prd" for mypy's benefit self.base_model.move_track_add_note( self.new_row_number, existing_prd, note ) else: self.base_model.insert_row(self.new_row_number, track_id, note) self.new_row_number += 1 def add_selected_and_close(self) -> None: """Handle Add and Close button""" self.add_selected() self.accept() def chars_typed(self, s: str) -> None: """Handle text typed in search box""" self.ui.matchList.clear() if len(s) > 0: if s.startswith("a/") and len(s) > 2: matches = Tracks.search_artists(self.session, "%" + s[2:]) elif self.ui.radioTitle.isChecked(): matches = Tracks.search_titles(self.session, "%" + s) else: matches = Tracks.search_artists(self.session, "%" + s) if matches: for track in matches: last_played = None last_playdate = max( track.playdates, key=lambda p: p.lastplayed, default=None ) if last_playdate: last_played = last_playdate.lastplayed t = QListWidgetItem() track_text = ( f"{track.title} - {track.artist} " f"[{ms_to_mmss(track.duration)}] " f"({get_relative_date(last_played)})" ) t.setText(track_text) t.setData(Qt.ItemDataRole.UserRole, track) self.ui.matchList.addItem(t) def closeEvent(self, event: Optional[QEvent]) -> None: """ Override close and save dialog coordinates """ if not event: return record = Settings.get_setting(self.session, "dbdialog_height") record.f_int = self.height() record = Settings.get_setting(self.session, "dbdialog_width") record.f_int = self.width() self.session.commit() event.accept() def keyPressEvent(self, event: QKeyEvent | None) -> None: """ Clear selection on ESC if there is one """ if event and event.key() == Qt.Key.Key_Escape: if self.ui.matchList.selectedItems(): self.ui.matchList.clearSelection() return super(TrackSelectDialog, self).keyPressEvent(event) def select_searchtext(self) -> None: """Select the searchbox""" self.ui.searchString.selectAll() self.ui.searchString.setFocus() def selection_changed(self) -> None: """Display selected track path in dialog box""" if not self.ui.matchList.selectedItems(): return item = self.ui.matchList.currentItem() track = item.data(Qt.ItemDataRole.UserRole) last_playdate = max(track.playdates, key=lambda p: p.lastplayed, default=None) if last_playdate: last_played = last_playdate.lastplayed else: last_played = None path_text = f"{track.path} ({get_relative_date(last_played)})" self.ui.dbPath.setText(path_text) def title_artist_toggle(self) -> None: """ Handle switching between searching for artists and searching for titles """ # Logic is handled already in chars_typed(), so just call that. self.chars_typed(self.ui.searchString.text())