#!/usr/bin/env python from log import log # import argparse # import psutil import sys # import threading # import urllib.parse # import webbrowser # # # from datetime import datetime, timedelta # from typing import Callable, Dict, List, Optional, Tuple # # from PyQt5.QtCore import QDate, QEvent, QProcess, Qt, QTime, QTimer, QUrl from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor # from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWidgets import ( QApplication, QDialog, # QFileDialog, # QInputDialog, QLabel, # QLineEdit, QListWidgetItem, QMainWindow, # QMessageBox, ) # from dbconfig import engine, Session # import helpers # import music # # from config import Config 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 from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # from ui.downloadcsv_ui import Ui_DateSelect from config import Config from ui.main_window_ui import Ui_MainWindow # from utilities import create_track_from_file, update_db # # # log = logging.getLogger(Config.LOG_NAME) 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 InfoTab(QWebView): # """Subclass QWebView to show info about tracks""" # # def __init__(self, parent=None) -> None: # super().__init__(parent) # self.title = None 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.disable_play_next_controls() # # self.music: music.Music = music.Music() self.current_track: Optional[TrackData] = None self.current_track_playlist_tab: Optional[PlaylistTab] = None self.info_tabs: Optional[Dict[str, QWebView]] = {} 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.splitter.setStretchSizes[200,200] self.visible_playlist_tab: Callable[[], PlaylistTab] = \ self.tabPlaylist.currentWidget # self._load_last_playlists() # self.enable_play_next_controls() # self.check_audacity() # self.timer.start(Config.TIMER_MS) self.connect_signals_slots() # # 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) # return # # @staticmethod # def print_current_database(): # with Session() as session: # db = session.bind.engine.url.database # print(f"{db=}") # # @staticmethod # def check_audacity() -> None: # """Offer to run Audacity if not running""" # # if not Config.CHECK_AUDACITY_AT_STARTUP: # return # # if "audacity" in [i.name() for i in psutil.process_iter()]: # return # # if helpers.ask_yes_no("Audacity not running", "Start Audacity?"): # QProcess.startDetached(Config.AUDACITY_COMMAND, []) def clear_selection(self) -> None: """ Clear selected row""" if self.visible_playlist_tab(): self.visible_playlist_tab().clear_selection() # # def closeEvent(self, event: QEvent) -> None: # """Don't allow window to close when a track is playing""" # # if self.music.playing(): # log.debug("closeEvent() ignored as music is playing") # event.ignore() # helpers.show_warning( # "Track playing", # "Can't close application while track is playing") # else: # log.debug("closeEvent() accepted") # # 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()}) # # # Find a playlist tab (as opposed to an info tab) and # # save column widths # if self.current_track_playlist_tab: # self.current_track_playlist_tab.close() # elif self.next_track_playlist_tab: # self.next_track_playlist_tab.close() # # event.accept() def connect_signals_slots(self) -> None: # self.actionAdd_note.triggered.connect(self.create_note) 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.actionImport.triggered.connect(self.import_track) # self.actionFade.triggered.connect(self.fade) # 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.actionSearch_database.triggered.connect(self.search_database) # 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.actionSkip_next.triggered.connect(self.play_next) # self.actionStop.triggered.connect(self.stop) # # self.btnAddNote.clicked.connect(self.create_note) # # self.btnDatabase.clicked.connect(self.search_database) # self.btnDrop3db.clicked.connect(self.drop3db) # self.btnHidePlayed.clicked.connect(self.hide_played) # self.btnFade.clicked.connect(self.fade) # # self.btnPlay.clicked.connect(self.play_next) # # self.btnSetNext.clicked.connect( # # lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) # # self.btnSongInfo.clicked.connect(self.song_info_search) # self.btnStop.clicked.connect(self.stop) # self.tabPlaylist.tabCloseRequested.connect(self.close_tab) # self.txtSearch.returnPressed.connect(self.search_playlist_return) # self.txtSearch.textChanged.connect(self.search_playlist_update) # # self.timer.timeout.connect(self.tick) # # 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 close_playlist_tab(self) -> None: # """Close active playlist tab""" # # self.close_tab(self.tabPlaylist.currentIndex()) # # def close_tab(self, index: int) -> None: # """ # Close tab unless it holds the curren or next track # """ # # if hasattr(self.tabPlaylist.widget(index), 'playlist_id'): # if self.tabPlaylist.widget(index) == ( # self.current_track_playlist_tab): # self.statusbar.showMessage( # "Can't close current track playlist", 5000) # return # if self.tabPlaylist.widget(index) == self.next_track_playlist_tab: # self.statusbar.showMessage( # "Can't close next track playlist", 5000) # return # # It's OK to close this playlist so remove from open playlist list # self.tabPlaylist.widget(index).close() # # # Close regardless of tab type # self.tabPlaylist.removeTab(index) # # def create_note(self) -> None: # """Call playlist to create note""" # # try: # self.visible_playlist_tab().create_note() # except AttributeError: # # Just return if there's no visible playlist tab # return 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 = PlaylistTab( musicmuster=self, session=session, playlist_id=playlist.id) idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name) self.tabPlaylist.setCurrentIndex(idx) # # def disable_play_next_controls(self) -> None: # """ # Disable "play next" keyboard controls # """ # # log.debug("disable_play_next_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: Tuple[str, str] = QFileDialog.getSaveFileName( # self, 'Save CSV of tracks played', # directory="/tmp/playlist.csv", # filter="CSV files (*.csv)" # ) # if not pathspec: # return # # path: str = 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 # """ # # log.debug("enable_play_next_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 # # with Session() as session: # playlist = Playlists.get_by_id( # session, self.visible_playlist_tab().playlist_id) # # Get output filename # pathspec: Tuple[str, str] = QFileDialog.getSaveFileName( # self, 'Save Playlist', # directory=f"{playlist.name}.m3u", # filter="M3U files (*.m3u);;All files (*.*)" # ) # if not pathspec: # return # # path: str = pathspec[0] # if not path.endswith(".m3u"): # path += ".m3u" # # with open(path, "w") as f: # # Required directive on first line # f.write("#EXTM3U\n") # for _, track in playlist.tracks.items(): # 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""" # # log.debug("musicmuster:fade()", True) # # 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") # if self.current_track_playlist_tab: # with Session() as session: # self.current_track_playlist_tab.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) destination_visible_playlist_tab = None for tab in range(self.tabPlaylist.count()): # Non-playlist tabs won't have a 'playlist_id' attribute if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'): continue if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id: destination_visible_playlist_tab = ( self.tabPlaylist.widget(tab)) break if destination_visible_playlist_tab: destination_visible_playlist_tab.populate( session, dlg.playlist.id) def open_info_tabs(self) -> None: """ Ensure we have info tabs for next and current track titles """ title_list: List[str] = [] if self.previous_track: title_list.append(self.previous_track.title) if self.current_track: title_list.append(self.current_track.title) if self.next_track: title_list.append(self.next_track.title) for title in title_list: if title in self.info_tabs.keys(): # We already have a tab for this track continue if len(self.info_tabs) >= Config.MAX_log.info_TABS: # Find an unneeded info tab try: old_title = list( set(self.info_tabs.keys()) - set(title_list) )[0] except IndexError: log.debug( f"ensure_info_tabs({title_list}): unable to add " f"{title=}" ) return # Assign redundant widget a new title widget = self.info_tabs[title] = self.info_tabs[old_title] idx = self.tabPlaylist.indexOf(widget) self.tabPlaylist.setTabText( idx, title[:Config.log.info_TAB_TITLE_LENGTH]) del self.info_tabs[old_title] else: # Create a new tab for this title widget = self.info_tabs[title] = QWebView() widget.setZoomFactor(Config.WEB_ZOOM_FACTOR) self.tabPlaylist.addTab( widget, title[:Config.log.info_TAB_TITLE_LENGTH]) txt = urllib.parse.quote_plus(title) url = Config.log.info_TAB_URL % txt widget.setUrl(QUrl(url)) # # 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. # - Update record of current track playlist_tab # - If current track on different playlist_tab to last, reset # last track playlist_tab colour # - Set current track playlist_tab colour # - Restore volume if -3dB active # - Play (new) current track. # - Tell database to record it as played # - Tell playlist track is now playing # - Disable play next controls # - Update headers # - Update clocks # """ # # log.debug( # "musicmuster.play_next(), " # f"next_track={self.next_track.title if self.next_track else None} " # "current_track=" # f"{self.current_track.title if self.current_track else None}", # True # ) # # # If there is no next track set, return. # if not self.next_track: # log.debug("musicmuster.play_next(): no next track selected", True) # 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 # # # If current track on different playlist_tab to last, reset # # last track playlist_tab colour # # Set current 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) # # # Set last_played date # Tracks.update_lastplayed(session, self.current_track.id) # # # Tell playlist track is now playing # self.current_track_playlist_tab.play_started(session) # # # 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 search_database(self) -> None: # """Show dialog box to select and cue track from database""" # # with Session() as session: # dlg = DbDialog(self, session) # dlg.exec() # # def search_playlist(self): # """Show text box to search playlist""" # # self.disable_play_next_controls() # self.txtSearch.setHidden(False) # self.txtSearch.setFocus() # # def search_playlist_return(self): # """Close off search box when return pressed""" # # self.txtSearch.setText("") # self.txtSearch.setHidden(True) # self.enable_play_next_controls() # self.visible_playlist_tab().set_filter("") # # 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_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 song_info_search(self) -> None: # """ # Open browser tab for Wikipedia, searching for # the first that exists of: # - selected track # - next track # - current track # """ # # title: Optional[str] = self.visible_playlist_tab().get_selected_title() # if not title: # if self.next_track: # title = self.next_track.title # if not title: # if self.current_track: # title = self.current_track.title # if title: # txt = urllib.parse.quote_plus(title) # url = Config.log.info_TAB_URL % txt # webbrowser.open(url, new=2) # # def stop(self) -> None: # """Stop playing immediately""" # # log.debug("musicmuster.stop()") # # 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 # """ # # log.debug(f"musicmuster.stop_playing({fade=})", True) # # # Return if not playing # if not self.playing: # log.debug("musicmuster.stop_playing(): not playing", True) # return # # # Stop/fade track # self.previous_track_position = self.music.get_position() # if fade: # log.debug("musicmuster.stop_playing(): fading music", True) # self.music.fade() # else: # log.debug("musicmuster.stop_playing(): stopping music", True) # 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, playlist_tab: PlaylistTab, track: Tracks, session) -> 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.open_info_tabs() # # 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 timers 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 track is playing, update track clocks time and colours # if self.music.player and self.music.playing(): # self.playing = True # playtime: int = self.music.get_playtime() # time_to_fade: int = (self.current_track.fade_at - playtime) # time_to_silence: int = ( # self.current_track.silence_at - playtime) # time_to_end: int = (self.current_track.duration - playtime) # # # Elapsed time # if time_to_end < 500: # self.label_elapsed_timer.setText( # helpers.ms_to_mmss(playtime) # ) # else: # 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, DetachedInstanceError): self.hdrPreviousTrack.setText("") try: self.hdrCurrentTrack.setText( f"{self.current_track.title} - {self.current_track.artist}" ) except (AttributeError, DetachedInstanceError): self.hdrCurrentTrack.setText("") try: self.hdrNextTrack.setText( f"{self.next_track.title} - {self.next_track.artist}" ) except (AttributeError, DetachedInstanceError): self.hdrNextTrack.setText("") # # # class DbDialog(QDialog): # """Select track from database""" # # def __init__(self, parent, session): # review # 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): # review # 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): # review # 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): # review # self.add_selected() # self.close() # # def title_artist_toggle(self): # review # """ # 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()) # # def chars_typed(self, s): # review # if len(s) > 0: # if self.ui.radioTitle.isChecked(): # matches = Tracks.search_titles(self.session, s) # else: # matches = Tracks.search_artists(self.session, s) # self.ui.matchList.clear() # if matches: # for track in matches: # t = QListWidgetItem() # t.setText( # f"{track.title} - {track.artist} " # f"[{helpers.ms_to_mmss(track.duration)}] " # f"({helpers.get_relative_date(track.lastplayed)})" # ) # t.setData(Qt.UserRole, track) # self.ui.matchList.addItem(t) # # def double_click(self, entry): # review # track = entry.data(Qt.UserRole) # self.add_track(track) # # Select search text to make it easier for next search # self.select_searchtext() # # def add_track(self, track): # review # # Add 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 select_searchtext(self): # review # self.ui.searchString.selectAll() # self.ui.searchString.setFocus() # # def selection_changed(self): # review # if not self.ui.matchList.selectedItems(): # return # # item = self.ui.matchList.currentItem() # track = item.data(Qt.UserRole) # self.ui.dbPath.setText(track.path) # # # 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)