summaryrefslogtreecommitdiff
path: root/nephilim/plugins
diff options
context:
space:
mode:
authorAnton Khirnov <wyskas@gmail.com>2009-02-20 10:49:00 +0100
committerAnton Khirnov <wyskas@gmail.com>2009-02-20 10:49:00 +0100
commitae872ea3018fe1dac39f867f386b081598fb0812 (patch)
tree856d2b6d32050174682305b421548e680fe5647a /nephilim/plugins
parent000f9d5ba84426da6b5211dd0bea4a401b8f4289 (diff)
Move modules to a separate dir.
Diffstat (limited to 'nephilim/plugins')
-rw-r--r--nephilim/plugins/AlbumCover.py304
-rw-r--r--nephilim/plugins/Filebrowser.py47
-rw-r--r--nephilim/plugins/Library.py155
-rw-r--r--nephilim/plugins/Lyrics.py74
-rw-r--r--nephilim/plugins/Notify.py152
-rw-r--r--nephilim/plugins/PlayControl.py169
-rw-r--r--nephilim/plugins/Playlist.py92
-rw-r--r--nephilim/plugins/Systray.py125
-rw-r--r--nephilim/plugins/__init__.py97
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()