diff options
author | Anton Khirnov <wyskas@gmail.com> | 2009-02-20 10:49:00 +0100 |
---|---|---|
committer | Anton Khirnov <wyskas@gmail.com> | 2009-02-20 10:49:00 +0100 |
commit | ae872ea3018fe1dac39f867f386b081598fb0812 (patch) | |
tree | 856d2b6d32050174682305b421548e680fe5647a /nephilim/plugins | |
parent | 000f9d5ba84426da6b5211dd0bea4a401b8f4289 (diff) |
Move modules to a separate dir.
Diffstat (limited to 'nephilim/plugins')
-rw-r--r-- | nephilim/plugins/AlbumCover.py | 304 | ||||
-rw-r--r-- | nephilim/plugins/Filebrowser.py | 47 | ||||
-rw-r--r-- | nephilim/plugins/Library.py | 155 | ||||
-rw-r--r-- | nephilim/plugins/Lyrics.py | 74 | ||||
-rw-r--r-- | nephilim/plugins/Notify.py | 152 | ||||
-rw-r--r-- | nephilim/plugins/PlayControl.py | 169 | ||||
-rw-r--r-- | nephilim/plugins/Playlist.py | 92 | ||||
-rw-r--r-- | nephilim/plugins/Systray.py | 125 | ||||
-rw-r--r-- | nephilim/plugins/__init__.py | 97 |
9 files changed, 1215 insertions, 0 deletions
diff --git a/nephilim/plugins/AlbumCover.py b/nephilim/plugins/AlbumCover.py new file mode 100644 index 0000000..426df96 --- /dev/null +++ b/nephilim/plugins/AlbumCover.py @@ -0,0 +1,304 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +from traceback import print_exc +import os +import shutil +import logging + +from ..clPlugin import Plugin +from ..misc import ORGNAME, APPNAME + +# FETCH MODES +AC_NO_FETCH = 0 +AC_FETCH_LOCAL_DIR = 1 +AC_FETCH_INTERNET = 2 + +class wgAlbumCover(QtGui.QLabel): + " container for the image" + img = None + imgLoaded = False + p = None + cover_dirname = None + cover_filepath = None + menu = None + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self,parent) + self.p=p + self.setAlignment(QtCore.Qt.AlignCenter) + + # popup menu + self.menu = QtGui.QMenu("album") + select_file_action = self.menu.addAction('Select cover file...') + self.connect(select_file_action, QtCore.SIGNAL('triggered()'), self.select_cover_file) + fetch_amazon_action = self.menu.addAction('Fetch cover from Amazon.') + self.connect(fetch_amazon_action, QtCore.SIGNAL('triggered()'), self.fetch_amazon) + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.RightButton: + self.menu.popup(event.globalPos()) + + def select_cover_file(self): + try: + song = self.p.mpclient.getCurrentSong() + file = QtGui.QFileDialog.getOpenFileName(self + , "Select album cover for %s - %s"%(song.getArtist(), song.getAlbum()) + , self.cover_dirname + , "" + ) + if file: + shutil.copy(file, self.cover_filepath) + else: + return + except IOError: + logging.info("Error setting cover file.") + self.refresh() + + def get_cover(self): + if self.imgLoaded: + return self.pixmap() + return None + + def refresh(self, params = None): + logging.info("refreshing cover") + song = self.p.mpclient.getCurrentSong() + if not song: + self.clear() + self.update() + return + + dirname = unicode(self.p.settings.value(self.p.getName() + '/coverdir').toString()) + self.cover_dirname = dirname.replace('$musicdir', self.p.settings.value('MPD/music_dir').toString()).replace('$songdir', os.path.dirname(song.getFilepath())) + filebase = unicode(self.p.settings.value(self.p.getName() + '/covername').toString()) + self.cover_filepath = os.path.join(self.cover_dirname, song.expand_tags(filebase).replace(os.path.sep, '_')) + self.fetchCover(song) + + def fetchCover(self, song): + """Fetch cover (from internet or local dir)""" + # set default cover + + if not os.path.exists(self.cover_filepath): + success = False + for i in [0, 1]: + src = self.p.settings.value(self.p.getName() + '/action%i'%i).toInt()[0] + if src != AC_NO_FETCH: + if self.fetchCoverSrc(song, src): + success = True + break + if not success: + self.imgLoaded = False + self.setPixmap(QtGui.QPixmap('gfx/no-cd-cover.png').scaled(self.size(), QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation)) + return + + try: + self.setPixmap(QtGui.QPixmap(self.cover_filepath).scaled(self.size(), QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation)) + self.imgLoaded = True + logging.info("cover set!") + except IOError: + logging.warning("Error loading album cover" + self.cover_filepath) + + self.update() + + def getLocalACPath(self, song): + """Get the local path of an albumcover.""" + covers = ['cover', 'album', 'front'] + + # fetch gfx extensions + exts = QtGui.QImageReader().supportedImageFormats() + exts = map(lambda ext: '*.' + unicode(ext), exts) + + # fetch cover album titles + filter = [] + for cover in covers: + for ext in exts: + filter.append(cover.strip() + ext) + + dir = QtCore.QDir(self.cover_dirname) + if not dir: + logging.warning('Error opening directory' + self.cover_dirname) + return None; + dir.setNameFilters(filter) + files = dir.entryList() + if files: + return unicode(dir.filePath(files[0])) + # if this failed, try any supported image + dir.setNameFilters(exts) + files = dir.entryList() + if files: + return unicode(dir.filePath(files[0])) + logging.info("done probing: no matching albumcover found") + return None + + def fetch_amazon(self): + self.fetchCoverSrc(self.p.mpclient.getCurrentSong(), AC_FETCH_INTERNET) + self.refresh() + + def fetchCoverSrc(self, song, src): + """Fetch the album cover for $song from $src.""" + if not src in [AC_FETCH_INTERNET, AC_FETCH_LOCAL_DIR]: + logging.warning("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() + logging.info("fetch from Amazon") + if not coverURL: + logging.info("not found on Amazon") + return False + # read the url, i.e. retrieve image data + img=urllib.urlopen(coverURL) + # open file, and write the read of img! + f=open(self.cover_filepath,'wb') + f.write(img.read()) + f.close() + return True + except: + logging.info("failed to download cover from Amazon") + print_exc() + return False + + if src == AC_FETCH_LOCAL_DIR: + file=self.getLocalACPath(song) + try: + shutil.copy(file, self.cover_filepath) + return True + except: + logging.info("Failed to create cover file") + return False + +class pluginAlbumCover(Plugin): + o = None + DEFAULTS = {'coverdir' : '$musicdir/$songdir', 'covername' : '.cover_mpclient_$artist_$album', + 'action0' : 1, 'action1' : 1} + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'AlbumCover') + + def _load(self): + self.o = wgAlbumCover(self, None) + self.mpclient.add_listener('onSongChange' , self.o.refresh) + self.mpclient.add_listener('onReady' , self.o.refresh) + self.mpclient.add_listener('onDisconnect' , self.o.refresh) + self.mpclient.add_listener('onStateChange', self.o.refresh) + 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 _getDockWidget(self): + return self._createDock(self.o) + + class SettingsWidgetAlbumCover(Plugin.SettingsWidget): + actions = [] + coverdir = None + covername = None + + def __init__(self, plugin): + Plugin.SettingsWidget.__init__(self, plugin) + self.settings.beginGroup(self.plugin.getName()) + + self.actions = [QtGui.QComboBox(), QtGui.QComboBox()] + for i,action in enumerate(self.actions): + action.addItem("No action.") + action.addItem("Local dir") + action.addItem("Amazon") + action.setCurrentIndex(self.settings.value('action' + str(i)).toInt()[0]) + + self.coverdir = QtGui.QLineEdit(self.settings.value('coverdir').toString()) + self.covername = QtGui.QLineEdit(self.settings.value('covername').toString()) + + self.setLayout(QtGui.QVBoxLayout()) + self._add_widget(self.actions[0], 'Action 0') + self._add_widget(self.actions[1], 'Action 1') + self._add_widget(self.coverdir, 'Cover directory', + 'Where should %s store covers.\n' + '$musicdir will be expanded to path to MPD music library\n' + '$songdir will be expanded to path to the song (relative to $musicdir' + %APPNAME) + self._add_widget(self.covername, 'Cover filename', 'Filename for %s cover files.'%APPNAME) + self.settings.endGroup() + + def save_settings(self): + self.settings.beginGroup(self.plugin.getName()) + self.settings.setValue('action0', QVariant(self.actions[0].currentIndex())) + self.settings.setValue('action1', QVariant(self.actions[1].currentIndex())) + self.settings.setValue('coverdir', QVariant(self.coverdir.text())) + self.settings.setValue('covername', QVariant(self.covername.text())) + self.settings.endGroup() + self.plugin.o.refresh() + + def get_settings_widget(self): + return self.SettingsWidgetAlbumCover(self) + + +# This is the amazon cover fetcher using their webservice api +# Thank you, http://www.semicomplete.com/scripts/albumcover.py +import re +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 + + 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) diff --git a/nephilim/plugins/Filebrowser.py b/nephilim/plugins/Filebrowser.py new file mode 100644 index 0000000..0a2908d --- /dev/null +++ b/nephilim/plugins/Filebrowser.py @@ -0,0 +1,47 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +import os + +from ..clPlugin import Plugin +from ..misc import ORGNAME, APPNAME + +class pluginFilebrowser(Plugin): + view = None + model = None + + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Filebrowser') + + def _load(self): + self.model = QtGui.QDirModel() + self.model.setFilter(QtCore.QDir.AllDirs|QtCore.QDir.AllEntries) + self.model.setSorting(QtCore.QDir.DirsFirst) + + self.view = QtGui.QListView() + self.view.setModel(self.model) + self.view.setRootIndex(self.model.index(os.path.expanduser('~'))) + self.view.setSelectionMode(QtGui.QTreeWidget.ExtendedSelection) + self.view.connect(self.view, QtCore.SIGNAL('activated(const QModelIndex&)'), self.item_activated) + + def _unload(self): + self.view = None + self.model = None + + def getInfo(self): + return 'A file browser that allows adding files not in collection.' + + def _getDockWidget(self): + return self._createDock(self.view) + + def item_activated(self, index): + if self.model.hasChildren(index): + self.view.setRootIndex(index) + else: + if not 'file://' in self.mpclient.urlhandlers(): + self.setStatus('file:// handler not available. Connect via unix domain sockets.') + return + paths = [] + for index in self.view.selectedIndexes(): + paths.append(u'file://' + self.model.filePath(index)) + self.mpclient.addToPlaylist(paths) + diff --git a/nephilim/plugins/Library.py b/nephilim/plugins/Library.py new file mode 100644 index 0000000..60490d9 --- /dev/null +++ b/nephilim/plugins/Library.py @@ -0,0 +1,155 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant + +from ..clPlugin import Plugin +from ..misc import ORGNAME, APPNAME + +class pluginLibrary(Plugin): + o=None + DEFAULTS = {'modes' : 'artist\n'\ + 'artist/album\n'\ + 'artist/date/album\n'\ + 'genre\n'\ + 'genre/artist\n'\ + 'genre/artist/album\n'} + + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Library') + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + def _load(self): + self.o = LibraryWidget(self) + self.mpclient.add_listener('onReady', self.o.fill_library) + self.mpclient.add_listener('onDisconnect', self.o.fill_library) + self.mpclient.add_listener('onUpdateDBFinish', self.o.fill_library) + def _unload(self): + self.o = None + + def getInfo(self): + return "List showing all the songs allowing filtering and grouping." + + def _getDockWidget(self): + return self._createDock(self.o) + + class SettingsWidgetLibrary(Plugin.SettingsWidget): + modes = None + def __init__(self, plugin): + Plugin.SettingsWidget.__init__(self, plugin) + self.setLayout(QtGui.QVBoxLayout()) + + self.modes = QtGui.QTextEdit() + self.modes.insertPlainText(self.settings.value(self.plugin.getName() + '/modes').toString()) + self.layout().addWidget(self.modes) + + def save_settings(self): + self.settings.setValue(self.plugin.getName() + '/modes', QVariant(self.modes.toPlainText())) + self.plugin.o.refresh_modes() + + def get_settings_widget(self): + return self.SettingsWidgetLibrary(self) + +class LibraryWidget(QtGui.QWidget): + library = None + search_txt = None + modes = None + settings = None + plugin = None + + def __init__(self, plugin): + QtGui.QWidget.__init__(self) + self.plugin = plugin + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + self.settings.beginGroup(self.plugin.getName()) + + self.modes = QtGui.QComboBox() + self.refresh_modes() + self.connect(self.modes, QtCore.SIGNAL('activated(int)'), self.modes_activated) + + self.search_txt = QtGui.QLineEdit() + self.connect(self.search_txt, QtCore.SIGNAL('textChanged(const QString&)'), + self.filter_changed) + self.connect(self.search_txt, QtCore.SIGNAL('returnPressed()'), self.add_filtered) + + #construct the library + self.library = QtGui.QTreeWidget() + self.library.setColumnCount(1) + self.library.setAlternatingRowColors(True) + self.library.setSelectionMode(QtGui.QTreeWidget.ExtendedSelection) + self.library.headerItem().setHidden(True) + self.fill_library() + self.connect(self.library, QtCore.SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.add_selection) + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().setSpacing(2) + self.layout().setMargin(0) + self.layout().addWidget(self.modes) + self.layout().addWidget(self.search_txt) + self.layout().addWidget(self.library) + + def refresh_modes(self): + self.modes.clear() + for mode in self.settings.value('/modes').toString().split('\n'): + self.modes.addItem(mode) + self.modes.setCurrentIndex(self.settings.value('current_mode').toInt()[0]) + + + def fill_library(self, params = None): + self.library.clear() + + #build a tree from library + tree = [{},self.library.invisibleRootItem()] + for song in self.plugin.mpclient.listLibrary(): + cur_item = tree + for part in str(self.modes.currentText()).split('/'): + tag = song.getTag(part) + if isinstance(tag, list): + tag = tag[0] #FIXME hack to make songs with multiple genres work. + if not tag: + tag = 'Unknown' + if tag in cur_item[0]: + cur_item = cur_item[0][tag] + else: + it = QtGui.QTreeWidgetItem([tag]) + cur_item[1].addChild(it) + cur_item[0][tag] = [{}, it] + cur_item = cur_item[0][tag] + it = QtGui.QTreeWidgetItem(['%02d %s'%(song.getTrack() if song.getTrack() else 0, + song.getTitle() if song.getTitle() else song.getFilepath())], 1000) + it.setData(0, QtCore.Qt.UserRole, QVariant(song)) + cur_item[1].addChild(it) + + self.library.sortItems(0, QtCore.Qt.AscendingOrder) + + def filter_changed(self, text): + items = self.library.findItems(text, QtCore.Qt.MatchContains|QtCore.Qt.MatchRecursive) + for i in range(self.library.topLevelItemCount()): + self.library.topLevelItem(i).setHidden(True) + for item in items: + while item.parent(): + item = item.parent() + item.setHidden(False) + self.filtered_items = items + + def add_filtered(self): + self.library.clearSelection() + for item in self.filtered_items: + item.setSelected(True) + self.add_selection() + self.library.clearSelection() + self.search_txt.clear() + + def add_selection(self): + paths = [] + for item in self.library.selectedItems(): + self.item_to_playlist(item, paths) + self.plugin.mpclient.addToPlaylist(paths) + + def item_to_playlist(self, item, add_queue): + if item.type() == 1000: + add_queue.append(item.data(0, QtCore.Qt.UserRole).toPyObject().getFilepath()) + else: + for i in range(item.childCount()): + self.item_to_playlist(item.child(i), add_queue) + + def modes_activated(self): + self.settings.setValue('current_mode', QVariant(self.modes.currentIndex())) + self.fill_library() diff --git a/nephilim/plugins/Lyrics.py b/nephilim/plugins/Lyrics.py new file mode 100644 index 0000000..1983f2a --- /dev/null +++ b/nephilim/plugins/Lyrics.py @@ -0,0 +1,74 @@ +from PyQt4 import QtGui,QtCore +from PyQt4.QtCore import QVariant + +from thread import start_new_thread +import logging + +from ..clPlugin import Plugin +from .. import LyricWiki_client + +class wgLyrics(QtGui.QWidget): + " contains the lyrics" + txtView = None # text-object + p = None # plugin + def __init__(self, p, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p = p + self.curLyrics = '' + + self.txtView = QtGui.QTextEdit(self) + self.txtView.setReadOnly(True) + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().setSpacing(0) + self.layout().setMargin(0) + self.layout().addWidget(self.txtView) + + def set_lyrics(self, song, lyrics): + self.txtView.clear() + if song: + self.txtView.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\ + '<br />\n\n'%(song.getTitle(), song.getArtist())) + if lyrics: + self.txtView.insertPlainText(lyrics) + +class pluginLyrics(Plugin): + o = None + DEFAULTS = {'sites' : ['lyricwiki'], 'lyricdir' : '$musicdir/$songdir', + 'lyricname' : '.lyric_mpclient_$artist_$album_$song'} + + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Lyrics') + self.addListener('onSongChange', self.refresh) + self.addListener('onReady', self.refresh) + def _load(self): + self.o = wgLyrics(self) + 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 = None): + lyrics = None + song = self.mpclient.getCurrentSong() + if not song: + self.o.set_lyrics(None, None) + return + for site in self.settings.value(self.getName() + '/sites').toStringList(): + lyrics = eval('self.fetch_%s(song)'%site) + if lyrics: + self.o.set_lyrics(song, lyrics) + return + + def fetch_lyricwiki(self, song): + soap = LyricWiki_client.LyricWikiBindingSOAP("http://lyricwiki.org/server.php") + req = LyricWiki_client.getSongRequest() + req.Artist = song.getArtist() + req.Song = song.getTitle() + result = soap.getSong(req) + + return result.Return.Lyrics + diff --git a/nephilim/plugins/Notify.py b/nephilim/plugins/Notify.py new file mode 100644 index 0000000..b3c2285 --- /dev/null +++ b/nephilim/plugins/Notify.py @@ -0,0 +1,152 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +from traceback import print_exc + +from ..misc import sec2min, ORGNAME, APPNAME +from ..clPlugin import Plugin +from .. import plugins + +NOTIFY_PRIORITY_SONG = 1 +NOTIFY_PRIORITY_VOLUME = 2 + +class winNotify(QtGui.QWidget): + _timerID=None + winMain=None + p=None + + _current_priority = 0 + + timer=None + + cover_label = None + text_label = None + + def __init__(self, p, winMain, parent=None): + QtGui.QWidget.__init__(self, parent) + self.p=p + self.winMain=winMain + + layout = QtGui.QHBoxLayout() + self.cover_label = QtGui.QLabel() + self.text_label = QtGui.QLabel() + self.text_label.setWordWrap(True) + layout.addWidget(self.cover_label) + layout.addWidget(self.text_label) + self.setLayout(layout) + + self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint) + self.setWindowOpacity(0.7) + + font = QtGui.QFont() + font.setPixelSize(20) + self.setFont(font) + + def mousePressEvent(self, event): + self.hide() + + def show(self, text, time = 3, priority = 0): + if not priority >= self._current_priority: + return + self._current_priority = priority + + cover = plugins.getPlugin('albumcover').getWidget().get_cover() + if cover: + self.cover_label.setPixmap(cover.scaledToHeight(self.fontInfo().pixelSize()*4)) + else: + self.cover_label.clear() + + self.text_label.setText(text) + if self._timerID: + self.killTimer(self._timerID) + self._timerID=self.startTimer(500) + self.timer = time*2 + self.resize(self.layout().sizeHint()) + self.centerH() + self.setVisible(True) + self.timerEvent(None) + + def hide(self): + if self._timerID: + self.killTimer(self._timerID) + self._timerID=None + self.setHidden(True) + self._current_priority = -1 + + def centerH(self): + screen = QtGui.QDesktopWidget().screenGeometry() + size = self.geometry() + self.move((screen.width()-size.width())/2, 100) + + def timerEvent(self, event): + self.timer-=1 + if self.timer<=0: + self.hide() + self.update() + +class pluginNotify(Plugin): + o=None + DEFAULTS = {'songformat' : '$track - $artist - $title ($album) [$length]', + 'timer' : 3} + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Notify') + self.addListener('onSongChange', self.onSongChange) + self.addListener('onReady', self.onReady) + self.addListener('onDisconnect', self.onDisconnect) + self.addListener('onStateChange', self.onStateChange) + self.addListener('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): + song = self.mpclient.getCurrentSong() + if not song: + return + self.settings.beginGroup(self.name) + self.o.show(song.expand_tags(self.settings.value('songformat').toString()), self.settings.value('timer').toInt()[0], + NOTIFY_PRIORITY_SONG) + self.settings.endGroup() + + def onReady(self, params): + self.o.show('mpclientpc loaded!', self.settings.value(self.name + '/timer').toInt()[0]) + + def onDisconnect(self, params): + self.o.show('Disconnected!', self.settings.value(self.name + '/timer').toInt()[0]) + + def onStateChange(self, params): + self.o.show(params['newState'], self.settings.value(self.name + '/timer').toInt()[0]) + + def onVolumeChange(self, params): + self.o.show('Volume: %i%%'%(params['newVolume']), self.settings.value(self.name + '/timer').toInt()[0], priority = NOTIFY_PRIORITY_VOLUME) + + class SettingsWidgetNotify(Plugin.SettingsWidget): + format = None + timer = None + + def __init__(self, plugin): + Plugin.SettingsWidget.__init__(self, plugin) + self.settings.beginGroup(self.plugin.getName()) + + self.format = QtGui.QLineEdit(self.settings.value('songformat').toString()) + + self.timer = QtGui.QLineEdit(self.settings.value('timer').toString()) + self.timer.setValidator(QtGui.QIntValidator(self.timer)) + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().addWidget(self.format) + self.layout().addWidget(self.timer) + self.settings.endGroup() + + def save_settings(self): + self.settings.beginGroup(self.plugin.getName()) + self.settings.setValue('songformat', QVariant(self.format.text())) + self.settings.setValue('timer', QVariant(self.timer.text().toInt()[0])) + self.settings.endGroup() + self.plugin.onSongChange(None) + + def get_settings_widget(self): + return self.SettingsWidgetNotify(self) diff --git a/nephilim/plugins/PlayControl.py b/nephilim/plugins/PlayControl.py new file mode 100644 index 0000000..c5690be --- /dev/null +++ b/nephilim/plugins/PlayControl.py @@ -0,0 +1,169 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +import logging + +from ..misc import ORGNAME, APPNAME, Button +from ..clPlugin import Plugin + +class wgPlayControl(QtGui.QToolBar): + """Displays controls for interacting with playing, like play, volume ...""" + " control buttons" + btnPlayPause = None + btnStop = None + btnPrevious = None + btnNext = None + slrVolume=None + repeat = None + random = None + p = None + + " queued songs: int*" + queuedSongs=[] + # what mode where we in before the queue started? + beforeQueuedMode=None + + class VolumeSlider(QtGui.QSlider): + + def __init__(self, parent): + QtGui.QSlider.__init__(self, parent) + self.setOrientation(parent.orientation()) + self.setMaximum(100) + self.setToolTip('Volume control') + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.eraseRect(self.rect()) + + grad = QtGui.QLinearGradient(0, 0, self.width(), self.height()) + grad.setColorAt(0, self.palette().color(QtGui.QPalette.Window)) + grad.setColorAt(1, self.palette().color(QtGui.QPalette.Highlight)) + if self.orientation() == QtCore.Qt.Horizontal: + rect = QtCore.QRect(0, 0, self.width() * self.value() / self.maximum(), self.height()) + else: + rect = QtCore.QRect(0, self.height() * (1 - float(self.value()) / self.maximum()), self.width(), self.height()) + painter.fillRect(rect, QtGui.QBrush(grad)) + + def __init__(self, p, parent = None): + QtGui.QToolBar.__init__(self, parent) + self.setMovable(True) + self.p = p + + self.slrVolume = self.VolumeSlider(self) + self.connect(self.slrVolume, QtCore.SIGNAL('valueChanged(int)'),self.onVolumeSliderChange) + + 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.random = QtGui.QPushButton(QtGui.QIcon('gfx/random.png'), '', self) + self.random.setToolTip('Random') + self.random.setCheckable(True) + self.connect(self.random, QtCore.SIGNAL('toggled(bool)'), self.p.mpclient.random) + + self.repeat = QtGui.QPushButton(QtGui.QIcon('gfx/repeat.png'), '', self) + self.repeat.setToolTip('Repeat') + self.repeat.setCheckable(True) + self.connect(self.repeat, QtCore.SIGNAL('toggled(bool)'), self.p.mpclient.repeat) + + self.addWidget(self.btnPlayPause) + self.addWidget(self.btnStop) + self.addWidget(self.btnPrevious) + self.addWidget(self.btnNext) + self.addSeparator() + self.addWidget(self.slrVolume) + self.addSeparator() + self.addWidget(self.random) + self.addWidget(self.repeat) + + self.connect(self, QtCore.SIGNAL('orientationChanged(Qt::Orientation)'), self.slrVolume.setOrientation) + + # queue gets loaded in _load of pluginPlayControl + self.queuedSongs=[] + + def addSongsToQueue(self, songs): + self.queuedSongs.extend(songs) + + def onStateChange(self, params): + status = self.p.mpclient.getStatus() + + if status['state'] == 'play': + self.btnPlayPause.changeIcon('gfx/media-playback-pause.svg') + self.btnPlayPause.setToolTip('pauze') + elif status['state'] == 'pause' or status['state'] == 'stop': + self.btnPlayPause.changeIcon('gfx/media-playback-start.svg') + self.btnPlayPause.setToolTip('play') + + def onVolumeChange(self, params): + self.slrVolume.setValue(params['newVolume']) + + def onBtnPlayPauseClick(self): + status=self.p.mpclient.getStatus() + if status['state']=='play': + self.p.mpclient.pause() + logging.info("Toggling playback") + elif status['state']=='stop': + self.p.mpclient.play(None) + logging.info("Pausing playback") + else: + self.p.mpclient.resume() + def onBtnStopClick(self): + self.p.mpclient.stop() + logging.info("Stopping playback") + def onBtnPreviousClick(self): + self.p.mpclient.previous() + logging.info("Playing previous") + def onBtnNextClick(self): + self.p.mpclient.next() + logging.info("Playing next") + def onVolumeSliderChange(self): + v=self.slrVolume.value() + self.p.mpclient.setVolume(v) + if v<=1: + mode='mute' + else: + mode=('0', 'min', 'med', 'max')[int(3*v/100)] + + # save and load the queue + def saveQueue(self): + # save the ids as a list of space-separated numbers + logging.info("saving queue") + self.p.settings.setValue(self.p.getName() + '/queue', QVariant(str(self.queuedSongs)[1:-1].replace(',', ''))) + def loadQueue(self): + # just read all the numbers! + logging.info("loading queue") + self.queuedSongs=[] + i=0 + ids=self.p.settings.value(self.p.getName() + '/queue').toString().split(' ') + for id in ids: + try: + self.queuedSongs.append(int(id)) + except: + pass + +class pluginPlayControl(Plugin): + o=None + DEFAULTS = {'queue' : ''} + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'PlayControl') + self.addListener('onStateChange', self.onStateChange) + self.addListener('onVolumeChange', self.onVolumeChange) + self.addListener('onReady', self.onStateChange) + def _load(self): + self.o = wgPlayControl(self, None) + self.o.loadQueue() + self.winMain.addToolBar(QtCore.Qt.TopToolBarArea, self.o) + def _unload(self): + self.o.saveQueue() + self.winMain.removeToolBar(self.o) + 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 onVolumeChange(self, params): + self.o.onVolumeChange(params) diff --git a/nephilim/plugins/Playlist.py b/nephilim/plugins/Playlist.py new file mode 100644 index 0000000..2258701 --- /dev/null +++ b/nephilim/plugins/Playlist.py @@ -0,0 +1,92 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant + +from ..clPlugin import Plugin + +# Dependencies: +# playcontrol +class pluginPlaylist(Plugin): + o = None + DEFAULTS = {'columns': ['track', 'title', 'artist', + 'date', 'album', 'length']} + + def __init__(self, winMain): + Plugin.__init__(self, winMain, 'Playlist') + def _load(self): + self.o = PlaylistWidget(self) + self.mpclient.add_listener('onPlaylistChange', self.on_playlist_change) + self.mpclient.add_listener('onDisconnect', self.on_playlist_change) + self.mpclient.add_listener('onReady', self.on_playlist_change) + + def _unload(self): + self.o = None + def getInfo(self): + return "The playlist showing the songs that will be played." + + def _getDockWidget(self): + return self._createDock(self.o) + + def on_playlist_change(self, params = None): + self.o.fill_playlist() + +class PlaylistWidget(QtGui.QWidget): + plugin = None + playlist = None + + def __init__(self, plugin): + QtGui.QWidget.__init__(self) + self.plugin = plugin + + self.playlist = self.Playlist(self.plugin) + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().setSpacing(0) + self.layout().setMargin(0) + self.layout().addWidget(self.playlist) + + class Playlist(QtGui.QTreeWidget): + song = None + plugin = None + + def __init__(self, plugin): + QtGui.QTreeWidget.__init__(self) + self.plugin = plugin + + self.setSelectionMode(QtGui.QTreeWidget.ExtendedSelection) + self.setAlternatingRowColors(True) + self.setRootIsDecorated(False) + columns = self.plugin.settings.value(self.plugin.getName() + '/columns').toStringList() + self.setColumnCount(len(columns)) + self.setHeaderLabels(columns) + self.header().restoreState(self.plugin.settings.value(self.plugin.getName() + '/header_state').toByteArray()) + self.connect(self, QtCore.SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self._song_activated) + self.connect(self.header(), QtCore.SIGNAL('geometriesChanged()'), self._save_state) + + def _save_state(self): + self.plugin.settings.setValue(self.plugin.getName() + '/header_state', QVariant(self.header().saveState())) + + def _song_activated(self, item): + self.plugin.mpclient.play(item.data(0, QtCore.Qt.UserRole).toPyObject().getID()) + + def fill(self): + columns = self.plugin.settings.value(self.plugin.getName() + '/columns').toStringList() + self.clear() + for song in self.plugin.mpclient.listPlaylist(): + item = QtGui.QTreeWidgetItem() + for i in range(len(columns)): + item.setText(i, unicode(song.getTag(str(columns[i])))) + item.setData(0, QtCore.Qt.UserRole, QVariant(song)) + self.addTopLevelItem(item) + + def keyPressEvent(self, event): + if event.matches(QtGui.QKeySequence.Delete): + ids = [] + for item in self.selectedItems(): + ids.append(item.data(0, QtCore.Qt.UserRole).toPyObject().getID()) + + self.plugin.mpclient.deleteFromPlaylist(ids) + else: + QtGui.QTreeWidget.keyPressEvent(self, event) + + def fill_playlist(self): + self.playlist.fill() diff --git a/nephilim/plugins/Systray.py b/nephilim/plugins/Systray.py new file mode 100644 index 0000000..49a1414 --- /dev/null +++ b/nephilim/plugins/Systray.py @@ -0,0 +1,125 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant + +from ..clPlugin import Plugin +from ..misc import sec2min, ORGNAME, APPNAME, appIcon + +class pluginSystray(Plugin): + DEFAULTS = {'format': '$track - $title by $artist on $album ($length)'} + 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.addListener('onSongChange', self.update) + self.addListener('onReady', self.update) + self.addListener('onConnect', self.update) + self.addListener('onDisconnect', self.update) + self.addListener('onTimeChange', self.update) # TODO only update this when necessary, i.e. mouse-hover etc + self.appIcon=QtGui.QIcon(appIcon) + + def _load(self): + self.format = self.settings.value(self.name + '/format').toString() + 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 + self.plugin.mpclient.setVolume(self.plugin.mpclient.getVolume() + numSteps) + event.accept() + return True + return False + + self.o=QtGui.QSystemTrayIcon(QtGui.QIcon(appIcon), self.winMain) + self.eventObj=SystrayWheelEventObject() + self.eventObj.plugin = self + self.o.installEventFilter(self.eventObj) + self.winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)') + , self.onSysTrayClick) + self.o.show() + + 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 mpclientpc icon in the systray." + + def update(self, params): + status = self.mpclient.getStatus() + if not status: + return + song = self.mpclient.getCurrentSong() + + values={'state':''} + values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']] + if 'time' in status: + values['length']=sec2min(status['length']) + values['time']=sec2min(status['time']) + + if song: + self.o.setToolTip(song.expand_tags(self.format)) + else: + self.o.setToolTip("mpclientpc 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, self.winMain.palette().brush(QtGui.QPalette.Base)) + self.appIcon.paint(painter, 1, 0, 63, 64) + self.o.setIcon(QtGui.QIcon(self.pixmap)) + elif not song: + self.time=None + self.o.setIcon(QtGui.QIcon(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 self.mpclient.isPlaying(): + self.mpclient.pause() + else: + self.mpclient.resume() + + class SettingsWidgetSystray(Plugin.SettingsWidget): + format = None + + def __init__(self, plugin): + Plugin.SettingsWidget.__init__(self, plugin) + + self.format = QtGui.QLineEdit(self.settings.value(self.plugin.getName() + '/format').toString()) + + self.setLayout(QtGui.QVBoxLayout()) + self._add_widget(self.format, 'Tooltip format') + + def save_settings(self): + self.settings.beginGroup(self.plugin.getName()) + self.settings.setValue('format', QVariant(self.format.text())) + self.settings.endGroup() + + def get_settings_widget(self): + return self.SettingsWidgetSystray(self) diff --git a/nephilim/plugins/__init__.py b/nephilim/plugins/__init__.py new file mode 100644 index 0000000..affbf45 --- /dev/null +++ b/nephilim/plugins/__init__.py @@ -0,0 +1,97 @@ +import os +import sys +import logging + +# { className => [module, className, instance, msg] } +_plugins=None +PLUGIN_MODULE=0 +PLUGIN_CLASS=1 +PLUGIN_INSTANCE=2 +PLUGIN_MSG=3 + + +class IPlaylist: + def ensureVisible(self, song_id): + raise Exception("TODO implement") + +def loadPlugins(): + """(Re)load all modules in the plugins directory.""" + global _plugins + _plugins={} + for file in os.listdir('nephilim/plugins'): + if file[-3:]=='.py' and file!='__init__.py': + name=file[:-3] # name without ext + mod='nephilim.plugins.%s'%(name) # mod name + className='plugin%s'%(name) # classname + + _plugins[className.lower()]=[mod, className, None, None] + loadPlugin(className, None) + +def setPluginMessage(name, msg): + global _plugins + try: + _plugins[name.lower()][PLUGIN_MSG]=msg + except: + try: + _plugins["plugin%s"%(name.lower())][PLUGIN_MODULE]=msg + except: + pass + +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 + +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 + logging.warning("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 listImplementors(interface, loaded = True): + """Return a list of plugin-instances that implement an interface""" + global _plugins + return map(lambda plugin: plugin[PLUGIN_INSTANCE] + , filter(lambda plugin: isinstance(plugin[PLUGIN_INSTANCE], interface) + and ((loaded != None and plugin[PLUGIN_INSTANCE].loaded == loaded) or (loaded == None)), _plugins.values())) + +def listPlugins(): + """Get the list of plugins available as { className => [mod, className, instance, msg] }.""" + global _plugins + return _plugins + + +loadPlugins() |