#!/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.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, 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 # self.lastplayed = track.lastplayed 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[Tracks] = None # self.current_track_playlist_tab: Optional[PlaylistTab] = None # self.info_tabs: Optional[Dict[str, QWebView]] = {} # self.next_track: Optional[Tracks] = None # self.next_track_playlist_tab: Optional[PlaylistTab] = None # self.previous_track: Optional[Tracks] = None # self.previous_track_position: Optional[int] = None # # self.set_main_window_size() # self.lblSumPlaytime: QLabel = 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.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 kae(): # with Session() as session: # db = session.bind.engine.url.database # print(f"kae(): {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): # """ Clear selected row""" # # if self.visible_playlist_tab(): # self.visible_playlist_tab().clearSelection() # # 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): """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""" # # with Session() as session: # visible_tab = self.visible_playlist_tab() # visible_tab_id = visible_tab.playlist_id # # source_playlist = None # playlists = [] # for playlist in Playlists.get_all(session): # if playlist.id == visible_tab_id: # source_playlist = playlist # 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 # # self.visible_playlist_tab().move_selected_to_playlist( # session, destination_playlist.id) # # # 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: # # We need to commit to database for populate to work # session.commit() # 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, colour) -> None: # """ # Find the tab containing the widget and set the text colour # """ # # idx: int = 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 # # If multiple tracks are played quickly in succession, it's possible # for the self.{previous,current,next} track to not be in the session. # Unlikely to happen in normal use so handle by blanking title. # """ # # 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)