#!/usr/bin/env python from log import log # import argparse import sys # import threading from datetime import datetime, timedelta # from typing import Callable, Dict, List, Optional, Tuple from PyQt5.QtCore import QDate, QEvent, Qt, QTime, QTimer from PyQt5.QtGui import QColor from PyQt5.QtWidgets import ( QApplication, QDialog, QFileDialog, QInputDialog, QLabel, QLineEdit, QListWidgetItem, QMainWindow, # QMessageBox, ) # from dbconfig import engine, Session import helpers import music # from models import ( Base, Playdates, PlaylistRows, Playlists, Settings, Tracks ) from playlists import PlaylistTab from sqlalchemy.orm.exc import DetachedInstanceError from ui.dlg_search_database_ui import Ui_Dialog # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from config import Config from ui.main_window_ui import Ui_MainWindow # type: ignore # from utilities import create_track_from_file, update_db class TrackData: def __init__(self, track): self.id = track.id self.title = track.title self.artist = track.artist self.duration = track.duration self.start_gap = track.start_gap self.fade_at = track.fade_at self.silence_at = track.silence_at self.path = track.path self.mtime = track.mtime class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None) -> None: super().__init__(parent) self.setupUi(self) self.timer: QTimer = QTimer() self.even_tick: bool = True self.playing: bool = False self.music: music.Music = music.Music() self.current_track: Optional[TrackData] = None self.current_track_playlist_tab: Optional[PlaylistTab] = None self.next_track: Optional[TrackData] = None self.next_track_playlist_tab: Optional[PlaylistTab] = None self.previous_track: Optional[TrackData] = None self.previous_track_position: Optional[int] = None self.set_main_window_size() self.lblSumPlaytime = QLabel("") self.statusbar.addPermanentWidget(self.lblSumPlaytime) self.txtSearch = QLineEdit() self.statusbar.addWidget(self.txtSearch) self.txtSearch.setHidden(True) self.hide_played_tracks = False self.visible_playlist_tab: Callable[[], PlaylistTab] = \ self.tabPlaylist.currentWidget self.load_last_playlists() self.enable_play_next_controls() self.timer.start(Config.TIMER_MS) self.connect_signals_slots() # # @staticmethod # def print_current_database(): # with Session() as session: # db = session.bind.engine.url.database # print(f"{db=}") # def clear_selection(self) -> None: """ Clear selected row""" # Unselect any selected rows if self.visible_playlist_tab(): self.visible_playlist_tab().clear_selection() # Clear the search bar self.search_playlist_clear() def closeEvent(self, event: QEvent) -> None: """Handle attempt to close main window""" # Don't allow window to close when a track is playing if self.music.player and self.music.player.is_playing(): event.ignore() helpers.show_warning( "Track playing", "Can't close application while track is playing") else: with Session() as session: record = Settings.get_int_settings( session, "mainwindow_height") if record.f_int != self.height(): record.update(session, {'f_int': self.height()}) record = Settings.get_int_settings(session, "mainwindow_width") if record.f_int != self.width(): record.update(session, {'f_int': self.width()}) record = Settings.get_int_settings(session, "mainwindow_x") if record.f_int != self.x(): record.update(session, {'f_int': self.x()}) record = Settings.get_int_settings(session, "mainwindow_y") if record.f_int != self.y(): record.update(session, {'f_int': self.y()}) # Save splitter settings splitter_sizes = self.splitter.sizes() assert len(splitter_sizes) == 2 splitter_top, splitter_bottom = splitter_sizes record = Settings.get_int_settings(session, "splitter_top") if record.f_int != splitter_top: record.update(session, {'f_int': splitter_top}) record = Settings.get_int_settings(session, "splitter_bottom") if record.f_int != splitter_bottom: record.update(session, {'f_int': splitter_bottom}) event.accept() def connect_signals_slots(self) -> None: self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionDownload_CSV_of_played_tracks.triggered.connect( self.download_played_tracks) self.actionEnable_controls.triggered.connect( self.enable_play_next_controls) self.actionExport_playlist.triggered.connect(self.export_playlist_tab) self.actionFade.triggered.connect(self.fade) self.actionFind_next.triggered.connect( lambda: self.tabPlaylist.currentWidget().search_next()) self.actionFind_previous.triggered.connect( lambda: self.tabPlaylist.currentWidget().search_previous()) # self.actionImport.triggered.connect(self.import_track) self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertTrack.triggered.connect(self.insert_track) self.actionMoveSelected.triggered.connect(self.move_selected) self.actionNewPlaylist.triggered.connect(self.create_playlist) self.actionOpenPlaylist.triggered.connect(self.open_playlist) self.actionPlay_next.triggered.connect(self.play_next) self.actionSearch.triggered.connect(self.search_playlist) self.actionSelect_next_track.triggered.connect(self.select_next_row) # self.actionSelect_played_tracks.triggered.connect(self.select_played) self.actionSelect_previous_track.triggered.connect( self.select_previous_row) # self.actionSelect_unplayed_tracks.triggered.connect( # self.select_unplayed) self.actionSetNext.triggered.connect( lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) self.actionSkipToNext.triggered.connect(self.play_next) self.actionStop.triggered.connect(self.stop) self.btnDrop3db.clicked.connect(self.drop3db) self.btnFade.clicked.connect(self.fade) self.btnHidePlayed.clicked.connect(self.hide_played) self.btnStop.clicked.connect(self.stop) self.tabPlaylist.tabCloseRequested.connect(self.close_tab) self.txtSearch.returnPressed.connect(self.search_playlist_return) self.timer.timeout.connect(self.tick) def close_playlist_tab(self) -> None: """ Close active playlist tab, called by menu item """ self.close_tab(self.tabPlaylist.currentIndex()) def close_tab(self, tab_index: int) -> None: """ Close active playlist tab unless it holds the curren or next track. Called from close_playlist_tab() or by clicking close button on tab. """ # Don't close current track playlist if self.tabPlaylist.widget(tab_index) == ( self.current_track_playlist_tab): self.statusbar.showMessage( "Can't close current track playlist", 5000) return # Don't close next track playlist if self.tabPlaylist.widget(tab_index) == self.next_track_playlist_tab: self.statusbar.showMessage( "Can't close next track playlist", 5000) return # Close playlist and remove tab self.tabPlaylist.widget(tab_index).close() self.tabPlaylist.removeTab(tab_index) def insert_header(self) -> None: """Show dialog box to enter header text and add to playlist""" try: playlist_tab = self.visible_playlist_tab() except AttributeError: # Just return if there's no visible playlist tab return # Get header text dlg: QInputDialog = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) dlg.setLabelText("Header text:") dlg.resize(500, 100) ok = dlg.exec() if ok: with Session() as session: playlist_tab.insert_header(session, dlg.textValue()) def create_playlist(self) -> None: """Create new playlist""" dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) dlg.setLabelText("Playlist name:") dlg.resize(500, 100) ok = dlg.exec() if ok: with Session() as session: playlist = Playlists(session, dlg.textValue()) self.create_playlist_tab(session, playlist) def create_playlist_tab(self, session: Session, playlist: Playlists) -> None: """ Take the passed playlist database object, create a playlist tab and add tab to display. """ playlist_tab = PlaylistTab( musicmuster=self, session=session, playlist_id=playlist.id) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) self.tabPlaylist.setCurrentIndex(idx) def disable_play_next_controls(self) -> None: """ Disable "play next" keyboard controls """ self.actionPlay_next.setEnabled(False) self.statusbar.showMessage("Play controls: Disabled", 0) def download_played_tracks(self) -> None: """Download a CSV of played tracks""" dlg = DownloadCSV(self) if dlg.exec(): start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime() # Get output filename pathspec = QFileDialog.getSaveFileName( self, 'Save CSV of tracks played', directory="/tmp/playlist.csv", filter="CSV files (*.csv)" ) if not pathspec: return path = pathspec[0] if not path.endswith(".csv"): path += ".csv" with open(path, "w") as f: with Session() as session: for playdate in Playdates.played_after(session, start_dt): f.write( f"{playdate.track.artist},{playdate.track.title}\n" ) def drop3db(self) -> None: """Drop music level by 3db if button checked""" if self.btnDrop3db.isChecked(): self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False) else: self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False) def enable_play_next_controls(self) -> None: """ Enable "play next" keyboard controls """ self.actionPlay_next.setEnabled(True) self.statusbar.showMessage("Play controls: Enabled", 0) def end_of_track_actions(self) -> None: """ Clean up after track played Actions required: - Set flag to say we're not playing a track - Reset current track - Tell playlist_tab track has finished - Reset current playlist_tab - Reset clocks - Update headers - Enable controls """ # Set flag to say we're not playing a track so that tick() # doesn't see player=None and kick off end-of-track actions self.playing = False # Reset current track if self.current_track: self.previous_track = self.current_track self.current_track = None # Tell playlist_tab track has finished and # reset current playlist_tab if self.current_track_playlist_tab: self.current_track_playlist_tab.play_stopped() self.current_track_playlist_tab = None # Reset clocks self.frame_fade.setStyleSheet("") self.frame_silent.setStyleSheet("") self.label_elapsed_timer.setText("00:00") self.label_end_timer.setText("00:00") self.label_fade_length.setText("0:00") self.label_fade_timer.setText("00:00") self.label_silent_timer.setText("00:00") self.label_track_length.setText("0:00") self.label_start_time.setText("00:00:00") self.label_end_time.setText("00:00:00") # Update headers self.update_headers() # Enable controls self.enable_play_next_controls() def export_playlist_tab(self) -> None: """Export the current playlist to an m3u file""" if not self.visible_playlist_tab(): return playlist_id = self.visible_playlist_tab().playlist_id with Session() as session: # Get output filename playlist = session.get(Playlists, playlist_id) pathspec = QFileDialog.getSaveFileName( self, 'Save Playlist', directory=f"{playlist.name}.m3u", filter="M3U files (*.m3u);;All files (*.*)" ) if not pathspec: return path = pathspec[0] if not path.endswith(".m3u"): path += ".m3u" # Get list of track rows for this playlist plrs = PlaylistRows.get_rows_with_tracks(session, playlist_id) with open(path, "w") as f: # Required directive on first line f.write("#EXTM3U\n") for track in [a.track for a in plrs]: f.write( "#EXTINF:" f"{int(track.duration / 1000)}," f"{track.title} - " f"{track.artist}" "\n" f"{track.path}" "\n" ) def fade(self) -> None: """Fade currently playing track""" self.stop_playing(fade=True) def hide_played(self): """Toggle hide played tracks""" if self.hide_played_tracks: self.hide_played_tracks = False self.btnHidePlayed.setText("Hide played") else: self.hide_played_tracks = True self.btnHidePlayed.setText("Show played") # Update all displayed playlists with Session() as session: for i in range(self.tabPlaylist.count()): self.tabPlaylist.widget(i).update_display(session) # # def import_track(self) -> None: # """Import track file""" # # dlg = QFileDialog() # dlg.setFileMode(QFileDialog.ExistingFiles) # dlg.setViewMode(QFileDialog.Detail) # dlg.setDirectory(Config.IMPORT_DESTINATION) # dlg.setNameFilter("Music files (*.flac *.mp3)") # # if dlg.exec_(): # with Session() as session: # txt: str = "" # new_tracks = [] # for fname in dlg.selectedFiles(): # tags = helpers.get_tags(fname) # new_tracks.append((fname, tags)) # title = tags['title'] # artist = tags['artist'] # possible_matches = Tracks.search_titles(session, title) # if possible_matches: # txt += 'Similar to new track ' # txt += f'"{title}" by "{artist} ({fname})":\n\n' # for track in possible_matches: # txt += f' "{track.title}" by {track.artist}' # txt += f' ({track.path})\n' # txt += "\n" # # Check whether to proceed if there were potential matches # if txt: # txt += "Proceed with import?" # result = QMessageBox.question(self, # "Possible duplicates", # txt, # QMessageBox.Ok, # QMessageBox.Cancel # ) # if result == QMessageBox.Cancel: # return # # # Import in separate thread # thread = threading.Thread(target=self._import_tracks, # args=(new_tracks,)) # thread.start() # # def _import_tracks(self, tracks: list): # """ # Import passed files. Don't use parent session as that may be invalid # by the time we need it. # """ # # with Session() as session: # for (fname, tags) in tracks: # track = create_track_from_file(session, fname, tags=tags) # # Add to playlist on screen # # If we don't specify "repaint=False", playlist will # # also be saved to database # self.visible_playlist_tab().insert_track(session, track) def load_last_playlists(self) -> None: """Load the playlists that were open when the last session closed""" with Session() as session: for playlist in Playlists.get_open(session): self.create_playlist_tab(session, playlist) playlist.mark_open(session) def move_selected(self) -> None: """ Move selected rows to another playlist Actions required: - identify destination playlist - update playlist for the rows in the database - remove them from the display - update destination playlist display if loaded """ # Identify destination playlist with Session() as session: visible_tab = self.visible_playlist_tab() source_playlist = visible_tab.playlist_id playlists = [] for playlist in Playlists.get_all(session): if playlist.id == source_playlist: continue else: playlists.append(playlist) # Get destination playlist id dlg = SelectPlaylistDialog(self, playlists=playlists, session=session) dlg.exec() if not dlg.playlist: return destination_playlist = dlg.playlist # Update playlist for the rows in the database plr_ids = visible_tab.get_selected_playlistrow_ids() PlaylistRows.move_to_playlist( session, plr_ids, destination_playlist.id ) # Remove moved rows from display visible_tab.remove_selected_rows() # Update destination playlist_tab if visible (if not visible, it # will be re-populated when it is opened) destionation_playlist_tab = None for tab in range(self.tabPlaylist.count()): if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id: destionation_playlist_tab = self.tabPlaylist.widget(tab) break if destionation_playlist_tab: destionation_playlist_tab.populate(session, dlg.playlist.id) def play_next(self) -> None: """ Play next track. Actions required: - If there is no next track set, return. - If there's currently a track playing, fade it. - Move next track to current track. - Ensure playlist tabs are the correct colour - Restore volume if -3dB active - Play (new) current track. - Tell database to record it as played - Tell playlist track is now playing - Note that track is now playing - Disable play next controls - Update headers - Update clocks """ # If there is no next track set, return. if not self.next_track: log.debug("musicmuster.play_next(): no next track selected") return with Session() as session: # If there's currently a track playing, fade it. self.stop_playing(fade=True) # Move next track to current track. self.current_track = self.next_track self.next_track = None # Ensure playlist tabs are the correct colour # If current track on different playlist_tab to last, reset # last track playlist_tab colour if self.current_track_playlist_tab != self.next_track_playlist_tab: self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)) # # Update record of current track playlist_tab self.current_track_playlist_tab = self.next_track_playlist_tab self.next_track_playlist_tab = None # Set current track playlist_tab colour self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_CURRENT_TAB)) # Restore volume if -3dB active if self.btnDrop3db.isChecked(): self.btnDrop3db.setChecked(False) # Play (new) current track start_at = datetime.now() self.music.play(self.current_track.path) # Tell database to record it as played Playdates(session, self.current_track.id) # Tell playlist track is now playing self.current_track_playlist_tab.play_started(session) # Note that track is now playing self.playing = True # Disable play next controls self.disable_play_next_controls() # Update headers self.update_headers() # Update clocks self.label_track_length.setText( helpers.ms_to_mmss(self.current_track.duration) ) fade_at = self.current_track.fade_at silence_at = self.current_track.silence_at length = self.current_track.duration self.label_fade_length.setText( helpers.ms_to_mmss(silence_at - fade_at)) self.label_start_time.setText( start_at.strftime(Config.TRACK_TIME_FORMAT)) end_at = start_at + timedelta( milliseconds=self.current_track.duration) self.label_end_time.setText( end_at.strftime(Config.TRACK_TIME_FORMAT)) def insert_track(self) -> None: """Show dialog box to select and add track from database""" with Session() as session: dlg = DbDialog(self, session) dlg.exec() def search_playlist(self) -> None: """Show text box to search playlist""" # Disable play controls so that 'return' in search box doesn't # play next track self.disable_play_next_controls() self.txtSearch.setHidden(False) self.txtSearch.setFocus() # Select any text that may already be there self.txtSearch.selectAll() def search_playlist_clear(self) -> None: """Tidy up and reset search bar""" # Clear the search text self.visible_playlist_tab().search("") # Clean up search bar self.txtSearch.setText("") self.txtSearch.setHidden(True) def search_playlist_return(self) -> None: """Initiate search when return pressed""" self.visible_playlist_tab().set_search(self.txtSearch.text()) self.enable_play_next_controls() # def search_playlist_update(self): # """Update search when search string changes""" # self.visible_playlist_tab().set_filter(self.txtSearch.text()) def open_playlist(self): with Session() as session: playlists = Playlists.get_closed(session) dlg = SelectPlaylistDialog(self, playlists=playlists, session=session) dlg.exec() playlist = dlg.playlist if playlist: playlist.mark_open(session) self.create_playlist_tab(session, playlist) def select_next_row(self) -> None: """Select next or first row in playlist""" self.visible_playlist_tab().select_next_row() # # def select_played(self) -> None: # """Select all played tracks in playlist""" # # self.visible_playlist_tab().select_played_tracks() def select_previous_row(self) -> None: """Select previous or first row in playlist""" self.visible_playlist_tab().select_previous_row() # # def select_unplayed(self) -> None: # """Select all unplayed tracks in playlist""" # # self.visible_playlist_tab().select_unplayed_tracks() def set_main_window_size(self) -> None: """Set size of window from database""" with Session() as session: record = Settings.get_int_settings(session, "mainwindow_x") x = record.f_int or 1 record = Settings.get_int_settings(session, "mainwindow_y") y = record.f_int or 1 record = Settings.get_int_settings(session, "mainwindow_width") width = record.f_int or 1599 record = Settings.get_int_settings(session, "mainwindow_height") height = record.f_int or 981 self.setGeometry(x, y, width, height) record = Settings.get_int_settings(session, "splitter_top") splitter_top = record.f_int or 256 record = Settings.get_int_settings(session, "splitter_bottom") splitter_bottom = record.f_int or 256 self.splitter.setSizes([splitter_top, splitter_bottom]) return def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: """ Find the tab containing the widget and set the text colour """ idx = self.tabPlaylist.indexOf(widget) self.tabPlaylist.tabBar().setTabTextColor(idx, colour) def stop(self) -> None: """Stop playing immediately""" self.stop_playing(fade=False) def stop_playing(self, fade=True) -> None: """ Stop playing current track Actions required: - Return if not playing - Stop/fade track - Reset playlist_tab colour - Run end-of-track actions """ # Return if not playing if not self.playing: return # Stop/fade track self.previous_track_position = self.music.get_position() if fade: self.music.fade() else: self.music.stop() # Reset playlist_tab colour if self.current_track_playlist_tab == self.next_track_playlist_tab: self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_NEXT_TAB)) else: self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)) # Run end-of-track actions self.end_of_track_actions() def this_is_the_next_track(self, session: Session, playlist_tab: PlaylistTab, track: Tracks) -> None: """ This is notification from a playlist tab that it holds the next track to be played. Actions required: - Clear next track if on other tab - Reset tab colour if on other tab - Note next playlist tab - Set next playlist_tab tab colour - Note next track - Update headers - Populate ‘info’ tabs """ # Clear next track if on another tab if self.next_track_playlist_tab != playlist_tab: # We need to reset the ex-next-track playlist if self.next_track_playlist_tab: self.next_track_playlist_tab.clear_next(session) # Reset tab colour if on other tab if (self.next_track_playlist_tab != self.current_track_playlist_tab): self.set_tab_colour( self.next_track_playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)) # Note next playlist tab self.next_track_playlist_tab = playlist_tab # Set next playlist_tab tab colour if it isn't the # currently-playing tab if (self.next_track_playlist_tab != self.current_track_playlist_tab): self.set_tab_colour( self.next_track_playlist_tab, QColor(Config.COLOUR_NEXT_TAB)) # Note next track self.next_track = TrackData(track) # Update headers self.update_headers() # Populate 'info' tabs self.tabInfolist.open_tab(track.title) def tick(self) -> None: """ Carry out clock tick actions. The Time of Day clock is updated every tick (500ms). All other timers are updated every second. As the timer displays have a one-second resolution, updating every 500ms can result in some timers updating and then, 500ms later, other timers updating. That looks odd. Actions required: - Update TOD clock - If track is playing: update track clocks time and colours - Else: run stop_track """ # Update TOD clock self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT)) self.even_tick = not self.even_tick if not self.even_tick: return if not self.playing: return # If track is playing, update track clocks time and colours if self.music.player and self.music.player.is_playing(): playtime = self.music.get_playtime() time_to_fade = (self.current_track.fade_at - playtime) time_to_silence = ( self.current_track.silence_at - playtime) time_to_end = (self.current_track.duration - playtime) # Elapsed time self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) # Time to fade self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) # If silent in the next 5 seconds, put warning colour on # time to silence box and enable play controls if time_to_silence <= 5500: self.frame_silent.setStyleSheet( f"background: {Config.COLOUR_ENDING_TIMER}" ) self.enable_play_next_controls() # Set warning colour on time to silence box when fade starts elif time_to_fade <= 500: self.frame_silent.setStyleSheet( f"background: {Config.COLOUR_WARNING_TIMER}" ) # Five seconds before fade starts, set warning colour on # time to silence box and enable play controls elif time_to_fade <= 5500: self.frame_fade.setStyleSheet( f"background: {Config.COLOUR_WARNING_TIMER}" ) self.enable_play_next_controls() else: self.frame_silent.setStyleSheet("") self.frame_fade.setStyleSheet("") self.label_silent_timer.setText( helpers.ms_to_mmss(time_to_silence) ) # Time to end self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) else: if self.playing: self.stop_playing() def update_headers(self) -> None: """ Update last / current / next track headers """ try: self.hdrPreviousTrack.setText( f"{self.previous_track.title} - {self.previous_track.artist}") except AttributeError: self.hdrPreviousTrack.setText("") try: self.hdrCurrentTrack.setText( f"{self.current_track.title} - {self.current_track.artist}") except AttributeError: self.hdrCurrentTrack.setText("") try: self.hdrNextTrack.setText( f"{self.next_track.title} - {self.next_track.artist}" ) except AttributeError: self.hdrNextTrack.setText("") class DbDialog(QDialog): """Select track from database""" def __init__(self, parent: QMainWindow, session: Session) -> None: """Subclassed QDialog to manage track selection""" super().__init__(parent) self.session = session self.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.double_click) 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) record = Settings.get_int_settings(self.session, "dbdialog_width") width = record.f_int or 800 record = Settings.get_int_settings(self.session, "dbdialog_height") height = record.f_int or 600 self.resize(width, height) def __del__(self) -> None: """Save dialog size and position""" record = Settings.get_int_settings(self.session, "dbdialog_height") if record.f_int != self.height(): record.update(self.session, {'f_int': self.height()}) record = Settings.get_int_settings(self.session, "dbdialog_width") if record.f_int != self.width(): record.update(self.session, {'f_int': self.width()}) def add_selected(self) -> None: """Handle Add button""" if not self.ui.matchList.selectedItems(): return item = self.ui.matchList.currentItem() track = item.data(Qt.UserRole) self.add_track(track) def add_selected_and_close(self) -> None: """Handle Add and Close button""" self.add_selected() self.close() def add_track(self, track: Tracks) -> None: """Add passed track to playlist on screen""" self.parent().visible_playlist_tab().insert_track(self.session, track) # Commit session to get correct row numbers if more tracks added self.session.commit() # Select search text to make it easier for next search self.select_searchtext() def chars_typed(self, s: str) -> None: """Handle text typed in search box""" self.ui.matchList.clear() if len(s) > 1: if 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 = Playdates.last_played(self.session, track.id) t = QListWidgetItem() t.setText( f"{track.title} - {track.artist} " f"[{helpers.ms_to_mmss(track.duration)}] " f"({helpers.get_relative_date(last_played)})" ) t.setData(Qt.UserRole, track) self.ui.matchList.addItem(t) def double_click(self, entry: QListWidgetItem) -> None: """Add items that are double-clicked""" track = entry.data(Qt.UserRole) self.add_track(track) # Select search text to make it easier for next search self.select_searchtext() 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.UserRole) self.ui.dbPath.setText(track.path) 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()) class DownloadCSV(QDialog): def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_DateSelect() self.ui.setupUi(self) self.ui.dateTimeEdit.setDate(QDate.currentDate()) self.ui.dateTimeEdit.setTime(QTime(19, 59, 0)) self.ui.buttonBox.accepted.connect(self.accept) self.ui.buttonBox.rejected.connect(self.reject) class SelectPlaylistDialog(QDialog): def __init__(self, parent=None, playlists=None, session=None): super().__init__(parent) if playlists is None: return self.ui = Ui_dlgSelectPlaylist() self.ui.setupUi(self) self.ui.lstPlaylists.itemDoubleClicked.connect(self.list_doubleclick) self.ui.buttonBox.accepted.connect(self.open) self.ui.buttonBox.rejected.connect(self.close) self.session = session self.playlist = None self.plid = None record = Settings.get_int_settings( self.session, "select_playlist_dialog_width") width = record.f_int or 800 record = Settings.get_int_settings( self.session, "select_playlist_dialog_height") height = record.f_int or 600 self.resize(width, height) for playlist in playlists: p = QListWidgetItem() p.setText(playlist.name) p.setData(Qt.UserRole, playlist) self.ui.lstPlaylists.addItem(p) def __del__(self): # review record = Settings.get_int_settings( self.session, "select_playlist_dialog_height") if record.f_int != self.height(): record.update(self.session, {'f_int': self.height()}) record = Settings.get_int_settings( self.session, "select_playlist_dialog_width") if record.f_int != self.width(): record.update(self.session, {'f_int': self.width()}) def list_doubleclick(self, entry): # review self.playlist = entry.data(Qt.UserRole) self.accept() def open(self): # review if self.ui.lstPlaylists.selectedItems(): item = self.ui.lstPlaylists.currentItem() self.playlist = item.data(Qt.UserRole) self.accept() if __name__ == "__main__": # p = argparse.ArgumentParser() # # Only allow at most one option to be specified # group = p.add_mutually_exclusive_group() # group.add_argument('-u', '--update', # action="store_true", dest="update", # default=False, help="Update database") # # group.add_argument('-f', '--full-update', # # action="store_true", dest="full_update", # # default=False, help="Update database") # # group.add_argument('-i', '--import', dest="fname", help="Input # file") # args = p.parse_args() # # # Run as required # if args.update: # log.debug("Updating database") # with Session() as session: # update_db(session) # # elif args.full_update: # # log.debug("Full update of database") # # with Session() as session: # # full_update_db(session) # else: # # Normal run try: Base.metadata.create_all(engine) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec()) except Exception: msg = "Unhandled Exception caught by musicmuster.main()" log.exception(msg, exc_info=True, stack_info=True)