#!/usr/bin/env python import webbrowser import os import psutil import sys import urllib.parse from datetime import datetime, timedelta from log import DEBUG, EXCEPTION from PyQt5.QtCore import Qt, QTimer, QUrl from PyQt5.QtGui import QColor, QFontMetrics, QPainter from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWidgets import ( QApplication, QDialog, QFileDialog, QInputDialog, QLabel, QListWidgetItem, QMainWindow, ) import helpers import music from config import Config from model import (Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks) from playlists import PlaylistTab from songdb import create_track_from_file from ui.dlg_search_database_ui import Ui_Dialog from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist from ui.main_window_ui import Ui_MainWindow class ElideLabel(QLabel): """ From https://stackoverflow.com/questions/11446478/ pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize """ def paintEvent(self, event): painter = QPainter(self) metrics = QFontMetrics(self.font()) elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) painter.drawText(self.rect(), self.alignment(), elided) class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.timer = QTimer() self.even_tick = True self.playing = False self.connect_signals_slots() self.disable_play_next_controls() self.music = music.Music() self.current_track = None self.current_track_playlist_tab = None self.info_tabs = {} self.next_track = None self.next_track_playlist_tab = None self.previous_track = None self.previous_track_position = None self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT) self.menuTest.menuAction().setVisible(Config.TESTMODE) self.set_main_window_size() self.lblSumPlaytime = QLabel("") self.statusbar.addPermanentWidget(self.lblSumPlaytime) self.visible_playlist_tab = self.tabPlaylist.currentWidget self.load_last_playlists() self.enable_play_next_controls() self.check_audacity() self.timer.start(Config.TIMER_MS) def add_file(self): dlg = QFileDialog() dlg.setFileMode(QFileDialog.ExistingFiles) dlg.setViewMode(QFileDialog.Detail) dlg.setDirectory(Config.ROOT) dlg.setNameFilter("Music files (*.flac *.mp3)") if dlg.exec_(): with Session() as session: for fname in dlg.selectedFiles(): track = create_track_from_file(session, fname) # 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 set_main_window_size(self): "Set size of window from database" with Session() as session: record = Settings.get_int(session, "mainwindow_x") x = record.f_int or 1 record = Settings.get_int(session, "mainwindow_y") y = record.f_int or 1 record = Settings.get_int(session, "mainwindow_width") width = record.f_int or 1599 record = Settings.get_int(session, "mainwindow_height") height = record.f_int or 981 self.setGeometry(x, y, width, height) def check_audacity(self): "Warn user if Audacity not running" if "audacity" in [i.name() for i in psutil.process_iter()]: return helpers.show_warning("Audacity check", "Audacity is not running") def clear_selection(self): if self.visible_playlist_tab(): self.visible_playlist_tab().clearSelection() def closeEvent(self, event): "Don't allow window to close when a track is playing" if self.music.playing(): DEBUG("closeEvent() ignored as music is playing") event.ignore() helpers.show_warning( "Track playing", "Can't close application while track is playing") else: DEBUG("closeEvent() accepted") with Session() as session: record = Settings.get_int(session, "mainwindow_height") if record.f_int != self.height(): record.update(session, {'f_int': self.height()}) record = Settings.get_int(session, "mainwindow_width") if record.f_int != self.width(): record.update(session, {'f_int': self.width()}) record = Settings.get_int(session, "mainwindow_x") if record.f_int != self.x(): record.update(session, {'f_int': self.x()}) record = Settings.get_int(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(session) elif self.next_track_playlist_tab: self.next_track_playlist_tab.close(session) event.accept() def connect_signals_slots(self): self.actionAdd_file.triggered.connect(self.add_file) self.actionAdd_note.triggered.connect(self.insert_note) self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionExport_playlist.triggered.connect(self.export_playlist_tab) 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_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(self.set_next_track) self.actionSkip_next.triggered.connect(self.play_next) self.actionSkipToEnd.triggered.connect(self.test_skip_to_end) self.actionSkipToFade.triggered.connect(self.test_skip_to_fade) self.actionStop.triggered.connect(self.stop) self.actionTestFunction.triggered.connect(self.test_function) self.btnAddFile.clicked.connect(self.add_file) self.btnAddNote.clicked.connect(self.insert_note) self.btnDatabase.clicked.connect(self.search_database) self.btnFade.clicked.connect(self.fade) self.btnPlay.clicked.connect(self.play_next) self.btnSetNext.clicked.connect(self.set_next_track) self.btnSongInfo.clicked.connect(self.song_info_search) self.btnStop.clicked.connect(self.stop) self.spnVolume.valueChanged.connect(self.change_volume) self.timer.timeout.connect(self.tick) def create_playlist(self): "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_db = Playlists.new(session, dlg.textValue()) self.load_playlist(session, playlist_db) def change_volume(self, volume): "Change player maximum volume" DEBUG(f"change_volume({volume})") self.music.set_volume(volume) def close_playlist_tab(self): with Session() as session: playlist_db = session.query(Playlists).filter( Playlists.id == self.visible_playlist_tab().id).one() playlist_db.close(session) index = self.tabPlaylist.currentIndex() self.tabPlaylist.removeTab(index) def create_note(self, session, text): """ Create note If a row is selected, set note row to be rows above. Otherwise, set note row to be end of playlist. Return note. """ row = self.visible_playlist_tab().get_selected_row() if row is None: row = self.visible_playlist_tab().rowCount() DEBUG(f"musicmuster.create_note(text={text}): row={row}") note = Notes.add_note( session, self.visible_playlist_tab().id, row, text) return note def disable_play_next_controls(self): DEBUG("disable_play_next_controls()") self.actionPlay_next.setEnabled(False) self.statusbar.showMessage("Play controls: Disabled", 0) def enable_play_next_controls(self): DEBUG("enable_play_next_controls()") self.actionPlay_next.setEnabled(True) self.statusbar.showMessage("Play controls: Enabled", 0) def end_of_track_actions(self): "Clean up after track played" # Set self.playing to False so that tick() doesn't see # player=None and kick off end-of-track actions self.playing = False # Clean up metadata if self.current_track: self.previous_track = self.current_track if self.current_track_playlist_tab: self.current_track_playlist_tab.play_stopped() self.current_track_playlist_tab.clear_current() self.current_track_playlist_tab = None self.current_track = None # Clean up display self.frame_fade.setStyleSheet("") self.label_silent_timer.setText("00:00") self.frame_silent.setStyleSheet("") self.label_end_timer.setText("00:00") self.update_headers() # Enable controls self.enable_play_next_controls() def ensure_info_tabs(self, title_list): """ Ensure we have info tabs for each of the passed titles """ 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_INFO_TABS: # Find an unneeded info tab try: old_title = list( set(self.info_tabs.keys()) - set(title_list) )[0] except IndexError: 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.INFO_TAB_TITLE_LENGTH]) del self.info_tabs[old_title] else: widget = self.info_tabs[title] = QWebView() self.tabPlaylist.addTab( widget, title[:Config.INFO_TAB_TITLE_LENGTH]) str = urllib.parse.quote_plus(title) url = f"https://www.wikipedia.org/w/index.php?search={str}" widget.setUrl(QUrl(url)) def export_playlist_tab(self): "Export the current playlist to an m3u file" if not self.visible_playlist_tab(): return # Get output filename pathspec = QFileDialog.getSaveFileName( self, 'Save Playlist', directory=f"{self.visible_playlist_tab().name}.m3u", filter="M3U files (*.m3u);;All files (*.*)" ) if not pathspec: return path = pathspec[0] if not path.endswith(".m3u"): path += ".m3u" # Get playlist db object with Session() as session: playlist_db = Playlists.get_playlist( session, self.visible_playlist_tab().id) with open(path, "w") as f: # Required directive on first line f.write("#EXTM3U\n") for track in playlist_db.get_tracks(): f.write( "#EXTINF:" f"{int(track.duration / 1000)}," f"{track.title} - " f"{track.artist}" "\n" f"{track.path}" "\n" ) def fade(self): "Fade currently playing track" DEBUG("musicmuster:fade()", True) if not self.current_track: return self.previous_track_position = self.music.fade() self.end_of_track_actions() def file_is_readable(self, path): "Return True if path is readable else False" return os.access(path, os.R_OK) def insert_note(self): "Add non-track row to playlist" dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) dlg.setLabelText("Note:") dlg.resize(500, 100) ok = dlg.exec() if ok: with Session() as session: note = self.create_note(session, dlg.textValue()) self.visible_playlist_tab().insert_note(session, note) def load_last_playlists(self): "Load the playlists that we loaded at end of last session" with Session() as session: for playlist_db in Playlists.get_last_used(session): self.load_playlist(session, playlist_db) def load_playlist(self, session, playlist_db): """ Take the passed database object, create a playlist display, attach the database object, get it populated and then add tab. """ playlist_tab = PlaylistTab(self) playlist_tab.populate(session, playlist_db) idx = self.tabPlaylist.addTab(playlist_tab, playlist_db.name) self.tabPlaylist.setCurrentIndex(idx) def move_selected(self): "Move selected rows to another playlist" with Session() as session: playlist_dbs = [p for p in Playlists.get_all_playlists(session) if p.id != self.visible_playlist_tab().id] dlg = SelectPlaylistDialog(self, playlist_dbs=playlist_dbs) dlg.exec() if not dlg.plid: return # If destination playlist is visible, we need to add the moved # tracks to it. If not, they will be automatically loaded when # the playlistis opened. destination_visible_playlist_tab = None for tab in range(self.tabPlaylist.count()): if self.tabPlaylist.widget(tab).id == dlg.plid: destination_visible_playlist_tab = ( self.tabPlaylist.widget(tab)) break rows = [] for (row, track_id) in ( self.visible_playlist_tab().get_selected_rows_and_tracks() ): rows.append(row) track = Tracks.track_from_id(session, track_id) if destination_visible_playlist_tab: # Insert with repaint=False to not update database destination_visible_playlist_tab.insert_track( session, track, repaint=False) # Update database PlaylistTracks.move_track( session, self.visible_playlist_tab().id, row, dlg.plid) # Update destination playlist if visible if destination_visible_playlist_tab: destination_visible_playlist_tab.repaint() # Update source playlist self.visible_playlist_tab().remove_rows(rows) def play_next(self): """ Play next track. If there is no next track set, return. If there's currently a track playing, fade it. Move next track to current track. Play (new) current. Update playlist "current track" metadata Cue up next track in playlist if there is one. Tell database to record it as played Remember it was played for this session Update metadata and headers, and repaint """ 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: DEBUG("musicmuster.play_next(): no next track selected", True) return with Session() as session: # Stop current track, if any self.stop_playing() # Play next track self.current_track = self.next_track self.current_track_playlist_tab = self.next_track_playlist_tab self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_CURRENT_TAB)) self.next_track = None self.next_track_playlist_tab = None DEBUG( "musicmuster.play_next: calling music.play(" f"{self.current_track.path=})" ) self.music.play(self.current_track.path) # Update metadata # Get next track for this playlist. May be None if there is # no automatic next track, and may later be overriden by # user selecting a different track on this or another # playlist. next_track_id = self.current_track_playlist_tab.play_started() if next_track_id is not None: self.next_track = Tracks.get_track(session, next_track_id) self.next_track_playlist_tab = self.current_track_playlist_tab else: self.next_track = self.next_track_playlist_tab = None if self.next_track_playlist_tab and ( self.current_track_playlist_tab != self.next_track_playlist_tab): self.set_tab_colour(self.next_track_playlist_tab, QColor(Config.COLOUR_NEXT_TAB)) # Tell database to record it as played Playdates.add_playdate(session, self.current_track) self.disable_play_next_controls() self.update_headers() # Set time clocks now = datetime.now() self.label_start_tod.setText(now.strftime("%H:%M:%S")) silence_at = self.current_track.silence_at silence_time = now + timedelta(milliseconds=silence_at) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) self.label_fade_length.setText(helpers.ms_to_mmss( silence_at - self.current_track.fade_at )) def search_database(self): with Session() as session: dlg = DbDialog(self, session) dlg.exec() def open_playlist(self): with Session() as session: playlist_dbs = Playlists.get_all_closed_playlists(session) dlg = SelectPlaylistDialog(self, playlist_dbs=playlist_dbs) dlg.exec() if dlg.plid: playlist_db = Playlists.open(session, dlg.plid) self.load_playlist(session, playlist_db) def select_next_row(self): "Select next or first row in playlist" self.visible_playlist_tab().select_next_row() def select_played(self): "Select all played tracks in playlist" self.visible_playlist_tab().select_played_tracks() def select_previous_row(self): "Select previous or first row in playlist" self.visible_playlist_tab().select_previous_row() def set_next_track(self, next_track_id=None): "Set selected track as next" with Session() as session: if not next_track_id: next_track_id = ( self.visible_playlist_tab().set_selected_as_next()) if not next_track_id: return # The next track has been selected on the currently-visible # playlist. However, there may already be a 'next track' # selected on another playlist that the user is overriding, # in which case we need to reset that playlist. if self.next_track_playlist_tab != self.visible_playlist_tab(): # We need to reset the ex-next-track playlist if self.next_track_playlist_tab: self.next_track_playlist_tab.clear_next() # Reset tab colour if it NOT the current 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_NORMAL_TAB)) self.next_track_playlist_tab = self.visible_playlist_tab() # self.next_track_playlist_tab is now set to correct # playlist 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)) self.next_track = Tracks.get_track(session, next_track_id) self.update_headers() def select_unplayed(self): "Select all unplayed tracks in playlist" self.visible_playlist_tab().select_unplayed_tracks() def set_tab_colour(self, widget, colour): """ 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): """ Open browser tabs for Wikipedia, searching for the first that exists of: - selected track - next track - current track """ title = 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: # Wikipedia str = urllib.parse.quote_plus(title) url = f"https://www.wikipedia.org/w/index.php?search={str}" webbrowser.open(url, new=2) def stop(self): "Stop playing immediately" DEBUG("musicmuster.stop()") self.stop_playing(fade=False) def stop_playing(self, fade=True): "Stop playing current track" DEBUG(f"musicmuster.stop_playing({fade=})", True) 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)) if not self.music.playing(): DEBUG("musicmuster.stop_playing(): not playing", True) self.end_of_track_actions() return self.previous_track_position = self.music.get_position() if fade: DEBUG("musicmuster.stop_playing(): fading music", True) self.music.fade() else: DEBUG("musicmuster.stop_playing(): stopping music", True) self.music.stop() self.end_of_track_actions() # Release player self.music.stop() self.update_headers() def test_function(self): "Placeholder for test function" import ipdb ipdb.set_trace() def test_skip_to_end(self): "Skip current track to 1 second before silence" if not self.playing(): return self.music.set_position(self.current_track.silence_at - 1000) def test_skip_to_fade(self): "Skip current track to 1 second before fade" if not self.music.playing(): return self.music.set_position(self.current_track.fade_at - 1000) def tick(self): """ Update screen 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. """ now = datetime.now() self.lblTOD.setText(now.strftime("%H:%M:%S")) self.even_tick = not self.even_tick if not self.even_tick: return if self.music.player and self.music.playing(): self.playing = True 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 if time_to_end < 500: self.label_elapsed_timer.setText( helpers.ms_to_mmss(self.current_track.duration) ) 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): """ Update last / current / next track headers. Ensure a Wikipedia tab for each title. """ titles = [] try: self.hdrPreviousTrack.setText( f"{self.previous_track.title} - {self.previous_track.artist}" ) titles.append(self.previous_track.title) except AttributeError: self.hdrPreviousTrack.setText("") try: self.hdrCurrentTrack.setText( f"{self.current_track.title} - {self.current_track.artist}" ) titles.append(self.current_track.title) except AttributeError: self.hdrCurrentTrack.setText("") try: self.hdrNextTrack.setText( f"{self.next_track.title} - {self.next_track.artist}" ) titles.append(self.next_track.title) except AttributeError: self.hdrNextTrack.setText("") self.ensure_info_tabs(titles) class DbDialog(QDialog): def __init__(self, parent, session): 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.radio_toggle) self.ui.searchString.textEdited.connect(self.chars_typed) record = Settings.get_int(self.session, "dbdialog_width") width = record.f_int or 800 record = Settings.get_int(self.session, "dbdialog_height") height = record.f_int or 600 self.resize(width, height) def __del__(self): record = Settings.get_int(self.session, "dbdialog_height") if record.f_int != self.height(): record.update(self.session, {'f_int': self.height()}) record = Settings.get_int(self.session, "dbdialog_width") if record.f_int != self.width(): record.update(self.session, {'f_int': self.width()}) def add_selected(self): if not self.ui.matchList.selectedItems(): return item = self.ui.matchList.currentItem() track_id = item.data(Qt.UserRole) self.add_track(track_id) def add_selected_and_close(self): self.add_selected() self.close() def radio_toggle(self): """ 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): 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)}]" ) t.setData(Qt.UserRole, track.id) self.ui.matchList.addItem(t) def double_click(self, entry): track_id = entry.data(Qt.UserRole) self.add_track(track_id) # Select search text to make it easier for next search self.select_searchtext() def add_track(self, track_id): track = Tracks.track_from_id(self.session, track_id) # Add to playlist on screen # If we don't specify "repaint=False", playlist will # also be saved to database self.parent().visible_playlist_tab().insert_track( self.session, track) # Select search text to make it easier for next search self.select_searchtext() def select_searchtext(self): self.ui.searchString.selectAll() self.ui.searchString.setFocus() def selection_changed(self): if not self.ui.matchList.selectedItems(): return item = self.ui.matchList.currentItem() track_id = item.data(Qt.UserRole) self.ui.dbPath.setText(Tracks.get_path(self.session, track_id)) class SelectPlaylistDialog(QDialog): def __init__(self, parent=None, playlist_dbs=None): super().__init__(parent) if playlist_dbs 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.plid = None with Session() as session: record = Settings.get_int(session, "select_playlist_dialog_width") width = record.f_int or 800 record = Settings.get_int(session, "select_playlist_dialog_height") height = record.f_int or 600 self.resize(width, height) for (plid, plname) in [(a.id, a.name) for a in playlist_dbs]: p = QListWidgetItem() p.setText(plname) p.setData(Qt.UserRole, plid) self.ui.lstPlaylists.addItem(p) def __del__(self): with Session() as session: record = Settings.get_int(session, "select_playlist_dialog_height") if record.f_int != self.height(): record.update(session, {'f_int': self.height()}) record = Settings.get_int(session, "select_playlist_dialog_width") if record.f_int != self.width(): record.update(session, {'f_int': self.width()}) def list_doubleclick(self, entry): self.plid = entry.data(Qt.UserRole) self.accept() def open(self): if self.ui.lstPlaylists.selectedItems(): item = self.ui.lstPlaylists.currentItem() self.plid = item.data(Qt.UserRole) self.accept() def main(): try: app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec()) except Exception: EXCEPTION("Unhandled Exception caught by musicmuster.main()") if __name__ == "__main__": main()