musicmuster/app/musicmuster.py
2022-03-02 09:24:35 +00:00

892 lines
31 KiB
Python
Executable File

#!/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 QProcess, Qt, QTimer, QUrl
from PyQt5.QtGui import QColor
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 models import (db_init, Playdates, Playlists, PlaylistTracks,
Session, Settings, Tracks)
from playlists import PlaylistTab
from utilities 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 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.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):
# TODO: V2 enahancement to import tracks
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)
@staticmethod
def check_audacity():
"""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):
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.create_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(
lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
self.actionSkip_next.triggered.connect(self.play_next)
self.actionStop.triggered.connect(self.stop)
self.btnAddFile.clicked.connect(self.add_file)
self.btnAddNote.clicked.connect(self.create_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.this_is_the_next_track)
self.btnSongInfo.clicked.connect(self.song_info_search)
self.btnStop.clicked.connect(self.stop)
self.spnVolume.valueChanged.connect(self.change_volume)
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
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 = Playlists(session, dlg.textValue())
self.create_playlist_tab(session, playlist)
def change_volume(self, volume):
"""Change player maximum volume"""
DEBUG(f"change_volume({volume})")
self.music.set_volume(volume)
def close_playlist_tab(self):
self.close_tab(self.tabPlaylist.currentIndex())
def close_tab(self, index):
if hasattr(self.tabPlaylist.widget(index), 'playlist'):
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
with Session() as session:
self.tabPlaylist.widget(index).playlist.close(session)
# Close regardless of tab type
self.tabPlaylist.removeTab(index)
def create_note(self):
"""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 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
self.current_track = None
if self.current_track_playlist_tab:
self.current_track_playlist_tab.play_stopped()
self.current_track_playlist_tab = 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 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"
with open(path, "w") as f:
# Required directive on first line
f.write("#EXTM3U\n")
for track in self.playlist.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.get_position()
self.music.fade()
self.end_of_track_actions()
def insert_note(self):
#TODO: V2 check
"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 in Playlists.get_open(session):
self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session, playlist):
"""
Take the passed playlist database object, create a playlist tab and
add tab to display.
"""
playlist_tab = PlaylistTab(parent=self,
session=session, playlist=playlist)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx)
def move_selected(self):
"""Move selected rows to another playlist"""
with Session() as session:
playlists = [p for p in Playlists.get_all(session)
if p.id != self.visible_playlist_tab().id]
dlg = SelectPlaylistDialog(self, playlists=playlists)
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()):
# Non-playlist tabs won't have a 'playlist' attribute
if not hasattr(self.tabPlaylist.widget(tab), 'playlist'):
continue
if self.tabPlaylist.widget(tab).id == dlg.plid:
destination_visible_playlist_tab = (
self.tabPlaylist.widget(tab))
break
rows = []
for (row, track) in (
self.visible_playlist_tab().get_selected_rows_and_tracks()
):
rows.append(row)
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 for both source and destination playlists
PlaylistTracks.move_rows(
session, self.visible_playlist_tab().id, rows, dlg.plid)
# Update destination playlist if visible
if destination_visible_playlist_tab:
destination_visible_playlist_tab.update_display()
# Update source playlist
self.visible_playlist_tab().remove_rows(rows)
def open_info_tab(self, title_list):
"""
Ensure we have info tabs for each of the passed titles
Called from update_headers
"""
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:
# Create a new tab for this title
widget = self.info_tabs[title] = QWebView()
self.tabPlaylist.addTab(
widget, title[:Config.INFO_TAB_TITLE_LENGTH])
txt = urllib.parse.quote_plus(title)
url = Config.INFO_TAB_URL % txt
widget.setUrl(QUrl(url))
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.
Tell playlist to update "current track" metadata. This will also
trigger a call to
Cue up next track in playlist if there is one.
Tell database to record it as played
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.next_track = None
self.current_track_playlist_tab = self.next_track_playlist_tab
self.next_track_playlist_tab = None
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_CURRENT_TAB))
self.music.play(self.current_track.path)
# Tell database to record it as played
Playdates(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:
playlists = Playlists.get_closed(session)
dlg = SelectPlaylistDialog(self, playlists=playlists)
dlg.exec()
if dlg.plid:
playlist = Playlists.get_by_id(session, dlg.plid)
self.create_playlist_tab(session, playlist)
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 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 tab 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
txt = urllib.parse.quote_plus(title)
url = Config.INFO_TAB_URL % txt
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)
# Set 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))
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 this_is_the_next_track(self, playlist_tab, track):
"""
This is notification from a playlist tab that it holds the next
track to be played.
"""
# The next track has been selected on the playlist_tab
# 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 != 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 = playlist_tab
# self.next_track_playlist_tab is now set to correct playlist
# Set the colour of the next playlist tab 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))
self.next_track = track
self.update_headers()
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.open_info_tab(titles)
class DbDialog(QDialog):
"""Select track from database"""
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.title_artist_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 = item.data(Qt.UserRole)
self.add_track(track)
def add_selected_and_close(self):
self.add_selected()
self.close()
def title_artist_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)
self.ui.matchList.addItem(t)
def double_click(self, entry):
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):
# Add to playlist on screen
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 = item.data(Qt.UserRole)
self.ui.dbPath.setText(track.path)
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlists=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.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 playlists]:
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)
db_init()
win = Window()
win.show()
sys.exit(app.exec())
except Exception:
EXCEPTION("Unhandled Exception caught by musicmuster.main()")
if __name__ == "__main__":
main()