diff options
-rw-r--r-- | clMonty.py | 560 | ||||
-rw-r--r-- | clPlugin.py | 414 | ||||
-rw-r--r-- | clSettings.py | 108 | ||||
-rw-r--r-- | clSong.py | 116 | ||||
-rw-r--r-- | format.py | 200 | ||||
-rw-r--r-- | jmpc.py | 154 | ||||
-rw-r--r-- | jmpd.py | 386 | ||||
-rw-r--r-- | log.py | 36 | ||||
-rw-r--r-- | misc.py | 274 | ||||
-rwxr-xr-x | montypc.py | 12 | ||||
-rw-r--r-- | mpd.py | 8 | ||||
-rw-r--r-- | plugins/AlbumCover.py | 462 | ||||
-rw-r--r-- | plugins/Library.py | 120 | ||||
-rw-r--r-- | plugins/Logger.py | 110 | ||||
-rw-r--r-- | plugins/Lyrics.py | 494 | ||||
-rw-r--r-- | plugins/MPD.py | 34 | ||||
-rw-r--r-- | plugins/Notify.py | 288 | ||||
-rw-r--r-- | plugins/PlayControl.py | 682 | ||||
-rw-r--r-- | plugins/Playlist.py | 136 | ||||
-rw-r--r-- | plugins/Scrobbler.py | 698 | ||||
-rw-r--r-- | plugins/Shortcuts.py | 150 | ||||
-rw-r--r-- | plugins/SongStatus.py | 136 | ||||
-rw-r--r-- | plugins/Systray.py | 202 | ||||
-rw-r--r-- | plugins/Tabs.py | 276 | ||||
-rw-r--r-- | plugins/__init__.py | 112 | ||||
-rw-r--r-- | wgPlaylist.py | 242 | ||||
-rw-r--r-- | wgSongList.py | 1860 | ||||
-rw-r--r-- | winConnect.py | 134 | ||||
-rw-r--r-- | winMain.py | 496 | ||||
-rw-r--r-- | winSettings.py | 182 |
30 files changed, 4541 insertions, 4541 deletions
@@ -8,314 +8,314 @@ import mpd from threading import Thread class Monty(QtCore.QObject): - """The Monty class offers another layer above pympd, with usefull events.""" - _client=None # MPD client - _listeners=None # array of listeners: { event: (listeners)* } + """The Monty class offers another layer above pympd, with usefull events.""" + _client=None # MPD client + _listeners=None # array of listeners: { event: (listeners)* } - # cached objects - _curLib=None - _curPlaylist=None - _curSong=None + # cached objects + _curLib=None + _curPlaylist=None + _curSong=None - # objects used for comparison with previous value - _curSongID=None - _curTime=None - _curState=None - _curVolume=None - _updatings_db=None + # objects used for comparison with previous value + _curSongID=None + _curTime=None + _curState=None + _curVolume=None + _updatings_db=None - _timerID=None + _timerID=None - events={ - 'beforeSongChange':'curSongID', - 'onSongChange':'oldSongID, newSongID', - 'onTimeChange':'oldTime, newTime', - 'onStateChange':'oldState, newState', - 'onVolumeChange':'oldVolume, newVolume', - 'onConnect':'', - 'onDisconnect':'', - 'onReady':'', # when connected, and initialisation is ready - 'onUpdateDBStart':'', # start updating database - 'onUpdateDBFinish':'', # when updating database has finished - } + events={ + 'beforeSongChange':'curSongID', + 'onSongChange':'oldSongID, newSongID', + 'onTimeChange':'oldTime, newTime', + 'onStateChange':'oldState, newState', + 'onVolumeChange':'oldVolume, newVolume', + 'onConnect':'', + 'onDisconnect':'', + 'onReady':'', # when connected, and initialisation is ready + 'onUpdateDBStart':'', # start updating database + 'onUpdateDBFinish':'', # when updating database has finished + } - def __init__(self): - QtCore.QObject.__init__(self) - self._client=None - self._listeners={} - - self._curSongID=-1 - self._curTime=-1 - self._curState=-1 - self._curVolume=-1 - self._curLib=[] - self._curPlaylist=[] + def __init__(self): + QtCore.QObject.__init__(self) + self._client=None + self._listeners={} + + self._curSongID=-1 + self._curTime=-1 + self._curState=-1 + self._curVolume=-1 + self._curLib=[] + self._curPlaylist=[] - for event in self.events: - self._listeners[event]=[] + for event in self.events: + self._listeners[event]=[] - def connect(self, host, port): - """Connect to MPD@$host:$port. Returns true at success, false otherwise.""" - if self._client: - return - try: - #self._client = jmpc.jmpc() - #self._client = mpdclient2.connect() - self._client = mpd.MPDClient() - self._client.connect(host, port) - except: - self._client=None - return False - - self._raiseEvent('onConnect', None) - try: - self._updateLib() - self._updatePlaylist() - self._updateCurrentSong() - self._timerID=self.startTimer(500) - except Exception: - print_exc() - self._raiseEvent('onStateChange', {'oldState':'stop', 'newState':self.getStatus()['state']}) - self._raiseEvent('onReady', None) - doEvents() - return True + def connect(self, host, port): + """Connect to MPD@$host:$port. Returns true at success, false otherwise.""" + if self._client: + return + try: + #self._client = jmpc.jmpc() + #self._client = mpdclient2.connect() + self._client = mpd.MPDClient() + self._client.connect(host, port) + except: + self._client=None + return False + + self._raiseEvent('onConnect', None) + try: + self._updateLib() + self._updatePlaylist() + self._updateCurrentSong() + self._timerID=self.startTimer(500) + except Exception: + print_exc() + self._raiseEvent('onStateChange', {'oldState':'stop', 'newState':self.getStatus()['state']}) + self._raiseEvent('onReady', None) + doEvents() + return True - def disconnect(self): - """Disconnect from MPD.""" - if self._client: - self._client.close() - self._client.disconnect() - self._client=None - # don't kill timer, as it'll happen in timerEvent - - def isConnected(self): - """Returns true if we're connected to MPD, false otherwise.""" - return self._client!=None + def disconnect(self): + """Disconnect from MPD.""" + if self._client: + self._client.close() + self._client.disconnect() + self._client=None + # don't kill timer, as it'll happen in timerEvent + + def isConnected(self): + """Returns true if we're connected to MPD, false otherwise.""" + return self._client!=None - def listPlaylist(self): - """Returns the current playlist.""" - if self.isConnected()==False: - return None - return self._curPlaylist + def listPlaylist(self): + """Returns the current playlist.""" + if self.isConnected()==False: + return None + return self._curPlaylist - def listLibrary(self): - """Returns the library.""" - if self.isConnected()==False: - return None - return self._curLib + def listLibrary(self): + """Returns the library.""" + if self.isConnected()==False: + return None + return self._curLib - def getCurrentSong(self): - """Returns the current playing song.""" - if self.isConnected()==False: - return None - return self._curSong - - def updateDB(self, paths): - self._client.command_list_ok_begin() - for path in paths: - self._client.update(path) - self._client.command_list_end() + def getCurrentSong(self): + """Returns the current playing song.""" + if self.isConnected()==False: + return None + return self._curSong + + def updateDB(self, paths): + self._client.command_list_ok_begin() + for path in paths: + self._client.update(path) + self._client.command_list_end() - def getStatus(self): - """Returns the status.""" - try: - if self.isConnected()==False: - return None - ret=self._retrieve(self._client.status) - if 'time' in ret: - len=int(ret['time'][ret['time'].find(':')+1:]) - cur=int(ret['time'][:ret['time'].find(':')]) - ret['length']=len - ret['time']=cur - return ret - except Exception, d: - print_exc() - return None - - def repeat(self,val): - self._client.repeat(val) - def random(self,val): - self._client.random(val) + def getStatus(self): + """Returns the status.""" + try: + if self.isConnected()==False: + return None + ret=self._retrieve(self._client.status) + if 'time' in ret: + len=int(ret['time'][ret['time'].find(':')+1:]) + cur=int(ret['time'][:ret['time'].find(':')]) + ret['length']=len + ret['time']=cur + return ret + except Exception, d: + print_exc() + return None + + def repeat(self,val): + self._client.repeat(val) + def random(self,val): + self._client.random(val) - _retrMutex=QtCore.QMutex() - def _retrieve(self, method): - """Makes sure only one call is made at a time to MPD.""" - self._retrMutex.lock() - try: - ret=method() - except: - self._retrMutex.unlock() - raise + _retrMutex=QtCore.QMutex() + def _retrieve(self, method): + """Makes sure only one call is made at a time to MPD.""" + self._retrMutex.lock() + try: + ret=method() + except: + self._retrMutex.unlock() + raise - self._retrMutex.unlock() - return ret + self._retrMutex.unlock() + return ret - def isPlaying(self): - return self.getStatus()['state']=='play' + def isPlaying(self): + return self.getStatus()['state']=='play' - def play(self, id): - """Play song with ID $id.""" - self._playCalled=True - if id!=None: - self._client.playid(id) - else: - self._client.playid() - - def pause(self): - """Pause playing.""" - self._client.pause(1) - def resume(self): - """Resume playing.""" - self._client.pause(0) - def next(self): - """Move on to the next song in the playlist.""" - self._playCalled=False - self._raiseEvent('beforeSongChange', {'curSongID': self._curSongID}) - # we only switch to the next song, if some of beforeSongChange's listeners - # didn't explicitly call play. If it did, then it ain't good to immediatly - # skip to the next song! - if not self._playCalled: - self._client.next() - def previous(self): - """Move back to the previous song in the playlist.""" - self._client.previous() - def stop(self): - """Stop playing.""" - self._client.stop() + def play(self, id): + """Play song with ID $id.""" + self._playCalled=True + if id!=None: + self._client.playid(id) + else: + self._client.playid() + + def pause(self): + """Pause playing.""" + self._client.pause(1) + def resume(self): + """Resume playing.""" + self._client.pause(0) + def next(self): + """Move on to the next song in the playlist.""" + self._playCalled=False + self._raiseEvent('beforeSongChange', {'curSongID': self._curSongID}) + # we only switch to the next song, if some of beforeSongChange's listeners + # didn't explicitly call play. If it did, then it ain't good to immediatly + # skip to the next song! + if not self._playCalled: + self._client.next() + def previous(self): + """Move back to the previous song in the playlist.""" + self._client.previous() + def stop(self): + """Stop playing.""" + self._client.stop() - def seek(self, time): - """Move the current playing time to $time.""" - self._client.seekid(self._curSongID, time) - - def deleteFromPlaylist(self, list): - """Remove all songIDs in $list from the playlist.""" - self._client.command_list_ok_begin() - for id in list: - self._client.deleteid(id) - self._client.command_list_end() - self._updatePlaylist() - - def addToPlaylist(self, paths): - """Add all files in $paths to the current playlist.""" - self._client.command_list_ok_begin() - for path in paths: - self._client.add(path) - self._client.command_list_end() - self._updatePlaylist() + def seek(self, time): + """Move the current playing time to $time.""" + self._client.seekid(self._curSongID, time) + + def deleteFromPlaylist(self, list): + """Remove all songIDs in $list from the playlist.""" + self._client.command_list_ok_begin() + for id in list: + self._client.deleteid(id) + self._client.command_list_end() + self._updatePlaylist() + + def addToPlaylist(self, paths): + """Add all files in $paths to the current playlist.""" + self._client.command_list_ok_begin() + for path in paths: + self._client.add(path) + self._client.command_list_end() + self._updatePlaylist() - def setVolume(self, volume): - """Set volumne to $volume.""" - volume=min(100, max(0, volume)) - self._client.setvol(volume) - def getVolume(self): - return int(self.getStatus()['volume']) - - def addListener(self, event, callback): - """Add $callback to the listeners for $event.""" - if not(event in self.events): - raise Exception("Unknown event "+event) - self._listeners[event].append(callback) - def removeListener(self, event, callback): - if not(event in self.events): - raise Exception("Unknown event "+event) - self._listeners[event].remove(callback) + def setVolume(self, volume): + """Set volumne to $volume.""" + volume=min(100, max(0, volume)) + self._client.setvol(volume) + def getVolume(self): + return int(self.getStatus()['volume']) + + def addListener(self, event, callback): + """Add $callback to the listeners for $event.""" + if not(event in self.events): + raise Exception("Unknown event "+event) + self._listeners[event].append(callback) + def removeListener(self, event, callback): + if not(event in self.events): + raise Exception("Unknown event "+event) + self._listeners[event].remove(callback) - def _updateLib(self): - """Update the library.""" - self._curLib=self._arrayToSongArray(self._retrieve(self._client.listallinfo)) - id=0 - for song in self._curLib: - song._data['id']=id - id+=1 - def _updatePlaylist(self): - """Update the playlist.""" - self._curPlaylist=self._arrayToSongArray(self._retrieve(self._client.playlistinfo)) - def _arrayToSongArray(self, array): - """Convert an array to an array of Songs.""" - return map(lambda entry: Song(entry) - , filter(lambda entry: not('directory' in entry), array) - ) - def _updateCurrentSong(self): - """Update the current song.""" - self._curSong=self._retrieve(self._client.currentsong) - if self._curSong==None: - return - self._curSong=Song(self._curSong) + def _updateLib(self): + """Update the library.""" + self._curLib=self._arrayToSongArray(self._retrieve(self._client.listallinfo)) + id=0 + for song in self._curLib: + song._data['id']=id + id+=1 + def _updatePlaylist(self): + """Update the playlist.""" + self._curPlaylist=self._arrayToSongArray(self._retrieve(self._client.playlistinfo)) + def _arrayToSongArray(self, array): + """Convert an array to an array of Songs.""" + return map(lambda entry: Song(entry) + , filter(lambda entry: not('directory' in entry), array) + ) + def _updateCurrentSong(self): + """Update the current song.""" + self._curSong=self._retrieve(self._client.currentsong) + if self._curSong==None: + return + self._curSong=Song(self._curSong) - class simpleThread(Thread): - callback=None - params=None - def __init__(self,callback,params): - Thread.__init__(self) - self.callback=callback - self.params=params - def run(self): - self.callback(self.params) + class simpleThread(Thread): + callback=None + params=None + def __init__(self,callback,params): + Thread.__init__(self) + self.callback=callback + self.params=params + def run(self): + self.callback(self.params) - def _raiseEvent(self, event, params): - """Call all listeners for $event with parameters $params.""" - if not(event in self.events): - raise Exception("Unknown raised event "+event) + def _raiseEvent(self, event, params): + """Call all listeners for $event with parameters $params.""" + if not(event in self.events): + raise Exception("Unknown raised event "+event) - for listener in self._listeners[event]: - try: - self.simpleThread(listener, params).run() - except: - print_exc() - - def timerEvent(self, event): - "Check for changes since last check." - try: - self._updateCurrentSong() - status=self.getStatus() - except: - self._curSong=None + for listener in self._listeners[event]: + try: + self.simpleThread(listener, params).run() + except: + print_exc() + + def timerEvent(self, event): + "Check for changes since last check." + try: + self._updateCurrentSong() + status=self.getStatus() + except: + self._curSong=None - song=self._curSong - if song==None or status==None: - self._client=None - self._raiseEvent('onDisconnect', None) - self.killTimer(self._timerID) - return - - " check if song has changed" - if song.getID()>=0: - curID=song.getID() - if curID!=self._curSongID: - self._raiseEvent('onSongChange', {'oldSongID':self._curSongID, 'newSongID':curID}) - self._curSongID=curID + song=self._curSong + if song==None or status==None: + self._client=None + self._raiseEvent('onDisconnect', None) + self.killTimer(self._timerID) + return + + " check if song has changed" + if song.getID()>=0: + curID=song.getID() + if curID!=self._curSongID: + self._raiseEvent('onSongChange', {'oldSongID':self._curSongID, 'newSongID':curID}) + self._curSongID=curID - " check if the time has changed" - if 'time' in status: - curTime=status['time'] - if curTime!=self._curTime: - self._raiseEvent('onTimeChange', {'oldTime':self._curTime, 'newTime':curTime}) - self._curTime=curTime - if curTime>=status['length']-1: - self._raiseEvent('beforeSongChange', {'curSongID':curID}) + " check if the time has changed" + if 'time' in status: + curTime=status['time'] + if curTime!=self._curTime: + self._raiseEvent('onTimeChange', {'oldTime':self._curTime, 'newTime':curTime}) + self._curTime=curTime + if curTime>=status['length']-1: + self._raiseEvent('beforeSongChange', {'curSongID':curID}) - " check if the playing state has changed" - if 'state' in status: - curState=status['state'] - if curState!=self._curState: - self._raiseEvent('onStateChange', {'oldState':self._curState, 'newState':curState}) - self._curState=curState + " check if the playing state has changed" + if 'state' in status: + curState=status['state'] + if curState!=self._curState: + self._raiseEvent('onStateChange', {'oldState':self._curState, 'newState':curState}) + self._curState=curState - " check if the volume has changed" - if 'volume' in status: - curVolume=int(status['volume']) - if curVolume!=self._curVolume: - self._raiseEvent('onVolumeChange', {'oldVolume':self._curVolume, 'newVolume':curVolume}) - self._curVolume=curVolume + " check if the volume has changed" + if 'volume' in status: + curVolume=int(status['volume']) + if curVolume!=self._curVolume: + self._raiseEvent('onVolumeChange', {'oldVolume':self._curVolume, 'newVolume':curVolume}) + self._curVolume=curVolume - " update has started" - if 'updatings_db' in status and self._updatings_db==None: - self._updatings_db=status['updatings_db'] - self._raiseEvent('onUpdateDBStart', {}) - if not('updatings_db' in status) and self._updatings_db: - self._updatings_db=None - self._raiseEvent('onUpdateDBFinish') + " update has started" + if 'updatings_db' in status and self._updatings_db==None: + self._updatings_db=status['updatings_db'] + self._raiseEvent('onUpdateDBStart', {}) + if not('updatings_db' in status) and self._updatings_db: + self._updatings_db=None + self._raiseEvent('onUpdateDBFinish') monty=Monty() diff --git a/clPlugin.py b/clPlugin.py index fb26768..f825458 100644 --- a/clPlugin.py +++ b/clPlugin.py @@ -7,221 +7,221 @@ from misc import * import log class Plugin: - name=None - dockWidget=None - settingsWidget=None - settings=None - winMain=None - loaded=None - listeners=[] - - def __init__(self, winMain, name): - self.name=name - self.winMain=winMain - self.loaded=False - self.listeners=[] + name=None + dockWidget=None + settingsWidget=None + settings=None + winMain=None + loaded=None + listeners=[] + + def __init__(self, winMain, name): + self.name=name + self.winMain=winMain + self.loaded=False + self.listeners=[] - def getName(self, lower=False): - if lower: - return self.name.lower() - return self.name - def getInfo(self): - return '' - def getExtInfo(self): - return '' - def getWinMain(self): - return self.winMain - def setStatus(self, status): - self.winMain.setStatus(status) - - def log(self, msg, level=log.LOG_NORMAL): - log.log("%s: %s"%(self.getName(), msg), level) - def normal(self, msg): - self.log(msg) - def important(self, msg): - self.log(msg, log.LOG_IMPORTANT) - def extended(self, msg): - self.log(msg, log.LOG_EXTENDED) - def debug(self, msg): - self.log(msg, log.LOG_DEBUG) - - def load(self): - self.extended("loading") - if len(self.listeners): - self.debug("adding %s listeners"%(len(self.listeners))) - for listener in self.listeners: - monty.addListener(listener[0], listener[1]) + def getName(self, lower=False): + if lower: + return self.name.lower() + return self.name + def getInfo(self): + return '' + def getExtInfo(self): + return '' + def getWinMain(self): + return self.winMain + def setStatus(self, status): + self.winMain.setStatus(status) + + def log(self, msg, level=log.LOG_NORMAL): + log.log("%s: %s"%(self.getName(), msg), level) + def normal(self, msg): + self.log(msg) + def important(self, msg): + self.log(msg, log.LOG_IMPORTANT) + def extended(self, msg): + self.log(msg, log.LOG_EXTENDED) + def debug(self, msg): + self.log(msg, log.LOG_DEBUG) + + def load(self): + self.extended("loading") + if len(self.listeners): + self.debug("adding %s listeners"%(len(self.listeners))) + for listener in self.listeners: + monty.addListener(listener[0], listener[1]) - self._load() - opts=QtGui.QDockWidget.DockWidgetClosable|QtGui.QDockWidget.DockWidgetMovable - self.winMain.addDock(self.getDockWidget(opts)) - self.loaded=True - def unload(self): - if self.loaded==False: - return - self.extended("unloading") - if len(self.listeners): - self.debug("removing %s listeners"%(len(self.listeners))) - for listener in self.listeners: - monty.removeListener(listener[0], listener[1]) + self._load() + opts=QtGui.QDockWidget.DockWidgetClosable|QtGui.QDockWidget.DockWidgetMovable + self.winMain.addDock(self.getDockWidget(opts)) + self.loaded=True + def unload(self): + if self.loaded==False: + return + self.extended("unloading") + if len(self.listeners): + self.debug("removing %s listeners"%(len(self.listeners))) + for listener in self.listeners: + monty.removeListener(listener[0], listener[1]) - self._unload() - self.winMain.removeDock(self.getDockWidget()) - self.dockWidget=None - self.settingsWidget=None - self.loaded=False - def isLoaded(self): - return self.loaded - - def addMontyListener(self, event, callback): - self.listeners.append([event, callback]) - - def getDockWidget(self, opts=None): - try: - if not self.dockWidget: - self.dockWidget=self._getDockWidget() - self.dockWidget.setFeatures(opts) - self.dockWidget.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) - except: - pass - return self.dockWidget + self._unload() + self.winMain.removeDock(self.getDockWidget()) + self.dockWidget=None + self.settingsWidget=None + self.loaded=False + def isLoaded(self): + return self.loaded + + def addMontyListener(self, event, callback): + self.listeners.append([event, callback]) + + def getDockWidget(self, opts=None): + try: + if not self.dockWidget: + self.dockWidget=self._getDockWidget() + self.dockWidget.setFeatures(opts) + self.dockWidget.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + except: + pass + return self.dockWidget - def getSettingsWidget(self): - """Get the widget containing all settings.""" - # is the widget constructed yet? - if not self.settingsWidget: - self.settings=self._getSettings() - # do we have any settings? - if len(self.settings): - # create a widget - self.settingsWidget=QtGui.QWidget(None) - layout=QtGui.QGridLayout() - self.settingsWidget.setLayout(layout) - # add for every setting a new entry - for i in xrange(len(self.settings)): - setting=self.settings[i] - try: - setting[4] - except: - # if setting[0] doesn't contain a dot, it means it - # is for this one, else it might be a setting from - # another plugin. But this only applies when using - # the default settings-manager - if setting[0] and setting[0].count('.')==0: - setting[0]="%s.%s"%(self._getPluginClassname(self.__class__), setting[0]) - setting.append(settings) - tooltip="%s\n\n(%s)"%(setting[2],setting[0]) - label=QtGui.QLabel(setting[1]) - wg=setting[3] - label.setToolTip(tooltip) - wg.setToolTip(tooltip) - layout.addWidget(label) - layout.addWidget(wg, i, 1) + def getSettingsWidget(self): + """Get the widget containing all settings.""" + # is the widget constructed yet? + if not self.settingsWidget: + self.settings=self._getSettings() + # do we have any settings? + if len(self.settings): + # create a widget + self.settingsWidget=QtGui.QWidget(None) + layout=QtGui.QGridLayout() + self.settingsWidget.setLayout(layout) + # add for every setting a new entry + for i in xrange(len(self.settings)): + setting=self.settings[i] + try: + setting[4] + except: + # if setting[0] doesn't contain a dot, it means it + # is for this one, else it might be a setting from + # another plugin. But this only applies when using + # the default settings-manager + if setting[0] and setting[0].count('.')==0: + setting[0]="%s.%s"%(self._getPluginClassname(self.__class__), setting[0]) + setting.append(settings) + tooltip="%s\n\n(%s)"%(setting[2],setting[0]) + label=QtGui.QLabel(setting[1]) + wg=setting[3] + label.setToolTip(tooltip) + wg.setToolTip(tooltip) + layout.addWidget(label) + layout.addWidget(wg, i, 1) - layout.addWidget(Button('Save settings for '+self.getName(), self.saveSettings), i+1,1) + layout.addWidget(Button('Save settings for '+self.getName(), self.saveSettings), i+1,1) - return self.settingsWidget - - def resetSettingCache(self): - self.settings=None - self.settingsWidget=None + return self.settingsWidget + + def resetSettingCache(self): + self.settings=None + self.settingsWidget=None - def saveSettings(self): - if not self.settings: - self.important('Plugin::saveSettings - no settings to save?') - return - for i in xrange(len(self.settings)): - setting=self.settings[i] - name=setting[0] - obj=setting[3] - settingsMgr=setting[4] - if isinstance(obj,QtGui.QLineEdit): - settingsMgr.set(setting[0], setting[3].text()) - elif isinstance(obj,QtGui.QComboBox): - settingsMgr.set(setting[0], setting[3].currentIndex()) - elif isinstance(obj,QtGui.QTextEdit): - settingsMgr.set(setting[0], setting[3].toPlainText()) - elif isinstance(obj,QtGui.QCheckBox): - settingsMgr.set(setting[0], "1" if setting[3].checkState()==QtCore.Qt.Checked else "0") - elif isinstance(obj,QtGui.QPushButton): - # what the *#$ should we do with this? Just skip! - print "I don't know what to do with my object!" - pass - else: - print "Can't handle %s"%(obj.__class__) - if self.isLoaded(): - self.afterSaveSettings() - - def getSettingWidget(self, name): - for i in xrange(len(self.settings)): - setting=self.settings[i] - if name==setting[0]: - return setting[3] - return None - - def _getPluginClassname(self, cl): - """Returns the name of a plugin (without 'plugin'-prefix)""" - return str(cl).split('.')[-1].lower()[len('plugin'):] - - - def getSetting(self, setting, default=None, pluginClass=None): - if pluginClass==None: - pluginClass=self.__class__ + def saveSettings(self): + if not self.settings: + self.important('Plugin::saveSettings - no settings to save?') + return + for i in xrange(len(self.settings)): + setting=self.settings[i] + name=setting[0] + obj=setting[3] + settingsMgr=setting[4] + if isinstance(obj,QtGui.QLineEdit): + settingsMgr.set(setting[0], setting[3].text()) + elif isinstance(obj,QtGui.QComboBox): + settingsMgr.set(setting[0], setting[3].currentIndex()) + elif isinstance(obj,QtGui.QTextEdit): + settingsMgr.set(setting[0], setting[3].toPlainText()) + elif isinstance(obj,QtGui.QCheckBox): + settingsMgr.set(setting[0], "1" if setting[3].checkState()==QtCore.Qt.Checked else "0") + elif isinstance(obj,QtGui.QPushButton): + # what the *#$ should we do with this? Just skip! + print "I don't know what to do with my object!" + pass + else: + print "Can't handle %s"%(obj.__class__) + if self.isLoaded(): + self.afterSaveSettings() + + def getSettingWidget(self, name): + for i in xrange(len(self.settings)): + setting=self.settings[i] + if name==setting[0]: + return setting[3] + return None + + def _getPluginClassname(self, cl): + """Returns the name of a plugin (without 'plugin'-prefix)""" + return str(cl).split('.')[-1].lower()[len('plugin'):] + + + def getSetting(self, setting, default=None, pluginClass=None): + if pluginClass==None: + pluginClass=self.__class__ - # fetch the name - pluginClass=self._getPluginClassname(pluginClass) - - if default==None: - # what module is this class in? - module='.'.join(str(self.__class__).split('.')[0:2]) - # import the module - __import__(module, globals(), locals(), [], -1) - # set the default - default=eval("%s.%s_%s_DEFAULT"%(module, pluginClass.upper(), setting.upper())) - - return settings.get("%s.%s"%(str(pluginClass).lower(), setting), default) - - def setSetting(self, setting, value, pluginClass=None): - if pluginClass==None: - pluginClass=self.__class__ - # fetch the name - pluginClass=self._getPluginClassname(pluginClass) - settings.set("%s.%s"%(str(pluginClass).lower(), setting), value) + # fetch the name + pluginClass=self._getPluginClassname(pluginClass) + + if default==None: + # what module is this class in? + module='.'.join(str(self.__class__).split('.')[0:2]) + # import the module + __import__(module, globals(), locals(), [], -1) + # set the default + default=eval("%s.%s_%s_DEFAULT"%(module, pluginClass.upper(), setting.upper())) + + return settings.get("%s.%s"%(str(pluginClass).lower(), setting), default) + + def setSetting(self, setting, value, pluginClass=None): + if pluginClass==None: + pluginClass=self.__class__ + # fetch the name + pluginClass=self._getPluginClassname(pluginClass) + settings.set("%s.%s"%(str(pluginClass).lower(), setting), value) - - - def afterSaveSettings(self): - """Override this one.""" - pass - def _getDockWidget(self): - """Override this one.""" - return None - def _getSettings(self): - """Override this one. + + + def afterSaveSettings(self): + """Override this one.""" + pass + def _getDockWidget(self): + """Override this one.""" + return None + def _getSettings(self): + """Override this one. - This method must return a list of arrays. - Format: [ [setting, title, tooltip, widget, opt settingsmgr]+ ]""" - # setting[0] == setting. If setting contains a dot (.), setting is absolute, i.e. - # it belongs to a plugin. If it doesn't contain a dot, it belongs to the - # current class. E.g. 'lyrics.engine' and 'engine' are equal, if we are in - # the class pluginLyrics. Note that this only applies when setting[4] is - # not set. - # setting[1] == caption - # setting[2] == help message - # setting[3] == widget containing the value - # setting[4] == setting-class to use - return [] - def _createDock(self, widget): - """Creates a QDockWidget with parent $parent containing widget $widget.""" - dock=QtGui.QDockWidget(self.name, self.winMain) - dock.setObjectName(self.name) - dock.setWidget(widget) - - return dock - def _load(self): - """Override this one.""" - return - def _unload(self): - """Override this one.""" - return + This method must return a list of arrays. + Format: [ [setting, title, tooltip, widget, opt settingsmgr]+ ]""" + # setting[0] == setting. If setting contains a dot (.), setting is absolute, i.e. + # it belongs to a plugin. If it doesn't contain a dot, it belongs to the + # current class. E.g. 'lyrics.engine' and 'engine' are equal, if we are in + # the class pluginLyrics. Note that this only applies when setting[4] is + # not set. + # setting[1] == caption + # setting[2] == help message + # setting[3] == widget containing the value + # setting[4] == setting-class to use + return [] + def _createDock(self, widget): + """Creates a QDockWidget with parent $parent containing widget $widget.""" + dock=QtGui.QDockWidget(self.name, self.winMain) + dock.setObjectName(self.name) + dock.setWidget(widget) + + return dock + def _load(self): + """Override this one.""" + return + def _unload(self): + """Override this one.""" + return diff --git a/clSettings.py b/clSettings.py index 832ef08..36f6a16 100644 --- a/clSettings.py +++ b/clSettings.py @@ -3,60 +3,60 @@ from traceback import print_exc from PyQt4 import QtCore class Settings: - fileName=None - lines=[] - - def __init__(self, file='settings.txt'): - self.fileName=file - self.read() - - def __del__(self): - self.write() - - def get(self, name, default=None): - for line in self.lines: - if line[0:len(name)]==name: - return line[len(name):].strip() - return default - - def set(self, name, value): - newvalue="%s\t%s"%(name,value) - for i in xrange(len(self.lines)): - line=self.lines[i] - if line[0:len(name)]==name: - self.lines[i]=newvalue - return - # new value to write - self.lines.append(newvalue) - - def getIntTuple(self, name): - """Note this might return an exception!""" - val=self.get(name) - i=val.find(' ') - x,y=int(val[0:i]), int(val[i:]) - return x,y - - def setIntTuple(self, name, val1, val2): - self.set(name, "%i %i"%(val1, val2)) - - def read(self): - try: - self.lines=[] - f=open(self.fileName) - while True: - line=f.readline() - if line=='': break - self.lines.append(line.strip().replace('$NEWLINE', '\n')) - f.close() - except IOError: - pass - except: - print_exc() - def write(self): - f=open(self.fileName, 'wb') - for line in self.lines: - f.write("%s\n"%(line.replace('\n', '$NEWLINE'))) - f.close() + fileName=None + lines=[] + + def __init__(self, file='settings.txt'): + self.fileName=file + self.read() + + def __del__(self): + self.write() + + def get(self, name, default=None): + for line in self.lines: + if line[0:len(name)]==name: + return line[len(name):].strip() + return default + + def set(self, name, value): + newvalue="%s\t%s"%(name,value) + for i in xrange(len(self.lines)): + line=self.lines[i] + if line[0:len(name)]==name: + self.lines[i]=newvalue + return + # new value to write + self.lines.append(newvalue) + + def getIntTuple(self, name): + """Note this might return an exception!""" + val=self.get(name) + i=val.find(' ') + x,y=int(val[0:i]), int(val[i:]) + return x,y + + def setIntTuple(self, name, val1, val2): + self.set(name, "%i %i"%(val1, val2)) + + def read(self): + try: + self.lines=[] + f=open(self.fileName) + while True: + line=f.readline() + if line=='': break + self.lines.append(line.strip().replace('$NEWLINE', '\n')) + f.close() + except IOError: + pass + except: + print_exc() + def write(self): + f=open(self.fileName, 'wb') + for line in self.lines: + f.write("%s\n"%(line.replace('\n', '$NEWLINE'))) + f.close() settings=Settings() @@ -2,74 +2,74 @@ from PyQt4 import QtCore # compare two songs with respect to their album def isSameAlbum(song1, song2): - return song1.getAlbum()==song2.getAlbum() \ - and song1.getTag('date')==song2.getTag('date') + return song1.getAlbum()==song2.getAlbum() \ + and song1.getTag('date')==song2.getTag('date') class Song: - """The Song class offers an abstraction of a song.""" - _data=None + """The Song class offers an abstraction of a song.""" + _data=None - def __init__(self, data): - self._data=data - if 'id' in self._data: - self._data['id']=int(self._data['id']) - if 'track' in self._data: - # make sure the track is a valid number! - t=self._data['track'] - for i in xrange(len(t)): - if ord(t[i])<ord('0') or ord(t[i])>ord('9'): - try: - self._data['track']=int(t[0:i]) - except: - self._data['track']=-1 - break - self._data['track']=int(self._data['track']) - for tag in self._data.keys(): - if isinstance(self._data[tag], str): - self._data[tag]=unicode(self._data[tag], "utf-8") - try: - self._data['time']=int(self._data['time']) - self._data['timems']='%i:%i'%(self._data['time'] / 60, self._data['time'] % 60) - except: - pass + def __init__(self, data): + self._data=data + if 'id' in self._data: + self._data['id']=int(self._data['id']) + if 'track' in self._data: + # make sure the track is a valid number! + t=self._data['track'] + for i in xrange(len(t)): + if ord(t[i])<ord('0') or ord(t[i])>ord('9'): + try: + self._data['track']=int(t[0:i]) + except: + self._data['track']=-1 + break + self._data['track']=int(self._data['track']) + for tag in self._data.keys(): + if isinstance(self._data[tag], str): + self._data[tag]=unicode(self._data[tag], "utf-8") + try: + self._data['time']=int(self._data['time']) + self._data['timems']='%i:%i'%(self._data['time'] / 60, self._data['time'] % 60) + except: + pass - def getID(self): - """Get the ID.""" - return self.getTag('id', -1) - - def getTitle(self): - """Get the title.""" - return self.getTag('title', self._data['file']) + def getID(self): + """Get the ID.""" + return self.getTag('id', -1) + + def getTitle(self): + """Get the title.""" + return self.getTag('title', self._data['file']) - def getArtist(self): - """Get the artist.""" - return self.getTag('artist', self._data['file']) + def getArtist(self): + """Get the artist.""" + return self.getTag('artist', self._data['file']) - def getTrack(self): - """Get the track.""" - return self.getTag('track') + def getTrack(self): + """Get the track.""" + return self.getTag('track') - def getAlbum(self): - """Get the album.""" - return self.getTag('album') + def getAlbum(self): + """Get the album.""" + return self.getTag('album') - def getFilepath(self): - """Get the filepath.""" - return self._data['file'] + def getFilepath(self): + """Get the filepath.""" + return self._data['file'] - def match(self, str): - """Checks if the string str matches this song. Assumes str is lowercase.""" - return str in self.__str__().lower() + def match(self, str): + """Checks if the string str matches this song. Assumes str is lowercase.""" + return str in self.__str__().lower() - def __str__(self): - return "%s - %s [%s]" % (self.getTag('artist'), self.getTag('title'), self.getTag('album')) + def __str__(self): + return "%s - %s [%s]" % (self.getTag('artist'), self.getTag('title'), self.getTag('album')) - def getTag(self, tag, default=''): - """Get a tag. If it doesn't exist, return $default.""" - if tag in self._data: - return self._data[tag] - if tag=='song': - return self.__str__() + def getTag(self, tag, default=''): + """Get a tag. If it doesn't exist, return $default.""" + if tag in self._data: + return self._data[tag] + if tag=='song': + return self.__str__() - return default + return default @@ -2,115 +2,115 @@ import re import os def compile(string): - """Compile a string into a function which fills in correct values.""" - - # This function will extract all variables and functions from $string - # and convert these to functions. That way, it is much faster to run - # than parse everytime $string. - - # we will split the string into sections; a section is either a string - # or a variable. - - str, params=format_parse_rec(string.replace("%", "%%"), 0) - ps=merge(params) - func='lambda params: "%s" %% (%s)' % (str.replace('"', '\\"').replace("\n", ""), ps) - return eval(func) + """Compile a string into a function which fills in correct values.""" + + # This function will extract all variables and functions from $string + # and convert these to functions. That way, it is much faster to run + # than parse everytime $string. + + # we will split the string into sections; a section is either a string + # or a variable. + + str, params=format_parse_rec(string.replace("%", "%%"), 0) + ps=merge(params) + func='lambda params: "%s" %% (%s)' % (str.replace('"', '\\"').replace("\n", ""), ps) + return eval(func) def merge(list): - ret="" - for l in list: - ret="%s%s, "%(ret, l) - return ret[:-2] + ret="" + for l in list: + ret="%s%s, "%(ret, l) + return ret[:-2] func=re.compile('\$[a-z_]+', re.IGNORECASE) def format_parse_rec(string, start): - """Compiles a function. Returns [special str, params].""" - match=func.search(string, start) - if not match: - return string[start:], [] + """Compiles a function. Returns [special str, params].""" + match=func.search(string, start) + if not match: + return string[start:], [] - ret1="" # string - ret2=[] # params - # set fixed string - ret1+=string[start:match.start()] - name=string[match.start()+1:match.end()] - - # here are all functions defined - if name in ['if', 'ifn', 'dirname']: - try: - end=match_bracket(string, string.find("(", match.start())) - except: - end=-1 - if end<0: - raise Exception("No matching brackets") - - if name=='if' or name=='ifn': - # print if something is true/false - # $if($expr-true, $output) - # $ifn($expr-false, $output) - body=string[match.end():end] - comma=body.find(',') - s1,p1=format_parse_rec(body[1:comma], 0) # get the if-part - s2,p2=format_parse_rec(body[comma+1:-1], 0) # if true! - ret1+="%s" - if name=='if': - ret2.append("('%s' %% (%s) if (%s) else '')"%(s2, merge(p2), merge(p1))) - elif name=='ifn': - ret2.append("('%s' %% (%s) if not(%s) else '')"%(s2, merge(p2), merge(p1))) - elif name=='dirname': - # get the dir of a path - # $dirname(path) - s,p=format_parse_rec(string[match.end()+1:end-1], 0) - ret1+="%s" - ret2.append('os.path.dirname("%s"%%(%s))'%(s, merge(p))) - else: - end=match.end() - # variable - ret1+="%s" - ret2.append('params["%s"]'%(name)) - - ret=format_parse_rec(string, end) - ret1+=ret[0] - if len(ret[1]): - ret2.extend(ret[1]) - return ret1, ret2 + ret1="" # string + ret2=[] # params + # set fixed string + ret1+=string[start:match.start()] + name=string[match.start()+1:match.end()] + + # here are all functions defined + if name in ['if', 'ifn', 'dirname']: + try: + end=match_bracket(string, string.find("(", match.start())) + except: + end=-1 + if end<0: + raise Exception("No matching brackets") + + if name=='if' or name=='ifn': + # print if something is true/false + # $if($expr-true, $output) + # $ifn($expr-false, $output) + body=string[match.end():end] + comma=body.find(',') + s1,p1=format_parse_rec(body[1:comma], 0) # get the if-part + s2,p2=format_parse_rec(body[comma+1:-1], 0) # if true! + ret1+="%s" + if name=='if': + ret2.append("('%s' %% (%s) if (%s) else '')"%(s2, merge(p2), merge(p1))) + elif name=='ifn': + ret2.append("('%s' %% (%s) if not(%s) else '')"%(s2, merge(p2), merge(p1))) + elif name=='dirname': + # get the dir of a path + # $dirname(path) + s,p=format_parse_rec(string[match.end()+1:end-1], 0) + ret1+="%s" + ret2.append('os.path.dirname("%s"%%(%s))'%(s, merge(p))) + else: + end=match.end() + # variable + ret1+="%s" + ret2.append('params["%s"]'%(name)) + + ret=format_parse_rec(string, end) + ret1+=ret[0] + if len(ret[1]): + ret2.extend(ret[1]) + return ret1, ret2 def match_bracket(string, start): - """Returns position of matching bracket.""" - assert(string[start:start+1]=="(") - balance=1 - start+=1 - while balance>0: - if start<=0: - raise Exception("No matching bracket found") - l=string.find("(", start) - r=string.find(")", start) - - if l<0: - if r>0: balance-=1 - else: return -1 - start=r+1 - else: - if l<r: balance+=1 - elif l>r: balance-=1 - start=min(l,r)+1 - - if balance==0: - return r+1 - return max(l,r)+1 - + """Returns position of matching bracket.""" + assert(string[start:start+1]=="(") + balance=1 + start+=1 + while balance>0: + if start<=0: + raise Exception("No matching bracket found") + l=string.find("(", start) + r=string.find(")", start) + + if l<0: + if r>0: balance-=1 + else: return -1 + start=r+1 + else: + if l<r: balance+=1 + elif l>r: balance-=1 + start=min(l,r)+1 + + if balance==0: + return r+1 + return max(l,r)+1 + def params(song, overwrite={}, ensure={"title":"", "artist":""\ - , "album":"", "genre":"", "track":"", "length":"" \ - , "date":"", "time":""}): - params={} - for param in ensure: - params[param]=ensure[param] - if song: - for param in song._data: - params[param]=song._data[param] - for param in overwrite: - params[param]=overwrite[param] - return params + , "album":"", "genre":"", "track":"", "length":"" \ + , "date":"", "time":""}): + params={} + for param in ensure: + params[param]=ensure[param] + if song: + for param in song._data: + params[param]=song._data[param] + for param in overwrite: + params[param]=overwrite[param] + return params #f=compile("$if($date, ($date) )") #f=compile("a$if($title,$title)b$if($artist,$artist)c") @@ -13,83 +13,83 @@ class CommandListError(MPDError): class client: - _csock=None - _rfile=None - _wfile=None - def __init__(self): - pass - - def connect(self, host, port, type=socket.SOCK_STREAM): - if self._csock: - raise ConnectionError("Already connected") - - self._csock=socket.socket(socket.AF_INET, type) - self._csock.connect((host, port)) - self._file = self._csock.makefile("rwb") - handshake=self._read() - if handshake[0:len("hello jmpd")]!="hello jmpd": - print("Failed to handshake: %s" % (handshake)) - self.disconnect() - return - self._write("ack") - - - def _read(self): - return self._file.readline().strip() - def _write(self, line): - self._file.write("%s\n" % (line)) - - def disconnect(self): - self._csock.close() - self._csock=None - - def command_list_ok_begin(self): - pass - - def command_list_end(self): - pass - - def update(self): - pass - - def status(self): - pass - - def playid(id): - pass - def stop(self): - pass - - def pause(self): - pass - def resume(self): - pass - - def next(self): - pass - def previous(self): - pass - - def seekid(time): - pass - - # Playlist management - def add(path): - pass - def deleteid(path): - pass - - # volume - def setvol(newvolume): - pass - - # info - def listallinfo(self): - pass - def listplaylistinfo(self): - pass - def currentsong(self): - pass + _csock=None + _rfile=None + _wfile=None + def __init__(self): + pass + + def connect(self, host, port, type=socket.SOCK_STREAM): + if self._csock: + raise ConnectionError("Already connected") + + self._csock=socket.socket(socket.AF_INET, type) + self._csock.connect((host, port)) + self._file = self._csock.makefile("rwb") + handshake=self._read() + if handshake[0:len("hello jmpd")]!="hello jmpd": + print("Failed to handshake: %s" % (handshake)) + self.disconnect() + return + self._write("ack") + + + def _read(self): + return self._file.readline().strip() + def _write(self, line): + self._file.write("%s\n" % (line)) + + def disconnect(self): + self._csock.close() + self._csock=None + + def command_list_ok_begin(self): + pass + + def command_list_end(self): + pass + + def update(self): + pass + + def status(self): + pass + + def playid(id): + pass + def stop(self): + pass + + def pause(self): + pass + def resume(self): + pass + + def next(self): + pass + def previous(self): + pass + + def seekid(time): + pass + + # Playlist management + def add(path): + pass + def deleteid(path): + pass + + # volume + def setvol(newvolume): + pass + + # info + def listallinfo(self): + pass + def listplaylistinfo(self): + pass + def currentsong(self): + pass from test import port @@ -13,205 +13,205 @@ MUSIC_DIRECTORY="music_directory" ENCODING="UTF-8" class server: - ssocket=None - _sockets=[] - - def __init__(self): - self._jmpd=jmpd() - self._sockets=[] - - def start(self, port, type=socket.SOCK_STREAM): - try: - self.ssocket=socket.socket(socket.AF_INET, type) - self._start_server(port) - except: - print_exc() - return - - def _start_server(self, port): - self.ssocket.bind(('localhost', port)) - self.ssocket.listen(5) - self.ssocket.settimeout(None) - - print("Started jmpd v%s" % (VERSION)) - while True: - csock,caddr=self.ssocket.accept() - self._sockets.append(csock) - #start_new_thread(self.handle, (csock, caddr)) - self.handle(csock, caddr) - - def _read(self, file): - line="" - while line=="": - line=file.readline() - return line.strip() - def _write(self, file, line): - file.write("%s\n" % (line)) + ssocket=None + _sockets=[] + + def __init__(self): + self._jmpd=jmpd() + self._sockets=[] + + def start(self, port, type=socket.SOCK_STREAM): + try: + self.ssocket=socket.socket(socket.AF_INET, type) + self._start_server(port) + except: + print_exc() + return + + def _start_server(self, port): + self.ssocket.bind(('localhost', port)) + self.ssocket.listen(5) + self.ssocket.settimeout(None) + + print("Started jmpd v%s" % (VERSION)) + while True: + csock,caddr=self.ssocket.accept() + self._sockets.append(csock) + #start_new_thread(self.handle, (csock, caddr)) + self.handle(csock, caddr) + + def _read(self, file): + line="" + while line=="": + line=file.readline() + return line.strip() + def _write(self, file, line): + file.write("%s\n" % (line)) - def handle(self, csocket, caddr): - print("Incoming connection from %s" % (str(caddr))) - file=csocket.makefile('rw', 0) - self._write(file, 'hello jmpd %s'%(VERSION)) - - try: - while True: - line=self._read(file) - print line - except: - print_exc() - - def closeAll(self): - for socket in self._sockets: - socket.close() + def handle(self, csocket, caddr): + print("Incoming connection from %s" % (str(caddr))) + file=csocket.makefile('rw', 0) + self._write(file, 'hello jmpd %s'%(VERSION)) + + try: + while True: + line=self._read(file) + print line + except: + print_exc() + + def closeAll(self): + for socket in self._sockets: + socket.close() def _getName(line): - return line[0:line.find(': ')] + return line[0:line.find(': ')] def _getValue(line): - return line[line.find(': ')+2:] - + return line[line.find(': ')+2:] + tags=['title', 'artist', 'album', 'tracknumber', 'genre'] class jmpd: - _lib=None # library - _settings=None - - def __init__(self): - self._lib=[] - self._settings=Settings("mpdconf") - self.readLib() - - def writeLib(self): - all_tags=tags - encoding=ENCODING - tags.append('filename') - - fout=codecs.open(self._settings.get("db_file"), mode='w', encoding=encoding) - fout.write("# jmpd\n") - fout.write("begin info\n") - fout.write("encoding: %s\n" % (encoding)) - fout.write("jmpd: %s\n" % (VERSION)) - fout.write("end info\n") - fout.write("\n") - - fout.write("begin library\n") - cur_path="" - for entry in self._lib: - if entry['path']!=cur_path: - fout.write("set path: %s\n" % entry['path']) - cur_path=entry['path'] - - fout.write("begin file\n") - for tag in all_tags: - if tag in entry: - fout.write("%s: %s\n" % (tag, entry[tag])) - fout.write("end file\n") - - fout.write("end library\n") - fout.close() - - def readLib(self): - """Restore the library from file""" - db_file=self._settings.get("db_file") - encoding=ENCODING - if os.path.exists(db_file)==False: - self._lib=[] - return - - # we first open the file in ASCII-mode, and read on till the real encoding - # then we reopen it in that encoding - fin=open(db_file, 'r') - fin.close() - fin=codecs.open(db_file, mode='r', encoding=encoding) - - self._readLine(fin) # begin info - encoding=_getValue(self._readLine(fin)) - version=_getValue(self._readLine(fin)) - if version!=VERSION: - raise Exception("Invalid database file") - self._readNext(fin, 'end info') - - self._readNext(fin, 'begin library') - cur_path="" - while True: - line=self._readLine(fin) - if line=="end library": - break - if _getName(line)=="set path: ": - cur_path=_getValue(line) - elif line=="begin file": - self._readFile(fin, cur_path, encoding) - - fin.close() - - def _readFile(self, fin, cur_path, encoding): - """Read the data for a file""" - entry={'filepath': cur_path} - line="" - while line!="end file": - line=self._readLine(fin) - if line.find(': ')>0: - entry[_getName(line)]=_getValue(line) - - self._lib.append(entry) - def _readEnd(self, fin, section): - """Read the next line containing an end section""" - line="" - while line!="end %s"%(section): - line=self._readLine(fin) - return line - def _readBegin(self, fin, section): - """Read the next line containing a begin section""" - line="" - while line!="begin %s"%(section): - line=self._readLine(fin) - return line - - def _readNext(self, fin, find): - line="" - while line!=find: - line=self._readLine(fin) - return line - def _readLine(self, fin): - """Reads the next line having characters and not being a commented line""" - ret="\n" - try: - while ret=="\n" or ret[0]=='#': - ret=fin.readline() - except: - pass - if len(ret)==0: - raise Exception("EOF") - return ret.strip() - - - def update(self): - """Look for songs in the music_directory""" - self._lib=[] - for dir in self._settings.get(MUSIC_DIRECTORY).split(":"): - self._update_rec(dir) - - self.writeLib() - - def _update_rec(self, path): - subdirs=[] - for filename in os.listdir(path): - filepath="%s/%s"%(path,filename) - if os.path.isdir(filepath): - subdirs.append(filepath) - if filename[-4:]==".mp3": - audio=EasyID3(filepath) - entry = {'path': path.decode(ENCODING) - , 'filename': filename.decode(ENCODING) - } - for tag in tags: - try: - value=str(audio[tag]) - entry[tag]=value[3:-2] - except: - entry[tag]='' - self._lib.append(entry) - - for subdir in subdirs: - self._update_rec(subdir) + _lib=None # library + _settings=None + + def __init__(self): + self._lib=[] + self._settings=Settings("mpdconf") + self.readLib() + + def writeLib(self): + all_tags=tags + encoding=ENCODING + tags.append('filename') + + fout=codecs.open(self._settings.get("db_file"), mode='w', encoding=encoding) + fout.write("# jmpd\n") + fout.write("begin info\n") + fout.write("encoding: %s\n" % (encoding)) + fout.write("jmpd: %s\n" % (VERSION)) + fout.write("end info\n") + fout.write("\n") + + fout.write("begin library\n") + cur_path="" + for entry in self._lib: + if entry['path']!=cur_path: + fout.write("set path: %s\n" % entry['path']) + cur_path=entry['path'] + + fout.write("begin file\n") + for tag in all_tags: + if tag in entry: + fout.write("%s: %s\n" % (tag, entry[tag])) + fout.write("end file\n") + + fout.write("end library\n") + fout.close() + + def readLib(self): + """Restore the library from file""" + db_file=self._settings.get("db_file") + encoding=ENCODING + if os.path.exists(db_file)==False: + self._lib=[] + return + + # we first open the file in ASCII-mode, and read on till the real encoding + # then we reopen it in that encoding + fin=open(db_file, 'r') + fin.close() + fin=codecs.open(db_file, mode='r', encoding=encoding) + + self._readLine(fin) # begin info + encoding=_getValue(self._readLine(fin)) + version=_getValue(self._readLine(fin)) + if version!=VERSION: + raise Exception("Invalid database file") + self._readNext(fin, 'end info') + + self._readNext(fin, 'begin library') + cur_path="" + while True: + line=self._readLine(fin) + if line=="end library": + break + if _getName(line)=="set path: ": + cur_path=_getValue(line) + elif line=="begin file": + self._readFile(fin, cur_path, encoding) + + fin.close() + + def _readFile(self, fin, cur_path, encoding): + """Read the data for a file""" + entry={'filepath': cur_path} + line="" + while line!="end file": + line=self._readLine(fin) + if line.find(': ')>0: + entry[_getName(line)]=_getValue(line) + + self._lib.append(entry) + def _readEnd(self, fin, section): + """Read the next line containing an end section""" + line="" + while line!="end %s"%(section): + line=self._readLine(fin) + return line + def _readBegin(self, fin, section): + """Read the next line containing a begin section""" + line="" + while line!="begin %s"%(section): + line=self._readLine(fin) + return line + + def _readNext(self, fin, find): + line="" + while line!=find: + line=self._readLine(fin) + return line + def _readLine(self, fin): + """Reads the next line having characters and not being a commented line""" + ret="\n" + try: + while ret=="\n" or ret[0]=='#': + ret=fin.readline() + except: + pass + if len(ret)==0: + raise Exception("EOF") + return ret.strip() + + + def update(self): + """Look for songs in the music_directory""" + self._lib=[] + for dir in self._settings.get(MUSIC_DIRECTORY).split(":"): + self._update_rec(dir) + + self.writeLib() + + def _update_rec(self, path): + subdirs=[] + for filename in os.listdir(path): + filepath="%s/%s"%(path,filename) + if os.path.isdir(filepath): + subdirs.append(filepath) + if filename[-4:]==".mp3": + audio=EasyID3(filepath) + entry = {'path': path.decode(ENCODING) + , 'filename': filename.decode(ENCODING) + } + for tag in tags: + try: + value=str(audio[tag]) + entry[tag]=value[3:-2] + except: + entry[tag]='' + self._lib.append(entry) + + for subdir in subdirs: + self._update_rec(subdir) from test import port s=server() @@ -1,36 +1,36 @@ # what is there to log? -LOG_NONE=0 # nothing -LOG_IMPORTANT=1 # important things like errors -LOG_NORMAL=2 # LOG_IMPORTANT + some events, like connect -LOG_EXTENDED=3 # LOG_NORMAL + more log information -LOG_DEBUG=4 # LOG_EXTENDED + debug information +LOG_NONE=0 # nothing +LOG_IMPORTANT=1 # important things like errors +LOG_NORMAL=2 # LOG_IMPORTANT + some events, like connect +LOG_EXTENDED=3 # LOG_NORMAL + more log information +LOG_DEBUG=4 # LOG_EXTENDED + debug information def setLevel(newLevel=LOG_NORMAL): - """Set the current level to log.""" - global curLevel - curLevel=newLevel + """Set the current level to log.""" + global curLevel + curLevel=newLevel def setWriter(writer): - """Set the writer to use for logging. This method should have two params: item, level""" - global curOutput - curOutput=writer + """Set the writer to use for logging. This method should have two params: item, level""" + global curOutput + curOutput=writer def _writer(item, level): - print "%s %s"%(str(level), item) + print "%s %s"%(str(level), item) curLevel=LOG_NORMAL curOutput=_writer def normal(item): - log(item, LOG_NORMAL) + log(item, LOG_NORMAL) def important(item): - log(item, LOG_IMPORTANT) + log(item, LOG_IMPORTANT) def extended(item): - log(item, LOG_EXTENDED) + log(item, LOG_EXTENDED) def debug(item): - log(item, LOG_DEBUG) + log(item, LOG_DEBUG) def log(item, level=LOG_NORMAL): - if level<=curLevel and item!="": - curOutput(item, level) + if level<=curLevel and item!="": + curOutput(item, level) @@ -15,156 +15,156 @@ appIcon=QtGui.QIcon('gfx/icon.png') eventLoop=QtCore.QEventLoop() def doEvents(): - """Make some time for necessary events.""" - eventLoop.processEvents(QtCore.QEventLoop.AllEvents) + """Make some time for necessary events.""" + eventLoop.processEvents(QtCore.QEventLoop.AllEvents) def sec2min(secs): - """Converts seconds to min:sec.""" - min=int(secs/60) - sec=secs%60 - if sec<10:sec='0'+str(sec) - return str(min)+':'+str(sec) + """Converts seconds to min:sec.""" + min=int(secs/60) + sec=secs%60 + if sec<10:sec='0'+str(sec) + return str(min)+':'+str(sec) def numeric_compare(x, y): - if x>y: - return 1 - elif x==y: - return 0 - return -1 + if x>y: + return 1 + elif x==y: + return 0 + return -1 def unique(seq): - """Retrieve list of unique elements.""" - seen = [] - return t(c for c in seq if not (c in seen or seen.append(c))) + """Retrieve list of unique elements.""" + seen = [] + return t(c for c in seq if not (c in seen or seen.append(c))) def toAscii(ustr): - if type(ustr)==str: - return ustr - return unicodedata.normalize('NFKD', ustr).encode('ascii', 'ignore') + if type(ustr)==str: + return ustr + return unicodedata.normalize('NFKD', ustr).encode('ascii', 'ignore') def fetch(SE, sites, song=None, xtra_tags={}, stripHTML=True): - """Returns None when nothing found, or [site,source-url].""" - # compose the search-engine URL - f=format.compile(SE) - SE_url=toAscii(f(format.params(song, xtra_tags))) - SE_url=SE_url.replace(' ', '+') - - # fetch the page from the search-engine with the results - request=urllib2.Request(SE_url) - request.add_header('User-Agent', 'montypc') - opener=urllib2.build_opener() - data=opener.open(request).read() - - # look for urls on the search page! - regex=re.compile('<a href="(.*?)".*?>.*?<\/a>') - urls=regex.findall(data) - log.debug("all urls") - for url in urls: - log.debug(" %s"%(url)) - - # look for urls which are defined in $sites. - # The first matching URL is taken - finalRegex=None - log.debug("Checking %i URLs on %s"%(len(sites), SE_url)) - # loop over all sites which may have what we're interested in - for site in sites: - finalURL=None - finalRegex=None - # check if on the results-page there is a link to $site - for url in urls: - if url.find(site)>=0: - log.debug(" Found site %s in results: %s"%(site, url)) - finalURL=url - finalRegex=sites[site] - break - - if finalURL: - match=None - # open the url - cj = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) - log.debug(" Reading URL %s"%(finalURL)) - try: - # read the page - r = opener.open(finalURL) - data=r.read() - # perform the regular expression - regex=re.compile(finalRegex, re.IGNORECASE|re.MULTILINE|re.DOTALL) - match=regex.search(data) - except Exception, e: - log.debug(" Failed to open site %s"%(finalURL)) - continue - - if match: - # if the regex matches, then we arrive here - # we assume the content we want is in the first group - log.debug(" Regex succeeded!") - try: - charset=re.compile('charset=["\']?([\w-]+)').search(data).group(1) - log.debug(" charset=%s"%(charset)) - except: - charset='iso-8859-1' - log.debug(" charset not found. Assuming %s"%(charset)) - data=match.group(1) - data=re.sub(chr(13), '', data) # replace ^M aka \r - data=unicode(data, charset) - if stripHTML: - # do we want HTML? - data=re.sub('<br.*?>', '\n', data) # replace <br />'s with newline - data=re.sub('\n\n', '\n', data) - data=re.sub('<[^>]*?>', '', data) # strip all other HTML - data=decodeHTMLEntities(data) # convert HTML entities - data=data.strip() - log.debug("Succeeded fetching.") - return [data,finalURL] - else: - log.debug(" Regex for %s%s failed"%(site, (" (%s)"%(finalURL) if finalURL else ""))) - else: - log.debug(" Site %s not found on results-page"%(site)) - - - log.debug("Failed fetching.") - return None + """Returns None when nothing found, or [site,source-url].""" + # compose the search-engine URL + f=format.compile(SE) + SE_url=toAscii(f(format.params(song, xtra_tags))) + SE_url=SE_url.replace(' ', '+') + + # fetch the page from the search-engine with the results + request=urllib2.Request(SE_url) + request.add_header('User-Agent', 'montypc') + opener=urllib2.build_opener() + data=opener.open(request).read() + + # look for urls on the search page! + regex=re.compile('<a href="(.*?)".*?>.*?<\/a>') + urls=regex.findall(data) + log.debug("all urls") + for url in urls: + log.debug(" %s"%(url)) + + # look for urls which are defined in $sites. + # The first matching URL is taken + finalRegex=None + log.debug("Checking %i URLs on %s"%(len(sites), SE_url)) + # loop over all sites which may have what we're interested in + for site in sites: + finalURL=None + finalRegex=None + # check if on the results-page there is a link to $site + for url in urls: + if url.find(site)>=0: + log.debug(" Found site %s in results: %s"%(site, url)) + finalURL=url + finalRegex=sites[site] + break + + if finalURL: + match=None + # open the url + cj = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) + log.debug(" Reading URL %s"%(finalURL)) + try: + # read the page + r = opener.open(finalURL) + data=r.read() + # perform the regular expression + regex=re.compile(finalRegex, re.IGNORECASE|re.MULTILINE|re.DOTALL) + match=regex.search(data) + except Exception, e: + log.debug(" Failed to open site %s"%(finalURL)) + continue + + if match: + # if the regex matches, then we arrive here + # we assume the content we want is in the first group + log.debug(" Regex succeeded!") + try: + charset=re.compile('charset=["\']?([\w-]+)').search(data).group(1) + log.debug(" charset=%s"%(charset)) + except: + charset='iso-8859-1' + log.debug(" charset not found. Assuming %s"%(charset)) + data=match.group(1) + data=re.sub(chr(13), '', data) # replace ^M aka \r + data=unicode(data, charset) + if stripHTML: + # do we want HTML? + data=re.sub('<br.*?>', '\n', data) # replace <br />'s with newline + data=re.sub('\n\n', '\n', data) + data=re.sub('<[^>]*?>', '', data) # strip all other HTML + data=decodeHTMLEntities(data) # convert HTML entities + data=data.strip() + log.debug("Succeeded fetching.") + return [data,finalURL] + else: + log.debug(" Regex for %s%s failed"%(site, (" (%s)"%(finalURL) if finalURL else ""))) + else: + log.debug(" Site %s not found on results-page"%(site)) + + + log.debug("Failed fetching.") + return None def substEntity(match): - ent = match.group(2) - if match.group(1) == "#": - return unichr(int(ent)) - else: - cp = n2cp.get(ent) + ent = match.group(2) + if match.group(1) == "#": + return unichr(int(ent)) + else: + cp = n2cp.get(ent) - if cp: - return unichr(cp) - else: - return match.group() + if cp: + return unichr(cp) + else: + return match.group() def decodeHTMLEntities(string): - # replace entities with their UTF-counterpart - entity_re = re.compile("&(#?)(\d{1,5}|\w{1,8});") - return entity_re.subn(substEntity, string)[0] + # replace entities with their UTF-counterpart + entity_re = re.compile("&(#?)(\d{1,5}|\w{1,8});") + return entity_re.subn(substEntity, string)[0] - + class Button(QtGui.QPushButton): - iconSize=32 - """A simple Button class which calls $onClick when clicked.""" - def __init__(self, caption, onClick=None, iconPath=None, iconOnly=False, parent=None): - QtGui.QPushButton.__init__(self, parent) - - if onClick: - self.connect(self, QtCore.SIGNAL('clicked(bool)'), onClick) - if iconPath: - self.changeIcon(iconPath) - - if not(iconPath and iconOnly): - QtGui.QPushButton.setText(self, caption) - - self.setToolTip(caption) - - def setText(self, caption): - self.setToolTip(caption) - if self.icon()==None: - self.setText(caption) - - def changeIcon(self, iconPath): - icon=QtGui.QIcon() - icon.addFile(iconPath, QtCore.QSize(self.iconSize, self.iconSize)) - self.setIcon(icon) + iconSize=32 + """A simple Button class which calls $onClick when clicked.""" + def __init__(self, caption, onClick=None, iconPath=None, iconOnly=False, parent=None): + QtGui.QPushButton.__init__(self, parent) + + if onClick: + self.connect(self, QtCore.SIGNAL('clicked(bool)'), onClick) + if iconPath: + self.changeIcon(iconPath) + + if not(iconPath and iconOnly): + QtGui.QPushButton.setText(self, caption) + + self.setToolTip(caption) + + def setText(self, caption): + self.setToolTip(caption) + if self.icon()==None: + self.setText(caption) + + def changeIcon(self, iconPath): + icon=QtGui.QIcon() + icon.addFile(iconPath, QtCore.QSize(self.iconSize, self.iconSize)) + self.setIcon(icon) @@ -18,7 +18,7 @@ homePage = "jerous.psboard.org/montypc" bugEmail = "jerous@gmail.com" aboutData = KAboutData (appName, catalog, programName, version, description, - license, copyright, text, homePage, bugEmail) + license, copyright, text, homePage, bugEmail) KCmdLineArgs.init (sys.argv, aboutData) app = KApplication() @@ -29,10 +29,10 @@ from clMonty import monty from traceback import print_exc try: - wMain = winMain() - wMain.show() - app.exec_() - wMain.quit() + wMain = winMain() + wMain.show() + app.exec_() + wMain.quit() except Exception, e: - print_exc() + print_exc() @@ -147,10 +147,10 @@ class MPDClient(object): def _writecommand(self, command, args=[]): parts = [command] for arg in args: - if type(arg)==int: - parts.append('"%i"' % arg) - else: - parts.append(u'"%s"' % escape(arg)) + if type(arg)==int: + parts.append('"%i"' % arg) + else: + parts.append(u'"%s"' % escape(arg)) self._writeline(u" ".join(parts)) def _readline(self): diff --git a/plugins/AlbumCover.py b/plugins/AlbumCover.py index cb67f51..d2952ca 100644 --- a/plugins/AlbumCover.py +++ b/plugins/AlbumCover.py @@ -17,207 +17,207 @@ ALBUMCOVER_FETCH0_DEFAULT=1 ALBUMCOVER_FETCH1_DEFAULT=1 class wgAlbumCover(QtGui.QWidget): - " container for the image" - img=None - imgLoaded=False - p=None # plugin - acFormat=None - def __init__(self, p, parent=None): - QtGui.QWidget.__init__(self,parent) - self.p=p - self.img=QtGui.QImage() - self.setMinimumSize(64,64) - - def mousePressEvent(self, event): - if event.button()==QtCore.Qt.RightButton: - song=monty.getCurrentSong() - file=QtGui.QFileDialog.getOpenFileName(self - , "Select album cover for %s - %s"%(song.getArtist(), song.getAlbum()) - , "" - , "Images ("+" ".join(map(lambda ext: "*.%s"%(ext), ALBUMCOVER_GFX_EXTS_DEFAULT.split(',')))+")" - ) - if file: - cur=self.getLocalACPath(song, probe=True) - # remove the previous cover - try: - if cur: - self.p.extended("removing %s"%(cur)) - os.remove(cur) - else: - cur=self.getLocalACPath(song, probe=False) - self.p.extended("copy %s to %s"%(file, cur)) - shutil.copy(file, cur) - except Exception, e: - self.p.normal("failed to set new cover: %s"%(str(e))) - else: - return - self.refresh() - - def getIMG(self): - if self.imgLoaded: - return self.img - return None + " container for the image" + img=None + imgLoaded=False + p=None # plugin + acFormat=None + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self,parent) + self.p=p + self.img=QtGui.QImage() + self.setMinimumSize(64,64) + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.RightButton: + song=monty.getCurrentSong() + file=QtGui.QFileDialog.getOpenFileName(self + , "Select album cover for %s - %s"%(song.getArtist(), song.getAlbum()) + , "" + , "Images ("+" ".join(map(lambda ext: "*.%s"%(ext), ALBUMCOVER_GFX_EXTS_DEFAULT.split(',')))+")" + ) + if file: + cur=self.getLocalACPath(song, probe=True) + # remove the previous cover + try: + if cur: + self.p.extended("removing %s"%(cur)) + os.remove(cur) + else: + cur=self.getLocalACPath(song, probe=False) + self.p.extended("copy %s to %s"%(file, cur)) + shutil.copy(file, cur) + except Exception, e: + self.p.normal("failed to set new cover: %s"%(str(e))) + else: + return + self.refresh() + + def getIMG(self): + if self.imgLoaded: + return self.img + return None - def paintEvent(self, event): - l=min(self.width(),self.height()) - p=QtGui.QPainter(self) - rect=QtCore.QRectF((self.width()-l)/2,0,l,l) - p.drawImage(rect,self.img) + def paintEvent(self, event): + l=min(self.width(),self.height()) + p=QtGui.QPainter(self) + rect=QtCore.QRectF((self.width()-l)/2,0,l,l) + p.drawImage(rect,self.img) - def refresh(self): - self.p.extended("refreshing cover") - song=monty.getCurrentSong() - try: - song._data['file'] - except: - self.img.load('') - self.update() - return - start_new_thread(self.fetchCover, (song,)) - - def fetchCover(self, song): - """Fetch cover (from internet or local dir)""" - # set default cover - self.imgLoaded=False - self.img.load('gfx/no-cd-cover.png') - self.acFormat=format.compile(self.p.getSetting('downloadto')) - for i in [0, 1]: - src=self.p.getSetting('fetch%i'%(i)) - if src!=AC_NO_FETCH and self.fetchCoverSrc(song, src): - # ACK! - self.imgLoaded=True - break - self.update() - - def getLocalACPath(self, song, probe): - """Get the local path of an albumcover. If $probe, then try covers*exts for existing file.""" - # fetch gfx extensions - exts=self.p.getSetting('gfx_exts').split(',') - exts=map(lambda ext: ext.strip(), exts) - - # fetch cover album titles - covers=self.p.getSetting('files').split(',') - covers=map(lambda title: title.strip(), covers) - - params={'music_dir': mpdSettings.get('music_directory'), 'cover':'%s.%s'%(covers[0], exts[0])} - if probe: - self.p.debug("probing ...") - for cover in covers: - for ext in exts: - params['cover']='%s.%s'%(cover, ext) - path=self.acFormat(format.params(song, params)) - self.p.debug(" path: %s"%(path)) - fInfo=QtCore.QFileInfo(path) - if fInfo.exists(): - self.p.debug(" OK!") - return path - self.p.debug("done probing: no matching albumcover found") - return None - else: - self.p.debug("no probing") - path=self.acFormat(format.params(song, params)) - self.p.debug(" path: %s"%(path)) - return path + def refresh(self): + self.p.extended("refreshing cover") + song=monty.getCurrentSong() + try: + song._data['file'] + except: + self.img.load('') + self.update() + return + start_new_thread(self.fetchCover, (song,)) + + def fetchCover(self, song): + """Fetch cover (from internet or local dir)""" + # set default cover + self.imgLoaded=False + self.img.load('gfx/no-cd-cover.png') + self.acFormat=format.compile(self.p.getSetting('downloadto')) + for i in [0, 1]: + src=self.p.getSetting('fetch%i'%(i)) + if src!=AC_NO_FETCH and self.fetchCoverSrc(song, src): + # ACK! + self.imgLoaded=True + break + self.update() + + def getLocalACPath(self, song, probe): + """Get the local path of an albumcover. If $probe, then try covers*exts for existing file.""" + # fetch gfx extensions + exts=self.p.getSetting('gfx_exts').split(',') + exts=map(lambda ext: ext.strip(), exts) + + # fetch cover album titles + covers=self.p.getSetting('files').split(',') + covers=map(lambda title: title.strip(), covers) + + params={'music_dir': mpdSettings.get('music_directory'), 'cover':'%s.%s'%(covers[0], exts[0])} + if probe: + self.p.debug("probing ...") + for cover in covers: + for ext in exts: + params['cover']='%s.%s'%(cover, ext) + path=self.acFormat(format.params(song, params)) + self.p.debug(" path: %s"%(path)) + fInfo=QtCore.QFileInfo(path) + if fInfo.exists(): + self.p.debug(" OK!") + return path + self.p.debug("done probing: no matching albumcover found") + return None + else: + self.p.debug("no probing") + path=self.acFormat(format.params(song, params)) + self.p.debug(" path: %s"%(path)) + return path - - def fetchCoverSrc(self, song, src): - """Fetch the album cover for $song from $src.""" - if not src in [AC_FETCH_INTERNET, AC_FETCH_LOCAL_DIR]: - print "wgAlbumCover::fetchCover - invalid source "+str(src) - return False - - if src==AC_FETCH_INTERNET: - # look on the internetz! - try: - if not song.getArtist() or not song.getAlbum(): - return False - # get the url from amazon WS - coverURL=AmazonAlbumImage(song.getArtist(), song.getAlbum()).fetch() - self.p.extended("fetch from Amazon") - if not coverURL: - self.p.normal("not found on Amazon") - return False - # read the url, i.e. retrieve image data - img=urllib.urlopen(coverURL) - # where do we save to? - file=self.getLocalACPath(song, False) - # open file, and write the read of img! - f=open(file,'wb') - f.write(img.read()) - f.close() + + def fetchCoverSrc(self, song, src): + """Fetch the album cover for $song from $src.""" + if not src in [AC_FETCH_INTERNET, AC_FETCH_LOCAL_DIR]: + print "wgAlbumCover::fetchCover - invalid source "+str(src) + return False + + if src==AC_FETCH_INTERNET: + # look on the internetz! + try: + if not song.getArtist() or not song.getAlbum(): + return False + # get the url from amazon WS + coverURL=AmazonAlbumImage(song.getArtist(), song.getAlbum()).fetch() + self.p.extended("fetch from Amazon") + if not coverURL: + self.p.normal("not found on Amazon") + return False + # read the url, i.e. retrieve image data + img=urllib.urlopen(coverURL) + # where do we save to? + file=self.getLocalACPath(song, False) + # open file, and write the read of img! + f=open(file,'wb') + f.write(img.read()) + f.close() - # load image; should work now! - self.img.load(file) - return True - except: - self.p.normal("failed to download cover from Amazon") - print_exc() - return False - - file=self.getLocalACPath(song, True) - if file: - try: - self.img.load(file) - self.p.extended("cover set!") - return True - except: - self.p.normal("failed to load %s"%(path)) + # load image; should work now! + self.img.load(file) + return True + except: + self.p.normal("failed to download cover from Amazon") + print_exc() + return False + + file=self.getLocalACPath(song, True) + if file: + try: + self.img.load(file) + self.p.extended("cover set!") + return True + except: + self.p.normal("failed to load %s"%(path)) class pluginAlbumCover(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'AlbumCover') - self.addMontyListener('onSongChange', self.onEvent) - self.addMontyListener('onReady', self.onEvent) - self.addMontyListener('onDisconnect', self.onEvent) - self.addMontyListener('onStateChange', self.onEvent) - - def _load(self): - self.o=wgAlbumCover(self, None) - self.o.refresh() - def _unload(self): - self.o=None - def getInfo(self): - return "Display the album cover of the currently playing album." - def getExtInfo(self): - return "Displays the album cover of the currently playing album in a widget.\n" \ - "This album cover can be fetched from various locations:\n" \ - " local dir: the directory in which the album is located;\n" \ - " internet: look on amazon for the album and corresponding cover\n" \ - "Settings:\n" \ - " albumcover.fetch$i: what source to fetch from on step $i. If step $i fails, move on to step $i+1;\n" \ - " albumcover.downloadto: where to download album covers from internet to. This string can contain the normal tags of the current playing song, plus $music_dir and $cover.\n" \ - " albumcover.files: comma separated list of filenames (without extension)to be considered an album cover. Extensions jpg, jpeg, png, gif and bmp are used.\n" + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'AlbumCover') + self.addMontyListener('onSongChange', self.onEvent) + self.addMontyListener('onReady', self.onEvent) + self.addMontyListener('onDisconnect', self.onEvent) + self.addMontyListener('onStateChange', self.onEvent) + + def _load(self): + self.o=wgAlbumCover(self, None) + self.o.refresh() + def _unload(self): + self.o=None + def getInfo(self): + return "Display the album cover of the currently playing album." + def getExtInfo(self): + return "Displays the album cover of the currently playing album in a widget.\n" \ + "This album cover can be fetched from various locations:\n" \ + " local dir: the directory in which the album is located;\n" \ + " internet: look on amazon for the album and corresponding cover\n" \ + "Settings:\n" \ + " albumcover.fetch$i: what source to fetch from on step $i. If step $i fails, move on to step $i+1;\n" \ + " albumcover.downloadto: where to download album covers from internet to. This string can contain the normal tags of the current playing song, plus $music_dir and $cover.\n" \ + " albumcover.files: comma separated list of filenames (without extension)to be considered an album cover. Extensions jpg, jpeg, png, gif and bmp are used.\n" - def getWidget(self): - return self.o + def getWidget(self): + return self.o - def _getDockWidget(self): - return self._createDock(self.o) + def _getDockWidget(self): + return self._createDock(self.o) - def onEvent(self, params): - self.o.refresh() + def onEvent(self, params): + self.o.refresh() - def _getSettings(self): - ret=[] - nums=['first', 'second'] - actions=[QtGui.QComboBox(), QtGui.QComboBox()] - for i,action in enumerate(actions): - setting='fetch%i'%(i) - action.addItem("On the %s action, Monty rested."%(nums[i])) - action.addItem("Local dir") - action.addItem("Internet") - action.setCurrentIndex(int(self.getSetting(setting, str(i+1)))) - ret.append([setting, 'Action %i'%(i+1), 'What to do on the %s step.'%(nums[i]), action]) + def _getSettings(self): + ret=[] + nums=['first', 'second'] + actions=[QtGui.QComboBox(), QtGui.QComboBox()] + for i,action in enumerate(actions): + setting='fetch%i'%(i) + action.addItem("On the %s action, Monty rested."%(nums[i])) + action.addItem("Local dir") + action.addItem("Internet") + action.setCurrentIndex(int(self.getSetting(setting, str(i+1)))) + ret.append([setting, 'Action %i'%(i+1), 'What to do on the %s step.'%(nums[i]), action]) - ret.append(['downloadto', 'Local dir', 'Specifies where to save album covers fetched from internet to.\nPossible tags: music_dir, cover, file, artist, title, album', QtGui.QLineEdit(self.getSetting('downloadto'))]) - ret.append(['files', 'Album cover files', 'Comma separated list of titles that are to be considered album covers. E.g. cover,album.', QtGui.QLineEdit(self.getSetting('files'))]) - - return ret - - def afterSaveSettings(self): - self.o.refresh() + ret.append(['downloadto', 'Local dir', 'Specifies where to save album covers fetched from internet to.\nPossible tags: music_dir, cover, file, artist, title, album', QtGui.QLineEdit(self.getSetting('downloadto'))]) + ret.append(['files', 'Album cover files', 'Comma separated list of titles that are to be considered album covers. E.g. cover,album.', QtGui.QLineEdit(self.getSetting('files'))]) + + return ret + + def afterSaveSettings(self): + self.o.refresh() # This is the amazon cover fetcher using their webservice api @@ -228,48 +228,48 @@ import urllib AMAZON_AWS_ID = "0K4RZZKHSB5N2XYJWF02" class AmazonAlbumImage(object): - awsurl = "http://ecs.amazonaws.com/onca/xml" - def __init__(self, artist, album): - self.artist = artist - self.album = album - - def fetch(self): - url = self._GetResultURL(self._SearchAmazon()) - if not url: - return None - img_re = re.compile(r'''registerImage\("original_image", "([^"]+)"''') - try: - prod_data = urllib.urlopen(url).read() - except: - self.important("timeout opening %s"%(url)) - return None - m = img_re.search(prod_data) - if not m: - return None - img_url = m.group(1) - return img_url + awsurl = "http://ecs.amazonaws.com/onca/xml" + def __init__(self, artist, album): + self.artist = artist + self.album = album + + def fetch(self): + url = self._GetResultURL(self._SearchAmazon()) + if not url: + return None + img_re = re.compile(r'''registerImage\("original_image", "([^"]+)"''') + try: + prod_data = urllib.urlopen(url).read() + except: + self.important("timeout opening %s"%(url)) + return None + m = img_re.search(prod_data) + if not m: + return None + img_url = m.group(1) + return img_url - def _SearchAmazon(self): - data = { - "Service": "AWSECommerceService", - "Version": "2005-03-23", - "Operation": "ItemSearch", - "ContentType": "text/xml", - "SubscriptionId": AMAZON_AWS_ID, - "SearchIndex": "Music", - "ResponseGroup": "Small", - } - - data["Artist"] = self.artist - data["Keywords"] = self.album - - fd = urllib.urlopen("%s?%s" % (self.awsurl, urllib.urlencode(data))) - return fd.read() + def _SearchAmazon(self): + data = { + "Service": "AWSECommerceService", + "Version": "2005-03-23", + "Operation": "ItemSearch", + "ContentType": "text/xml", + "SubscriptionId": AMAZON_AWS_ID, + "SearchIndex": "Music", + "ResponseGroup": "Small", + } + + data["Artist"] = self.artist + data["Keywords"] = self.album + + fd = urllib.urlopen("%s?%s" % (self.awsurl, urllib.urlencode(data))) + return fd.read() - def _GetResultURL(self, xmldata): - if not xmldata: - return None - url_re = re.compile(r"<DetailPageURL>([^<]+)</DetailPageURL>") - m = url_re.search(xmldata) - return m and m.group(1) + def _GetResultURL(self, xmldata): + if not xmldata: + return None + url_re = re.compile(r"<DetailPageURL>([^<]+)</DetailPageURL>") + m = url_re.search(xmldata) + return m and m.group(1) diff --git a/plugins/Library.py b/plugins/Library.py index b1d86e9..bcd4ae7 100644 --- a/plugins/Library.py +++ b/plugins/Library.py @@ -6,69 +6,69 @@ from wgPlaylist import Playlist from wgSongList import clrRowSel LIBRARY_MODES_DEFAULT='$artist\n'\ - '$artist/$date - $album\n'\ - '$artist - $album\n'\ - '$album ($artist)\n'\ - '$genre\n'\ - '$genre/$artist\n'\ - '$genre/$artist - $album\n' + '$artist/$date - $album\n'\ + '$artist - $album\n'\ + '$album ($artist)\n'\ + '$genre\n'\ + '$genre/$artist\n'\ + '$genre/$artist - $album\n' class pluginLibrary(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Library') - def _load(self): - self.o=Playlist(self.winMain, self, ['song'], 'Library' - , self.onDoubleClick, self.onKeyPress, self.getSetting('modes').split('\n')) - def _unload(self): - self.o=None - def getInfo(self): - return "List showing all the songs allowing filtering and grouping." - - def getList(self): - return self.o - - def _getDockWidget(self): - return self._createDock(self.o) - - def onDoubleClick(self): - self.addLibrarySelToPlaylist() + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Library') + def _load(self): + self.o=Playlist(self.winMain, self, ['song'], 'Library' + , self.onDoubleClick, self.onKeyPress, self.getSetting('modes').split('\n')) + def _unload(self): + self.o=None + def getInfo(self): + return "List showing all the songs allowing filtering and grouping." + + def getList(self): + return self.o + + def _getDockWidget(self): + return self._createDock(self.o) + + def onDoubleClick(self): + self.addLibrarySelToPlaylist() - def onKeyPress(self, event): - # Add selection, or entire library to playlist using ENTER-key. - if event.key()==QtCore.Qt.Key_Enter or event.key()==QtCore.Qt.Key_Return: - self.addLibrarySelToPlaylist() - return QtGui.QWidget.keyPressEvent(self.o, event) + def onKeyPress(self, event): + # Add selection, or entire library to playlist using ENTER-key. + if event.key()==QtCore.Qt.Key_Enter or event.key()==QtCore.Qt.Key_Return: + self.addLibrarySelToPlaylist() + return QtGui.QWidget.keyPressEvent(self.o, event) - def addLibrarySelToPlaylist(self): - """Add the library selection to the playlist.""" - songs=self.o.selectedSongs() - self.setStatus('Adding '+str(len(songs))+' songs to library ...') - doEvents() + def addLibrarySelToPlaylist(self): + """Add the library selection to the playlist.""" + songs=self.o.selectedSongs() + self.setStatus('Adding '+str(len(songs))+' songs to library ...') + doEvents() - # add filepaths of selected songs to path - paths=map(lambda song: unicode(song.getFilepath()), songs) - # add in chunks of 1000 - CHUNK_SIZE=1000 - start=0 - while start<len(paths): - if start+CHUNK_SIZE<len(paths): - end=start+CHUNK_SIZE - else: - end=len(paths) - self.setStatus('Adding '+str(len(songs))+' songs to library: %i%%'%(int(100*start/len(paths)))) - doEvents() - monty.addToPlaylist(paths[start:end]) - start+=CHUNK_SIZE + # add filepaths of selected songs to path + paths=map(lambda song: unicode(song.getFilepath()), songs) + # add in chunks of 1000 + CHUNK_SIZE=1000 + start=0 + while start<len(paths): + if start+CHUNK_SIZE<len(paths): + end=start+CHUNK_SIZE + else: + end=len(paths) + self.setStatus('Adding '+str(len(songs))+' songs to library: %i%%'%(int(100*start/len(paths)))) + doEvents() + monty.addToPlaylist(paths[start:end]) + start+=CHUNK_SIZE - self.setStatus('') - doEvents() - self.getWinMain().fillPlaylist() + self.setStatus('') + doEvents() + self.getWinMain().fillPlaylist() - def _getSettings(self): - modes=QtGui.QTextEdit() - modes.insertPlainText(self.getSetting('modes')) - return [ - ['modes', 'Modes', 'Sets the available modes.', modes], - ] - def afterSaveSettings(self): - self.o.setModes(self.getSetting('modes').split('\n')) + def _getSettings(self): + modes=QtGui.QTextEdit() + modes.insertPlainText(self.getSetting('modes')) + return [ + ['modes', 'Modes', 'Sets the available modes.', modes], + ] + def afterSaveSettings(self): + self.o.setModes(self.getSetting('modes').split('\n')) diff --git a/plugins/Logger.py b/plugins/Logger.py index 575fd5b..4e8ce6b 100644 --- a/plugins/Logger.py +++ b/plugins/Logger.py @@ -6,62 +6,62 @@ from misc import Button LOGGER_LEVEL_DEFAULT=log.LOG_NORMAL class wgLogger(QtGui.QWidget): - """Shows log information""" - " list containing the log" - log=None - btnClear=None - p=None - def __init__(self, p, parent=None): - QtGui.QWidget.__init__(self, parent) - self.p=p - - self.log=QtGui.QListWidget(self) - self.btnClear=Button("Clear", self.clear) - self.cmbLevel=QtGui.QComboBox(self) - - self.cmbLevel.addItem("Nothing") - self.cmbLevel.addItem("Important") - self.cmbLevel.addItem("Normal") - self.cmbLevel.addItem("Extended") - self.cmbLevel.addItem("Debug") - - self.cmbLevel.setCurrentIndex(int(self.p.getSetting("level"))) - self.onCmbLevelChanged(self.cmbLevel.currentIndex()) + """Shows log information""" + " list containing the log" + log=None + btnClear=None + p=None + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p + + self.log=QtGui.QListWidget(self) + self.btnClear=Button("Clear", self.clear) + self.cmbLevel=QtGui.QComboBox(self) + + self.cmbLevel.addItem("Nothing") + self.cmbLevel.addItem("Important") + self.cmbLevel.addItem("Normal") + self.cmbLevel.addItem("Extended") + self.cmbLevel.addItem("Debug") + + self.cmbLevel.setCurrentIndex(int(self.p.getSetting("level"))) + self.onCmbLevelChanged(self.cmbLevel.currentIndex()) - layout=QtGui.QVBoxLayout() - layout2=QtGui.QHBoxLayout() - self.setLayout(layout) + layout=QtGui.QVBoxLayout() + layout2=QtGui.QHBoxLayout() + self.setLayout(layout) - layout2.addWidget(self.cmbLevel) - layout2.addWidget(self.btnClear) - layout.addLayout(layout2) - layout.addWidget(self.log) - - self.connect(self.cmbLevel, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbLevelChanged) - - def onCmbLevelChanged(self, newval): - log.setLevel(newval) - log.extended("Switching loglevel to %s"%(str(newval))) - settings.set("logger.level", newval) - def writer(self, item, level): - self.log.addItem(item) - self.log.setCurrentRow(self.log.count()-1) - #self.log.scrollToItem(self.log.item(self.log.count())) - def clear(self): - self.log.clear() + layout2.addWidget(self.cmbLevel) + layout2.addWidget(self.btnClear) + layout.addLayout(layout2) + layout.addWidget(self.log) + + self.connect(self.cmbLevel, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbLevelChanged) + + def onCmbLevelChanged(self, newval): + log.setLevel(newval) + log.extended("Switching loglevel to %s"%(str(newval))) + settings.set("logger.level", newval) + def writer(self, item, level): + self.log.addItem(item) + self.log.setCurrentRow(self.log.count()-1) + #self.log.scrollToItem(self.log.item(self.log.count())) + def clear(self): + self.log.clear() class pluginLogger(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Logger') - def _load(self): - self.o=wgLogger(self, None) - log.setWriter(self.o.writer) - def _unload(self): - self.o=None - log.setWriter(log._writer) - def getInfo(self): - return "Shows information from the log." - - def _getDockWidget(self): - return self._createDock(self.o) + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Logger') + def _load(self): + self.o=wgLogger(self, None) + log.setWriter(self.o.writer) + def _unload(self): + self.o=None + log.setWriter(log._writer) + def getInfo(self): + return "Shows information from the log." + + def _getDockWidget(self): + return self._createDock(self.o) diff --git a/plugins/Lyrics.py b/plugins/Lyrics.py index 2e4d31c..840ec06 100644 --- a/plugins/Lyrics.py +++ b/plugins/Lyrics.py @@ -13,273 +13,273 @@ from clMonty import monty from clPlugin import * class ResetEvent(QtCore.QEvent): - song=None - def __init__(self, song=None): - QtCore.QEvent.__init__(self,QtCore.QEvent.User) - self.song=song + song=None + def __init__(self, song=None): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.song=song class AddHtmlEvent(QtCore.QEvent): - html=None - def __init__(self,html): - QtCore.QEvent.__init__(self,QtCore.QEvent.User) - self.html=html + html=None + def __init__(self,html): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.html=html class AddTextEvent(QtCore.QEvent): - text=None - def __init__(self,text): - QtCore.QEvent.__init__(self,QtCore.QEvent.User) - self.text=text + text=None + def __init__(self,text): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.text=text LYRICS_ENGINE_DEFAULT='http://www.google.com/search?q=lyrics+"$artist"+"$title"' LYRICS_SITES_DEFAULT=\ - 'absolutelyrics.com <div id="realText">(.*?)</div>\n'\ - 'azlyrics.com <br><br>.*?<br><br>(.*?)<br><br>\n'\ - 'oldielyrics.com song_in_top2.*?<br>(.*?)<script\n'\ - 'lyricstime.com phone-left.gif.*?<p>(.*?)</p>\n'\ - 'lyricsfire.com class="lyric">.*?Song.*?<br>(.*?)</pre>\n'\ - 'lyricsfreak.com <div.*?id="content".*?>(.*?)<blockquote\n'\ - 'artists.letssingit.com <pre>(.*?)</pre>\n'\ - 'gugalyrics.com </h1>(.*?)<a\n'\ - 'lyricsmania.com </strong> :(.*?)[\n'\ - 'leoslyrics.com <font face=.*?size=-1>(.*?)</font>\n'\ - 'bluesforpeace.com <blockquote>(.*?)</blockquote>\n' + 'absolutelyrics.com <div id="realText">(.*?)</div>\n'\ + 'azlyrics.com <br><br>.*?<br><br>(.*?)<br><br>\n'\ + 'oldielyrics.com song_in_top2.*?<br>(.*?)<script\n'\ + 'lyricstime.com phone-left.gif.*?<p>(.*?)</p>\n'\ + 'lyricsfire.com class="lyric">.*?Song.*?<br>(.*?)</pre>\n'\ + 'lyricsfreak.com <div.*?id="content".*?>(.*?)<blockquote\n'\ + 'artists.letssingit.com <pre>(.*?)</pre>\n'\ + 'gugalyrics.com </h1>(.*?)<a\n'\ + 'lyricsmania.com </strong> :(.*?)[\n'\ + 'leoslyrics.com <font face=.*?size=-1>(.*?)</font>\n'\ + 'bluesforpeace.com <blockquote>(.*?)</blockquote>\n' LYRICS_DOWNLOADTO_DEFAULT='~/.lyrics/$artist/$artist - $title.txt' LYRICS_AUTOSCROLL_DEFAULT='1' class wgLyrics(QtGui.QWidget): - " contains the lyrics" - txtView=None # text-object - curLyrics=None # current lyrics - btnEdit=None - btnRefetch=None - btnSave=None - btnSearch=None - editMode=False - lyFormat=None - p=None # plugin - def __init__(self, p, parent=None): - QtGui.QWidget.__init__(self, parent) - self.p=p - self.curLyrics="" - self.btnEdit=Button("Edit lyrics", self.onBtnEditClick) - self.btnRefetch=Button("Refetch", self.onBtnRefetchClick) - self.btnSave=Button("Save lyrics", self.onBtnSaveClick) - self.btnSearch=Button("Search www", self.onBtnSearch) - - self.txtView=QtGui.QTextEdit(parent) - self.txtView.setReadOnly(True) - - self.setMode(False) - - layout=QtGui.QVBoxLayout() - hlayout=QtGui.QHBoxLayout() - layout.addLayout(hlayout) - hlayout.addWidget(self.btnEdit) - hlayout.addWidget(self.btnSave) - hlayout.addWidget(self.btnRefetch) - hlayout.addWidget(self.btnSearch) - layout.addWidget(self.txtView) - self.setLayout(layout) + " contains the lyrics" + txtView=None # text-object + curLyrics=None # current lyrics + btnEdit=None + btnRefetch=None + btnSave=None + btnSearch=None + editMode=False + lyFormat=None + p=None # plugin + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p + self.curLyrics="" + self.btnEdit=Button("Edit lyrics", self.onBtnEditClick) + self.btnRefetch=Button("Refetch", self.onBtnRefetchClick) + self.btnSave=Button("Save lyrics", self.onBtnSaveClick) + self.btnSearch=Button("Search www", self.onBtnSearch) + + self.txtView=QtGui.QTextEdit(parent) + self.txtView.setReadOnly(True) + + self.setMode(False) + + layout=QtGui.QVBoxLayout() + hlayout=QtGui.QHBoxLayout() + layout.addLayout(hlayout) + hlayout.addWidget(self.btnEdit) + hlayout.addWidget(self.btnSave) + hlayout.addWidget(self.btnRefetch) + hlayout.addWidget(self.btnSearch) + layout.addWidget(self.txtView) + self.setLayout(layout) - - def setMode(self, mode): - self.editMode=mode - self.btnSave.setVisible(mode) - self.btnEdit.setVisible(not mode) - self.txtView.setReadOnly(not mode) - - def onBtnSearch(self): - SE=self.p.getSetting('engine') - f=format.compile(SE) - SE_url=toAscii(f(format.params(monty.getCurrentSong()))) - webbrowser.open(urllib.quote(SE_url, ":/+?=")) - - def onBtnEditClick(self): - self.setMode(True) - self.txtView.setPlainText(self.curLyrics) - - def onBtnSaveClick(self): - self.setMode(False) - self.curLyrics=self.txtView.toPlainText() - file=open(self.getLyricsFilePath(monty.getCurrentSong()), 'w') - file.write(self.curLyrics) - file.close() - - # just to test if everything's still fine - self.fetchLyrics(monty.getCurrentSong()) - - def onBtnRefetchClick(self): - # force refetch - lyFName=self.getLyricsFilePath(monty.getCurrentSong()) - try: - os.remove(lyFName) - except: - pass - self.refresh() - - def refresh(self): - # if we're editing while song changes, too bad: we don't save, for the moment! - if self.editMode: - self.setMode(False) - - self.lyFormat=format.compile(self.p.getSetting('downloadto')) - song=monty.getCurrentSong() - try: - song._data['file'] - except: - self.resetTxt() - return - - self.resetTxt(song) - start_new_thread(self.fetchLyrics, (song,)) - - def customEvent(self, event): - if isinstance(event,ResetEvent): - self.resetTxt(event.song) - elif isinstance(event,AddTextEvent): - self.txtView.insertPlainText(event.text) - elif isinstance(event,AddHtmlEvent): - self.txtView.insertHtml(event.html) + + def setMode(self, mode): + self.editMode=mode + self.btnSave.setVisible(mode) + self.btnEdit.setVisible(not mode) + self.txtView.setReadOnly(not mode) + + def onBtnSearch(self): + SE=self.p.getSetting('engine') + f=format.compile(SE) + SE_url=toAscii(f(format.params(monty.getCurrentSong()))) + webbrowser.open(urllib.quote(SE_url, ":/+?=")) + + def onBtnEditClick(self): + self.setMode(True) + self.txtView.setPlainText(self.curLyrics) + + def onBtnSaveClick(self): + self.setMode(False) + self.curLyrics=self.txtView.toPlainText() + file=open(self.getLyricsFilePath(monty.getCurrentSong()), 'w') + file.write(self.curLyrics) + file.close() + + # just to test if everything's still fine + self.fetchLyrics(monty.getCurrentSong()) + + def onBtnRefetchClick(self): + # force refetch + lyFName=self.getLyricsFilePath(monty.getCurrentSong()) + try: + os.remove(lyFName) + except: + pass + self.refresh() + + def refresh(self): + # if we're editing while song changes, too bad: we don't save, for the moment! + if self.editMode: + self.setMode(False) + + self.lyFormat=format.compile(self.p.getSetting('downloadto')) + song=monty.getCurrentSong() + try: + song._data['file'] + except: + self.resetTxt() + return + + self.resetTxt(song) + start_new_thread(self.fetchLyrics, (song,)) + + def customEvent(self, event): + if isinstance(event,ResetEvent): + self.resetTxt(event.song) + elif isinstance(event,AddTextEvent): + self.txtView.insertPlainText(event.text) + elif isinstance(event,AddHtmlEvent): + self.txtView.insertHtml(event.html) - def getLyricsFilePath(self, song): - params={'music_dir': mpdSettings.get('music_directory')} - path=self.lyFormat(format.params(song, params)) - return toAscii(os.path.expanduser(path)) - - _mutex=QtCore.QMutex() - _fetchCnt=0 - def fetchLyrics(self, song): - # only allow 1 instance to look lyrics! - self._mutex.lock() - if self._fetchCnt: - self._mutex.unlock() - return - self._fetchCnt=1 - self._mutex.unlock() + def getLyricsFilePath(self, song): + params={'music_dir': mpdSettings.get('music_directory')} + path=self.lyFormat(format.params(song, params)) + return toAscii(os.path.expanduser(path)) + + _mutex=QtCore.QMutex() + _fetchCnt=0 + def fetchLyrics(self, song): + # only allow 1 instance to look lyrics! + self._mutex.lock() + if self._fetchCnt: + self._mutex.unlock() + return + self._fetchCnt=1 + self._mutex.unlock() - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - - lyFName=self.getLyricsFilePath(song) - self.p.debug("checking %s"%(lyFName)) - self.curLyrics="" - # does the file exist? if yes, read that one! - try: - file=open(lyFName, 'r') - self.curLyrics=file.read() - file.close() - QtCore.QCoreApplication.postEvent(self, AddTextEvent(self.curLyrics)) - self._fetchCnt=0 - self.p.extended("Fetch lyrics from file") - return - except Exception, e: - self.p.debug("fail - %s"%(str(e))) + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + + lyFName=self.getLyricsFilePath(song) + self.p.debug("checking %s"%(lyFName)) + self.curLyrics="" + # does the file exist? if yes, read that one! + try: + file=open(lyFName, 'r') + self.curLyrics=file.read() + file.close() + QtCore.QCoreApplication.postEvent(self, AddTextEvent(self.curLyrics)) + self._fetchCnt=0 + self.p.extended("Fetch lyrics from file") + return + except Exception, e: + self.p.debug("fail - %s"%(str(e))) - # fetch from inet - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<i>Searching lyrics ...</i>')) - self.p.extended("Fetch lyrics from internet") - - lines=self.p.getSetting('sites').split('\n') - sites={} - for line in lines: - if line.strip(): - sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip() - # construct URL to search! - SE=self.p.getSetting('engine') - try: - ret=fetch(SE, sites, song, {}) - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - if ret: - self.p.extended("Success!") - self.curLyrics=ret[0] - # save for later use! - if lyFName: - # we can't save if the path isn't correct - try: - self.p.extended("Saving to %s"%(lyFName)) - try: - # fails when dir exists - os.makedirs(os.path.dirname(os.path.expanduser(lyFName))) - except Exception, e: - pass - file=open(lyFName, 'w') - file.write(self.curLyrics) - file.close() - except Exception, e: - # probably a wrong path! - self.p.normal("Failed to write lyrics %s"%(str(e))) - else: - self.curLyrics="" - txt="No lyrics found :'(" - self.p.extended("Lyrics not found!") - QtCore.QCoreApplication.postEvent(self, AddTextEvent(txt)) - - QtCore.QCoreApplication.postEvent(self, AddTextEvent(self.curLyrics)) - if ret: - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<br /><br /><a href="%s">%s</a>'%(ret[1],ret[1]))) + # fetch from inet + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<i>Searching lyrics ...</i>')) + self.p.extended("Fetch lyrics from internet") + + lines=self.p.getSetting('sites').split('\n') + sites={} + for line in lines: + if line.strip(): + sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip() + # construct URL to search! + SE=self.p.getSetting('engine') + try: + ret=fetch(SE, sites, song, {}) + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + if ret: + self.p.extended("Success!") + self.curLyrics=ret[0] + # save for later use! + if lyFName: + # we can't save if the path isn't correct + try: + self.p.extended("Saving to %s"%(lyFName)) + try: + # fails when dir exists + os.makedirs(os.path.dirname(os.path.expanduser(lyFName))) + except Exception, e: + pass + file=open(lyFName, 'w') + file.write(self.curLyrics) + file.close() + except Exception, e: + # probably a wrong path! + self.p.normal("Failed to write lyrics %s"%(str(e))) + else: + self.curLyrics="" + txt="No lyrics found :'(" + self.p.extended("Lyrics not found!") + QtCore.QCoreApplication.postEvent(self, AddTextEvent(txt)) + + QtCore.QCoreApplication.postEvent(self, AddTextEvent(self.curLyrics)) + if ret: + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<br /><br /><a href="%s">%s</a>'%(ret[1],ret[1]))) - except Exception, e: - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, error! Possible causes:'\ - '<br />no internet connection?'\ - '<br />site unavailable'\ - '<br />an error in the fetching regular expression'\ - '<br />(exception: %s)'%(str(e)))) - self._fetchCnt=0 + except Exception, e: + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, error! Possible causes:'\ + '<br />no internet connection?'\ + '<br />site unavailable'\ + '<br />an error in the fetching regular expression'\ + '<br />(exception: %s)'%(str(e)))) + self._fetchCnt=0 - def resetTxt(self, song=None): - self.txtView.clear() - if song: - self.txtView.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\ - '<br />\n\n'%(song.getTitle(), song.getArtist())) - - def autoScroll(self, time): - t=self.txtView - max=t.verticalScrollBar().maximum() - if max<=0: - return - t.verticalScrollBar().setValue((max+t.height()/t.currentFont().pointSize())*time/monty.getCurrentSong()._data['time']) - + def resetTxt(self, song=None): + self.txtView.clear() + if song: + self.txtView.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\ + '<br />\n\n'%(song.getTitle(), song.getArtist())) + + def autoScroll(self, time): + t=self.txtView + max=t.verticalScrollBar().maximum() + if max<=0: + return + t.verticalScrollBar().setValue((max+t.height()/t.currentFont().pointSize())*time/monty.getCurrentSong()._data['time']) + class pluginLyrics(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Lyrics') - self.addMontyListener('onSongChange', self.refresh) - self.addMontyListener('onReady', self.refresh) - self.addMontyListener('onDisconnect', self.onDisconnect) - self.addMontyListener('onTimeChange', self.onTimeChange) - def _load(self): - self.o=wgLyrics(self, None) - self.o.refresh() - def _unload(self): - self.o=None - def getInfo(self): - return "Show (and fetch) the lyrics of the currently playing song." - - def _getDockWidget(self): - return self._createDock(self.o) + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Lyrics') + self.addMontyListener('onSongChange', self.refresh) + self.addMontyListener('onReady', self.refresh) + self.addMontyListener('onDisconnect', self.onDisconnect) + self.addMontyListener('onTimeChange', self.onTimeChange) + def _load(self): + self.o=wgLyrics(self, None) + self.o.refresh() + def _unload(self): + self.o=None + def getInfo(self): + return "Show (and fetch) the lyrics of the currently playing song." + + def _getDockWidget(self): + return self._createDock(self.o) - def refresh(self, params): - self.o.refresh() - def onDisconnect(self, params): - self.o.resetTxt() - def onTimeChange(self, params): - if self.getSetting("autoScroll")!="0" and self.o.editMode==False: - # could be done better, but this is just plain simple :) - self.o.autoScroll(params['newTime']) + def refresh(self, params): + self.o.refresh() + def onDisconnect(self, params): + self.o.resetTxt() + def onTimeChange(self, params): + if self.getSetting("autoScroll")!="0" and self.o.editMode==False: + # could be done better, but this is just plain simple :) + self.o.autoScroll(params['newTime']) - def _getSettings(self): - sites=QtGui.QTextEdit() - sites.insertPlainText(self.getSetting('sites')) - autoscroll=QtGui.QCheckBox("Autoscroll") - autoscroll.setCheckState(QtCore.Qt.Checked if self.getSetting("autoScroll")!=0 else QtCore.Qt.Unchecked) - return [ - ['engine', 'Search engine', 'The URL that is used to search. $artist, $title and $album are replaced in the URL.', QtGui.QLineEdit(self.getSetting('engine'))], - ['sites', 'Sites & regexes', 'This field contains all sites, together with the regex needed to fetch the lyrics.\nEvery line must look like this: $domain $regex-start(.*?)$regex-end\n$domain is the domain of the lyrics website, $regex-start is the regex indicating the start of the lyrics, $regex-end indicates the end. E.g. foolyrics.org <lyrics>(.*?)</lyrics>', sites], - ['downloadto', 'Target', 'Specifies where to save lyrics fetched from internet to.\nPossible tags: music_dir, file, artist, title, album', QtGui.QLineEdit(self.getSetting('downloadto'))], - ['autoScroll', 'Autoscroll', 'Check this box to scroll the contents automatically as the time advances', autoscroll], - ] - def afterSaveSettings(self): - self.o.refresh() + def _getSettings(self): + sites=QtGui.QTextEdit() + sites.insertPlainText(self.getSetting('sites')) + autoscroll=QtGui.QCheckBox("Autoscroll") + autoscroll.setCheckState(QtCore.Qt.Checked if self.getSetting("autoScroll")!=0 else QtCore.Qt.Unchecked) + return [ + ['engine', 'Search engine', 'The URL that is used to search. $artist, $title and $album are replaced in the URL.', QtGui.QLineEdit(self.getSetting('engine'))], + ['sites', 'Sites & regexes', 'This field contains all sites, together with the regex needed to fetch the lyrics.\nEvery line must look like this: $domain $regex-start(.*?)$regex-end\n$domain is the domain of the lyrics website, $regex-start is the regex indicating the start of the lyrics, $regex-end indicates the end. E.g. foolyrics.org <lyrics>(.*?)</lyrics>', sites], + ['downloadto', 'Target', 'Specifies where to save lyrics fetched from internet to.\nPossible tags: music_dir, file, artist, title, album', QtGui.QLineEdit(self.getSetting('downloadto'))], + ['autoScroll', 'Autoscroll', 'Check this box to scroll the contents automatically as the time advances', autoscroll], + ] + def afterSaveSettings(self): + self.o.refresh() diff --git a/plugins/MPD.py b/plugins/MPD.py index dd41230..c7e8c9c 100644 --- a/plugins/MPD.py +++ b/plugins/MPD.py @@ -8,21 +8,21 @@ MPD_HOST_DEFAULT='localhost' MPD_PORT_DEFAULT='6600' class pluginMPD(Plugin): - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'MPD') - def getInfo(self): - return "Provides an interface to the MPD settings." - - - def _getSettings(self): - return [ - ['host', 'Host', 'Host where mpd resides.', QtGui.QLineEdit(self.getSetting('host'))], - ['port', 'Port', 'Port of mpd.', QtGui.QLineEdit(self.getSetting('port'))], - ['music_directory', 'Music directory', 'Root directory where all music is located.', QtGui.QLineEdit(mpdSettings.get('music_directory')), mpdSettings], - ['', 'Update database', 'Updates the database.\nUse this if you have changed the music_directory. Updating will save all entries on the MPD tab.', Button('Update db', self.onBtnUpdateDBClick)], - ] - def onBtnUpdateDBClick(self): - self.saveSettings() - monty.updateDB([mpdSettings.get('music_directory')]) - pass + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'MPD') + def getInfo(self): + return "Provides an interface to the MPD settings." + + + def _getSettings(self): + return [ + ['host', 'Host', 'Host where mpd resides.', QtGui.QLineEdit(self.getSetting('host'))], + ['port', 'Port', 'Port of mpd.', QtGui.QLineEdit(self.getSetting('port'))], + ['music_directory', 'Music directory', 'Root directory where all music is located.', QtGui.QLineEdit(mpdSettings.get('music_directory')), mpdSettings], + ['', 'Update database', 'Updates the database.\nUse this if you have changed the music_directory. Updating will save all entries on the MPD tab.', Button('Update db', self.onBtnUpdateDBClick)], + ] + def onBtnUpdateDBClick(self): + self.saveSettings() + monty.updateDB([mpdSettings.get('music_directory')]) + pass diff --git a/plugins/Notify.py b/plugins/Notify.py index 14e35af..4c6de63 100644 --- a/plugins/Notify.py +++ b/plugins/Notify.py @@ -12,156 +12,156 @@ NOTIFY_SONGFORMAT_DEFAULT='$if($artist,$artist)$if($album, - [$album #$track])\n NOTIFY_TIMER_DEFAULT=3 class winNotify(QtGui.QWidget): - _timerID=None - resizeWindow=True - winMain=None - p=None + _timerID=None + resizeWindow=True + winMain=None + p=None - # data used for showing off - timer=None - msg=None - song=None - xtra_tags=None + # data used for showing off + timer=None + msg=None + song=None + xtra_tags=None - def __init__(self, p, winMain, parent=None): - QtGui.QWidget.__init__(self, parent) - self.p=p - self.winMain=winMain - - self.setWindowFlags(QtCore.Qt.ToolTip) - self.setWindowOpacity(0.7) - - font=QtGui.QFont() - font.setPixelSize(30) - font.setFamily('Comic Sans Ms') - self.setFont(font) - - def mousePressEvent(self, event): - self.hide() + def __init__(self, p, winMain, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p + self.winMain=winMain + + self.setWindowFlags(QtCore.Qt.ToolTip) + self.setWindowOpacity(0.7) + + font=QtGui.QFont() + font.setPixelSize(30) + font.setFamily('Comic Sans Ms') + self.setFont(font) + + def mousePressEvent(self, event): + self.hide() - def show(self, msg, song=None, xtra_tags={}, time=3): - if not self.isVisible(): - self.setVisible(True) - self.resizeWindow=True - self.msg=format.compile(msg) - self.song=song - self.xtra_tags=xtra_tags - if self._timerID: - self.killTimer(self._timerID) - self._timerID=self.startTimer(500) - try: - self.timer=int(time)*2 - except: - self.timer=3 - self.raise_() - self.timerEvent(None) - - def hide(self): - if self._timerID: - self.killTimer(self._timerID) - self._timerID=None - self.setHidden(True) + def show(self, msg, song=None, xtra_tags={}, time=3): + if not self.isVisible(): + self.setVisible(True) + self.resizeWindow=True + self.msg=format.compile(msg) + self.song=song + self.xtra_tags=xtra_tags + if self._timerID: + self.killTimer(self._timerID) + self._timerID=self.startTimer(500) + try: + self.timer=int(time)*2 + except: + self.timer=3 + self.raise_() + self.timerEvent(None) + + def hide(self): + if self._timerID: + self.killTimer(self._timerID) + self._timerID=None + self.setHidden(True) - - def centerH(self): - screen = QtGui.QDesktopWidget().screenGeometry() - size = self.geometry() - self.move((screen.width()-size.width())/2, 100) - def paintEvent(self, event): - p=QtGui.QPainter(self) - margin=3 # margin in pixels - spacing=10 # space between album cover and text - - # determine the Rect our message must fit in - txt=self.msg(format.params(self.song, self.xtra_tags)) - rect=p.boundingRect(0,0,1,1, QtCore.Qt.AlignHCenter, txt) - - # check if 1/ albumcover plugin is loaded, and 2/ there is an - # album cover - width=0 - try: - cover=plugins.getPlugin('albumcover') - img=cover.getWidget().getIMG() - if img: - width=128 - else: - spacing=0 - except: - img=None - pass - - # do we have to resize? - if self.resizeWindow: - self.resizeWindow=False - self.resize(rect.width()+width+margin*3+spacing, max(width,rect.height())+margin*2) - self.centerH() - - # fill up with a nice color :) - p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(QtGui.QColor(230,230,255))) - - # draw album cover if necessary - if img: - rImg=QtCore.QRectF(margin,margin,width,width) - p.drawImage(rImg, img) - - # Pen In Black ... - p.setPen(QtCore.Qt.black) - rect=p.boundingRect(width+margin+spacing,margin, - rect.width(),self.height(), QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter, txt) - p.drawText(rect, QtCore.Qt.AlignHCenter, txt) - - def timerEvent(self, event): - self.timer-=1 - if self.timer<=0: - self.hide() - self.update() - + + def centerH(self): + screen = QtGui.QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width()-size.width())/2, 100) + def paintEvent(self, event): + p=QtGui.QPainter(self) + margin=3 # margin in pixels + spacing=10 # space between album cover and text + + # determine the Rect our message must fit in + txt=self.msg(format.params(self.song, self.xtra_tags)) + rect=p.boundingRect(0,0,1,1, QtCore.Qt.AlignHCenter, txt) + + # check if 1/ albumcover plugin is loaded, and 2/ there is an + # album cover + width=0 + try: + cover=plugins.getPlugin('albumcover') + img=cover.getWidget().getIMG() + if img: + width=128 + else: + spacing=0 + except: + img=None + pass + + # do we have to resize? + if self.resizeWindow: + self.resizeWindow=False + self.resize(rect.width()+width+margin*3+spacing, max(width,rect.height())+margin*2) + self.centerH() + + # fill up with a nice color :) + p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(QtGui.QColor(230,230,255))) + + # draw album cover if necessary + if img: + rImg=QtCore.QRectF(margin,margin,width,width) + p.drawImage(rImg, img) + + # Pen In Black ... + p.setPen(QtCore.Qt.black) + rect=p.boundingRect(width+margin+spacing,margin, + rect.width(),self.height(), QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter, txt) + p.drawText(rect, QtCore.Qt.AlignHCenter, txt) + + def timerEvent(self, event): + self.timer-=1 + if self.timer<=0: + self.hide() + self.update() + class pluginNotify(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Notify') - self.addMontyListener('onSongChange', self.onSongChange) - self.addMontyListener('onReady', self.onReady) - self.addMontyListener('onDisconnect', self.onDisconnect) - self.addMontyListener('onStateChange', self.onStateChange) - self.addMontyListener('onVolumeChange', self.onVolumeChange) - - def _load(self): - self.o=winNotify(self, self.winMain) - def _unload(self): - self.o=None - def getInfo(self): - return "Show interesting events in a popup window." + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Notify') + self.addMontyListener('onSongChange', self.onSongChange) + self.addMontyListener('onReady', self.onReady) + self.addMontyListener('onDisconnect', self.onDisconnect) + self.addMontyListener('onStateChange', self.onStateChange) + self.addMontyListener('onVolumeChange', self.onVolumeChange) + + def _load(self): + self.o=winNotify(self, self.winMain) + def _unload(self): + self.o=None + def getInfo(self): + return "Show interesting events in a popup window." - def onSongChange(self, params): - self.o.show(self.getSetting('songformat').replace("\n", "\\n"), monty.getCurrentSong() - , time=self.getSetting('timer')) + def onSongChange(self, params): + self.o.show(self.getSetting('songformat').replace("\n", "\\n"), monty.getCurrentSong() + , time=self.getSetting('timer')) - def onReady(self, params): - self.o.show('montypc loaded!', monty.getCurrentSong(), time=self.getSetting('timer')) - - def onDisconnect(self, params): - self.o.show('Disconnected!', time=self.getSetting('timer')) - - def onStateChange(self, params): - self.o.show(params['newState'], monty.getCurrentSong(), time=self.getSetting('timer')) - - def onVolumeChange(self, params): - self.o.show('Volume: %i%%'%(params['newVolume']), monty.getCurrentSong(), time=self.getSetting('timer')) - - def _getSettings(self): - txt=QtGui.QTextEdit() - txt.insertPlainText(self.getSetting('songformat')) - return [ - ['songformat', 'Song format', 'How to format the current playing song.', txt], - ['timer', 'Show seconds', 'How many seconds does the notification have to be shown.', QtGui.QLineEdit(str(self.getSetting('timer')))], - ] - def afterSaveSettings(self): - try: - int(self.getSetting('timer')) - except: - self.getSetting('timer') - self.getSettingWidget('notify.timer').setText(str(NOTIFY_DEFAULT_TIMER)) - self.onSongChange(None) + def onReady(self, params): + self.o.show('montypc loaded!', monty.getCurrentSong(), time=self.getSetting('timer')) + + def onDisconnect(self, params): + self.o.show('Disconnected!', time=self.getSetting('timer')) + + def onStateChange(self, params): + self.o.show(params['newState'], monty.getCurrentSong(), time=self.getSetting('timer')) + + def onVolumeChange(self, params): + self.o.show('Volume: %i%%'%(params['newVolume']), monty.getCurrentSong(), time=self.getSetting('timer')) + + def _getSettings(self): + txt=QtGui.QTextEdit() + txt.insertPlainText(self.getSetting('songformat')) + return [ + ['songformat', 'Song format', 'How to format the current playing song.', txt], + ['timer', 'Show seconds', 'How many seconds does the notification have to be shown.', QtGui.QLineEdit(str(self.getSetting('timer')))], + ] + def afterSaveSettings(self): + try: + int(self.getSetting('timer')) + except: + self.getSetting('timer') + self.getSettingWidget('notify.timer').setText(str(NOTIFY_DEFAULT_TIMER)) + self.onSongChange(None) diff --git a/plugins/PlayControl.py b/plugins/PlayControl.py index 586527c..be7006b 100644 --- a/plugins/PlayControl.py +++ b/plugins/PlayControl.py @@ -12,15 +12,15 @@ import plugins # Note that REPEAT precedes RANDOM. E.g. if repeat # is ALBUM, and random is SONG, then it'll take a random song # from the current album ... -PC_RANDOM_NO=0 # no randomness -PC_RANDOM_SONG=1 # choose a song random from the playlist -PC_RANDOM_ALBUM=2 # choose a random album, and play that fully -PC_RANDOM_QUEUE=3 # choose next song from the queue +PC_RANDOM_NO=0 # no randomness +PC_RANDOM_SONG=1 # choose a song random from the playlist +PC_RANDOM_ALBUM=2 # choose a random album, and play that fully +PC_RANDOM_QUEUE=3 # choose next song from the queue -PC_REPEAT_NO=0 # no repeat -PC_REPEAT_SONG=1 # repeat current song -PC_REPEAT_ALBUM=2 # repeat current album -PC_REPEAT_PLAYLIST=3 # repeat playlist +PC_REPEAT_NO=0 # no repeat +PC_REPEAT_SONG=1 # repeat current song +PC_REPEAT_ALBUM=2 # repeat current album +PC_REPEAT_PLAYLIST=3 # repeat playlist PLAYCONTROL_SHUFFLE_DEFAULT=PC_RANDOM_ALBUM PLAYCONTROL_OLDSHUFFLE_DEFAULT=PLAYCONTROL_SHUFFLE_DEFAULT @@ -29,355 +29,355 @@ PLAYCONTROL_REPEAT_DEFAULT=PC_REPEAT_PLAYLIST class wgPlayControl(QtGui.QWidget): - """Displays controls for interacting with playing, like play, volume ...""" - " control buttons" - btnPlayPause=None - btnStop=None - btnPrevious=None - btnNext=None - " button to jump to current song" - btnJmpCurrent=None - " slider for current time" - slrTime=None - " slider for volume" - slrVolume=None - " indicator for volume" - svgVolume=None - " all objects in this widget" - objects=None + """Displays controls for interacting with playing, like play, volume ...""" + " control buttons" + btnPlayPause=None + btnStop=None + btnPrevious=None + btnNext=None + " button to jump to current song" + btnJmpCurrent=None + " slider for current time" + slrTime=None + " slider for volume" + slrVolume=None + " indicator for volume" + svgVolume=None + " all objects in this widget" + objects=None - cmbRepeat=None - cmbShuffle=None - p=None - - " contains the songs of the album the current song is playing. None, if the album is not set" - curAlbumSongs=None - - " queued songs: int*" - queuedSongs=[] - # what mode where we in before the queue started? - beforeQueuedMode=None + cmbRepeat=None + cmbShuffle=None + p=None + + " contains the songs of the album the current song is playing. None, if the album is not set" + curAlbumSongs=None + + " queued songs: int*" + queuedSongs=[] + # what mode where we in before the queue started? + beforeQueuedMode=None - def __init__(self, p, parent=None): - QtGui.QWidget.__init__(self, parent) - self.p=p + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p - class wgSvgSwitcher(QtSvg.QSvgWidget): - """Widget showing an svg-image, which, when clicked, will (un)hide an element.""" - # the element we wish to hide/show - scroller=None - def __init__(self,scroller,parent=None): - QtSvg.QSvgWidget.__init__(self,parent) - self.scroller=scroller - def mousePressEvent(self,event): - self.scroller.setVisible(not self.scroller.isVisible()) - def wheelEvent(self, event): - event.accept() - numDegrees=event.delta() / 8 - numSteps=5*numDegrees/15 - self.scroller.setValue(self.scroller.value()+numSteps) + class wgSvgSwitcher(QtSvg.QSvgWidget): + """Widget showing an svg-image, which, when clicked, will (un)hide an element.""" + # the element we wish to hide/show + scroller=None + def __init__(self,scroller,parent=None): + QtSvg.QSvgWidget.__init__(self,parent) + self.scroller=scroller + def mousePressEvent(self,event): + self.scroller.setVisible(not self.scroller.isVisible()) + def wheelEvent(self, event): + event.accept() + numDegrees=event.delta() / 8 + numSteps=5*numDegrees/15 + self.scroller.setValue(self.scroller.value()+numSteps) - self.slrTime=QtGui.QSlider(QtCore.Qt.Horizontal, self) - self.slrTime.setMinimumWidth(100) - self.slrVolume=QtGui.QSlider(QtCore.Qt.Vertical, self) - self.slrVolume.setMaximum(100) - self.slrVolume.setMinimumWidth(100) - self.slrVolume.setMaximumWidth(350) - # set to some value that'll never be chosen, that way onChange will be called automatically :) - self.slrVolume.setValue(3.141595) - self.slrVolume.setVisible(False) - self.svgVolume=wgSvgSwitcher(self.slrVolume) - self.svgVolume.setMaximumSize(64,64) - self.svgVolume.setMinimumSize(64,64) - self.btnPlayPause=Button("play", self.onBtnPlayPauseClick, 'gfx/media-playback-start.svg', True) - self.btnStop=Button("stop", self.onBtnStopClick, 'gfx/media-playback-stop.svg', True) - self.btnPrevious=Button("prev", self.onBtnPreviousClick, 'gfx/media-skip-backward.svg', True) - self.btnNext=Button("next", self.onBtnNextClick, 'gfx/media-skip-forward.svg', True) - self.btnJmpCurrent=Button("Current", self.onBtnJmpCurrentClick) + self.slrTime=QtGui.QSlider(QtCore.Qt.Horizontal, self) + self.slrTime.setMinimumWidth(100) + self.slrVolume=QtGui.QSlider(QtCore.Qt.Vertical, self) + self.slrVolume.setMaximum(100) + self.slrVolume.setMinimumWidth(100) + self.slrVolume.setMaximumWidth(350) + # set to some value that'll never be chosen, that way onChange will be called automatically :) + self.slrVolume.setValue(3.141595) + self.slrVolume.setVisible(False) + self.svgVolume=wgSvgSwitcher(self.slrVolume) + self.svgVolume.setMaximumSize(64,64) + self.svgVolume.setMinimumSize(64,64) + self.btnPlayPause=Button("play", self.onBtnPlayPauseClick, 'gfx/media-playback-start.svg', True) + self.btnStop=Button("stop", self.onBtnStopClick, 'gfx/media-playback-stop.svg', True) + self.btnPrevious=Button("prev", self.onBtnPreviousClick, 'gfx/media-skip-backward.svg', True) + self.btnNext=Button("next", self.onBtnNextClick, 'gfx/media-skip-forward.svg', True) + self.btnJmpCurrent=Button("Current", self.onBtnJmpCurrentClick) - self.cmbShuffle=QtGui.QComboBox(self) - self.cmbShuffle.addItem('Don\'t play dices') - self.cmbShuffle.addItem('Random song') - self.cmbShuffle.addItem('Random album') - self.cmbShuffle.addItem('Queue') - self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('shuffle'))) - - self.cmbRepeat=QtGui.QComboBox(self) - self.cmbRepeat.addItem('No repeat') - self.cmbRepeat.addItem('Repeat current song') - self.cmbRepeat.addItem('Repeat album') - self.cmbRepeat.addItem('Playlist') - self.cmbRepeat.setCurrentIndex(int(self.p.getSetting('repeat'))) + self.cmbShuffle=QtGui.QComboBox(self) + self.cmbShuffle.addItem('Don\'t play dices') + self.cmbShuffle.addItem('Random song') + self.cmbShuffle.addItem('Random album') + self.cmbShuffle.addItem('Queue') + self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('shuffle'))) + + self.cmbRepeat=QtGui.QComboBox(self) + self.cmbRepeat.addItem('No repeat') + self.cmbRepeat.addItem('Repeat current song') + self.cmbRepeat.addItem('Repeat album') + self.cmbRepeat.addItem('Playlist') + self.cmbRepeat.setCurrentIndex(int(self.p.getSetting('repeat'))) - self.objects=[self.slrVolume, self.slrTime, - self.btnStop, self.btnNext, self.btnPrevious] + self.objects=[self.slrVolume, self.slrTime, + self.btnStop, self.btnNext, self.btnPrevious] - layout=QtGui.QHBoxLayout(parent) - layout2=QtGui.QHBoxLayout(parent) - layoutWidget=QtGui.QVBoxLayout(parent) - layoutWidget.addLayout(layout) - layoutWidget.addLayout(layout2) - self.setLayout(layoutWidget) - - layout.addWidget(self.btnPrevious) - layout.addWidget(self.btnPlayPause) - layout.addWidget(self.btnStop) - layout.addWidget(self.btnNext) - layout.addWidget(self.slrTime) - layout.addWidget(self.slrVolume) - layout.addWidget(self.svgVolume) + layout=QtGui.QHBoxLayout(parent) + layout2=QtGui.QHBoxLayout(parent) + layoutWidget=QtGui.QVBoxLayout(parent) + layoutWidget.addLayout(layout) + layoutWidget.addLayout(layout2) + self.setLayout(layoutWidget) + + layout.addWidget(self.btnPrevious) + layout.addWidget(self.btnPlayPause) + layout.addWidget(self.btnStop) + layout.addWidget(self.btnNext) + layout.addWidget(self.slrTime) + layout.addWidget(self.slrVolume) + layout.addWidget(self.svgVolume) - layout2.addWidget(self.cmbRepeat) - layout2.addWidget(self.cmbShuffle) - layout2.addWidget(self.btnJmpCurrent) - - # queue gets loaded in _load of pluginPlayControl - self.queuedSongs=[] - self._onQueueUpdate() + layout2.addWidget(self.cmbRepeat) + layout2.addWidget(self.cmbShuffle) + layout2.addWidget(self.btnJmpCurrent) + + # queue gets loaded in _load of pluginPlayControl + self.queuedSongs=[] + self._onQueueUpdate() - self.connect(self.slrVolume, QtCore.SIGNAL('valueChanged(int)'),self.onVolumeSliderChange) - self.connect(self.slrTime, QtCore.SIGNAL('sliderReleased()'),self.onTimeSliderChange) - - self.connect(self.cmbRepeat, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbRepeatChanged) - self.connect(self.cmbShuffle, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbShuffleChanged) - - def addSongsToQueue(self, songs): - self.queuedSongs.extend(songs) - self._onQueueUpdate() - if self.cmbShuffle.currentIndex()!=PC_RANDOM_QUEUE: - self.cmbShuffle.setCurrentIndex(PC_RANDOM_QUEUE) - - def _onQueueUpdate(self): - """This method gets called whenever the queue is updated""" - self.cmbShuffle.setItemText(PC_RANDOM_QUEUE, "Queue (%i)"%(len(self.queuedSongs))) - - def onBtnJmpCurrentClick(self): - plugins.getPlugin("Playlist").getPlaylist().ensureVisible(monty.getCurrentSong().getID()) - def onStateChange(self, params): - newState=monty.getStatus()['state'] + self.connect(self.slrVolume, QtCore.SIGNAL('valueChanged(int)'),self.onVolumeSliderChange) + self.connect(self.slrTime, QtCore.SIGNAL('sliderReleased()'),self.onTimeSliderChange) + + self.connect(self.cmbRepeat, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbRepeatChanged) + self.connect(self.cmbShuffle, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbShuffleChanged) + + def addSongsToQueue(self, songs): + self.queuedSongs.extend(songs) + self._onQueueUpdate() + if self.cmbShuffle.currentIndex()!=PC_RANDOM_QUEUE: + self.cmbShuffle.setCurrentIndex(PC_RANDOM_QUEUE) + + def _onQueueUpdate(self): + """This method gets called whenever the queue is updated""" + self.cmbShuffle.setItemText(PC_RANDOM_QUEUE, "Queue (%i)"%(len(self.queuedSongs))) + + def onBtnJmpCurrentClick(self): + plugins.getPlugin("Playlist").getPlaylist().ensureVisible(monty.getCurrentSong().getID()) + def onStateChange(self, params): + newState=monty.getStatus()['state'] - map(lambda o: o.setEnabled(newState!='stop'), self.objects) + map(lambda o: o.setEnabled(newState!='stop'), self.objects) - if newState=='play': - self.btnPlayPause.changeIcon('gfx/media-playback-pause.svg') - self.btnPlayPause.setText('pauze') - elif newState=='pause' or newState=='stop': - self.btnPlayPause.changeIcon('gfx/media-playback-start.svg') - self.btnPlayPause.setText('play') - def onVolumeChange(self, params): - self.slrVolume.setValue(params['newVolume']) - def onDisconnect(self, params): - map(lambda o: o.setEnabled(False), self.objects) - def onTimeChange(self, params): - if not self.slrTime.isSliderDown(): - self.slrTime.setValue(params['newTime']) - def onSongChange(self, params): - try: - self.slrTime.setMaximum(monty.getStatus()['length']) - self.slrTime.setEnabled(True) - except: - pass - # look in another thread for the songs in the current album! - params=() - start_new_thread(self.findAlbumSongs, params) + if newState=='play': + self.btnPlayPause.changeIcon('gfx/media-playback-pause.svg') + self.btnPlayPause.setText('pauze') + elif newState=='pause' or newState=='stop': + self.btnPlayPause.changeIcon('gfx/media-playback-start.svg') + self.btnPlayPause.setText('play') + def onVolumeChange(self, params): + self.slrVolume.setValue(params['newVolume']) + def onDisconnect(self, params): + map(lambda o: o.setEnabled(False), self.objects) + def onTimeChange(self, params): + if not self.slrTime.isSliderDown(): + self.slrTime.setValue(params['newTime']) + def onSongChange(self, params): + try: + self.slrTime.setMaximum(monty.getStatus()['length']) + self.slrTime.setEnabled(True) + except: + pass + # look in another thread for the songs in the current album! + params=() + start_new_thread(self.findAlbumSongs, params) - def beforeSongChange(self, params): - nextID=None - song=monty.getCurrentSong() - # decide here what next song to play! - repeat=self.cmbRepeat.currentIndex() - random=self.cmbShuffle.currentIndex() - # is the current song the last of the album? - try: - eofAlbum=int(song.getTrack())==int(self.curAlbumSongs[-1].getTrack()) - except: - eofAlbum=False - if repeat==PC_REPEAT_NO: - # no repeat, nothing to see here! Pass on! - pass - elif repeat==PC_REPEAT_SONG: - # we must repeat the previous song! - nextID=params['curSongID'] - elif repeat==PC_REPEAT_ALBUM: - # check if we are at the last track, if it is, we must start a new! - if eofAlbum: - nextID=self.curAlbumSongs[0].getID() - elif repeat==PC_REPEAT_PLAYLIST: - # repeating the playlist is handled by monty itself; - # it is set in onCmbRepeatChanged. - pass - - if repeat!=PC_REPEAT_SONG: - if random==PC_RANDOM_QUEUE: - # we must check here for the queue, because, if the queue is empty, we must - # choose the next one according the method! - # pick the next song from the queue. Simple as that :) - if len(self.queuedSongs): - # pick the front - nextID=self.queuedSongs.pop(0) - self._onQueueUpdate() - else: - # no songs anymore, so we must restore the old mode! - # We should never arrive here though ... - self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('oldshuffle'))) - - if random==PC_RANDOM_NO: - # just follow our leader Monty. - pass - elif random==PC_RANDOM_SONG: - # pick a random song! This depends on what repeat-mode we're in. - if repeat==PC_REPEAT_NO or repeat==PC_REPEAT_PLAYLIST: - # we don't repeat anything, so we can just let monty pick the - # next random one! - pass - elif repeat==PC_REPEAT_ALBUM and self.curAlbumSongs: - # pick random song from current album - nextID=self.curAlbumSongs[randint(0,len(self.curAlbumSongs)-1)].getID() - elif random==PC_RANDOM_ALBUM: - # pick a random album! This means, we pick the first song of a random - # album. - if eofAlbum and (repeat==PC_REPEAT_PLAYLIST or repeat==PC_REPEAT_NO): - # all first songs of an album - albums=filter(lambda s: s.getAlbum() and s.getTrack()==1, monty.listPlaylist()) - nextID=albums[randint(0,len(albums)-1)].getID() - else: - # we're not at end of album, so we fetch the next id - # We must do this, because albums are not necesseraly in the same order - for i in xrange(len(self.curAlbumSongs)): - if self.curAlbumSongs[i].getID()==song.getID(): - nextID=self.curAlbumSongs[i+1].getID() - break - - if random==PC_RANDOM_QUEUE: - # now here reset the mode to the previous, if needed - if len(self.queuedSongs)==0: - self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('oldshuffle'))) + def beforeSongChange(self, params): + nextID=None + song=monty.getCurrentSong() + # decide here what next song to play! + repeat=self.cmbRepeat.currentIndex() + random=self.cmbShuffle.currentIndex() + # is the current song the last of the album? + try: + eofAlbum=int(song.getTrack())==int(self.curAlbumSongs[-1].getTrack()) + except: + eofAlbum=False + if repeat==PC_REPEAT_NO: + # no repeat, nothing to see here! Pass on! + pass + elif repeat==PC_REPEAT_SONG: + # we must repeat the previous song! + nextID=params['curSongID'] + elif repeat==PC_REPEAT_ALBUM: + # check if we are at the last track, if it is, we must start a new! + if eofAlbum: + nextID=self.curAlbumSongs[0].getID() + elif repeat==PC_REPEAT_PLAYLIST: + # repeating the playlist is handled by monty itself; + # it is set in onCmbRepeatChanged. + pass + + if repeat!=PC_REPEAT_SONG: + if random==PC_RANDOM_QUEUE: + # we must check here for the queue, because, if the queue is empty, we must + # choose the next one according the method! + # pick the next song from the queue. Simple as that :) + if len(self.queuedSongs): + # pick the front + nextID=self.queuedSongs.pop(0) + self._onQueueUpdate() + else: + # no songs anymore, so we must restore the old mode! + # We should never arrive here though ... + self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('oldshuffle'))) + + if random==PC_RANDOM_NO: + # just follow our leader Monty. + pass + elif random==PC_RANDOM_SONG: + # pick a random song! This depends on what repeat-mode we're in. + if repeat==PC_REPEAT_NO or repeat==PC_REPEAT_PLAYLIST: + # we don't repeat anything, so we can just let monty pick the + # next random one! + pass + elif repeat==PC_REPEAT_ALBUM and self.curAlbumSongs: + # pick random song from current album + nextID=self.curAlbumSongs[randint(0,len(self.curAlbumSongs)-1)].getID() + elif random==PC_RANDOM_ALBUM: + # pick a random album! This means, we pick the first song of a random + # album. + if eofAlbum and (repeat==PC_REPEAT_PLAYLIST or repeat==PC_REPEAT_NO): + # all first songs of an album + albums=filter(lambda s: s.getAlbum() and s.getTrack()==1, monty.listPlaylist()) + nextID=albums[randint(0,len(albums)-1)].getID() + else: + # we're not at end of album, so we fetch the next id + # We must do this, because albums are not necesseraly in the same order + for i in xrange(len(self.curAlbumSongs)): + if self.curAlbumSongs[i].getID()==song.getID(): + nextID=self.curAlbumSongs[i+1].getID() + break + + if random==PC_RANDOM_QUEUE: + # now here reset the mode to the previous, if needed + if len(self.queuedSongs)==0: + self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('oldshuffle'))) - if nextID!=None: - monty.play(nextID) - - def getCurAlbumSongs(self): - return self.curAlbumSongs - def findAlbumSongs(self): - """This method looks for the songs in the album of current playing song.""" - song=monty.getCurrentSong() - if self.curAlbumSongs and clSong.isSameAlbum(song, self.curAlbumSongs[0]): - return - self.curAlbumSongs=None - if not song or not song.getAlbum(): - return - self.curAlbumSongs=filter(lambda s: clSong.isSameAlbum(s, song), monty.listPlaylist()) - self.curAlbumSongs=sorted(self.curAlbumSongs, lambda l,r: numeric_compare(l.getTrack(), r.getTrack())) + if nextID!=None: + monty.play(nextID) + + def getCurAlbumSongs(self): + return self.curAlbumSongs + def findAlbumSongs(self): + """This method looks for the songs in the album of current playing song.""" + song=monty.getCurrentSong() + if self.curAlbumSongs and clSong.isSameAlbum(song, self.curAlbumSongs[0]): + return + self.curAlbumSongs=None + if not song or not song.getAlbum(): + return + self.curAlbumSongs=filter(lambda s: clSong.isSameAlbum(s, song), monty.listPlaylist()) + self.curAlbumSongs=sorted(self.curAlbumSongs, lambda l,r: numeric_compare(l.getTrack(), r.getTrack())) - def onBtnPlayPauseClick(self): - status=monty.getStatus() - if status['state']=='play': - monty.pause() - self.p.extended("Toggling playback") - elif status['state']=='stop': - monty.play(None) - self.p.extended("Pausing playback") - else: - monty.resume() - def onBtnStopClick(self): - monty.stop() - self.p.extended("Stopping playback") - def onBtnPreviousClick(self): - monty.previous() - self.p.extended("Playing previous") - def onBtnNextClick(self): - monty.next() - self.p.extended("Playing next") - def onTimeSliderChange(self): - monty.seek(self.slrTime.value()) - def onVolumeSliderChange(self): - v=self.slrVolume.value() - monty.setVolume(v) - if v<=1: - mode='mute' - else: - mode=('0', 'min', 'med', 'max')[int(3*v/100)] - self.svgVolume.load('gfx/stock_volume-%s.svg'%(mode)) - - def onCmbRepeatChanged(self, newval): - self.p.setSetting('repeat', newval) - if newval==PC_REPEAT_PLAYLIST: - monty.repeat(1) - else: - monty.repeat(0) - def onCmbShuffleChanged(self, newval): - if newval==PC_RANDOM_QUEUE: - # must do some extra's if moving to queued-mode - if len(self.queuedSongs): - self.p.setSetting('oldshuffle', self.p.getSetting('shuffle')) - else: - self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('shuffle'))) - return - else: - # clear the queued songs when switching - self.queuedSongs=[] - self._onQueueUpdate() + def onBtnPlayPauseClick(self): + status=monty.getStatus() + if status['state']=='play': + monty.pause() + self.p.extended("Toggling playback") + elif status['state']=='stop': + monty.play(None) + self.p.extended("Pausing playback") + else: + monty.resume() + def onBtnStopClick(self): + monty.stop() + self.p.extended("Stopping playback") + def onBtnPreviousClick(self): + monty.previous() + self.p.extended("Playing previous") + def onBtnNextClick(self): + monty.next() + self.p.extended("Playing next") + def onTimeSliderChange(self): + monty.seek(self.slrTime.value()) + def onVolumeSliderChange(self): + v=self.slrVolume.value() + monty.setVolume(v) + if v<=1: + mode='mute' + else: + mode=('0', 'min', 'med', 'max')[int(3*v/100)] + self.svgVolume.load('gfx/stock_volume-%s.svg'%(mode)) + + def onCmbRepeatChanged(self, newval): + self.p.setSetting('repeat', newval) + if newval==PC_REPEAT_PLAYLIST: + monty.repeat(1) + else: + monty.repeat(0) + def onCmbShuffleChanged(self, newval): + if newval==PC_RANDOM_QUEUE: + # must do some extra's if moving to queued-mode + if len(self.queuedSongs): + self.p.setSetting('oldshuffle', self.p.getSetting('shuffle')) + else: + self.cmbShuffle.setCurrentIndex(int(self.p.getSetting('shuffle'))) + return + else: + # clear the queued songs when switching + self.queuedSongs=[] + self._onQueueUpdate() - self.p.setSetting('shuffle', newval) - if newval==PC_RANDOM_SONG: - monty.random(1) - else: - monty.random(0) + self.p.setSetting('shuffle', newval) + if newval==PC_RANDOM_SONG: + monty.random(1) + else: + monty.random(0) - # save and load the queue - def saveQueue(self): - # save the ids as a list of space-separated numbers - self.p.extended("saving queue") - self.p.setSetting('queue', str(self.queuedSongs)[1:-1].replace(',', '')) - def loadQueue(self): - # just read all the numbers! - self.p.extended("loading queue") - self.queuedSongs=[] - i=0 - ids=self.p.getSetting('queue').split(' ') - for id in ids: - try: - self.queuedSongs.append(int(id)) - except: - pass - self._onQueueUpdate() + # save and load the queue + def saveQueue(self): + # save the ids as a list of space-separated numbers + self.p.extended("saving queue") + self.p.setSetting('queue', str(self.queuedSongs)[1:-1].replace(',', '')) + def loadQueue(self): + # just read all the numbers! + self.p.extended("loading queue") + self.queuedSongs=[] + i=0 + ids=self.p.getSetting('queue').split(' ') + for id in ids: + try: + self.queuedSongs.append(int(id)) + except: + pass + self._onQueueUpdate() class pluginPlayControl(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'PlayControl') - self.addMontyListener('onStateChange', self.onStateChange) - self.addMontyListener('beforeSongChange', self.beforeSongChange) - self.addMontyListener('onSongChange', self.onSongChange) - self.addMontyListener('onVolumeChange', self.onVolumeChange) - self.addMontyListener('onReady', self.onStateChange) - self.addMontyListener('onDisconnect', self.onDisconnect) - self.addMontyListener('onTimeChange', self.onTimeChange) - def _load(self): - self.o=wgPlayControl(self, None) - self.o.loadQueue() - def _unload(self): - self.o.saveQueue() - self.o=None - def getInfo(self): - return "Have total control over the playing!" - - def addSongsToQueue(self, songs): - return self.o.addSongsToQueue(songs) - - def onStateChange(self, params): - self.o.onStateChange(params) - def beforeSongChange(self, params): - self.o.beforeSongChange(params) - def onSongChange(self, params): - self.o.onSongChange(params) - def onVolumeChange(self, params): - self.o.onVolumeChange(params) - def onDisconnect(self, params): - self.o.onDisconnect(params) - def onTimeChange(self, params): - self.o.onTimeChange(params) - - def _getDockWidget(self): - return self._createDock(self.o) + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'PlayControl') + self.addMontyListener('onStateChange', self.onStateChange) + self.addMontyListener('beforeSongChange', self.beforeSongChange) + self.addMontyListener('onSongChange', self.onSongChange) + self.addMontyListener('onVolumeChange', self.onVolumeChange) + self.addMontyListener('onReady', self.onStateChange) + self.addMontyListener('onDisconnect', self.onDisconnect) + self.addMontyListener('onTimeChange', self.onTimeChange) + def _load(self): + self.o=wgPlayControl(self, None) + self.o.loadQueue() + def _unload(self): + self.o.saveQueue() + self.o=None + def getInfo(self): + return "Have total control over the playing!" + + def addSongsToQueue(self, songs): + return self.o.addSongsToQueue(songs) + + def onStateChange(self, params): + self.o.onStateChange(params) + def beforeSongChange(self, params): + self.o.beforeSongChange(params) + def onSongChange(self, params): + self.o.onSongChange(params) + def onVolumeChange(self, params): + self.o.onVolumeChange(params) + def onDisconnect(self, params): + self.o.onDisconnect(params) + def onTimeChange(self, params): + self.o.onTimeChange(params) + + def _getDockWidget(self): + return self._createDock(self.o) diff --git a/plugins/Playlist.py b/plugins/Playlist.py index 3cb812b..811441d 100644 --- a/plugins/Playlist.py +++ b/plugins/Playlist.py @@ -7,85 +7,85 @@ from wgSongList import clrRowSel import plugins PLAYLIST_MODES_DEFAULT='$artist\n'\ - '$artist/$date - $album\n'\ - '$artist - $album\n'\ - '$album ($artist)\n'\ - '$genre\n'\ - '$genre/$artist\n'\ - '$genre/$artist - $album\n' + '$artist/$date - $album\n'\ + '$artist - $album\n'\ + '$album ($artist)\n'\ + '$genre\n'\ + '$genre/$artist\n'\ + '$genre/$artist - $album\n' # Dependencies: # playcontrol class pluginPlaylist(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Playlist') - self.addMontyListener('onSongChange', self.onSongChange) - def _load(self): - self.o=Playlist(self.winMain, self, ['artist', 'title', 'track', 'album'], 'Playlist' - , self.onDoubleClick, self.onKeyPress, self.getSetting('modes').split('\n')) - def _unload(self): - self.o=None - def getPlaylist(self): - return self.o - def getInfo(self): - return "The playlist showing the songs that will be played." - - def getList(self): - return self.o + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Playlist') + self.addMontyListener('onSongChange', self.onSongChange) + def _load(self): + self.o=Playlist(self.winMain, self, ['artist', 'title', 'track', 'album'], 'Playlist' + , self.onDoubleClick, self.onKeyPress, self.getSetting('modes').split('\n')) + def _unload(self): + self.o=None + def getPlaylist(self): + return self.o + def getInfo(self): + return "The playlist showing the songs that will be played." + + def getList(self): + return self.o - def _getDockWidget(self): - return self._createDock(self.o) + def _getDockWidget(self): + return self._createDock(self.o) - def onDoubleClick(self): - monty.play(self.o.getSelItemID()) + def onDoubleClick(self): + monty.play(self.o.getSelItemID()) - def onKeyPress(self, event): - if event.matches(QtGui.QKeySequence.Delete): - # remove selection from playlist using DELETE-key - ids=self.o.selectedIds() - self.setStatus('Deleting '+str(len(ids))+' songs from playlist ...') - doEvents() + def onKeyPress(self, event): + if event.matches(QtGui.QKeySequence.Delete): + # remove selection from playlist using DELETE-key + ids=self.o.selectedIds() + self.setStatus('Deleting '+str(len(ids))+' songs from playlist ...') + doEvents() - monty.deleteFromPlaylist(ids) + monty.deleteFromPlaylist(ids) - self.setStatus('') - doEvents() + self.setStatus('') + doEvents() - self.getWinMain().fillPlaylist() - elif event.key()==QtCore.Qt.Key_Q: - # queue selected songs - # Hoho, this one needs the playcontrol plugin! - plugins.getPlugin('playcontrol').addSongsToQueue(self.o.selectedIds()) - return QtGui.QWidget.keyPressEvent(self.o, event) - - def onSongChange(self, params): - lst=self.o - lst.colorID(int(params['newSongID']), clrRowSel) + self.getWinMain().fillPlaylist() + elif event.key()==QtCore.Qt.Key_Q: + # queue selected songs + # Hoho, this one needs the playcontrol plugin! + plugins.getPlugin('playcontrol').addSongsToQueue(self.o.selectedIds()) + return QtGui.QWidget.keyPressEvent(self.o, event) + + def onSongChange(self, params): + lst=self.o + lst.colorID(int(params['newSongID']), clrRowSel) - if params['newSongID']!=-1: - lst.ensureVisible(params['newSongID']) + if params['newSongID']!=-1: + lst.ensureVisible(params['newSongID']) - _rowColorModifier=0 - _rowColorAdder=1 - def timerEvent(self, event): - curSong=monty.getCurrentSong() - if curSong: - lst=self.lstPlaylist - # color current playing song - lst.colorID(curSong.getID(), - QtGui.QColor(140-self._rowColorModifier*5,140-self._rowColorModifier*5,180)) + _rowColorModifier=0 + _rowColorAdder=1 + def timerEvent(self, event): + curSong=monty.getCurrentSong() + if curSong: + lst=self.lstPlaylist + # color current playing song + lst.colorID(curSong.getID(), + QtGui.QColor(140-self._rowColorModifier*5,140-self._rowColorModifier*5,180)) - # make sure color changes nicely over time - self._rowColorModifier=self._rowColorModifier+self._rowColorAdder - if abs(self._rowColorModifier)>4: - self._rowColorAdder=-1*self._rowColorAdder + # make sure color changes nicely over time + self._rowColorModifier=self._rowColorModifier+self._rowColorAdder + if abs(self._rowColorModifier)>4: + self._rowColorAdder=-1*self._rowColorAdder - def _getSettings(self): - modes=QtGui.QTextEdit() - modes.insertPlainText(self.getSetting('modes')) - return [ - ['modes', 'Modes', 'Sets the available modes.', modes], - ] - def afterSaveSettings(self): - self.o.setModes(self.getSetting('modes').split('\n')) + def _getSettings(self): + modes=QtGui.QTextEdit() + modes.insertPlainText(self.getSetting('modes')) + return [ + ['modes', 'Modes', 'Sets the available modes.', modes], + ] + def afterSaveSettings(self): + self.o.setModes(self.getSetting('modes').split('\n')) diff --git a/plugins/Scrobbler.py b/plugins/Scrobbler.py index bb2321f..ced3964 100644 --- a/plugins/Scrobbler.py +++ b/plugins/Scrobbler.py @@ -9,90 +9,90 @@ SCROBBLER_PASSWORD_DEFAULT='' # TODO cached failed submissions class pluginScrobbler(Plugin): - submitted=False - time=None - loggedIn=False - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Scrobbler') - self.addMontyListener('onSongChange', self.onSongChange) - self.addMontyListener('onTimeChange', self.onTimeChange) - - def _load(self): - self._login() - - def _login(self): - self.submitting=False - self.loggedIn=False - if self._username(): - self.normal("logging in %s"%(self._username())) - try: - login(self._username(), self._password()) - self.loggedIn=True - except Exception, e: - self.normal("failed to login: "+str(e)) - else: - self.debug("no username provided, not logging in") - - def _username(self): - return self.getSetting('username') - def _password(self): - return self.getSetting('password') - def onTimeChange(self, params): - if self.submitted==False and self.loggedIn: - song=monty.getCurrentSong() - if song.getTag('time')>30: - if int(params['newTime'])>int(song.getTag('time'))/2 \ - or int(params['newTime'])>240: - if not self.time: - self.onSongChange(None) - self.normal("submitting song") - submit(song.getArtist(), song.getTitle(), self.time, 'P', '', song.getTag('time'), song.getAlbum(), song.getTrack(), False) - self.debug("flushing ...") - try: - flush() - self.submitted=True - except Exception, e: - self.important("failed to submit song1 - %s"%(e)) - self.extended("Logging in ...") - self._login() - try: - flush() - self.submitted=True - except Exception, e: - self.important("failed to submit song2 - %s"%(e)) - if self.submitted: - self.debug("flushed") - else: - self.important("failed to submit the song!") - - def onSongChange(self, params): - if self.loggedIn==False: - return - self.time=int(time.mktime(datetime.utcnow().timetuple())) - self.submitted=False - song=monty.getCurrentSong() - # for loop: in case of SessionError, we need to retry once ... - for i in [0, 1]: - try: - self.extended("submitting now playing") - now_playing(song.getArtist(), song.getTitle(), song.getAlbum(), "", song.getTrack()) - self.debug("submitted") - break - except AuthError, e: - self.important("failed to submit playing song - %s"%(e)) - break - except SessionError, e: - self.normal("session error") - self._login() - def _getSettings(self): - return [ - ['username', 'Username', 'Username to submit to last.fm.', QtGui.QLineEdit(self._username())], - ['password', 'Password', 'Password to user to submit. Note that the password is stored *unencrypted* to file.', QtGui.QLineEdit(self._password())], - ] - def afterSaveSettings(self): - self._login() - def getInfo(self): - return "Submits tracks to last.fm" + submitted=False + time=None + loggedIn=False + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Scrobbler') + self.addMontyListener('onSongChange', self.onSongChange) + self.addMontyListener('onTimeChange', self.onTimeChange) + + def _load(self): + self._login() + + def _login(self): + self.submitting=False + self.loggedIn=False + if self._username(): + self.normal("logging in %s"%(self._username())) + try: + login(self._username(), self._password()) + self.loggedIn=True + except Exception, e: + self.normal("failed to login: "+str(e)) + else: + self.debug("no username provided, not logging in") + + def _username(self): + return self.getSetting('username') + def _password(self): + return self.getSetting('password') + def onTimeChange(self, params): + if self.submitted==False and self.loggedIn: + song=monty.getCurrentSong() + if song.getTag('time')>30: + if int(params['newTime'])>int(song.getTag('time'))/2 \ + or int(params['newTime'])>240: + if not self.time: + self.onSongChange(None) + self.normal("submitting song") + submit(song.getArtist(), song.getTitle(), self.time, 'P', '', song.getTag('time'), song.getAlbum(), song.getTrack(), False) + self.debug("flushing ...") + try: + flush() + self.submitted=True + except Exception, e: + self.important("failed to submit song1 - %s"%(e)) + self.extended("Logging in ...") + self._login() + try: + flush() + self.submitted=True + except Exception, e: + self.important("failed to submit song2 - %s"%(e)) + if self.submitted: + self.debug("flushed") + else: + self.important("failed to submit the song!") + + def onSongChange(self, params): + if self.loggedIn==False: + return + self.time=int(time.mktime(datetime.utcnow().timetuple())) + self.submitted=False + song=monty.getCurrentSong() + # for loop: in case of SessionError, we need to retry once ... + for i in [0, 1]: + try: + self.extended("submitting now playing") + now_playing(song.getArtist(), song.getTitle(), song.getAlbum(), "", song.getTrack()) + self.debug("submitted") + break + except AuthError, e: + self.important("failed to submit playing song - %s"%(e)) + break + except SessionError, e: + self.normal("session error") + self._login() + def _getSettings(self): + return [ + ['username', 'Username', 'Username to submit to last.fm.', QtGui.QLineEdit(self._username())], + ['password', 'Password', 'Password to user to submit. Note that the password is stored *unencrypted* to file.', QtGui.QLineEdit(self._password())], + ] + def afterSaveSettings(self): + self._login() + def getInfo(self): + return "Submits tracks to last.fm" @@ -110,293 +110,293 @@ from datetime import datetime, timedelta from md5 import md5 SESSION_ID = None -POST_URL = None -NOW_URL = None +POST_URL = None +NOW_URL = None HARD_FAILS = 0 -LAST_HS = None # Last handshake time -HS_DELAY = 0 # wait this many seconds until next handshake +LAST_HS = None # Last handshake time +HS_DELAY = 0 # wait this many seconds until next handshake SUBMIT_CACHE = [] -MAX_CACHE = 5 # keep only this many songs in the cache +MAX_CACHE = 5 # keep only this many songs in the cache PROTOCOL_VERSION = '1.2' class BackendError(Exception): - "Raised if the AS backend does something funny" - pass + "Raised if the AS backend does something funny" + pass class AuthError(Exception): - "Raised on authencitation errors" - pass + "Raised on authencitation errors" + pass class PostError(Exception): - "Raised if something goes wrong when posting data to AS" - pass + "Raised if something goes wrong when posting data to AS" + pass class SessionError(Exception): - "Raised when problems with the session exist" - pass + "Raised when problems with the session exist" + pass class ProtocolError(Exception): - "Raised on general Protocol errors" - pass + "Raised on general Protocol errors" + pass def login( user, password, client=('tst', '1.0') ): - """Authencitate with AS (The Handshake) - - @param user: The username - @param password: The password - @param client: Client information (see http://www.audioscrobbler.net/development/protocol/ for more info) - @type client: Tuple: (client-id, client-version)""" - global LAST_HS, SESSION_ID, POST_URL, NOW_URL, HARD_FAILS, HS_DELAY, PROTOCOL_VERSION - - if LAST_HS is not None: - next_allowed_hs = LAST_HS + timedelta(seconds=HS_DELAY) - if datetime.now() < next_allowed_hs: - delta = next_allowed_hs - datetime.now() - raise ProtocolError("""Please wait another %d seconds until next handshake (login) attempt.""" % delta.seconds) - - LAST_HS = datetime.now() - - tstamp = int(mktime(datetime.now().timetuple())) - url = "http://post.audioscrobbler.com/" - pwhash = md5(password).hexdigest() - token = md5( "%s%d" % (pwhash, int(tstamp))).hexdigest() - values = { - 'hs': 'true', - 'p' : PROTOCOL_VERSION, - 'c': client[0], - 'v': client[1], - 'u': user, - 't': tstamp, - 'a': token - } - data = urllib.urlencode(values) - req = urllib2.Request("%s?%s" % (url, data) ) - response = urllib2.urlopen(req) - result = response.read() - lines = result.split('\n') - - - if lines[0] == 'BADAUTH': - raise AuthError('Bad username/password') - - elif lines[0] == 'BANNED': - raise Exception('''This client-version was banned by Audioscrobbler. Please contact the author of this module!''') - - elif lines[0] == 'BADTIME': - raise ValueError('''Your system time is out of sync with Audioscrobbler.Consider using an NTP-client to keep you system time in sync.''') - - elif lines[0].startswith('FAILED'): - handle_hard_error() - raise BackendError("Authencitation with AS failed. Reason: %s" % - lines[0]) - - elif lines[0] == 'OK': - # wooooooohooooooo. We made it! - SESSION_ID = lines[1] - NOW_URL = lines[2] - POST_URL = lines[3] - HARD_FAILS = 0 - - else: - # some hard error - handle_hard_error() + """Authencitate with AS (The Handshake) + + @param user: The username + @param password: The password + @param client: Client information (see http://www.audioscrobbler.net/development/protocol/ for more info) + @type client: Tuple: (client-id, client-version)""" + global LAST_HS, SESSION_ID, POST_URL, NOW_URL, HARD_FAILS, HS_DELAY, PROTOCOL_VERSION + + if LAST_HS is not None: + next_allowed_hs = LAST_HS + timedelta(seconds=HS_DELAY) + if datetime.now() < next_allowed_hs: + delta = next_allowed_hs - datetime.now() + raise ProtocolError("""Please wait another %d seconds until next handshake (login) attempt.""" % delta.seconds) + + LAST_HS = datetime.now() + + tstamp = int(mktime(datetime.now().timetuple())) + url = "http://post.audioscrobbler.com/" + pwhash = md5(password).hexdigest() + token = md5( "%s%d" % (pwhash, int(tstamp))).hexdigest() + values = { + 'hs': 'true', + 'p' : PROTOCOL_VERSION, + 'c': client[0], + 'v': client[1], + 'u': user, + 't': tstamp, + 'a': token + } + data = urllib.urlencode(values) + req = urllib2.Request("%s?%s" % (url, data) ) + response = urllib2.urlopen(req) + result = response.read() + lines = result.split('\n') + + + if lines[0] == 'BADAUTH': + raise AuthError('Bad username/password') + + elif lines[0] == 'BANNED': + raise Exception('''This client-version was banned by Audioscrobbler. Please contact the author of this module!''') + + elif lines[0] == 'BADTIME': + raise ValueError('''Your system time is out of sync with Audioscrobbler.Consider using an NTP-client to keep you system time in sync.''') + + elif lines[0].startswith('FAILED'): + handle_hard_error() + raise BackendError("Authencitation with AS failed. Reason: %s" % + lines[0]) + + elif lines[0] == 'OK': + # wooooooohooooooo. We made it! + SESSION_ID = lines[1] + NOW_URL = lines[2] + POST_URL = lines[3] + HARD_FAILS = 0 + + else: + # some hard error + handle_hard_error() def handle_hard_error(): - "Handles hard errors." - global SESSION_ID, HARD_FAILS, HS_DELAY + "Handles hard errors." + global SESSION_ID, HARD_FAILS, HS_DELAY - if HS_DELAY == 0: - HS_DELAY = 60 - elif HS_DELAY < 120*60: - HS_DELAY *= 2 - if HS_DELAY > 120*60: - HS_DELAY = 120*60 + if HS_DELAY == 0: + HS_DELAY = 60 + elif HS_DELAY < 120*60: + HS_DELAY *= 2 + if HS_DELAY > 120*60: + HS_DELAY = 120*60 - HARD_FAILS += 1 - if HARD_FAILS == 3: - SESSION_ID = None + HARD_FAILS += 1 + if HARD_FAILS == 3: + SESSION_ID = None def now_playing( artist, track, album="", length="", trackno="", mbid="" ): - """Tells audioscrobbler what is currently running in your player. This won't - affect the user-profile on last.fm. To do submissions, use the "submit" - method - - @param artist: The artist name - @param track: The track name - @param album: The album name - @param length: The song length in seconds - @param trackno: The track number - @param mbid: The MusicBrainz Track ID - @return: True on success, False on failure""" - - global SESSION_ID, NOW_URL - - if SESSION_ID is None: - raise AuthError("Please 'login()' first. (No session available)") - - if POST_URL is None: - raise PostError("Unable to post data. Post URL was empty!") - - if length != "" and type(length) != type(1): - raise TypeError("length should be of type int") - - if trackno != "" and type(trackno) != type(1): - raise TypeError("trackno should be of type int") - - values = {'s': SESSION_ID, - 'a': unicode(artist).encode('utf-8'), - 't': unicode(track).encode('utf-8'), - 'b': unicode(album).encode('utf-8'), - 'l': length, - 'n': trackno, - 'm': mbid } - - data = urllib.urlencode(values) - req = urllib2.Request(NOW_URL, data) - response = urllib2.urlopen(req) - result = response.read() - - if result.strip() == "OK": - return True - elif result.strip() == "BADSESSION" : - raise SessionError('Invalid session') - else: - return False + """Tells audioscrobbler what is currently running in your player. This won't + affect the user-profile on last.fm. To do submissions, use the "submit" + method + + @param artist: The artist name + @param track: The track name + @param album: The album name + @param length: The song length in seconds + @param trackno: The track number + @param mbid: The MusicBrainz Track ID + @return: True on success, False on failure""" + + global SESSION_ID, NOW_URL + + if SESSION_ID is None: + raise AuthError("Please 'login()' first. (No session available)") + + if POST_URL is None: + raise PostError("Unable to post data. Post URL was empty!") + + if length != "" and type(length) != type(1): + raise TypeError("length should be of type int") + + if trackno != "" and type(trackno) != type(1): + raise TypeError("trackno should be of type int") + + values = {'s': SESSION_ID, + 'a': unicode(artist).encode('utf-8'), + 't': unicode(track).encode('utf-8'), + 'b': unicode(album).encode('utf-8'), + 'l': length, + 'n': trackno, + 'm': mbid } + + data = urllib.urlencode(values) + req = urllib2.Request(NOW_URL, data) + response = urllib2.urlopen(req) + result = response.read() + + if result.strip() == "OK": + return True + elif result.strip() == "BADSESSION" : + raise SessionError('Invalid session') + else: + return False def submit(artist, track, time, source='P', rating="", length="", album="", - trackno="", mbid="", autoflush=False): - """Append a song to the submission cache. Use 'flush()' to send the cache to - AS. You can also set "autoflush" to True. - - From the Audioscrobbler protocol docs: - --------------------------------------------------------------------------- - - The client should monitor the user's interaction with the music playing - service to whatever extent the service allows. In order to qualify for - submission all of the following criteria must be met: - - 1. The track must be submitted once it has finished playing. Whether it has - finished playing naturally or has been manually stopped by the user is - irrelevant. - 2. The track must have been played for a duration of at least 240 seconds or - half the track's total length, whichever comes first. Skipping or pausing - the track is irrelevant as long as the appropriate amount has been played. - 3. The total playback time for the track must be more than 30 seconds. Do - not submit tracks shorter than this. - 4. Unless the client has been specially configured, it should not attempt to - interpret filename information to obtain metadata instead of tags (ID3, - etc). - - @param artist: Artist name - @param track: Track name - @param time: Time the track *started* playing in the UTC timezone (see - datetime.utcnow()). - - Example: int(time.mktime(datetime.utcnow())) - @param source: Source of the track. One of: - 'P': Chosen by the user - 'R': Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1) - 'E': Personalised recommendation except Last.fm (e.g. - Pandora, Launchcast) - 'L': Last.fm (any mode). In this case, the 5-digit Last.fm - recommendation key must be appended to this source ID to - prove the validity of the submission (for example, - "L1b48a"). - 'U': Source unknown - @param rating: The rating of the song. One of: - 'L': Love (on any mode if the user has manually loved the - track) - 'B': Ban (only if source=L) - 'S': Skip (only if source=L) - '': Not applicable - @param length: The song length in seconds - @param album: The album name - @param trackno:The track number - @param mbid: MusicBrainz Track ID - @param autoflush: Automatically flush the cache to AS? - """ - - global SUBMIT_CACHE, MAX_CACHE - - source = source.upper() - rating = rating.upper() - - if source == 'L' and (rating == 'B' or rating == 'S'): - raise ProtocolError("""You can only use rating 'B' or 'S' on source 'L'.See the docs!""") - - if source == 'P' and length == '': - raise ProtocolError("""Song length must be specified when using 'P' as source!""") - - if type(time) != type(1): - raise ValueError("""The time parameter must be of type int (unix timestamp). Instead it was %s""" % time) - - SUBMIT_CACHE.append( - { 'a': unicode(artist).encode('utf-8'), - 't': unicode(track).encode('utf-8'), - 'i': time, - 'o': source, - 'r': rating, - 'l': length, - 'b': unicode(album).encode('utf-8'), - 'n': trackno, - 'm': mbid - } - ) - - if autoflush or len(SUBMIT_CACHE) >= MAX_CACHE: - flush() + trackno="", mbid="", autoflush=False): + """Append a song to the submission cache. Use 'flush()' to send the cache to + AS. You can also set "autoflush" to True. + + From the Audioscrobbler protocol docs: + --------------------------------------------------------------------------- + + The client should monitor the user's interaction with the music playing + service to whatever extent the service allows. In order to qualify for + submission all of the following criteria must be met: + + 1. The track must be submitted once it has finished playing. Whether it has + finished playing naturally or has been manually stopped by the user is + irrelevant. + 2. The track must have been played for a duration of at least 240 seconds or + half the track's total length, whichever comes first. Skipping or pausing + the track is irrelevant as long as the appropriate amount has been played. + 3. The total playback time for the track must be more than 30 seconds. Do + not submit tracks shorter than this. + 4. Unless the client has been specially configured, it should not attempt to + interpret filename information to obtain metadata instead of tags (ID3, + etc). + + @param artist: Artist name + @param track: Track name + @param time: Time the track *started* playing in the UTC timezone (see + datetime.utcnow()). + + Example: int(time.mktime(datetime.utcnow())) + @param source: Source of the track. One of: + 'P': Chosen by the user + 'R': Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1) + 'E': Personalised recommendation except Last.fm (e.g. + Pandora, Launchcast) + 'L': Last.fm (any mode). In this case, the 5-digit Last.fm + recommendation key must be appended to this source ID to + prove the validity of the submission (for example, + "L1b48a"). + 'U': Source unknown + @param rating: The rating of the song. One of: + 'L': Love (on any mode if the user has manually loved the + track) + 'B': Ban (only if source=L) + 'S': Skip (only if source=L) + '': Not applicable + @param length: The song length in seconds + @param album: The album name + @param trackno:The track number + @param mbid: MusicBrainz Track ID + @param autoflush: Automatically flush the cache to AS? + """ + + global SUBMIT_CACHE, MAX_CACHE + + source = source.upper() + rating = rating.upper() + + if source == 'L' and (rating == 'B' or rating == 'S'): + raise ProtocolError("""You can only use rating 'B' or 'S' on source 'L'.See the docs!""") + + if source == 'P' and length == '': + raise ProtocolError("""Song length must be specified when using 'P' as source!""") + + if type(time) != type(1): + raise ValueError("""The time parameter must be of type int (unix timestamp). Instead it was %s""" % time) + + SUBMIT_CACHE.append( + { 'a': unicode(artist).encode('utf-8'), + 't': unicode(track).encode('utf-8'), + 'i': time, + 'o': source, + 'r': rating, + 'l': length, + 'b': unicode(album).encode('utf-8'), + 'n': trackno, + 'm': mbid + } + ) + + if autoflush or len(SUBMIT_CACHE) >= MAX_CACHE: + flush() def flush(): - "Sends the cached songs to AS." - global SUBMIT_CACHE - - values = {} - - for i, item in enumerate(SUBMIT_CACHE): - for key in item: - values[key + "[%d]" % i] = item[key] - - values['s'] = SESSION_ID - - data = urllib.urlencode(values) - req = urllib2.Request(POST_URL, data) - response = urllib2.urlopen(req) - result = response.read() - lines = result.split('\n') - - if lines[0] == "OK": - SUBMIT_CACHE = [] - return True - elif lines[0] == "BADSESSION" : - raise SessionError('Invalid session') - elif lines[0].startswith('FAILED'): - handle_hard_error() - raise BackendError("Authencitation with AS failed. Reason: %s" % - lines[0]) - else: - # some hard error - handle_hard_error() - return False + "Sends the cached songs to AS." + global SUBMIT_CACHE + + values = {} + + for i, item in enumerate(SUBMIT_CACHE): + for key in item: + values[key + "[%d]" % i] = item[key] + + values['s'] = SESSION_ID + + data = urllib.urlencode(values) + req = urllib2.Request(POST_URL, data) + response = urllib2.urlopen(req) + result = response.read() + lines = result.split('\n') + + if lines[0] == "OK": + SUBMIT_CACHE = [] + return True + elif lines[0] == "BADSESSION" : + raise SessionError('Invalid session') + elif lines[0].startswith('FAILED'): + handle_hard_error() + raise BackendError("Authencitation with AS failed. Reason: %s" % + lines[0]) + else: + # some hard error + handle_hard_error() + return False if __name__ == "__main__": - login( 'user', 'password' ) - submit( - 'De/Vision', - 'Scars', - 1192374052, - source='P', - length=3*60+44 - ) - submit( - 'Spineshank', - 'Beginning of the End', - 1192374052+(5*60), - source='P', - length=3*60+32 - ) - submit( - 'Dry Cell', - 'Body Crumbles', - 1192374052+(10*60), - source='P', - length=3*60+3 - ) - print flush() + login( 'user', 'password' ) + submit( + 'De/Vision', + 'Scars', + 1192374052, + source='P', + length=3*60+44 + ) + submit( + 'Spineshank', + 'Beginning of the End', + 1192374052+(5*60), + source='P', + length=3*60+32 + ) + submit( + 'Dry Cell', + 'Body Crumbles', + 1192374052+(10*60), + source='P', + length=3*60+3 + ) + print flush() diff --git a/plugins/Shortcuts.py b/plugins/Shortcuts.py index baa462f..e76078f 100644 --- a/plugins/Shortcuts.py +++ b/plugins/Shortcuts.py @@ -6,81 +6,81 @@ from misc import * import format class pluginShortcuts(Plugin): - keys=None - col=None - actionPrefix=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Shortcuts') + keys=None + col=None + actionPrefix=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Shortcuts') - def _load(self): - winMain=self.getWinMain() - # Note people wanting to implement global shortcuts in KDE4 (and sniffing through - # this code): one needs to have a KApplication running, else shortcuts will fail - self.col=kdeui.KActionCollection(None) - self.col.addAssociatedWidget(winMain) - self.keys=[ - ['toggleplay', QtCore.Qt.META+QtCore.Qt.Key_Home, self.togglePlay], - ['volumeup', QtCore.Qt.META+QtCore.Qt.Key_PageDown, lambda b: monty.setVolume(monty.getVolume()-5)], - ['volumedown', QtCore.Qt.META+QtCore.Qt.Key_PageUp, lambda b: monty.setVolume(monty.getVolume()+5)], - ['playnext', QtCore.Qt.META+QtCore.Qt.Key_Right, monty.next], - ['playprevious', QtCore.Qt.META+QtCore.Qt.Key_Left, monty.previous], - ['showosd', QtCore.Qt.META+QtCore.Qt.Key_O, self.showOSD], - ['togglewin', QtCore.Qt.META+QtCore.Qt.Key_P, self.toggleWinMain], - ] - # Note: don't use any non-alphanumerics in the prefix, as else it won't work - self.actionPrefix="shortcuts" - - for entry in self.keys: - name=entry[0] - key=entry[1] - callback=entry[2] - - self.debug("%s - %s"%(name, QtGui.QKeySequence(key).toString())) - - action=kdeui.KAction(winMain) # winMain needed - action.setText(name) - action.setObjectName(name) - action.setGlobalShortcut(kdeui.KShortcut(key)) - QtCore.QObject.connect(action, QtCore.SIGNAL('triggered(bool)'), callback) - self.col.addAction("%s%s"%(self.actionPrefix, action.objectName()), action) - def _unload(self): - actions=self.col.actions() - for action in actions: - try: - if action.objectName()[0:len(self.actionPrefix)]==self.actionPrefix: - self.debug("removing %s"%(action.objectName())) - self.col.removeAction(action) - except Exception, e: - self.important(str(e)) - - def getInfo(self): - return "Shortcuts for mpd." + def _load(self): + winMain=self.getWinMain() + # Note people wanting to implement global shortcuts in KDE4 (and sniffing through + # this code): one needs to have a KApplication running, else shortcuts will fail + self.col=kdeui.KActionCollection(None) + self.col.addAssociatedWidget(winMain) + self.keys=[ + ['toggleplay', QtCore.Qt.META+QtCore.Qt.Key_Home, self.togglePlay], + ['volumeup', QtCore.Qt.META+QtCore.Qt.Key_PageDown, lambda b: monty.setVolume(monty.getVolume()-5)], + ['volumedown', QtCore.Qt.META+QtCore.Qt.Key_PageUp, lambda b: monty.setVolume(monty.getVolume()+5)], + ['playnext', QtCore.Qt.META+QtCore.Qt.Key_Right, monty.next], + ['playprevious', QtCore.Qt.META+QtCore.Qt.Key_Left, monty.previous], + ['showosd', QtCore.Qt.META+QtCore.Qt.Key_O, self.showOSD], + ['togglewin', QtCore.Qt.META+QtCore.Qt.Key_P, self.toggleWinMain], + ] + # Note: don't use any non-alphanumerics in the prefix, as else it won't work + self.actionPrefix="shortcuts" + + for entry in self.keys: + name=entry[0] + key=entry[1] + callback=entry[2] + + self.debug("%s - %s"%(name, QtGui.QKeySequence(key).toString())) + + action=kdeui.KAction(winMain) # winMain needed + action.setText(name) + action.setObjectName(name) + action.setGlobalShortcut(kdeui.KShortcut(key)) + QtCore.QObject.connect(action, QtCore.SIGNAL('triggered(bool)'), callback) + self.col.addAction("%s%s"%(self.actionPrefix, action.objectName()), action) + def _unload(self): + actions=self.col.actions() + for action in actions: + try: + if action.objectName()[0:len(self.actionPrefix)]==self.actionPrefix: + self.debug("removing %s"%(action.objectName())) + self.col.removeAction(action) + except Exception, e: + self.important(str(e)) + + def getInfo(self): + return "Shortcuts for mpd." - def showOSD(self, b): - plugins.getPlugin('notify').onSongChange(None) + def showOSD(self, b): + plugins.getPlugin('notify').onSongChange(None) - def togglePlay(self, btns=None, mods=None): - if monty.isPlaying(): - monty.pause() - else: - monty.resume() - - def toggleWinMain(self, b): - w=self.getWinMain() - if w.isVisible(): - w.setVisible(False) - else: - w.setVisible(True) - - def _getSettings(self): - txt=QtGui.QTextEdit() - txt.setReadOnly(True) - txt.insertPlainText("Keybindings (read-only)\n") - for entry in self.keys: - name=entry[0] - key=entry[1] - - txt.insertPlainText("%s\t%s\n"%(name, QtGui.QKeySequence(key).toString())) - return [ - ['', 'Keybindings', 'Current keybindings', txt], - ] + def togglePlay(self, btns=None, mods=None): + if monty.isPlaying(): + monty.pause() + else: + monty.resume() + + def toggleWinMain(self, b): + w=self.getWinMain() + if w.isVisible(): + w.setVisible(False) + else: + w.setVisible(True) + + def _getSettings(self): + txt=QtGui.QTextEdit() + txt.setReadOnly(True) + txt.insertPlainText("Keybindings (read-only)\n") + for entry in self.keys: + name=entry[0] + key=entry[1] + + txt.insertPlainText("%s\t%s\n"%(name, QtGui.QKeySequence(key).toString())) + return [ + ['', 'Keybindings', 'Current keybindings', txt], + ] diff --git a/plugins/SongStatus.py b/plugins/SongStatus.py index 052e99b..71aa6e4 100644 --- a/plugins/SongStatus.py +++ b/plugins/SongStatus.py @@ -12,79 +12,79 @@ SONGSTATUS_FORMAT_DEFAULT='<font size="4">now $state</font>'\ '$if($length,<br /><font size="4">$time/$length</font>)' class wgSongStatus(QtGui.QWidget): - """Displays the status of the current song, if playing.""" - " label containing the info" - lblInfo=None - format=None - p=None - def __init__(self, p, parent=None): - QtGui.QWidget.__init__(self, parent) - self.p=p + """Displays the status of the current song, if playing.""" + " label containing the info" + lblInfo=None + format=None + p=None + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p - self.lblInfo=QtGui.QLabel() - self.setMinimumWidth(400) - - layout=QtGui.QHBoxLayout() - self.setLayout(layout) + self.lblInfo=QtGui.QLabel() + self.setMinimumWidth(400) + + layout=QtGui.QHBoxLayout() + self.setLayout(layout) - layout.addWidget(self.lblInfo) - self.updateFormat() + layout.addWidget(self.lblInfo) + self.updateFormat() - def update(self, params): - status=monty.getStatus() - song=monty.getCurrentSong() + def update(self, params): + status=monty.getStatus() + song=monty.getCurrentSong() - values={'state':''} - try: - values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']] - if 'time' in status: - values['length']=sec2min(status['length']) - values['time']=sec2min(status['time']) - except: - pass - - if song: - self.lblInfo.setText(self.format(format.params(song, values))) - - def updateFormat(self): - try: - self.format=format.compile(self.p.getSetting('format')) - except Exception, e: - self.format=lambda p: "Invalid format: %s"%(e) + values={'state':''} + try: + values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']] + if 'time' in status: + values['length']=sec2min(status['length']) + values['time']=sec2min(status['time']) + except: + pass + + if song: + self.lblInfo.setText(self.format(format.params(song, values))) + + def updateFormat(self): + try: + self.format=format.compile(self.p.getSetting('format')) + except Exception, e: + self.format=lambda p: "Invalid format: %s"%(e) - def text(self): - return self.lblInfo.text() + def text(self): + return self.lblInfo.text() class pluginSongStatus(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'SongStatus') - self.addMontyListener('onSongChange', self.update) - self.addMontyListener('onTimeChange', self.update) - self.addMontyListener('onStateChange', self.update) - self.addMontyListener('onConnect', self.update) - self.addMontyListener('onDisconnect', self.update) - - def _load(self): - self.o=wgSongStatus(self, None) - self.update(None) - def _unload(self): - self.o=None - def getInfo(self): - return "Show information about the current song." - - def update(self, params): - self.o.update(params) - - def _getDockWidget(self): - return self._createDock(self.o) + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'SongStatus') + self.addMontyListener('onSongChange', self.update) + self.addMontyListener('onTimeChange', self.update) + self.addMontyListener('onStateChange', self.update) + self.addMontyListener('onConnect', self.update) + self.addMontyListener('onDisconnect', self.update) + + def _load(self): + self.o=wgSongStatus(self, None) + self.update(None) + def _unload(self): + self.o=None + def getInfo(self): + return "Show information about the current song." + + def update(self, params): + self.o.update(params) + + def _getDockWidget(self): + return self._createDock(self.o) - def _getSettings(self): - format=QtGui.QTextEdit() - format.insertPlainText(self.getSetting('format')) - return [ - ['format', 'Format', 'Format of the song status. Possible tags: $title, $artist, $album, $track, $time, $length, $state', format] - ] - def afterSaveSettings(self): - self.o.updateFormat() - self.o.update(None) + def _getSettings(self): + format=QtGui.QTextEdit() + format.insertPlainText(self.getSetting('format')) + return [ + ['format', 'Format', 'Format of the song status. Possible tags: $title, $artist, $album, $track, $time, $length, $state', format] + ] + def afterSaveSettings(self): + self.o.updateFormat() + self.o.update(None) diff --git a/plugins/Systray.py b/plugins/Systray.py index 75d2267..b933225 100644 --- a/plugins/Systray.py +++ b/plugins/Systray.py @@ -7,107 +7,107 @@ import format SYSTRAY_FORMAT="$if($title,$title)$if($artist, by $artist) - [$album # $track] ($time/$length)" class pluginSystray(Plugin): - o=None - format=None - eventObj=None - time=None # indicator of current time [0..64] - appIcon=None - pixmap=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Systray') - self.addMontyListener('onSongChange', self.update) - self.addMontyListener('onReady', self.update) - self.addMontyListener('onConnect', self.update) - self.addMontyListener('onDisconnect', self.update) - # TODO only update this when necessary, i.e. mouse-hover etc - self.addMontyListener('onTimeChange', self.update) - self.appIcon=appIcon - + o=None + format=None + eventObj=None + time=None # indicator of current time [0..64] + appIcon=None + pixmap=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Systray') + self.addMontyListener('onSongChange', self.update) + self.addMontyListener('onReady', self.update) + self.addMontyListener('onConnect', self.update) + self.addMontyListener('onDisconnect', self.update) + # TODO only update this when necessary, i.e. mouse-hover etc + self.addMontyListener('onTimeChange', self.update) + self.appIcon=appIcon + - def _load(self): - self.format=format.compile(SYSTRAY_FORMAT) - class SystrayWheelEventObject(QtCore.QObject): - """This class listens for systray-wheel events""" - def eventFilter(self, object, event): - if type(event)==QtGui.QWheelEvent: - numDegrees=event.delta() / 8 - numSteps=5*numDegrees/15 - monty.setVolume(int(monty.getStatus()['volume'])+numSteps) - event.accept() - return True - return False - - self.o=QtGui.QSystemTrayIcon(appIcon, self.winMain) - self.eventObj=SystrayWheelEventObject() - self.o.installEventFilter(self.eventObj) - self.winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)') - , self.onSysTrayClick) - self.o.show() - self.update(None) - - def _unload(self): - self.o.hide() - self.o.setIcon(QtGui.QIcon(None)) - self.o=None - self.winMain._wheelEvent=None - def getInfo(self): - return "Display the montypc icon in the systray." - - def update(self, params): - status=monty.getStatus() - song=monty.getCurrentSong() + def _load(self): + self.format=format.compile(SYSTRAY_FORMAT) + class SystrayWheelEventObject(QtCore.QObject): + """This class listens for systray-wheel events""" + def eventFilter(self, object, event): + if type(event)==QtGui.QWheelEvent: + numDegrees=event.delta() / 8 + numSteps=5*numDegrees/15 + monty.setVolume(int(monty.getStatus()['volume'])+numSteps) + event.accept() + return True + return False + + self.o=QtGui.QSystemTrayIcon(appIcon, self.winMain) + self.eventObj=SystrayWheelEventObject() + self.o.installEventFilter(self.eventObj) + self.winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)') + , self.onSysTrayClick) + self.o.show() + self.update(None) + + def _unload(self): + self.o.hide() + self.o.setIcon(QtGui.QIcon(None)) + self.o=None + self.winMain._wheelEvent=None + def getInfo(self): + return "Display the montypc icon in the systray." + + def update(self, params): + status=monty.getStatus() + song=monty.getCurrentSong() - values={'state':''} - try: - values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']] - if 'time' in status: - values['length']=sec2min(status['length']) - values['time']=sec2min(status['time']) - except: - pass - - if song: - self.o.setToolTip(self.format(format.params(song, values))) - else: - self.o.setToolTip("montypc not playing") - - try: - curTime=(64*status['time'])/status['length'] - except: - curTime=-1 - if self.time!=curTime: - self.time=curTime - # redraw the systray icon - self.pixmap=self.appIcon.pixmap(64,64) - painter=QtGui.QPainter(self.pixmap) - painter.fillRect(1, curTime, 63, 64, QtGui.QBrush(QtCore.Qt.white)) - appIcon.paint(painter, 1, 0, 63, 64) - self.o.setIcon(QtGui.QIcon(self.pixmap)) - - elif not song: - self.time=None - self.o.setIcon(appIcon) + values={'state':''} + try: + values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']] + if 'time' in status: + values['length']=sec2min(status['length']) + values['time']=sec2min(status['time']) + except: + pass + + if song: + self.o.setToolTip(self.format(format.params(song, values))) + else: + self.o.setToolTip("montypc not playing") + + try: + curTime=(64*status['time'])/status['length'] + except: + curTime=-1 + if self.time!=curTime: + self.time=curTime + # redraw the systray icon + self.pixmap=self.appIcon.pixmap(64,64) + painter=QtGui.QPainter(self.pixmap) + painter.fillRect(1, curTime, 63, 64, QtGui.QBrush(QtCore.Qt.white)) + appIcon.paint(painter, 1, 0, 63, 64) + self.o.setIcon(QtGui.QIcon(self.pixmap)) + + elif not song: + self.time=None + self.o.setIcon(appIcon) - - def onSysTrayClick(self, reason): - if reason==QtGui.QSystemTrayIcon.Trigger \ - or reason==QtGui.QSystemTrayIcon.Context: - w=self.getWinMain() - # left mouse button - if w.isVisible(): - settings.setIntTuple('winMain.pos', w.x(), w.y()) - w.setVisible(False) - else: - w.setVisible(True) - try: - x,y=settings.getIntTuple('winMain.pos') - except: - x,y=0,0 - w.move(x, y) - elif reason==QtGui.QSystemTrayIcon.MiddleClick: - # middle mouse button - if monty.isPlaying(): - monty.pause() - else: - monty.resume() - + + def onSysTrayClick(self, reason): + if reason==QtGui.QSystemTrayIcon.Trigger \ + or reason==QtGui.QSystemTrayIcon.Context: + w=self.getWinMain() + # left mouse button + if w.isVisible(): + settings.setIntTuple('winMain.pos', w.x(), w.y()) + w.setVisible(False) + else: + w.setVisible(True) + try: + x,y=settings.getIntTuple('winMain.pos') + except: + x,y=0,0 + w.move(x, y) + elif reason==QtGui.QSystemTrayIcon.MiddleClick: + # middle mouse button + if monty.isPlaying(): + monty.pause() + else: + monty.resume() + diff --git a/plugins/Tabs.py b/plugins/Tabs.py index 811096a..8914a4b 100644 --- a/plugins/Tabs.py +++ b/plugins/Tabs.py @@ -9,157 +9,157 @@ from clMonty import monty from clPlugin import * class ResetEvent(QtCore.QEvent): - song=None - def __init__(self, song=None): - QtCore.QEvent.__init__(self,QtCore.QEvent.User) - self.song=song + song=None + def __init__(self, song=None): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.song=song class AddHtmlEvent(QtCore.QEvent): - html=None - def __init__(self,html): - QtCore.QEvent.__init__(self,QtCore.QEvent.User) - self.html=html + html=None + def __init__(self,html): + QtCore.QEvent.__init__(self,QtCore.QEvent.User) + self.html=html TABS_DIR_DEFAULT='/jammin' TABS_ENGINE_DEFAULT='http://www.google.com/search?q=tabs|chords+"$artist"+"$title"' -TABS_SITES_DEFAULT='azchords.com <pre>(.*?)</pre>\n'\ - 'fretplay.com <P CLASS="tabs">(.*?)</P>\n'\ - 'guitaretab.com <pre style="COLOR: #000000; FONT-SIZE: 11px;">(.*)?</pre>'\ +TABS_SITES_DEFAULT='azchords.com <pre>(.*?)</pre>\n'\ + 'fretplay.com <P CLASS="tabs">(.*?)</P>\n'\ + 'guitaretab.com <pre style="COLOR: #000000; FONT-SIZE: 11px;">(.*)?</pre>'\ class wgTabs(QtGui.QWidget): - " contains the tabs" - txt=None - p=None # plugin - def __init__(self, p, parent=None): - QtGui.QWidget.__init__(self, parent) - self.p=p - self.txt=QtGui.QTextEdit(parent) - self.txt.setReadOnly(True) - - layout=QtGui.QVBoxLayout() - layout.addWidget(self.txt) - self.setLayout(layout) + " contains the tabs" + txt=None + p=None # plugin + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p + self.txt=QtGui.QTextEdit(parent) + self.txt.setReadOnly(True) + + layout=QtGui.QVBoxLayout() + layout.addWidget(self.txt) + self.setLayout(layout) - - def refresh(self): - song=monty.getCurrentSong() - try: - song._data['file'] - except: - self.resetTxt() - return - - self.resetTxt(song) - start_new_thread(self.fetchTabs, (song,)) - - def customEvent(self, event): - if isinstance(event,ResetEvent): - self.resetTxt(event.song) - elif isinstance(event,AddHtmlEvent): - self.txt.insertHtml(event.html) + + def refresh(self): + song=monty.getCurrentSong() + try: + song._data['file'] + except: + self.resetTxt() + return + + self.resetTxt(song) + start_new_thread(self.fetchTabs, (song,)) + + def customEvent(self, event): + if isinstance(event,ResetEvent): + self.resetTxt(event.song) + elif isinstance(event,AddHtmlEvent): + self.txt.insertHtml(event.html) - _mutex=QtCore.QMutex() - _fetchCnt=0 - def fetchTabs(self, song): - # only allow 1 instance to look tabs! - self._mutex.lock() - if self._fetchCnt: - self._mutex.unlock() - return - self._fetchCnt=1 - self._mutex.unlock() + _mutex=QtCore.QMutex() + _fetchCnt=0 + def fetchTabs(self, song): + # only allow 1 instance to look tabs! + self._mutex.lock() + if self._fetchCnt: + self._mutex.unlock() + return + self._fetchCnt=1 + self._mutex.unlock() - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - - # save the data to file! - save_dir=self.p.getSetting('dir') - fInfo=QtCore.QFileInfo(save_dir) - if fInfo.isDir(): - tabsFName=toAscii('%s/%s - %s.txt'%(save_dir,song.getArtist(),song.getTitle())) - else: - tabsFName=None - # does the file exist? if yes, read that one! - try: - # we have it: load, and return! - file=open(tabsFName, 'r') - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(file.read())) - file.close() - self._fetchCnt=0 - return - except: - pass + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + + # save the data to file! + save_dir=self.p.getSetting('dir') + fInfo=QtCore.QFileInfo(save_dir) + if fInfo.isDir(): + tabsFName=toAscii('%s/%s - %s.txt'%(save_dir,song.getArtist(),song.getTitle())) + else: + tabsFName=None + # does the file exist? if yes, read that one! + try: + # we have it: load, and return! + file=open(tabsFName, 'r') + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(file.read())) + file.close() + self._fetchCnt=0 + return + except: + pass - # fetch from inet - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<i>Searching tabs ...</i>')) - - lines=self.p.getSetting('sites').split('\n') - sites={} - for line in lines: - if line.strip(): - sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip() - # construct URL to search! - SE=self.p.getSetting('engine') - try: - ret=fetch(SE, sites, song, {}, stripHTML=False) - if ret: - txt='<pre>%s<br /><br /><a href="%s">%s</a></pre>'%(ret[0],ret[1],ret[1]) - # save for later use! - if tabsFName: - # we can't save if the path isn't correct - try: - file=open(tabsFName, 'w') - file.write(ret[0]) - file.close() - except: - pass - else: - txt="No tabs found :'(" + # fetch from inet + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<i>Searching tabs ...</i>')) + + lines=self.p.getSetting('sites').split('\n') + sites={} + for line in lines: + if line.strip(): + sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip() + # construct URL to search! + SE=self.p.getSetting('engine') + try: + ret=fetch(SE, sites, song, {}, stripHTML=False) + if ret: + txt='<pre>%s<br /><br /><a href="%s">%s</a></pre>'%(ret[0],ret[1],ret[1]) + # save for later use! + if tabsFName: + # we can't save if the path isn't correct + try: + file=open(tabsFName, 'w') + file.write(ret[0]) + file.close() + except: + pass + else: + txt="No tabs found :'(" - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(txt)) - except: - print_exc() - QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) - QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, site unavailable!'\ - '<br />You have an internet connection?')) - self._fetchCnt=0 + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(txt)) + except: + print_exc() + QtCore.QCoreApplication.postEvent(self, ResetEvent(song)) + QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, site unavailable!'\ + '<br />You have an internet connection?')) + self._fetchCnt=0 - def resetTxt(self, song=None): - self.txt.clear() - if song: - self.txt.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\ - '<br />\n\n'%(song.getTitle(), song.getArtist())) + def resetTxt(self, song=None): + self.txt.clear() + if song: + self.txt.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\ + '<br />\n\n'%(song.getTitle(), song.getArtist())) class pluginTabs(Plugin): - o=None - def __init__(self, winMain): - Plugin.__init__(self, winMain, 'Tabs') - self.addMontyListener('onSongChange', self.refresh) - self.addMontyListener('onReady', self.refresh) - self.addMontyListener('onDisconnect', self.onDisconnect) - def _load(self): - self.o=wgTabs(self, None) - self.refresh(None) - def _unload(self): - self.o=None - def getInfo(self): - return "Show (and fetch) the tabs of the currently playing song." - - def _getDockWidget(self): - return self._createDock(self.o) - - def refresh(self, params): - self.o.refresh() - def onDisconnect(self, params): - self.o.resetTxt() + o=None + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Tabs') + self.addMontyListener('onSongChange', self.refresh) + self.addMontyListener('onReady', self.refresh) + self.addMontyListener('onDisconnect', self.onDisconnect) + def _load(self): + self.o=wgTabs(self, None) + self.refresh(None) + def _unload(self): + self.o=None + def getInfo(self): + return "Show (and fetch) the tabs of the currently playing song." + + def _getDockWidget(self): + return self._createDock(self.o) + + def refresh(self, params): + self.o.refresh() + def onDisconnect(self, params): + self.o.resetTxt() - def _getSettings(self): - sites=QtGui.QTextEdit() - sites.insertPlainText(self.getSetting('sites')) - return [ - ['engine', 'Search engine', 'The URL that is used to search. $artist, $title and $album are replaced in the URL.', QtGui.QLineEdit(self.getSetting('engine'))], - ['sites', 'Sites & regexes', 'This field contains all sites, together with the regex needed to fetch the tabs.\nEvery line must look like this: $domain $regex-start(.*?)$regex-end\n$domain is the domain of the tabs website, $regex-start is the regex indicating the start of the tabs, $regex-end indicates the end. E.g. footabs.org <tabs>(.*?)</tabs>', sites], - ['dir', 'Tabs directory', 'Directory where tabs should be stored and retrieved.', QtGui.QLineEdit(self.getSetting('dir'))], - ] - def afterSaveSettings(self): - self.o.onSongChange(None) + def _getSettings(self): + sites=QtGui.QTextEdit() + sites.insertPlainText(self.getSetting('sites')) + return [ + ['engine', 'Search engine', 'The URL that is used to search. $artist, $title and $album are replaced in the URL.', QtGui.QLineEdit(self.getSetting('engine'))], + ['sites', 'Sites & regexes', 'This field contains all sites, together with the regex needed to fetch the tabs.\nEvery line must look like this: $domain $regex-start(.*?)$regex-end\n$domain is the domain of the tabs website, $regex-start is the regex indicating the start of the tabs, $regex-end indicates the end. E.g. footabs.org <tabs>(.*?)</tabs>', sites], + ['dir', 'Tabs directory', 'Directory where tabs should be stored and retrieved.', QtGui.QLineEdit(self.getSetting('dir'))], + ] + def afterSaveSettings(self): + self.o.onSongChange(None) diff --git a/plugins/__init__.py b/plugins/__init__.py index aa940bc..0673752 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -11,69 +11,69 @@ PLUGIN_MSG=3 def loadPlugins(): - """(Re)load all modules in the plugins directory.""" - global _plugins - _plugins={} - for file in os.listdir('plugins'): - if file[-3:]=='.py' and file!='__init__.py': - name=file[:-3] # name without ext - mod='plugins.%s'%(name) # mod name - className='plugin%s'%(name) # classname - - _plugins[className.lower()]=[mod, className, None, None] - loadPlugin(className, None) + """(Re)load all modules in the plugins directory.""" + global _plugins + _plugins={} + for file in os.listdir('plugins'): + if file[-3:]=='.py' and file!='__init__.py': + name=file[:-3] # name without ext + mod='plugins.%s'%(name) # mod name + className='plugin%s'%(name) # classname + + _plugins[className.lower()]=[mod, className, None, None] + loadPlugin(className, None) def getPlugin(name): - global _plugins - try: - return _plugins[name.lower()][PLUGIN_INSTANCE] - except: - try: - return _plugins["plugin%s"%(name.lower())][PLUGIN_INSTANCE] - except: - return None - + global _plugins + try: + return _plugins[name.lower()][PLUGIN_INSTANCE] + except: + try: + return _plugins["plugin%s"%(name.lower())][PLUGIN_INSTANCE] + except: + return None + def loadPlugin(className, parent): - """Constructs a plugin.""" - global _plugins - entry=_plugins[className.lower()] - mod=entry[PLUGIN_MODULE] - # ensure we get the latest version - try: - try: - sys.modules[mod] - reimport=True - except: - reimport=False - - if reimport: - reload(sys.modules[mod]) - else: - module=__import__(mod, globals(), locals(), className, -1) - - except Exception, e: - _plugins[className.lower()][PLUGIN_MSG]=str(e) - _plugins[className.lower()][PLUGIN_INSTANCE]=None - log.important("Failed to load plugin %s: %s %s"%(className, str(type(e)), str(e))) - return None - - module=sys.modules[mod] - _plugins[className.lower()][PLUGIN_MSG]=None - - if parent: - # instantiate the plugin - _plugins[className.lower()][PLUGIN_INSTANCE]=module.__dict__[className](parent) - else: - _plugins[className.lower()][PLUGIN_INSTANCE]=None - return _plugins[className.lower()][PLUGIN_INSTANCE] - + """Constructs a plugin.""" + global _plugins + entry=_plugins[className.lower()] + mod=entry[PLUGIN_MODULE] + # ensure we get the latest version + try: + try: + sys.modules[mod] + reimport=True + except: + reimport=False + + if reimport: + reload(sys.modules[mod]) + else: + module=__import__(mod, globals(), locals(), className, -1) + + except Exception, e: + _plugins[className.lower()][PLUGIN_MSG]=str(e) + _plugins[className.lower()][PLUGIN_INSTANCE]=None + log.important("Failed to load plugin %s: %s %s"%(className, str(type(e)), str(e))) + return None + + module=sys.modules[mod] + _plugins[className.lower()][PLUGIN_MSG]=None + + if parent: + # instantiate the plugin + _plugins[className.lower()][PLUGIN_INSTANCE]=module.__dict__[className](parent) + else: + _plugins[className.lower()][PLUGIN_INSTANCE]=None + return _plugins[className.lower()][PLUGIN_INSTANCE] + def listPlugins(): - """Get the list of plugins available as { className => [mod, className, instance, msg] }.""" - global _plugins - return _plugins + """Get the list of plugins available as { className => [mod, className, instance, msg] }.""" + global _plugins + return _plugins loadPlugins() diff --git a/wgPlaylist.py b/wgPlaylist.py index 13c4790..587be87 100644 --- a/wgPlaylist.py +++ b/wgPlaylist.py @@ -8,128 +8,128 @@ from clSettings import settings class Playlist(QtGui.QWidget): - """The Playlist widget is a list optimized for displaying a playlist, with filtering.""" - name=None # name of this playlist - txtFilter=None # filter input - btnClearFilter=None - cmbMode=None # what mode? library, playlist? - modes=[] - lstSongs=None # list - winMain=None # the main window - onKeyPress=None - _timerID=None + """The Playlist widget is a list optimized for displaying a playlist, with filtering.""" + name=None # name of this playlist + txtFilter=None # filter input + btnClearFilter=None + cmbMode=None # what mode? library, playlist? + modes=[] + lstSongs=None # list + winMain=None # the main window + onKeyPress=None + _timerID=None - def __init__(self, parent, wMain, headers, name, onDoubleClick, onKeyPress, modes): - QtGui.QWidget.__init__(self, parent) - - self.name=name - self.winMain=wMain - - self.onKeyPress=onKeyPress - - self.lstSongs=SongList(parent, self.name, headers, onDoubleClick) - - self.txtFilter=QtGui.QLineEdit() - self.btnClearFilter=Button("Clear", self.clearTxtFilter) - - self.cmbMode=QtGui.QComboBox() - self.setModes(modes) - - frame=QtGui.QVBoxLayout() - filter=QtGui.QHBoxLayout() - filter.addWidget(self.txtFilter) - filter.addWidget(self.btnClearFilter) - frame.addWidget(self.cmbMode) - frame.addLayout(filter); - frame.addWidget(self.lstSongs) - self.setLayout(frame) - - self.connect(self.txtFilter, QtCore.SIGNAL('textChanged(const QString&)'), self.txtFilterChange) - self.connect(self.txtFilter, QtCore.SIGNAL('editingFinished()'), self.txtFilterQuit) - self.connect(self.cmbMode, QtCore.SIGNAL('currentIndexChanged(const QString&)'), self.onModeChangeChange) - - def clearTxtFilter(self, event): - self.txtFilter.setText("") - - _prevTxtFilter=None - def timerEvent(self, event): - if self.txtFilter.text()!=self._prevTxtFilter: - # only filter after a little while - self.killTimer(self._timerID) - self._timerID=None - self.filter() - def keyPressEvent(self, event): - self.onKeyPress(event) - def txtFilterChange(self): - if self._timerID==None: - # only start filtering after a little while - self._timerID=self.startTimer(500) - def txtFilterQuit(self): - if self._timerID: - self.killTimer(self._timerID) - self._timerID=None - def updateSongs(self, songs): - """Update the list of songs.""" - start_new_thread(self._update, (songs,)) - def _update(self, songs): - self.lstSongs.updateSongs(songs) - if len(self.txtFilter.text()): - self.filter() - - def setMode(self, mode, levels=''): - """Set the mode of showing the list.""" - if mode=='playlist': - self.cmbMode.setCurrentIndex(0) - else: - self.cmbMode.setCurrentIndex(self.cmbMode.findText(levels)) - def selectedSongs(self): - """Get the selected songs.""" - return self.lstSongs.selectedSongs() - def selectedIds(self): - """Get the selected songIDs.""" - return map(lambda song: song._data['id'], self.lstSongs.selectedSongs()) - def getSelItemID(self): - """Get the id of the first selected song.""" - try: - return self.lstSongs.selectedSongs()[0].getID() - except: - return -1 + def __init__(self, parent, wMain, headers, name, onDoubleClick, onKeyPress, modes): + QtGui.QWidget.__init__(self, parent) + + self.name=name + self.winMain=wMain + + self.onKeyPress=onKeyPress + + self.lstSongs=SongList(parent, self.name, headers, onDoubleClick) + + self.txtFilter=QtGui.QLineEdit() + self.btnClearFilter=Button("Clear", self.clearTxtFilter) + + self.cmbMode=QtGui.QComboBox() + self.setModes(modes) + + frame=QtGui.QVBoxLayout() + filter=QtGui.QHBoxLayout() + filter.addWidget(self.txtFilter) + filter.addWidget(self.btnClearFilter) + frame.addWidget(self.cmbMode) + frame.addLayout(filter); + frame.addWidget(self.lstSongs) + self.setLayout(frame) + + self.connect(self.txtFilter, QtCore.SIGNAL('textChanged(const QString&)'), self.txtFilterChange) + self.connect(self.txtFilter, QtCore.SIGNAL('editingFinished()'), self.txtFilterQuit) + self.connect(self.cmbMode, QtCore.SIGNAL('currentIndexChanged(const QString&)'), self.onModeChangeChange) + + def clearTxtFilter(self, event): + self.txtFilter.setText("") + + _prevTxtFilter=None + def timerEvent(self, event): + if self.txtFilter.text()!=self._prevTxtFilter: + # only filter after a little while + self.killTimer(self._timerID) + self._timerID=None + self.filter() + def keyPressEvent(self, event): + self.onKeyPress(event) + def txtFilterChange(self): + if self._timerID==None: + # only start filtering after a little while + self._timerID=self.startTimer(500) + def txtFilterQuit(self): + if self._timerID: + self.killTimer(self._timerID) + self._timerID=None + def updateSongs(self, songs): + """Update the list of songs.""" + start_new_thread(self._update, (songs,)) + def _update(self, songs): + self.lstSongs.updateSongs(songs) + if len(self.txtFilter.text()): + self.filter() + + def setMode(self, mode, levels=''): + """Set the mode of showing the list.""" + if mode=='playlist': + self.cmbMode.setCurrentIndex(0) + else: + self.cmbMode.setCurrentIndex(self.cmbMode.findText(levels)) + def selectedSongs(self): + """Get the selected songs.""" + return self.lstSongs.selectedSongs() + def selectedIds(self): + """Get the selected songIDs.""" + return map(lambda song: song._data['id'], self.lstSongs.selectedSongs()) + def getSelItemID(self): + """Get the id of the first selected song.""" + try: + return self.lstSongs.selectedSongs()[0].getID() + except: + return -1 - def filter(self): - """Filter the songs according to the inputbox.""" - self.lstSongs.killFilters() - start_new_thread(self.lstSongs.filter, (str(self.txtFilter.text()).strip().lower(), )) - - def colorID(self, id, clr): - """Color song with ID $id with color $clr.""" - self.lstSongs.colorID(id, clr) - - def onModeChangeChange(self, value): - if value=='playlist': - self.lstSongs.setMode('playlist') - else: - self.lstSongs.setMode('library', self.modes[self.cmbMode.currentIndex()]) - settings.set("l%s.mode"%(self.name), self.cmbMode.currentIndex()) - self.filter() + def filter(self): + """Filter the songs according to the inputbox.""" + self.lstSongs.killFilters() + start_new_thread(self.lstSongs.filter, (str(self.txtFilter.text()).strip().lower(), )) + + def colorID(self, id, clr): + """Color song with ID $id with color $clr.""" + self.lstSongs.colorID(id, clr) + + def onModeChangeChange(self, value): + if value=='playlist': + self.lstSongs.setMode('playlist') + else: + self.lstSongs.setMode('library', self.modes[self.cmbMode.currentIndex()]) + settings.set("l%s.mode"%(self.name), self.cmbMode.currentIndex()) + self.filter() - def showColumn(self, column, show=True): - """Show or hide column $column.""" - self.lstSongs.showColumn(column, show) - def selectRow(self, row): - """Select row $row.""" - self.lstSongs.selectRow(row) - def ensureVisible(self, id): - """Make sure song with ID $id is visible.""" - self.lstSongs.ensureVisible(id) + def showColumn(self, column, show=True): + """Show or hide column $column.""" + self.lstSongs.showColumn(column, show) + def selectRow(self, row): + """Select row $row.""" + self.lstSongs.selectRow(row) + def ensureVisible(self, id): + """Make sure song with ID $id is visible.""" + self.lstSongs.ensureVisible(id) - def setModes(self, modes): - i=int(settings.get("l%s.mode"%(self.name), 0)) - self.modes=['playlist'] - self.modes.extend(modes) - self.cmbMode.clear() - self.cmbMode.addItem('playlist') - for mode in modes: - if mode: - self.cmbMode.addItem(mode.replace('$', '')) - self.cmbMode.setCurrentIndex(i) - self.onModeChangeChange(str(self.cmbMode.itemText(i))) + def setModes(self, modes): + i=int(settings.get("l%s.mode"%(self.name), 0)) + self.modes=['playlist'] + self.modes.extend(modes) + self.cmbMode.clear() + self.cmbMode.addItem('playlist') + for mode in modes: + if mode: + self.cmbMode.addItem(mode.replace('$', '')) + self.cmbMode.setCurrentIndex(i) + self.onModeChangeChange(str(self.cmbMode.itemText(i))) diff --git a/wgSongList.py b/wgSongList.py index da9d842..f06791a 100644 --- a/wgSongList.py +++ b/wgSongList.py @@ -14,7 +14,7 @@ LIB_ROW=0 LIB_VALUE=1 LIB_INDENT=2 LIB_NEXTROW=3 -LIB_EXPANDED=4 # values: 0, 1 or 2 (==song) +LIB_EXPANDED=4 # values: 0, 1 or 2 (==song) LIB_PARENT=5 clrRowSel=QtGui.QColor(180,180,180) @@ -22,935 +22,935 @@ clrRowSel=QtGui.QColor(180,180,180) class DoUpdate(QtCore.QEvent): - def __init__(self): - QtCore.QEvent.__init__(self, QtCore.QEvent.User) + def __init__(self): + QtCore.QEvent.__init__(self, QtCore.QEvent.User) class DoResize(QtCore.QEvent): - def __init__(self): - QtCore.QEvent.__init__(self, QtCore.QEvent.User) + def __init__(self): + QtCore.QEvent.__init__(self, QtCore.QEvent.User) class SongList(QtGui.QWidget): - """The SongList widget is a list optimized for displaying an array of songs, with filtering option.""" - # CONFIGURATION VARIABLES - " height of line in pxl" - lineHeight=30 - " font size in pxl" - fontSize=16 - " margin" - margin=4 - vmargin=(lineHeight-fontSize)/2-1 - " width of the vscrollbar" - scrollbarWidth=15 - " minimum column width" - minColumnWidth=50 - " colors for alternating rows" - colors=[QtGui.QColor(255,255,255), QtGui.QColor(230,230,255)] - " color of selection" - clrSel=QtGui.QColor(100,100,180) - " background color" - clrBg=QtGui.QColor(255,255,255) - " indentation of hierarchy, in pixels" - indentation=lineHeight - - " what function to call when the list is double clicked" - onDoubleClick=None - - mode='playlist' # what mode is the songlist in? values: 'playlist', 'library' - " the headers: ( (header, width, visible)+ )" - headers=None - songs=None # original songs - numSongs=None # number of songs - - # 'edited' songs - # in playlist mode, this can only filtering - # in library mode, this indicates all entries: (row, tag-value, indentation, next-row, expanded)* - fSongs=None # filtered songs - numVisEntries=None # number of entries that are visible (including when scrolling) - - - levels=[] # levels from the groupBy in library-mode - groupByStr='' # groupBy used in library-mode - - vScrollbar=None - hScrollbar=None - - topRow=-1 - numRows=-1 # total number of rows that can be visible in 1 time - selRows=None # ranges of selected rows: ( (startROw,endRow)* ) - selIDs=None # ranges of selected IDs: [ [startID,endID] ] - selMiscs=None # array of indexes for selected non-songs in library mode - - selMode=False # currently in select mode? - resizeCol=None # resizing a column? - clrID=None # do we have to color a row with certain ID? [ID, color] - scrollMult=1 # how many rows do we jump when scrolling by dragging - xOffset=0 # offset for drawing. Is changed by hScrollbar - resizeColumn=None # indicates this column should be recalculated - redrawID=None # redraw this ID/row only - - wgGfxAlbum=QtSvg.QSvgRenderer('gfx/gnome-cd.svg') - wgGfxArtist=QtSvg.QSvgRenderer('gfx/user_icon.svg') - - - def __init__(self, parent, name, headers, onDoubleClick): - QtGui.QWidget.__init__(self, parent) - self.onDoubleClick=onDoubleClick - - # we receive an array of strings; we convert that to an array of (header, width) - self.name=name - # load the headers, and fetch from the settings the width and visibility - self.headers=map(lambda h: [h, int(settings.get('l%s.%s.width'%(self.name,h),250)) - , settings.get('l%s.%s.visible'%(self.name,h),'1')=='1'], headers) - self.headers.insert(0, ['id', 30, settings.get('l%s.%s.visible'%(self.name,'id'),'0')=='1']) - self.songs=None - self.numSongs=None - self.fSongs=None - self.selMiscs=[] - self.numVisEntries=None - self.xOffset=0 - self.resizeColumn=None - - self._filters=[] - - self.vScrollbar=QtGui.QScrollBar(QtCore.Qt.Vertical, self) - self.vScrollbar.setMinimum(0) - self.vScrollbar.setMaximum(1) - self.vScrollbar.setValue(0) - - self.hScrollbar=QtGui.QScrollBar(QtCore.Qt.Horizontal, self) - self.hScrollbar.setMinimum(0) - self.hScrollbar.setMaximum(1) - self.hScrollbar.setValue(0) - self.hScrollbar.setPageStep(200) - - self.topRow=0 - self.numRows=0 - self.selRows=[] - self.selMode=False - self.clrID=[-1,0] - - self.updateSongs([]) - doEvents() - - self.connect(self.vScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onVScroll) - self.connect(self.hScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onHScroll) - - self.setMouseTracking(True) - self.setFocusPolicy(QtCore.Qt.TabFocus or QtCore.Qt.ClickFocus - or QtCore.Qt.StrongFocus or QtCore.Qt.WheelFocus) - - self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) - font=QtGui.QFont() - font.setPixelSize(self.fontSize) - font.setFamily('Comic Sans Ms') - self.setFont(font) - - def sizeHint(self): - return QtCore.QSize(10000,10000) - - def getSongs(self): - return self.songs - - def customEvent(self, event): - if isinstance(event, DoResize): - self.resizeEvent(None) - self.update() - elif isinstance(event, DoUpdate): - self.update() - else: - Logger.extended("wgSongList::Unknown event "+str(event)) - - def setMode(self, mode, groupBy=''): - self.selRows=[] - self.selIDs=[] - self.selMode=False - - if mode=='playlist': - self.fSongs=self.songs - self.numVisEntries=len(self.fSongs) - elif mode=='library': - self.groupBy(groupBy) - else: - raise Exception('Unknown mode %' %(mode)) - - self.mode=mode - QtCore.QCoreApplication.postEvent(self, DoResize()) - - def groupBy(self, groupBy, strFilter=''): - self.groupByStr=groupBy - self.levels=groupBy.split('/') - strFilter=strFilter.strip() - - l=[] - formats=[] - for i in xrange(0,len(self.levels)): - formats.append(format.compile(self.levels[i])) - # TODO also take l[1] etc into account? - U="(Unknown)" - xtra={"album":U, "artist":U, "date":"", "genre":U} - compare=lambda left, right: cmp(\ - formats[0](format.params(left)).lower(), \ - formats[0](format.params(right)).lower() \ - ) - - songs=self.songs - if strFilter!='': - songs=filter(lambda song: song.match(strFilter), songs) - songs=sorted(songs, compare) - - numLevels=len(self.levels) - self.fSongs=[[0, 'dummy', 0, -1, False]] - row=0 - # four levels ought to be enough for everyone - curLevels=[[None,0], [None,0], [None,0], [None,0]] # contains the values of current levels - curLevel=0 # current level we're in - parents=[-1,-1,-1,-1] # index of parent - for song in songs: - if not(self.songs): - return - for level in xrange(numLevels): - # does the file have the required tag? - if not formats[level](format.params(song, {}, xtra))==curLevels[level][LIB_ROW]: - finalRow=row - for i in xrange(level,numLevels): - tagValue2=formats[i](format.params(song, {}, xtra)) - - self.fSongs[curLevels[i][1]][LIB_NEXTROW]=finalRow - self.fSongs.append([row, tagValue2, i, row+1, 0, parents[i]]) - parents[i+1]=row - - row+=1 - curLevels[i]=[tagValue2, row] - curLevel=numLevels - self.fSongs.append([row, song, curLevel, row+1, 2, parents[curLevel]]) - row+=1 - - # update last entries' next-row of each level - # If we have e.g. artist/album, then the last artist and last album of that - # artist have to be repointed to the end of the list, else problems arise - # showing those entries ... - # indicate for each level whether we have processed that level yet - processed=[False, False, False, False, False] - numFSongs=len(self.fSongs) - for i in xrange(numFSongs-1,0,-1): - song=self.fSongs[i] - # look for last top-level entry - if song[LIB_INDENT]==0: - song[LIB_NEXTROW]=numFSongs - break - if processed[song[LIB_INDENT]]==False: - song[LIB_NEXTROW]=numFSongs - processed[song[LIB_INDENT]]=True - - - # remove the dummy - self.fSongs.pop(0) - - self.numVisEntries=len(filter(lambda entry: entry[LIB_INDENT]==0, self.fSongs)) - - def updateSongs(self, songs): - """Update the displayed songs and clears selection.""" - self.songs=songs - self.numSongs=len(songs) - - self.setMode(self.mode, self.groupByStr) - - self.redrawID=None - QtCore.QCoreApplication.postEvent(self, DoUpdate()) - - def selectedSongs(self): - """Returns the list of selected songs.""" - ret=[] - if self.mode=='playlist': - cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1] - elif self.mode=='library': - cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1] - for range in self.selIDs: - # look for the songs in the current range - songs=filter(cmp, self.songs) - # add songs in range - ret.extend(songs) - return ret - - def killFilters(self): - songs=self.songs - self.songs=None - while (len(self._filters)): - # wait 'till everything's cleared - doEvents() - self.songs=songs - - - # contains filters ready to be applied; only the top one will be used - _filters=[] - def filter(self, strFilter): - """Filter songs according to $strFilter.""" - - self._filters.append(strFilter) - strFilter=self._filters[-1] - - try: - if self.mode=='playlist': - self.fSongs=filter(lambda song: song.match(strFilter), self.songs) - self.numVisEntries=len(self.fSongs) - else: - self.groupBy(self.groupByStr, strFilter) - except: - # we might get here because self.songs is None - pass - self._filters=[] - QtCore.QCoreApplication.postEvent(self, DoResize()) - - def colorID(self, id, clr): - """Color the row which contains song with id $id $clr.""" - self.clrID=[id, clr] - self.redrawID=id - - QtCore.QCoreApplication.postEvent(self, DoUpdate()) - - def selectRow(self, row): - """Make $row the current selection.""" - self.selRows=[[row,row]] - - QtCore.QCoreApplication.postEvent(self, DoUpdate()) - - def showColumn(self, column, show=True): - """Hide or show column $column.""" - self.headers[column][2]=show - - self.update() - - def autoSizeColumn(self, column): - """Resizes column $column to fit the widest entry in the non-filtered songs.""" - # we can't calculate it here, as retrieving the text-width can only - # be done in the paintEvent method ... - self.resizeColumn=column - - QtCore.QCoreApplication.postEvent(self, DoUpdate()) - - def visibleSongs(self): - """Get the songs currently visible.""" - ret=[] - if self.mode=='playlist': - for row in xrange(self.topRow, min(self.numSongs, self.topRow+self.numRows)-1): - ret.append(self.fSongs[row]) - elif self.mode=='library': - # note that if everything is folded, there'll be no songs! - entries=self.fSongs - index=self.libFirstVisRowIndex() - count=0 - while index>=0 and index<len(entries) and count<self.numRows: - entry=self.fSongs[index] - if isinstance(entry[LIB_VALUE], Song): - ret.append(entry[LIB_VALUE]) - index=self.libIthVisRowIndex(index) - count+=1 - return ret - - def ensureVisible(self, id): - """Make sure the song with $id is visible.""" - if len(filter(lambda song: song.getID()==id, self.visibleSongs())): - return - row=0 - ok=False - if self.mode=='playlist': - # playlist mode is simple: just hop to the song with id! - for song in self.fSongs: - row+=1 - if song.getID()==id: - ok=True - break - elif self.mode=='library': - # library mode is more complex: we must find out how many rows are visible, - # and expand the parents of the song, if necessary - indLevel=0 # indicates what is the deepest level that is expanded for the current entry - # thus if current indent<=indLevel, then it is visible - for entry in self.fSongs: - if entry[LIB_EXPANDED]==1: - indLevel=max(entry[LIB_INDENT]+1, indLevel) - elif entry[LIB_EXPANDED]==0: - indLevel=min(entry[LIB_INDENT], indLevel) - if entry[LIB_INDENT]<=indLevel: - row+=1 - - #print "%s -> %s"%(str(indLevel), str(entry)) - if isinstance(entry[LIB_VALUE], Song) and entry[LIB_VALUE].getID()==id: - # expand parents - # must be expanded in reverse order, else we will count too many - # entries ... - parents=[] - while entry[LIB_PARENT]>=0: - entry=self.fSongs[entry[LIB_PARENT]] - parents.append(entry) - row-=1 - parents.reverse() - for parent in parents: - self.libExpand(parent) - ok=True - break - - if ok: - self.vScrollbar.setValue(row-self.numRows/2) - self.update() - - - def onVScroll(self, value): - # 'if value<0' needed because minimum can be after init <0 at some point ... - if value<0: value=0 - if value>self.numVisEntries:value=self.numVisEntries - self.topRow=value - - self.update() - - def onHScroll(self, value): - self.xOffset=-self.hScrollbar.value()*2 - self.update() - - def _pos2row(self, pos): - return int(pos.y()/self.lineHeight)-1 - def _row2entry(self, row): - entry=self.fSongs[0] - try: - while row>0: - if entry[LIB_EXPANDED]: - entry=self.fSongs[entry[LIB_ROW]+1] - else: - entry=self.fSongs[entry[LIB_NEXTROW]] - row-=1 - except: - return None - return entry - - def focusOutEvent(self, event): - self.update() - def focusInEvent(self, event): - self.update() - def wheelEvent(self, event): - if self.vScrollbar.isVisible(): - event.accept() - numDegrees=event.delta() / 8 - numSteps=5*numDegrees/15 - self.vScrollbar.setValue(self.vScrollbar.value()-numSteps) - - def resizeEvent(self, event): - # max nr of rows shown - self.numRows=int(self.height()/self.lineHeight) - - # check vertical scrollbar - if self.numRows>self.numVisEntries: - self.vScrollbar.setVisible(False) - self.vScrollbar.setValue(0) - else: - self.vScrollbar.setVisible(True) - self.vScrollbar.setPageStep(self.numRows-2) - self.vScrollbar.setMinimum(0) - self.vScrollbar.setMaximum(self.numVisEntries-self.numRows+1) - self.vScrollbar.resize(self.scrollbarWidth, self.height()-self.lineHeight-1) - self.vScrollbar.move(self.width()-self.vScrollbar.width()-1, self.lineHeight-1) - - # check horizontal scrollbar - self.scrollWidth=0 - if self.mode=='playlist': - for hdr in self.headers: - if hdr[2]: - self.scrollWidth+=hdr[1] - - if self.scrollWidth>self.width(): - self.hScrollbar.setVisible(True) - self.hScrollbar.setMinimum(0) - self.hScrollbar.setMaximum((self.scrollWidth-self.width())/2) - self.hScrollbar.resize(self.width()-4, self.scrollbarWidth) - self.hScrollbar.move(2, self.height()-self.hScrollbar.height()-1) - - # some changes because the hScrollbar takes some vertical space ... - self.vScrollbar.resize(self.vScrollbar.width(), self.vScrollbar.height()-self.lineHeight) - self.vScrollbar.setMaximum(self.vScrollbar.maximum()+1) - - self.numRows-=1 - else: - self.hScrollbar.setVisible(False) - self.hScrollbar.setValue(0) - - def libExpand(self, entry): - if entry and entry[LIB_EXPANDED]==0: - self.libToggle(entry) - def libCollapse(self, entry): - if entry and entry[LIB_EXPANDED]==1: - self.libToggle(entry) - - def libToggle(self, entry): - """Toggles expanded state. Returns new state""" - expanded=entry[LIB_EXPANDED] - if expanded!=2: - # there was a '+' or a '-'! - entry[LIB_EXPANDED]=(expanded+1)%2 - ret=entry[LIB_EXPANDED] - # we must find out how many entries have appeared/disappeard - # while collapsing. - visibles=0 # how many new elements have appeared? - i=entry[LIB_ROW]+1 # current element looking at - indLevel=self.fSongs[i][LIB_INDENT] - while i<=entry[LIB_NEXTROW]-1 and i<len(self.fSongs): - entry2=self.fSongs[i] - if entry2[LIB_EXPANDED]==1: - indLevel=max(entry2[LIB_INDENT]+1, indLevel) - elif entry2[LIB_EXPANDED]==0: - indLevel=min(entry2[LIB_INDENT], indLevel) - if entry2[LIB_INDENT]<=indLevel: - visibles+=1 - - if entry2[LIB_EXPANDED]==0: - i=entry2[LIB_NEXTROW] - else: - i+=1 - - mult=1 - if ret==0: mult=-1 - self.numVisEntries+=mult*visibles - self.resizeEvent(None) - return ret - - return 2 - - def mousePressEvent(self, event): - self.setFocus() - pos=event.pos() - row=self._pos2row(pos) - - done=False # indicates whether some action has been done or not - if self.mode=='playlist': - self.scrollMult=1 - if row==-1: - # we're clicking in the header! - self.resizeCol=None - x=0+self.xOffset - i=0 - # check if we're clicking between two columns, if so: resize mode! - for hdr in self.headers: - if hdr[2]: - x+=hdr[1] - if abs(x-pos.x())<4: - self.resizeCol=i - done=True - i+=1 - elif self.mode=='library': - entry=self._row2entry(row+self.topRow) - if not entry: - entry=self.fSongs[len(self.fSongs)-1] - if entry and pos.x()>(1+entry[LIB_INDENT])*self.indentation \ - and pos.x()<(1+entry[LIB_INDENT]+3/2)*self.indentation: - # we clicked in the margin, to expand or collapse - self.libToggle(entry) - done=True - - if done==False: - self.selMode=True - self.selIDs=[] - self.selMiscs=[] - if row==-1 and self.resizeCol==None: - # we're not resizing, thus we can select all! - self.selRows=[[0, len(self.fSongs)]] - elif row>=0: - # we start selection mode - if self.mode=='playlist': - self.selRows=[[self.topRow+row,self.topRow+row]] - elif self.mode=='library': - self.selRows=[[entry[LIB_ROW], entry[LIB_NEXTROW]-1]] - self.selMode=True - - self.update() - - def mouseMoveEvent(self, event): - pos=event.pos() - row=self._pos2row(pos) - if self.selMode: - # we're in selection mode - if row<0: - # scroll automatically when going out of the widget - row=0 - if self.topRow>0: - self.scrollMult+=0.1 - jump=int(self.scrollMult)*int(abs(pos.y())/self.lineHeight) - self.vScrollbar.setValue(self.vScrollbar.value()-jump) - row=jump - elif row>=self.numRows: - # scroll automatically when going out of the widget - self.scrollMult+=0.1 - jump=int(self.scrollMult)*int(abs(self.height()-pos.y())/self.lineHeight) - self.vScrollbar.setValue(self.vScrollbar.value()+jump) - row=self.numRows-jump - else: - # reset the scrollMultiplier - self.scrollMult=1 - - if self.mode=='playlist': - self.selRows[0][1]=row+self.topRow - elif self.mode=='library': - self.selRows[0][1]=self.libIthVisRowIndex(self.libIthVisRowIndex(0,self.topRow), row) - self.update() - elif self.resizeCol!=None: - row-=1 - # ohla, we're resizing a column! - prev=0 - # calculate where we are - for i in xrange(self.resizeCol): - hdr=self.headers[i] - if hdr[2]: - prev+=hdr[1] - self.headers[self.resizeCol][1]=pos.x()-prev-self.xOffset - # minimum width check? - if self.headers[self.resizeCol][1]<self.minColumnWidth: - self.headers[self.resizeCol][1]=self.minColumnWidth - self.resizeEvent(None) - self.update() - - def mouseReleaseEvent(self, event): - if self.selMode and len(self.selRows): - # we were selecting, but now we're done. - # We have to transform one range of rows - # into range of selected IDs - # The problem is that the list can be filtered, and that - # consequtive, visible rows aren't always directly - # consequtive in the unfiltered list. - self.selMode=False # exit selection mode - fSongs=self.fSongs - self.selMiscs=[] - ranges=[] - curRange=[] - # loop over all rows that are selected - for entry in fSongs[min(self.selRows[0]):max(self.selRows[0])+1]: - song=None - if isinstance(entry,Song): - song=entry - elif isinstance(entry[LIB_VALUE],Song): - song=entry[LIB_VALUE] - else: - self.selMiscs.append(entry[LIB_ROW]) - - if song!=None: - id=song.getID() - # is this song directly after the previous row? - if len(curRange)==0 or curRange[-1]+1==id: - curRange.append(id) - else: - ranges.append(curRange) - curRange=[id] - if len(curRange): - ranges.append(curRange) - # clean up ranges - self.selRows=[] - self.selIDs=[] - for range in ranges: - self.selIDs.append([range[0], range[-1]]) - self.update() - - elif self.resizeCol!=None: - # store this new width! - self.saveColumnWidth(self.resizeCol) - # we're not resizing anymore! - self.resizeCol=None - self.update() - def saveColumnWidth(self, col): - settings.set('l%s.%s.width'%(self.name,self.headers[col][0]), self.headers[col][1]) - def mouseDoubleClickEvent(self, event): - pos=event.pos() - row=self._pos2row(pos) - if row>=0: - self.onDoubleClick() - else: - # auto-size column - x=0+self.xOffset - i=0 - for hdr in self.headers: - if hdr[2]: - x+=hdr[1] - if abs(x-pos.x())<4: - self.autoSizeColumn(i) - break - i+=1 - - def _paintPlaylist(self, p): - self.redrawID=None - - - lineHeight=self.lineHeight - margin=self.margin - vmargin=self.vmargin - selRows=self.selRows - width=self.width() - if self.vScrollbar.isVisible(): - width-=self.scrollbarWidth - - if self.resizeColumn!=None: - # we're autoresizing! - # must be done here, because only here we can check the textwidth! - # This is because of limitations it can be only be done in paintEvent - hdr=self.headers[self.resizeColumn][0] - w=self.minColumnWidth - # loop over all visible songs ... - for song in self.fSongs: - rect=p.boundingRect(10,10,1,1, QtCore.Qt.AlignLeft, song.getTag(hdr)) - w=max(rect.width(), w) - self.headers[self.resizeColumn][1]=w+2*margin - self.saveColumnWidth(self.resizeColumn) - self.resizeColumn=None - self.resizeEvent(None) - if self.redrawID!=None: - # only update one row - y=lineHeight - for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)): - if self.fSongs[row]._data['id']==self.redrawID: - self._paintPlaylistRow(p, row, y, width) - y+=lineHeight - - self.redrawID=None - return - - # paint the headers! - p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), QtGui.QBrush(QtCore.Qt.lightGray)) - p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1)) - x=margin+self.xOffset - for hdr in self.headers: - if hdr[2]: - p.drawText(x, vmargin, hdr[1], lineHeight, QtCore.Qt.AlignLeft, hdr[0]) - x+=hdr[1] - p.drawLine(QtCore.QPoint(x-margin,0), QtCore.QPoint(x-margin,lineHeight)) - - if self.songs==None: - return - # fill the records! - y=lineHeight - for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)): - self._paintPlaylistRow(p, row, y, width) - y+=lineHeight - if y<self.height(): - # if we're short on songs, draw up the remaining area in background color - p.fillRect(QtCore.QRect(0,y,width,self.height()-y), QtGui.QBrush(self.clrBg)) - - def _paintPlaylistRow(self, p, row, y, width): - """Paint row $row on $p on height $y and with width $width.""" - song=self.fSongs[row] - lineHeight=self.lineHeight - margin=self.margin - vmargin=self.vmargin - id=song._data['id'] - - # determine color of row. Default is row-color, but can be overridden by - # (in this order): selection, special row color! - clr=self.colors[row%2] # background color of the row - clrTxt=QtCore.Qt.black # color of the printed text - # is it selected? - values=[] - if self.selMode: - checkID=row - values=self.selRows - else: - checkID=id - values=self.selIDs - # if values==[], it won't run! - for range in values: - # is selected if in range, which depends on the selection-mode - if checkID>=min(range) and checkID<=max(range): - clr=self.clrSel - clrTxt=QtCore.Qt.white - # it has a VIP-status! - if id==int(self.clrID[0]): - clrTxt=QtCore.Qt.white - clr=self.clrID[1] - - # draw the row background - p.fillRect(QtCore.QRect(2, y, width-3, lineHeight), QtGui.QBrush(clr)) - - # draw a subtile rectangle - p.setPen(QtGui.QColor(230,230,255)) - p.drawRect(QtCore.QRect(2, y, width-3, lineHeight)) - - # Back To Black - p.setPen(QtCore.Qt.black) - - # draw the column - x=margin+self.xOffset - for hdr in self.headers: - if hdr[2]: - # only if visible, duh! - # rectangle we're allowed to print in - text=song.getTag(hdr[0]) - if type(text)!=str and type(text)!=unicode: - text=str(text) - rect=p.boundingRect(x, y, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text) - p.setPen(clrTxt) - p.drawText(x, y+vmargin, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text) - if rect.width()>hdr[1]-margin: - # print ellipsis, if necessary - p.fillRect(x+hdr[1]-15,y+1,15,lineHeight-1, QtGui.QBrush(clr)) - p.drawText(x+hdr[1]-15,y+vmargin,15,lineHeight-1, QtCore.Qt.AlignLeft, "...") - x+=hdr[1] - p.setPen(QtCore.Qt.black) - p.drawLine(QtCore.QPoint(x-margin,y), QtCore.QPoint(x-margin,y+lineHeight)) - - def libFirstVisRowIndex(self): - """Returns the index of the first visible row in library mode.""" - # if not in library mode, the unthinkable might happen! Wooo! - # TODO find better initial value - row=0 # the visible rows we're visiting - index=0 # what index does the current row have - entries=self.fSongs - - while index<len(entries): - if row>=self.topRow: - break - entry=entries[index] - if entry[LIB_EXPANDED]==0: - index=entry[LIB_NEXTROW] - else: - index+=1 - row+=1 - return index - def libIthVisRowIndex(self, index, i=1): - """Returns the index of the $i-th next row after $index that is visible (or -1) in library mode.""" - entries=self.fSongs - while i>0 and index<len(entries): - i-=1 - entry=self.fSongs[index] - if entry[LIB_EXPANDED]==0: - if index<0: - return -1 - index=entry[LIB_NEXTROW] - else: - index+=1 - - return index - - - def libPrint(self): - for entry in self.fSongs: - indent="" - for i in xrange(entry[2]): - indent=("%s ")%(indent) - print "%s%s" % (indent, entry) - - def _paintLibrary(self, p): - width=self.width() - height=self.height() - lineHeight=self.lineHeight - margin=self.margin - vmargin=self.vmargin - - # paint the headers! - p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), QtGui.QBrush(QtCore.Qt.lightGray)) - p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1)) - p.drawText(margin, vmargin, width, lineHeight, QtCore.Qt.AlignLeft, self.groupByStr.replace('$', '')) - - entries=self.fSongs - - y=lineHeight - x=margin - indent=self.indentation - index=self.libFirstVisRowIndex() - row=0 - while index<len(entries) and y<height: - entry=entries[index] - - level=entry[LIB_INDENT] - isSong=isinstance(entry[LIB_VALUE], Song) - - if isSong: - prefix='' - text="%s %s"%(entry[LIB_VALUE].getTrack(), entry[LIB_VALUE].getTitle()) - else: - if entry[LIB_EXPANDED]==1: prefix='-' - elif entry[LIB_EXPANDED]==0: prefix='+' - text=entry[LIB_VALUE] - - clr=self.colors[row%2] # background color of the row - clrTxt=QtCore.Qt.black - - values=[] - if self.selMode: - checkID=index - values=self.selRows - elif self.selMode==False and isSong: - checkID=entry[LIB_VALUE].getID() - values=self.selIDs - - # if values==[], then it won't run! - for range in values: - # is selected if in range, which depends on the selection-mode - if checkID>=min(range) and checkID<=max(range): - clr=self.clrSel - clrTxt=QtCore.Qt.white - - for i in self.selMiscs: - if index==i: - clr=self.clrSel - clrTxt=QtCore.Qt.white - - # it has a VIP-status! - if isSong and entry[LIB_VALUE].getID()==int(self.clrID[0]): - clrTxt=QtCore.Qt.white - clr=self.clrID[1] - - left=x+indent*(1+level) - top=y+vmargin - p.fillRect(QtCore.QRect(left,y,width-3,lineHeight), clr) - p.setPen(clrTxt) - p.drawText(left, top, 15, lineHeight, QtCore.Qt.AlignLeft, prefix) - p.drawText(left+15, top, width, lineHeight, QtCore.Qt.AlignLeft, text) - p.setPen(QtCore.Qt.black) - - obj=None - if level<len(self.levels): - if self.levels[level][0:len('$artist')]=='$artist': - obj=self.wgGfxArtist - elif self.levels[level][0:len('$album')]=='$album': - obj=self.wgGfxAlbum - - if obj: - obj.render(p, QtCore.QRectF(indent*level+1,y+1,lineHeight-1,lineHeight-1)) - - y+=lineHeight - row+=1 - index=self.libIthVisRowIndex(index) - if index<0: - break - - _mutex=QtCore.QMutex() - _paintCnt=0 - def paintEvent(self, event): - self._mutex.lock() - if self._paintCnt: - self._mutex.unlock() - return - self._paintCnt=1 - self._mutex.unlock() - - - p=QtGui.QPainter(self) - - # for the moment, redraw everything ... - p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(self.clrBg)) - if self.mode=='playlist': - self._paintPlaylist(p) - elif self.mode=='library': - self._paintLibrary(p) - - # draw a nice line around the widget! - p.drawRect(QtCore.QRect(0,0,self.width()-1,self.height()-1)) - if self.hasFocus(): - p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3)) - else: - p.setPen(QtCore.Qt.lightGray) - p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3)) - - self._paintCnt=0 - - text=None - if len(self._filters): - text="Please wait while filtering ... (%i)"%(len(self._filters)) - #text='%s - %s' % (self.selMiscs, '') - #text='%s - %s - %s' % (str(self.topRow), str(self.selRows), str(self.selIDs)) - #text='%s - %s'%(str(self.topRow), str(self.numVisEntries)) - if text: - r=QtCore.QRect(10,self.height()-40,self.width()-20,20) - p.fillRect(r, QtGui.QBrush(QtCore.Qt.white)) - p.drawText(r,QtCore.Qt.AlignLeft, text) - + """The SongList widget is a list optimized for displaying an array of songs, with filtering option.""" + # CONFIGURATION VARIABLES + " height of line in pxl" + lineHeight=30 + " font size in pxl" + fontSize=16 + " margin" + margin=4 + vmargin=(lineHeight-fontSize)/2-1 + " width of the vscrollbar" + scrollbarWidth=15 + " minimum column width" + minColumnWidth=50 + " colors for alternating rows" + colors=[QtGui.QColor(255,255,255), QtGui.QColor(230,230,255)] + " color of selection" + clrSel=QtGui.QColor(100,100,180) + " background color" + clrBg=QtGui.QColor(255,255,255) + " indentation of hierarchy, in pixels" + indentation=lineHeight + + " what function to call when the list is double clicked" + onDoubleClick=None + + mode='playlist' # what mode is the songlist in? values: 'playlist', 'library' + " the headers: ( (header, width, visible)+ )" + headers=None + songs=None # original songs + numSongs=None # number of songs + + # 'edited' songs + # in playlist mode, this can only filtering + # in library mode, this indicates all entries: (row, tag-value, indentation, next-row, expanded)* + fSongs=None # filtered songs + numVisEntries=None # number of entries that are visible (including when scrolling) + + + levels=[] # levels from the groupBy in library-mode + groupByStr='' # groupBy used in library-mode + + vScrollbar=None + hScrollbar=None + + topRow=-1 + numRows=-1 # total number of rows that can be visible in 1 time + selRows=None # ranges of selected rows: ( (startROw,endRow)* ) + selIDs=None # ranges of selected IDs: [ [startID,endID] ] + selMiscs=None # array of indexes for selected non-songs in library mode + + selMode=False # currently in select mode? + resizeCol=None # resizing a column? + clrID=None # do we have to color a row with certain ID? [ID, color] + scrollMult=1 # how many rows do we jump when scrolling by dragging + xOffset=0 # offset for drawing. Is changed by hScrollbar + resizeColumn=None # indicates this column should be recalculated + redrawID=None # redraw this ID/row only + + wgGfxAlbum=QtSvg.QSvgRenderer('gfx/gnome-cd.svg') + wgGfxArtist=QtSvg.QSvgRenderer('gfx/user_icon.svg') + + + def __init__(self, parent, name, headers, onDoubleClick): + QtGui.QWidget.__init__(self, parent) + self.onDoubleClick=onDoubleClick + + # we receive an array of strings; we convert that to an array of (header, width) + self.name=name + # load the headers, and fetch from the settings the width and visibility + self.headers=map(lambda h: [h, int(settings.get('l%s.%s.width'%(self.name,h),250)) + , settings.get('l%s.%s.visible'%(self.name,h),'1')=='1'], headers) + self.headers.insert(0, ['id', 30, settings.get('l%s.%s.visible'%(self.name,'id'),'0')=='1']) + self.songs=None + self.numSongs=None + self.fSongs=None + self.selMiscs=[] + self.numVisEntries=None + self.xOffset=0 + self.resizeColumn=None + + self._filters=[] + + self.vScrollbar=QtGui.QScrollBar(QtCore.Qt.Vertical, self) + self.vScrollbar.setMinimum(0) + self.vScrollbar.setMaximum(1) + self.vScrollbar.setValue(0) + + self.hScrollbar=QtGui.QScrollBar(QtCore.Qt.Horizontal, self) + self.hScrollbar.setMinimum(0) + self.hScrollbar.setMaximum(1) + self.hScrollbar.setValue(0) + self.hScrollbar.setPageStep(200) + + self.topRow=0 + self.numRows=0 + self.selRows=[] + self.selMode=False + self.clrID=[-1,0] + + self.updateSongs([]) + doEvents() + + self.connect(self.vScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onVScroll) + self.connect(self.hScrollbar, QtCore.SIGNAL('valueChanged(int)'),self.onHScroll) + + self.setMouseTracking(True) + self.setFocusPolicy(QtCore.Qt.TabFocus or QtCore.Qt.ClickFocus + or QtCore.Qt.StrongFocus or QtCore.Qt.WheelFocus) + + self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) + font=QtGui.QFont() + font.setPixelSize(self.fontSize) + font.setFamily('Comic Sans Ms') + self.setFont(font) + + def sizeHint(self): + return QtCore.QSize(10000,10000) + + def getSongs(self): + return self.songs + + def customEvent(self, event): + if isinstance(event, DoResize): + self.resizeEvent(None) + self.update() + elif isinstance(event, DoUpdate): + self.update() + else: + Logger.extended("wgSongList::Unknown event "+str(event)) + + def setMode(self, mode, groupBy=''): + self.selRows=[] + self.selIDs=[] + self.selMode=False + + if mode=='playlist': + self.fSongs=self.songs + self.numVisEntries=len(self.fSongs) + elif mode=='library': + self.groupBy(groupBy) + else: + raise Exception('Unknown mode %' %(mode)) + + self.mode=mode + QtCore.QCoreApplication.postEvent(self, DoResize()) + + def groupBy(self, groupBy, strFilter=''): + self.groupByStr=groupBy + self.levels=groupBy.split('/') + strFilter=strFilter.strip() + + l=[] + formats=[] + for i in xrange(0,len(self.levels)): + formats.append(format.compile(self.levels[i])) + # TODO also take l[1] etc into account? + U="(Unknown)" + xtra={"album":U, "artist":U, "date":"", "genre":U} + compare=lambda left, right: cmp(\ + formats[0](format.params(left)).lower(), \ + formats[0](format.params(right)).lower() \ + ) + + songs=self.songs + if strFilter!='': + songs=filter(lambda song: song.match(strFilter), songs) + songs=sorted(songs, compare) + + numLevels=len(self.levels) + self.fSongs=[[0, 'dummy', 0, -1, False]] + row=0 + # four levels ought to be enough for everyone + curLevels=[[None,0], [None,0], [None,0], [None,0]] # contains the values of current levels + curLevel=0 # current level we're in + parents=[-1,-1,-1,-1] # index of parent + for song in songs: + if not(self.songs): + return + for level in xrange(numLevels): + # does the file have the required tag? + if not formats[level](format.params(song, {}, xtra))==curLevels[level][LIB_ROW]: + finalRow=row + for i in xrange(level,numLevels): + tagValue2=formats[i](format.params(song, {}, xtra)) + + self.fSongs[curLevels[i][1]][LIB_NEXTROW]=finalRow + self.fSongs.append([row, tagValue2, i, row+1, 0, parents[i]]) + parents[i+1]=row + + row+=1 + curLevels[i]=[tagValue2, row] + curLevel=numLevels + self.fSongs.append([row, song, curLevel, row+1, 2, parents[curLevel]]) + row+=1 + + # update last entries' next-row of each level + # If we have e.g. artist/album, then the last artist and last album of that + # artist have to be repointed to the end of the list, else problems arise + # showing those entries ... + # indicate for each level whether we have processed that level yet + processed=[False, False, False, False, False] + numFSongs=len(self.fSongs) + for i in xrange(numFSongs-1,0,-1): + song=self.fSongs[i] + # look for last top-level entry + if song[LIB_INDENT]==0: + song[LIB_NEXTROW]=numFSongs + break + if processed[song[LIB_INDENT]]==False: + song[LIB_NEXTROW]=numFSongs + processed[song[LIB_INDENT]]=True + + + # remove the dummy + self.fSongs.pop(0) + + self.numVisEntries=len(filter(lambda entry: entry[LIB_INDENT]==0, self.fSongs)) + + def updateSongs(self, songs): + """Update the displayed songs and clears selection.""" + self.songs=songs + self.numSongs=len(songs) + + self.setMode(self.mode, self.groupByStr) + + self.redrawID=None + QtCore.QCoreApplication.postEvent(self, DoUpdate()) + + def selectedSongs(self): + """Returns the list of selected songs.""" + ret=[] + if self.mode=='playlist': + cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1] + elif self.mode=='library': + cmp=lambda song: song._data['id']>=range[0] and song._data['id']<=range[1] + for range in self.selIDs: + # look for the songs in the current range + songs=filter(cmp, self.songs) + # add songs in range + ret.extend(songs) + return ret + + def killFilters(self): + songs=self.songs + self.songs=None + while (len(self._filters)): + # wait 'till everything's cleared + doEvents() + self.songs=songs + + + # contains filters ready to be applied; only the top one will be used + _filters=[] + def filter(self, strFilter): + """Filter songs according to $strFilter.""" + + self._filters.append(strFilter) + strFilter=self._filters[-1] + + try: + if self.mode=='playlist': + self.fSongs=filter(lambda song: song.match(strFilter), self.songs) + self.numVisEntries=len(self.fSongs) + else: + self.groupBy(self.groupByStr, strFilter) + except: + # we might get here because self.songs is None + pass + self._filters=[] + QtCore.QCoreApplication.postEvent(self, DoResize()) + + def colorID(self, id, clr): + """Color the row which contains song with id $id $clr.""" + self.clrID=[id, clr] + self.redrawID=id + + QtCore.QCoreApplication.postEvent(self, DoUpdate()) + + def selectRow(self, row): + """Make $row the current selection.""" + self.selRows=[[row,row]] + + QtCore.QCoreApplication.postEvent(self, DoUpdate()) + + def showColumn(self, column, show=True): + """Hide or show column $column.""" + self.headers[column][2]=show + + self.update() + + def autoSizeColumn(self, column): + """Resizes column $column to fit the widest entry in the non-filtered songs.""" + # we can't calculate it here, as retrieving the text-width can only + # be done in the paintEvent method ... + self.resizeColumn=column + + QtCore.QCoreApplication.postEvent(self, DoUpdate()) + + def visibleSongs(self): + """Get the songs currently visible.""" + ret=[] + if self.mode=='playlist': + for row in xrange(self.topRow, min(self.numSongs, self.topRow+self.numRows)-1): + ret.append(self.fSongs[row]) + elif self.mode=='library': + # note that if everything is folded, there'll be no songs! + entries=self.fSongs + index=self.libFirstVisRowIndex() + count=0 + while index>=0 and index<len(entries) and count<self.numRows: + entry=self.fSongs[index] + if isinstance(entry[LIB_VALUE], Song): + ret.append(entry[LIB_VALUE]) + index=self.libIthVisRowIndex(index) + count+=1 + return ret + + def ensureVisible(self, id): + """Make sure the song with $id is visible.""" + if len(filter(lambda song: song.getID()==id, self.visibleSongs())): + return + row=0 + ok=False + if self.mode=='playlist': + # playlist mode is simple: just hop to the song with id! + for song in self.fSongs: + row+=1 + if song.getID()==id: + ok=True + break + elif self.mode=='library': + # library mode is more complex: we must find out how many rows are visible, + # and expand the parents of the song, if necessary + indLevel=0 # indicates what is the deepest level that is expanded for the current entry + # thus if current indent<=indLevel, then it is visible + for entry in self.fSongs: + if entry[LIB_EXPANDED]==1: + indLevel=max(entry[LIB_INDENT]+1, indLevel) + elif entry[LIB_EXPANDED]==0: + indLevel=min(entry[LIB_INDENT], indLevel) + if entry[LIB_INDENT]<=indLevel: + row+=1 + + #print "%s -> %s"%(str(indLevel), str(entry)) + if isinstance(entry[LIB_VALUE], Song) and entry[LIB_VALUE].getID()==id: + # expand parents + # must be expanded in reverse order, else we will count too many + # entries ... + parents=[] + while entry[LIB_PARENT]>=0: + entry=self.fSongs[entry[LIB_PARENT]] + parents.append(entry) + row-=1 + parents.reverse() + for parent in parents: + self.libExpand(parent) + ok=True + break + + if ok: + self.vScrollbar.setValue(row-self.numRows/2) + self.update() + + + def onVScroll(self, value): + # 'if value<0' needed because minimum can be after init <0 at some point ... + if value<0: value=0 + if value>self.numVisEntries:value=self.numVisEntries + self.topRow=value + + self.update() + + def onHScroll(self, value): + self.xOffset=-self.hScrollbar.value()*2 + self.update() + + def _pos2row(self, pos): + return int(pos.y()/self.lineHeight)-1 + def _row2entry(self, row): + entry=self.fSongs[0] + try: + while row>0: + if entry[LIB_EXPANDED]: + entry=self.fSongs[entry[LIB_ROW]+1] + else: + entry=self.fSongs[entry[LIB_NEXTROW]] + row-=1 + except: + return None + return entry + + def focusOutEvent(self, event): + self.update() + def focusInEvent(self, event): + self.update() + def wheelEvent(self, event): + if self.vScrollbar.isVisible(): + event.accept() + numDegrees=event.delta() / 8 + numSteps=5*numDegrees/15 + self.vScrollbar.setValue(self.vScrollbar.value()-numSteps) + + def resizeEvent(self, event): + # max nr of rows shown + self.numRows=int(self.height()/self.lineHeight) + + # check vertical scrollbar + if self.numRows>self.numVisEntries: + self.vScrollbar.setVisible(False) + self.vScrollbar.setValue(0) + else: + self.vScrollbar.setVisible(True) + self.vScrollbar.setPageStep(self.numRows-2) + self.vScrollbar.setMinimum(0) + self.vScrollbar.setMaximum(self.numVisEntries-self.numRows+1) + self.vScrollbar.resize(self.scrollbarWidth, self.height()-self.lineHeight-1) + self.vScrollbar.move(self.width()-self.vScrollbar.width()-1, self.lineHeight-1) + + # check horizontal scrollbar + self.scrollWidth=0 + if self.mode=='playlist': + for hdr in self.headers: + if hdr[2]: + self.scrollWidth+=hdr[1] + + if self.scrollWidth>self.width(): + self.hScrollbar.setVisible(True) + self.hScrollbar.setMinimum(0) + self.hScrollbar.setMaximum((self.scrollWidth-self.width())/2) + self.hScrollbar.resize(self.width()-4, self.scrollbarWidth) + self.hScrollbar.move(2, self.height()-self.hScrollbar.height()-1) + + # some changes because the hScrollbar takes some vertical space ... + self.vScrollbar.resize(self.vScrollbar.width(), self.vScrollbar.height()-self.lineHeight) + self.vScrollbar.setMaximum(self.vScrollbar.maximum()+1) + + self.numRows-=1 + else: + self.hScrollbar.setVisible(False) + self.hScrollbar.setValue(0) + + def libExpand(self, entry): + if entry and entry[LIB_EXPANDED]==0: + self.libToggle(entry) + def libCollapse(self, entry): + if entry and entry[LIB_EXPANDED]==1: + self.libToggle(entry) + + def libToggle(self, entry): + """Toggles expanded state. Returns new state""" + expanded=entry[LIB_EXPANDED] + if expanded!=2: + # there was a '+' or a '-'! + entry[LIB_EXPANDED]=(expanded+1)%2 + ret=entry[LIB_EXPANDED] + # we must find out how many entries have appeared/disappeard + # while collapsing. + visibles=0 # how many new elements have appeared? + i=entry[LIB_ROW]+1 # current element looking at + indLevel=self.fSongs[i][LIB_INDENT] + while i<=entry[LIB_NEXTROW]-1 and i<len(self.fSongs): + entry2=self.fSongs[i] + if entry2[LIB_EXPANDED]==1: + indLevel=max(entry2[LIB_INDENT]+1, indLevel) + elif entry2[LIB_EXPANDED]==0: + indLevel=min(entry2[LIB_INDENT], indLevel) + if entry2[LIB_INDENT]<=indLevel: + visibles+=1 + + if entry2[LIB_EXPANDED]==0: + i=entry2[LIB_NEXTROW] + else: + i+=1 + + mult=1 + if ret==0: mult=-1 + self.numVisEntries+=mult*visibles + self.resizeEvent(None) + return ret + + return 2 + + def mousePressEvent(self, event): + self.setFocus() + pos=event.pos() + row=self._pos2row(pos) + + done=False # indicates whether some action has been done or not + if self.mode=='playlist': + self.scrollMult=1 + if row==-1: + # we're clicking in the header! + self.resizeCol=None + x=0+self.xOffset + i=0 + # check if we're clicking between two columns, if so: resize mode! + for hdr in self.headers: + if hdr[2]: + x+=hdr[1] + if abs(x-pos.x())<4: + self.resizeCol=i + done=True + i+=1 + elif self.mode=='library': + entry=self._row2entry(row+self.topRow) + if not entry: + entry=self.fSongs[len(self.fSongs)-1] + if entry and pos.x()>(1+entry[LIB_INDENT])*self.indentation \ + and pos.x()<(1+entry[LIB_INDENT]+3/2)*self.indentation: + # we clicked in the margin, to expand or collapse + self.libToggle(entry) + done=True + + if done==False: + self.selMode=True + self.selIDs=[] + self.selMiscs=[] + if row==-1 and self.resizeCol==None: + # we're not resizing, thus we can select all! + self.selRows=[[0, len(self.fSongs)]] + elif row>=0: + # we start selection mode + if self.mode=='playlist': + self.selRows=[[self.topRow+row,self.topRow+row]] + elif self.mode=='library': + self.selRows=[[entry[LIB_ROW], entry[LIB_NEXTROW]-1]] + self.selMode=True + + self.update() + + def mouseMoveEvent(self, event): + pos=event.pos() + row=self._pos2row(pos) + if self.selMode: + # we're in selection mode + if row<0: + # scroll automatically when going out of the widget + row=0 + if self.topRow>0: + self.scrollMult+=0.1 + jump=int(self.scrollMult)*int(abs(pos.y())/self.lineHeight) + self.vScrollbar.setValue(self.vScrollbar.value()-jump) + row=jump + elif row>=self.numRows: + # scroll automatically when going out of the widget + self.scrollMult+=0.1 + jump=int(self.scrollMult)*int(abs(self.height()-pos.y())/self.lineHeight) + self.vScrollbar.setValue(self.vScrollbar.value()+jump) + row=self.numRows-jump + else: + # reset the scrollMultiplier + self.scrollMult=1 + + if self.mode=='playlist': + self.selRows[0][1]=row+self.topRow + elif self.mode=='library': + self.selRows[0][1]=self.libIthVisRowIndex(self.libIthVisRowIndex(0,self.topRow), row) + self.update() + elif self.resizeCol!=None: + row-=1 + # ohla, we're resizing a column! + prev=0 + # calculate where we are + for i in xrange(self.resizeCol): + hdr=self.headers[i] + if hdr[2]: + prev+=hdr[1] + self.headers[self.resizeCol][1]=pos.x()-prev-self.xOffset + # minimum width check? + if self.headers[self.resizeCol][1]<self.minColumnWidth: + self.headers[self.resizeCol][1]=self.minColumnWidth + self.resizeEvent(None) + self.update() + + def mouseReleaseEvent(self, event): + if self.selMode and len(self.selRows): + # we were selecting, but now we're done. + # We have to transform one range of rows + # into range of selected IDs + # The problem is that the list can be filtered, and that + # consequtive, visible rows aren't always directly + # consequtive in the unfiltered list. + self.selMode=False # exit selection mode + fSongs=self.fSongs + self.selMiscs=[] + ranges=[] + curRange=[] + # loop over all rows that are selected + for entry in fSongs[min(self.selRows[0]):max(self.selRows[0])+1]: + song=None + if isinstance(entry,Song): + song=entry + elif isinstance(entry[LIB_VALUE],Song): + song=entry[LIB_VALUE] + else: + self.selMiscs.append(entry[LIB_ROW]) + + if song!=None: + id=song.getID() + # is this song directly after the previous row? + if len(curRange)==0 or curRange[-1]+1==id: + curRange.append(id) + else: + ranges.append(curRange) + curRange=[id] + if len(curRange): + ranges.append(curRange) + # clean up ranges + self.selRows=[] + self.selIDs=[] + for range in ranges: + self.selIDs.append([range[0], range[-1]]) + self.update() + + elif self.resizeCol!=None: + # store this new width! + self.saveColumnWidth(self.resizeCol) + # we're not resizing anymore! + self.resizeCol=None + self.update() + def saveColumnWidth(self, col): + settings.set('l%s.%s.width'%(self.name,self.headers[col][0]), self.headers[col][1]) + def mouseDoubleClickEvent(self, event): + pos=event.pos() + row=self._pos2row(pos) + if row>=0: + self.onDoubleClick() + else: + # auto-size column + x=0+self.xOffset + i=0 + for hdr in self.headers: + if hdr[2]: + x+=hdr[1] + if abs(x-pos.x())<4: + self.autoSizeColumn(i) + break + i+=1 + + def _paintPlaylist(self, p): + self.redrawID=None + + + lineHeight=self.lineHeight + margin=self.margin + vmargin=self.vmargin + selRows=self.selRows + width=self.width() + if self.vScrollbar.isVisible(): + width-=self.scrollbarWidth + + if self.resizeColumn!=None: + # we're autoresizing! + # must be done here, because only here we can check the textwidth! + # This is because of limitations it can be only be done in paintEvent + hdr=self.headers[self.resizeColumn][0] + w=self.minColumnWidth + # loop over all visible songs ... + for song in self.fSongs: + rect=p.boundingRect(10,10,1,1, QtCore.Qt.AlignLeft, song.getTag(hdr)) + w=max(rect.width(), w) + self.headers[self.resizeColumn][1]=w+2*margin + self.saveColumnWidth(self.resizeColumn) + self.resizeColumn=None + self.resizeEvent(None) + if self.redrawID!=None: + # only update one row + y=lineHeight + for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)): + if self.fSongs[row]._data['id']==self.redrawID: + self._paintPlaylistRow(p, row, y, width) + y+=lineHeight + + self.redrawID=None + return + + # paint the headers! + p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), QtGui.QBrush(QtCore.Qt.lightGray)) + p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1)) + x=margin+self.xOffset + for hdr in self.headers: + if hdr[2]: + p.drawText(x, vmargin, hdr[1], lineHeight, QtCore.Qt.AlignLeft, hdr[0]) + x+=hdr[1] + p.drawLine(QtCore.QPoint(x-margin,0), QtCore.QPoint(x-margin,lineHeight)) + + if self.songs==None: + return + # fill the records! + y=lineHeight + for row in xrange(self.topRow, min(self.numVisEntries, self.topRow+self.numRows)): + self._paintPlaylistRow(p, row, y, width) + y+=lineHeight + if y<self.height(): + # if we're short on songs, draw up the remaining area in background color + p.fillRect(QtCore.QRect(0,y,width,self.height()-y), QtGui.QBrush(self.clrBg)) + + def _paintPlaylistRow(self, p, row, y, width): + """Paint row $row on $p on height $y and with width $width.""" + song=self.fSongs[row] + lineHeight=self.lineHeight + margin=self.margin + vmargin=self.vmargin + id=song._data['id'] + + # determine color of row. Default is row-color, but can be overridden by + # (in this order): selection, special row color! + clr=self.colors[row%2] # background color of the row + clrTxt=QtCore.Qt.black # color of the printed text + # is it selected? + values=[] + if self.selMode: + checkID=row + values=self.selRows + else: + checkID=id + values=self.selIDs + # if values==[], it won't run! + for range in values: + # is selected if in range, which depends on the selection-mode + if checkID>=min(range) and checkID<=max(range): + clr=self.clrSel + clrTxt=QtCore.Qt.white + # it has a VIP-status! + if id==int(self.clrID[0]): + clrTxt=QtCore.Qt.white + clr=self.clrID[1] + + # draw the row background + p.fillRect(QtCore.QRect(2, y, width-3, lineHeight), QtGui.QBrush(clr)) + + # draw a subtile rectangle + p.setPen(QtGui.QColor(230,230,255)) + p.drawRect(QtCore.QRect(2, y, width-3, lineHeight)) + + # Back To Black + p.setPen(QtCore.Qt.black) + + # draw the column + x=margin+self.xOffset + for hdr in self.headers: + if hdr[2]: + # only if visible, duh! + # rectangle we're allowed to print in + text=song.getTag(hdr[0]) + if type(text)!=str and type(text)!=unicode: + text=str(text) + rect=p.boundingRect(x, y, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text) + p.setPen(clrTxt) + p.drawText(x, y+vmargin, hdr[1]-margin, lineHeight, QtCore.Qt.AlignLeft, text) + if rect.width()>hdr[1]-margin: + # print ellipsis, if necessary + p.fillRect(x+hdr[1]-15,y+1,15,lineHeight-1, QtGui.QBrush(clr)) + p.drawText(x+hdr[1]-15,y+vmargin,15,lineHeight-1, QtCore.Qt.AlignLeft, "...") + x+=hdr[1] + p.setPen(QtCore.Qt.black) + p.drawLine(QtCore.QPoint(x-margin,y), QtCore.QPoint(x-margin,y+lineHeight)) + + def libFirstVisRowIndex(self): + """Returns the index of the first visible row in library mode.""" + # if not in library mode, the unthinkable might happen! Wooo! + # TODO find better initial value + row=0 # the visible rows we're visiting + index=0 # what index does the current row have + entries=self.fSongs + + while index<len(entries): + if row>=self.topRow: + break + entry=entries[index] + if entry[LIB_EXPANDED]==0: + index=entry[LIB_NEXTROW] + else: + index+=1 + row+=1 + return index + def libIthVisRowIndex(self, index, i=1): + """Returns the index of the $i-th next row after $index that is visible (or -1) in library mode.""" + entries=self.fSongs + while i>0 and index<len(entries): + i-=1 + entry=self.fSongs[index] + if entry[LIB_EXPANDED]==0: + if index<0: + return -1 + index=entry[LIB_NEXTROW] + else: + index+=1 + + return index + + + def libPrint(self): + for entry in self.fSongs: + indent="" + for i in xrange(entry[2]): + indent=("%s ")%(indent) + print "%s%s" % (indent, entry) + + def _paintLibrary(self, p): + width=self.width() + height=self.height() + lineHeight=self.lineHeight + margin=self.margin + vmargin=self.vmargin + + # paint the headers! + p.fillRect(QtCore.QRect(0,0,width+self.vScrollbar.width(),lineHeight), QtGui.QBrush(QtCore.Qt.lightGray)) + p.drawRect(QtCore.QRect(0,0,width+self.vScrollbar.width()-1,lineHeight-1)) + p.drawText(margin, vmargin, width, lineHeight, QtCore.Qt.AlignLeft, self.groupByStr.replace('$', '')) + + entries=self.fSongs + + y=lineHeight + x=margin + indent=self.indentation + index=self.libFirstVisRowIndex() + row=0 + while index<len(entries) and y<height: + entry=entries[index] + + level=entry[LIB_INDENT] + isSong=isinstance(entry[LIB_VALUE], Song) + + if isSong: + prefix='' + text="%s %s"%(entry[LIB_VALUE].getTrack(), entry[LIB_VALUE].getTitle()) + else: + if entry[LIB_EXPANDED]==1: prefix='-' + elif entry[LIB_EXPANDED]==0: prefix='+' + text=entry[LIB_VALUE] + + clr=self.colors[row%2] # background color of the row + clrTxt=QtCore.Qt.black + + values=[] + if self.selMode: + checkID=index + values=self.selRows + elif self.selMode==False and isSong: + checkID=entry[LIB_VALUE].getID() + values=self.selIDs + + # if values==[], then it won't run! + for range in values: + # is selected if in range, which depends on the selection-mode + if checkID>=min(range) and checkID<=max(range): + clr=self.clrSel + clrTxt=QtCore.Qt.white + + for i in self.selMiscs: + if index==i: + clr=self.clrSel + clrTxt=QtCore.Qt.white + + # it has a VIP-status! + if isSong and entry[LIB_VALUE].getID()==int(self.clrID[0]): + clrTxt=QtCore.Qt.white + clr=self.clrID[1] + + left=x+indent*(1+level) + top=y+vmargin + p.fillRect(QtCore.QRect(left,y,width-3,lineHeight), clr) + p.setPen(clrTxt) + p.drawText(left, top, 15, lineHeight, QtCore.Qt.AlignLeft, prefix) + p.drawText(left+15, top, width, lineHeight, QtCore.Qt.AlignLeft, text) + p.setPen(QtCore.Qt.black) + + obj=None + if level<len(self.levels): + if self.levels[level][0:len('$artist')]=='$artist': + obj=self.wgGfxArtist + elif self.levels[level][0:len('$album')]=='$album': + obj=self.wgGfxAlbum + + if obj: + obj.render(p, QtCore.QRectF(indent*level+1,y+1,lineHeight-1,lineHeight-1)) + + y+=lineHeight + row+=1 + index=self.libIthVisRowIndex(index) + if index<0: + break + + _mutex=QtCore.QMutex() + _paintCnt=0 + def paintEvent(self, event): + self._mutex.lock() + if self._paintCnt: + self._mutex.unlock() + return + self._paintCnt=1 + self._mutex.unlock() + + + p=QtGui.QPainter(self) + + # for the moment, redraw everything ... + p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(self.clrBg)) + if self.mode=='playlist': + self._paintPlaylist(p) + elif self.mode=='library': + self._paintLibrary(p) + + # draw a nice line around the widget! + p.drawRect(QtCore.QRect(0,0,self.width()-1,self.height()-1)) + if self.hasFocus(): + p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3)) + else: + p.setPen(QtCore.Qt.lightGray) + p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3)) + + self._paintCnt=0 + + text=None + if len(self._filters): + text="Please wait while filtering ... (%i)"%(len(self._filters)) + #text='%s - %s' % (self.selMiscs, '') + #text='%s - %s - %s' % (str(self.topRow), str(self.selRows), str(self.selIDs)) + #text='%s - %s'%(str(self.topRow), str(self.numVisEntries)) + if text: + r=QtCore.QRect(10,self.height()-40,self.width()-20,20) + p.fillRect(r, QtGui.QBrush(QtCore.Qt.white)) + p.drawText(r,QtCore.Qt.AlignLeft, text) + diff --git a/winConnect.py b/winConnect.py index 2a7fe5c..80fa011 100644 --- a/winConnect.py +++ b/winConnect.py @@ -6,82 +6,82 @@ from traceback import print_exc from clSettings import settings class winConnect(QtGui.QWidget): - txtHost=None - txtPort=None - lblInfo=None - _timerID=None + txtHost=None + txtPort=None + lblInfo=None + _timerID=None - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) - self.txtHost=QtGui.QLineEdit(settings.get('host', 'localhost')) - self.txtPort=QtGui.QLineEdit(settings.get('port', '6600')) - self.lblInfo=QtGui.QLabel("connecting ...") + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + self.txtHost=QtGui.QLineEdit(settings.get('host', 'localhost')) + self.txtPort=QtGui.QLineEdit(settings.get('port', '6600')) + self.lblInfo=QtGui.QLabel("connecting ...") - frame=QtGui.QVBoxLayout() - inputs=QtGui.QHBoxLayout() + frame=QtGui.QVBoxLayout() + inputs=QtGui.QHBoxLayout() - frame.addLayout(inputs) - frame.addWidget(self.lblInfo) + frame.addLayout(inputs) + frame.addWidget(self.lblInfo) - inputs.addWidget(self.txtHost) - inputs.addWidget(self.txtPort) + inputs.addWidget(self.txtHost) + inputs.addWidget(self.txtPort) - self.setWindowIcon(appIcon) - self.setWindowTitle('Connect to mpd') - self.setLayout(frame) - self.resize(200,80) - self.center() - doEvents() + self.setWindowIcon(appIcon) + self.setWindowTitle('Connect to mpd') + self.setLayout(frame) + self.resize(200,80) + self.center() + doEvents() - monty.addListener('onReady', self.onReady) - monty.addListener('onConnect', self.onConnect) + monty.addListener('onReady', self.onReady) + monty.addListener('onConnect', self.onConnect) - def center(self): - screen = QtGui.QDesktopWidget().screenGeometry() - size = self.geometry() - self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2+100) + def center(self): + screen = QtGui.QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2+100) - def monitor(self): - self.txtHost.setEnabled(True) - self.txtPort.setEnabled(True) - if self._timerID==None: - self._timerID=self.startTimer(200) - if self.isVisible()==False: - self.show() - self.activateWindow() - self.raise_() - doEvents() + def monitor(self): + self.txtHost.setEnabled(True) + self.txtPort.setEnabled(True) + if self._timerID==None: + self._timerID=self.startTimer(200) + if self.isVisible()==False: + self.show() + self.activateWindow() + self.raise_() + doEvents() - - def onConnect(self, params): - if self._timerID: - self.killTimer(self._timerID) - self._timerID=None - self.lblInfo.setText('Connected!\nRestoring library and playlist ...') - doEvents() - settings.set('host', str(self.txtHost.text())) - settings.set('port', str(self.txtPort.text())) - self.txtHost.setEnabled(False) - self.txtPort.setEnabled(False) - doEvents() + + def onConnect(self, params): + if self._timerID: + self.killTimer(self._timerID) + self._timerID=None + self.lblInfo.setText('Connected!\nRestoring library and playlist ...') + doEvents() + settings.set('host', str(self.txtHost.text())) + settings.set('port', str(self.txtPort.text())) + self.txtHost.setEnabled(False) + self.txtPort.setEnabled(False) + doEvents() - def onReady(self, params): - self.hide() + def onReady(self, params): + self.hide() - def timerEvent(self, event): - host=str(self.txtHost.text()) - try: - port=int(self.txtPort.text()) - except: - self.lblInfo.setText('Invalid port') - return + def timerEvent(self, event): + host=str(self.txtHost.text()) + try: + port=int(self.txtPort.text()) + except: + self.lblInfo.setText('Invalid port') + return - self.lblInfo.setText('Trying to connect to '+host+':'+str(port)+' ...') - doEvents() - monty.connect(host, port) - doEvents() - - def windowActivationChange(self, bool): - self.activateWindow() - self.raise_() - doEvents() + self.lblInfo.setText('Trying to connect to '+host+':'+str(port)+' ...') + doEvents() + monty.connect(host, port) + doEvents() + + def windowActivationChange(self, bool): + self.activateWindow() + self.raise_() + doEvents() @@ -16,274 +16,274 @@ import log class winMain(QtGui.QMainWindow): - """The winMain class is mpc's main window, showing the playlists and control-interface""" - docks=[] + """The winMain class is mpc's main window, showing the playlists and control-interface""" + docks=[] - " menus" - mConnect=None - mDisconnect=None - mLayout=None - - " connection window" - wConnect=None - wSettings=None + " menus" + mConnect=None + mDisconnect=None + mLayout=None + + " connection window" + wConnect=None + wSettings=None - def __init__(self, parent=None): - QtGui.QWidget.__init__(self, parent) - self.setWindowTitle("montypc - An MPD client") + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + self.setWindowTitle("montypc - An MPD client") - self.wConnect=winConnect() - - self.statusBar() # create a statusbar - mBar=self.menuBar() # create a menubar - # menu file - m=mBar.addMenu("File") - m.setTearOffEnabled(True) - # connect - self.mConnect=m.addAction('Connect ...', self.wConnect.monitor) - self.mConnect.setIcon(QtGui.QIcon('gfx/connect.png')) - # disconnect - self.mDisconnect=m.addAction('Disconnect', monty.disconnect) - self.mDisconnect.setIcon(QtGui.QIcon('gfx/disconnect.png')) - # separator - m.addSeparator() - # quit - m.addAction("Quit", self.quit).setIcon(QtGui.QIcon('gfx/gtk-quit.svg')) + self.wConnect=winConnect() + + self.statusBar() # create a statusbar + mBar=self.menuBar() # create a menubar + # menu file + m=mBar.addMenu("File") + m.setTearOffEnabled(True) + # connect + self.mConnect=m.addAction('Connect ...', self.wConnect.monitor) + self.mConnect.setIcon(QtGui.QIcon('gfx/connect.png')) + # disconnect + self.mDisconnect=m.addAction('Disconnect', monty.disconnect) + self.mDisconnect.setIcon(QtGui.QIcon('gfx/disconnect.png')) + # separator + m.addSeparator() + # quit + m.addAction("Quit", self.quit).setIcon(QtGui.QIcon('gfx/gtk-quit.svg')) - # menu options - m=mBar.addMenu("Options") - m.setTearOffEnabled(True) - # settings - m.addAction("Settings", self.showWinSettings).setIcon(QtGui.QIcon('gfx/gtk-preferences.svg')) + # menu options + m=mBar.addMenu("Options") + m.setTearOffEnabled(True) + # settings + m.addAction("Settings", self.showWinSettings).setIcon(QtGui.QIcon('gfx/gtk-preferences.svg')) - # menu layout - self.mLayout=mBar.addMenu("Layout") - self.mLayout.setTearOffEnabled(True) + # menu layout + self.mLayout=mBar.addMenu("Layout") + self.mLayout.setTearOffEnabled(True) - newPlugins=False # are there new plugins? - for k, entry in plugins.listPlugins().iteritems(): - # load the plugin - plugin=plugins.loadPlugin(entry[plugins.PLUGIN_CLASS], self) - if plugin: - newPlugins=newPlugins|(settings.get('%s.load'%(plugin.getName(True)), None)==None) - if settings.get('%s.load'%(plugin.getName(True)), '1')=='1': - # load new plugins by default - plugin.load() - else: - newPlugins=True - - self.updateLayoutMenu() - self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks \ - |QtGui.QMainWindow.AllowTabbedDocks \ - |QtGui.QMainWindow.VerticalTabs) - self.setDockNestingEnabled(True) + newPlugins=False # are there new plugins? + for k, entry in plugins.listPlugins().iteritems(): + # load the plugin + plugin=plugins.loadPlugin(entry[plugins.PLUGIN_CLASS], self) + if plugin: + newPlugins=newPlugins|(settings.get('%s.load'%(plugin.getName(True)), None)==None) + if settings.get('%s.load'%(plugin.getName(True)), '1')=='1': + # load new plugins by default + plugin.load() + else: + newPlugins=True + + self.updateLayoutMenu() + self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks \ + |QtGui.QMainWindow.AllowTabbedDocks \ + |QtGui.QMainWindow.VerticalTabs) + self.setDockNestingEnabled(True) - try: - x,y=settings.getIntTuple('winMain.pos') - except: - x,y=0,0 - try: - w,h=settings.getIntTuple('winMain.size') - except: - w,h=800,600 - self.resize(w,h) - self.move(x,y) - self.restoreLayout() + try: + x,y=settings.getIntTuple('winMain.pos') + except: + x,y=0,0 + try: + w,h=settings.getIntTuple('winMain.size') + except: + w,h=800,600 + self.resize(w,h) + self.move(x,y) + self.restoreLayout() - " add event handlers" - monty.addListener('onReady', self.onReady) - monty.addListener('onConnect', self.onConnect) - monty.addListener('onDisconnect', self.onDisconnect) - monty.addListener('onUpdateDBStart', self.onUpdateDBStart) - monty.addListener('onUpdateDBFinish', self.onUpdateDBFinish) + " add event handlers" + monty.addListener('onReady', self.onReady) + monty.addListener('onConnect', self.onConnect) + monty.addListener('onDisconnect', self.onDisconnect) + monty.addListener('onUpdateDBStart', self.onUpdateDBStart) + monty.addListener('onUpdateDBFinish', self.onUpdateDBFinish) - self.enableAll(True) - self.setWindowIcon(appIcon) - # set icon in system tray - self.wConnect.monitor() + self.enableAll(True) + self.setWindowIcon(appIcon) + # set icon in system tray + self.wConnect.monitor() - self.show() - if newPlugins: - self.showWinSettings() - doEvents() + self.show() + if newPlugins: + self.showWinSettings() + doEvents() - def quit(self): - # unload all plugins - for entry in plugins.listPlugins().values(): - p=entry[plugins.PLUGIN_INSTANCE] - if p and p.isLoaded(): - p.unload() + def quit(self): + # unload all plugins + for entry in plugins.listPlugins().values(): + p=entry[plugins.PLUGIN_INSTANCE] + if p and p.isLoaded(): + p.unload() - QtCore.QCoreApplication.exit() - - - def updateLayoutMenu(self): - self.mLayout.clear() - self.mLayout.addAction('Save layout', self.saveLayout) - self.mLayout.addAction('Restore layout', self.restoreLayout) - self.mLayout.addSeparator() - # create checkable menu - a=QtGui.QAction('Show titlebars', self) - a.setCheckable(True) - a.setChecked(settings.get('winMain.show.titlebars', '1')=='1') - self.toggleTitleBars(a.isChecked()) - self.connect(a, QtCore.SIGNAL('toggled(bool)'), self.toggleTitleBars) - self.mLayout.addAction(a) - self.mLayout.addSeparator() - # can not use iterators, as that gives some creepy error 'bout c++ - actions=self.createPopupMenu().actions() - for i in xrange(len(actions)): - self.mLayout.addAction(actions[i]) - - def toggleTitleBars(self, val): - if val: - settings.set('winMain.show.titlebars', '1') - else: - settings.set('winMain.show.titlebars', '0') - for dock in self.docks: - if val: - dock.setTitleBarWidget(None) - else: - dock.setTitleBarWidget(QtGui.QWidget()) - def addDock(self, dock): - if dock: - self.docks.append(dock) - self.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock) - self.updateLayoutMenu() - def removeDock(self, dock): - if dock: - self.docks.remove(dock) - self.removeDockWidget(dock) - self.updateLayoutMenu() + QtCore.QCoreApplication.exit() + + + def updateLayoutMenu(self): + self.mLayout.clear() + self.mLayout.addAction('Save layout', self.saveLayout) + self.mLayout.addAction('Restore layout', self.restoreLayout) + self.mLayout.addSeparator() + # create checkable menu + a=QtGui.QAction('Show titlebars', self) + a.setCheckable(True) + a.setChecked(settings.get('winMain.show.titlebars', '1')=='1') + self.toggleTitleBars(a.isChecked()) + self.connect(a, QtCore.SIGNAL('toggled(bool)'), self.toggleTitleBars) + self.mLayout.addAction(a) + self.mLayout.addSeparator() + # can not use iterators, as that gives some creepy error 'bout c++ + actions=self.createPopupMenu().actions() + for i in xrange(len(actions)): + self.mLayout.addAction(actions[i]) + + def toggleTitleBars(self, val): + if val: + settings.set('winMain.show.titlebars', '1') + else: + settings.set('winMain.show.titlebars', '0') + for dock in self.docks: + if val: + dock.setTitleBarWidget(None) + else: + dock.setTitleBarWidget(QtGui.QWidget()) + def addDock(self, dock): + if dock: + self.docks.append(dock) + self.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock) + self.updateLayoutMenu() + def removeDock(self, dock): + if dock: + self.docks.remove(dock) + self.removeDockWidget(dock) + self.updateLayoutMenu() - mMenuVisible=None - def createPopupMenu(self): - ret=QtGui.QMenu('Test', self) - if self.mMenuVisible==None: - # create checkable menu - a=QtGui.QAction('Menubar', self) - a.setCheckable(True) - a.setChecked(True) - self.connect(a, QtCore.SIGNAL('toggled(bool)'), self.switchMenubar) - - self.mMenuVisible=a - ret.addAction(self.mMenuVisible) - ret.addSeparator() - actions=QtGui.QMainWindow.createPopupMenu(self).actions() - for i in xrange(len(actions)-1): - ret.addAction(actions[i]) - return ret - def switchMenubar(self, val): - self.menuBar().setVisible(val) - def setStatus(self, status): - """Set the text of the statusbar.""" - self.statusBar().showMessage(status) - log.extended(status) + mMenuVisible=None + def createPopupMenu(self): + ret=QtGui.QMenu('Test', self) + if self.mMenuVisible==None: + # create checkable menu + a=QtGui.QAction('Menubar', self) + a.setCheckable(True) + a.setChecked(True) + self.connect(a, QtCore.SIGNAL('toggled(bool)'), self.switchMenubar) + + self.mMenuVisible=a + ret.addAction(self.mMenuVisible) + ret.addSeparator() + actions=QtGui.QMainWindow.createPopupMenu(self).actions() + for i in xrange(len(actions)-1): + ret.addAction(actions[i]) + return ret + def switchMenubar(self, val): + self.menuBar().setVisible(val) + def setStatus(self, status): + """Set the text of the statusbar.""" + self.statusBar().showMessage(status) + log.extended(status) - def saveLayout(self): - f=open('layout', 'wb') - f.write(self.saveState()) - f.close() - log.normal("Layout saved") - def restoreLayout(self): - try: - f=open('layout', 'rb') - self.restoreState(f.read()) - f.close() - except: - pass + def saveLayout(self): + f=open('layout', 'wb') + f.write(self.saveState()) + f.close() + log.normal("Layout saved") + def restoreLayout(self): + try: + f=open('layout', 'rb') + self.restoreState(f.read()) + f.close() + except: + pass - def showWinSettings(self): - if not self.wSettings: - self.wSettings=winSettings(self) - self.wSettings.show() - self.wSettings.raise_() + def showWinSettings(self): + if not self.wSettings: + self.wSettings=winSettings(self) + self.wSettings.show() + self.wSettings.raise_() - def onReady(self, params): - self.initialiseData() + def onReady(self, params): + self.initialiseData() - def onConnect(self, params): - log.normal("Connected to MPD") - self.setStatus('Restoring library and playlist ...') - self.mDisconnect.setEnabled(True) - self.mConnect.setEnabled(False) - doEvents() + def onConnect(self, params): + log.normal("Connected to MPD") + self.setStatus('Restoring library and playlist ...') + self.mDisconnect.setEnabled(True) + self.mConnect.setEnabled(False) + doEvents() - def enableAll(self, value): - for k,entry in plugins.listPlugins().iteritems(): - try: - plugin=entry[plugins.PLUGIN_INSTANCE] - plugin.o.setEnabled(value) - except: - pass + def enableAll(self, value): + for k,entry in plugins.listPlugins().iteritems(): + try: + plugin=entry[plugins.PLUGIN_INSTANCE] + plugin.o.setEnabled(value) + except: + pass - def initialiseData(self): - """Initialise the library, playlist and some other small things""" - self.setStatus("Filling library ...") - doEvents() - self.fillLibrary() - doEvents() - - self.setStatus("Filling playlist ...") - doEvents() - self.fillPlaylist() - doEvents() - - self.setStatus("Doing the rest ...") - doEvents() - self.enableAll(True) - self.setStatus("") - doEvents() - - def resizeEvent(self, event): - settings.set('winMain.size', '%i %i'%(self.width(),self.height())) - def moveEvent(self, event): - settings.set('winMain.pos', '%i %i'%(self.x(),self.y())) - - def onDisconnect(self, params): - log.normal("Disconnected from MPD") - try: - self.getPlaylistList().updateSongs([]) - except: - pass - try: - self.getLibraryList().updateSongs([]) - except: - pass - self.mDisconnect.setEnabled(False) - self.mConnect.setEnabled(True) - self.enableAll(False) - self.setStatus("You are disconnected. Choose File->Connect to reconnect!") + def initialiseData(self): + """Initialise the library, playlist and some other small things""" + self.setStatus("Filling library ...") + doEvents() + self.fillLibrary() + doEvents() + + self.setStatus("Filling playlist ...") + doEvents() + self.fillPlaylist() + doEvents() + + self.setStatus("Doing the rest ...") + doEvents() + self.enableAll(True) + self.setStatus("") + doEvents() + + def resizeEvent(self, event): + settings.set('winMain.size', '%i %i'%(self.width(),self.height())) + def moveEvent(self, event): + settings.set('winMain.pos', '%i %i'%(self.x(),self.y())) + + def onDisconnect(self, params): + log.normal("Disconnected from MPD") + try: + self.getPlaylistList().updateSongs([]) + except: + pass + try: + self.getLibraryList().updateSongs([]) + except: + pass + self.mDisconnect.setEnabled(False) + self.mConnect.setEnabled(True) + self.enableAll(False) + self.setStatus("You are disconnected. Choose File->Connect to reconnect!") - def fillPlaylist(self): - """Fill the playlist.""" - try: - self.getPlaylistList().updateSongs(monty.listPlaylist()) - except: - pass + def fillPlaylist(self): + """Fill the playlist.""" + try: + self.getPlaylistList().updateSongs(monty.listPlaylist()) + except: + pass - def fillLibrary(self): - """Fill the library.""" - try: - self.getLibraryList().updateSongs(monty.listLibrary()) - except: - pass - - def getPlaylistList(self): - try: - return plugins.getPlugin('Playlist').getList() - except: - print "Missing playlist plugin!" - return None + def fillLibrary(self): + """Fill the library.""" + try: + self.getLibraryList().updateSongs(monty.listLibrary()) + except: + pass + + def getPlaylistList(self): + try: + return plugins.getPlugin('Playlist').getList() + except: + print "Missing playlist plugin!" + return None - def getLibraryList(self): - try: - return plugins.getPlugin('Library').getList() - except: - print "Missing library plugin!" - return None + def getLibraryList(self): + try: + return plugins.getPlugin('Library').getList() + except: + print "Missing library plugin!" + return None - def onUpdateDBFinish(self, params): - self.setStatus('') - def onUpdateDBStart(self, params): - self.setStatus('Updating the database. Please wait ...') + def onUpdateDBFinish(self, params): + self.setStatus('') + def onUpdateDBStart(self, params): + self.setStatus('Updating the database. Please wait ...') diff --git a/winSettings.py b/winSettings.py index 0ae8cc4..f6f1a95 100644 --- a/winSettings.py +++ b/winSettings.py @@ -8,97 +8,97 @@ import plugins class winSettings(QtGui.QWidget): - btnSave=None - btnClose=None - lstPlugins=None - - winMain=None - - def __init__(self, winMain, parent=None): - QtGui.QWidget.__init__(self, parent) - self.winMain=winMain - - self.btnSave=Button('save all', self.onBtnSaveClick) - self.btnClose=Button('close', self.onBtnCloseClick) - - self.lstPlugins=QtGui.QListWidget(self) - layoutWin=QtGui.QVBoxLayout() - layoutButtons=QtGui.QHBoxLayout() - - tabWidget=QtGui.QTabWidget(parent) - self.setLayout(layoutWin) - - tabWidget.addTab(self.lstPlugins, "plugins") - for k,entry in plugins.listPlugins().iteritems(): - plugin=entry[plugins.PLUGIN_INSTANCE] - if plugin: - tabWidget.addTab(plugin.getSettingsWidget(), plugin.getName()) - self.fillList() - - layoutWin.addWidget(tabWidget) - layoutWin.addLayout(layoutButtons) - - layoutButtons.addStretch() - layoutButtons.addWidget(self.btnSave) - layoutButtons.addWidget(self.btnClose) - - self.connect(self.lstPlugins, QtCore.SIGNAL('itemChanged (QListWidgetItem*)'), self.onlstPluginItemChanged) - - self.setWindowIcon(appIcon) - self.setWindowTitle('Settings') - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.center() - self.resize(800,400) - doEvents() + btnSave=None + btnClose=None + lstPlugins=None + + winMain=None + + def __init__(self, winMain, parent=None): + QtGui.QWidget.__init__(self, parent) + self.winMain=winMain + + self.btnSave=Button('save all', self.onBtnSaveClick) + self.btnClose=Button('close', self.onBtnCloseClick) + + self.lstPlugins=QtGui.QListWidget(self) + layoutWin=QtGui.QVBoxLayout() + layoutButtons=QtGui.QHBoxLayout() + + tabWidget=QtGui.QTabWidget(parent) + self.setLayout(layoutWin) + + tabWidget.addTab(self.lstPlugins, "plugins") + for k,entry in plugins.listPlugins().iteritems(): + plugin=entry[plugins.PLUGIN_INSTANCE] + if plugin: + tabWidget.addTab(plugin.getSettingsWidget(), plugin.getName()) + self.fillList() + + layoutWin.addWidget(tabWidget) + layoutWin.addLayout(layoutButtons) + + layoutButtons.addStretch() + layoutButtons.addWidget(self.btnSave) + layoutButtons.addWidget(self.btnClose) + + self.connect(self.lstPlugins, QtCore.SIGNAL('itemChanged (QListWidgetItem*)'), self.onlstPluginItemChanged) + + self.setWindowIcon(appIcon) + self.setWindowTitle('Settings') + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.center() + self.resize(800,400) + doEvents() - def fillList(self): - self.lstPlugins.clear() - for k,entry in plugins.listPlugins().iteritems(): - plugin=entry[plugins.PLUGIN_INSTANCE] - if plugin: - item=QtGui.QListWidgetItem("%s\t%s"%(entry[plugins.PLUGIN_CLASS], plugin.getInfo())) - if plugin.isLoaded(): - item.setCheckState(QtCore.Qt.Checked) - else: - item.setCheckState(QtCore.Qt.Unchecked) - - if settings.get('%s.load'%(plugin.getName(True)),None)==None: - # load new plugins by default - item.setTextColor(QtCore.Qt.blue) - settings.set('%s.load'%(plugin.getName(True)), 1) - else: - item=QtGui.QListWidgetItem("%s\t%s"%(entry[plugins.PLUGIN_CLASS], entry[plugins.PLUGIN_MSG])) - item.setCheckState(QtCore.Qt.Unchecked) - item.setTextColor(QtCore.Qt.red) - self.lstPlugins.addItem(item) - + def fillList(self): + self.lstPlugins.clear() + for k,entry in plugins.listPlugins().iteritems(): + plugin=entry[plugins.PLUGIN_INSTANCE] + if plugin: + item=QtGui.QListWidgetItem("%s\t%s"%(entry[plugins.PLUGIN_CLASS], plugin.getInfo())) + if plugin.isLoaded(): + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + + if settings.get('%s.load'%(plugin.getName(True)),None)==None: + # load new plugins by default + item.setTextColor(QtCore.Qt.blue) + settings.set('%s.load'%(plugin.getName(True)), 1) + else: + item=QtGui.QListWidgetItem("%s\t%s"%(entry[plugins.PLUGIN_CLASS], entry[plugins.PLUGIN_MSG])) + item.setCheckState(QtCore.Qt.Unchecked) + item.setTextColor(QtCore.Qt.red) + self.lstPlugins.addItem(item) + - def center(self): - screen = QtGui.QDesktopWidget().screenGeometry() - size = self.geometry() - self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2+100) - - def onBtnSaveClick(self): - map(lambda entry: entry[plugins.PLUGIN_INSTANCE] and entry[plugins.PLUGIN_INSTANCE].saveSettings(), plugins.listPlugins().values()) + def center(self): + screen = QtGui.QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2+100) + + def onBtnSaveClick(self): + map(lambda entry: entry[plugins.PLUGIN_INSTANCE] and entry[plugins.PLUGIN_INSTANCE].saveSettings(), plugins.listPlugins().values()) - def onBtnCloseClick(self): - self.close() - def onlstPluginItemChanged(self, item): - # check here if we have to load or unload the plugin! - toload=int(item.checkState()==QtCore.Qt.Checked) - className=str(item.text()[0:str(item.text()).find('\t')]) - if toload: - # refresh the plugin file - plugin=plugins.loadPlugin(className, self.winMain) - if plugin: - plugin.load() - self.fillList() - else: - plugin=plugins.getPlugin(className) - if plugin: - plugin.unload() - if plugin: - settings.set('%s.load'%(plugin.getName(True)), toload) - def closeEvent(self, event): - map(lambda entry: entry[plugins.PLUGIN_INSTANCE] and entry[plugins.PLUGIN_INSTANCE].resetSettingCache(), plugins.listPlugins().values()) - self.winMain.wSettings=None + def onBtnCloseClick(self): + self.close() + def onlstPluginItemChanged(self, item): + # check here if we have to load or unload the plugin! + toload=int(item.checkState()==QtCore.Qt.Checked) + className=str(item.text()[0:str(item.text()).find('\t')]) + if toload: + # refresh the plugin file + plugin=plugins.loadPlugin(className, self.winMain) + if plugin: + plugin.load() + self.fillList() + else: + plugin=plugins.getPlugin(className) + if plugin: + plugin.unload() + if plugin: + settings.set('%s.load'%(plugin.getName(True)), toload) + def closeEvent(self, event): + map(lambda entry: entry[plugins.PLUGIN_INSTANCE] and entry[plugins.PLUGIN_INSTANCE].resetSettingCache(), plugins.listPlugins().values()) + self.winMain.wSettings=None |