From ae872ea3018fe1dac39f867f386b081598fb0812 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Fri, 20 Feb 2009 10:49:00 +0100 Subject: Move modules to a separate dir. --- nephilim/LyricWiki_client.py | 578 ++++++++++++++++++++++++++++++++++++++++ nephilim/LyricWiki_types.py | 162 +++++++++++ nephilim/clPlugin.py | 136 ++++++++++ nephilim/clSong.py | 85 ++++++ nephilim/default_layout | Bin 0 -> 302 bytes nephilim/misc.py | 85 ++++++ nephilim/mpclient.py | 364 +++++++++++++++++++++++++ nephilim/mpd.py | 360 +++++++++++++++++++++++++ nephilim/plugins/AlbumCover.py | 304 +++++++++++++++++++++ nephilim/plugins/Filebrowser.py | 47 ++++ nephilim/plugins/Library.py | 155 +++++++++++ nephilim/plugins/Lyrics.py | 74 +++++ nephilim/plugins/Notify.py | 152 +++++++++++ nephilim/plugins/PlayControl.py | 169 ++++++++++++ nephilim/plugins/Playlist.py | 92 +++++++ nephilim/plugins/Systray.py | 125 +++++++++ nephilim/plugins/__init__.py | 97 +++++++ nephilim/winConnect.py | 88 ++++++ nephilim/winMain.py | 285 ++++++++++++++++++++ nephilim/winSettings.py | 176 ++++++++++++ 20 files changed, 3534 insertions(+) create mode 100644 nephilim/LyricWiki_client.py create mode 100644 nephilim/LyricWiki_types.py create mode 100644 nephilim/clPlugin.py create mode 100644 nephilim/clSong.py create mode 100644 nephilim/default_layout create mode 100644 nephilim/misc.py create mode 100644 nephilim/mpclient.py create mode 100644 nephilim/mpd.py create mode 100644 nephilim/plugins/AlbumCover.py create mode 100644 nephilim/plugins/Filebrowser.py create mode 100644 nephilim/plugins/Library.py create mode 100644 nephilim/plugins/Lyrics.py create mode 100644 nephilim/plugins/Notify.py create mode 100644 nephilim/plugins/PlayControl.py create mode 100644 nephilim/plugins/Playlist.py create mode 100644 nephilim/plugins/Systray.py create mode 100644 nephilim/plugins/__init__.py create mode 100644 nephilim/winConnect.py create mode 100644 nephilim/winMain.py create mode 100644 nephilim/winSettings.py (limited to 'nephilim') diff --git a/nephilim/LyricWiki_client.py b/nephilim/LyricWiki_client.py new file mode 100644 index 0000000..76c24bb --- /dev/null +++ b/nephilim/LyricWiki_client.py @@ -0,0 +1,578 @@ +################################################## +# file: LyricWiki_client.py +# +# client stubs generated by "ZSI.generate.wsdl2python.WriteServiceModule" +# /usr/bin/wsdl2py --complexType http://lyricwiki.org/server.php?wsdl +# +################################################## + +from LyricWiki_types import * +import urlparse, types +from ZSI.TCcompound import ComplexType, Struct +from ZSI import client +from ZSI.schema import GED, GTD +import ZSI +from ZSI.generate.pyclass import pyclass_type + +# Locator +class LyricWikiLocator: + LyricWikiPort_address = "http://lyricwiki.org/server.php" + def getLyricWikiPortAddress(self): + return LyricWikiLocator.LyricWikiPort_address + def getLyricWikiPort(self, url=None, **kw): + return LyricWikiBindingSOAP(url or LyricWikiLocator.LyricWikiPort_address, **kw) + +# Methods +class LyricWikiBindingSOAP: + def __init__(self, url, **kw): + kw.setdefault("readerclass", None) + kw.setdefault("writerclass", None) + # no resource properties + self.binding = client.Binding(url=url, **kw) + # no ws-addressing + + # op: checkSongExists + def checkSongExists(self, request, **kw): + if isinstance(request, checkSongExistsRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#checkSongExists", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=checkSongExistsResponse.typecode.ofwhat, pyclass=checkSongExistsResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: searchArtists + def searchArtists(self, request, **kw): + if isinstance(request, searchArtistsRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#searchArtists", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=searchArtistsResponse.typecode.ofwhat, pyclass=searchArtistsResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: searchAlbums + def searchAlbums(self, request, **kw): + if isinstance(request, searchAlbumsRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#searchAlbums", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=searchAlbumsResponse.typecode.ofwhat, pyclass=searchAlbumsResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: searchSongs + def searchSongs(self, request, **kw): + if isinstance(request, searchSongsRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#searchSongs", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=searchSongsResponse.typecode.ofwhat, pyclass=searchSongsResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: getSOTD + def getSOTD(self, request, **kw): + if isinstance(request, getSOTDRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#getSOTD", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=getSOTDResponse.typecode.ofwhat, pyclass=getSOTDResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: getSong + def getSong(self, request, **kw): + if isinstance(request, getSongRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#getSong", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=getSongResponse.typecode.ofwhat, pyclass=getSongResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: getSongResult + def getSongResult(self, request, **kw): + if isinstance(request, getSongResultRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#getSongResult", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=getSongResultResponse.typecode.ofwhat, pyclass=getSongResultResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: getArtist + def getArtist(self, request, **kw): + if isinstance(request, getArtistRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#getArtist", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=getArtistResponse.typecode.ofwhat, pyclass=getArtistResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: getAlbum + def getAlbum(self, request, **kw): + if isinstance(request, getAlbumRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#getAlbum", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=getAlbumResponse.typecode.ofwhat, pyclass=getAlbumResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: getHometown + def getHometown(self, request, **kw): + if isinstance(request, getHometownRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#getHometown", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=getHometownResponse.typecode.ofwhat, pyclass=getHometownResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: postArtist + def postArtist(self, request, **kw): + if isinstance(request, postArtistRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#postArtist", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=postArtistResponse.typecode.ofwhat, pyclass=postArtistResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: postAlbum + def postAlbum(self, request, **kw): + if isinstance(request, postAlbumRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#postAlbum", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=postAlbumResponse.typecode.ofwhat, pyclass=postAlbumResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: postSong + def postSong(self, request, **kw): + if isinstance(request, postSongRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#postSong", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=postSongResponse.typecode.ofwhat, pyclass=postSongResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + + # op: postSong_flags + def postSong_flags(self, request, **kw): + if isinstance(request, postSong_flagsRequest) is False: + raise TypeError, "%s incorrect request type" % (request.__class__) + # no input wsaction + self.binding.Send(None, None, request, soapaction="urn:LyricWiki#postSong_flags", encodingStyle="http://schemas.xmlsoap.org/soap/encoding/", **kw) + # no output wsaction + typecode = Struct(pname=None, ofwhat=postSong_flagsResponse.typecode.ofwhat, pyclass=postSong_flagsResponse.typecode.pyclass) + response = self.binding.Receive(typecode) + return response + +_checkSongExistsRequestTypecode = Struct(pname=("urn:LyricWiki","checkSongExists"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class checkSongExistsRequest: + typecode = _checkSongExistsRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + song -- part song + """ + self._artist = kw.get("artist") + self._song = kw.get("song") +checkSongExistsRequest.typecode.pyclass = checkSongExistsRequest + +_checkSongExistsResponseTypecode = Struct(pname=("urn:LyricWiki","checkSongExistsResponse"), ofwhat=[ZSI.TC.Boolean(pname="return", aname="_return", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class checkSongExistsResponse: + typecode = _checkSongExistsResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + return -- part return + """ + self._return = kw.get("return") +checkSongExistsResponse.typecode.pyclass = checkSongExistsResponse + +_searchArtistsRequestTypecode = Struct(pname=("urn:LyricWiki","searchArtists"), ofwhat=[ZSI.TC.String(pname="searchString", aname="_searchString", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class searchArtistsRequest: + typecode = _searchArtistsRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + searchString -- part searchString + """ + self._searchString = kw.get("searchString") +searchArtistsRequest.typecode.pyclass = searchArtistsRequest + +_searchArtistsResponseTypecode = Struct(pname=("urn:LyricWiki","searchArtistsResponse"), ofwhat=[ns0.ArrayOfstring_Def(pname="return", aname="_return", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class searchArtistsResponse: + typecode = _searchArtistsResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + return -- part return + """ + self._return = kw.get("return") +searchArtistsResponse.typecode.pyclass = searchArtistsResponse + +_searchAlbumsRequestTypecode = Struct(pname=("urn:LyricWiki","searchAlbums"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="album", aname="_album", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TCnumbers.Iint(pname="year", aname="_year", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class searchAlbumsRequest: + typecode = _searchAlbumsRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + album -- part album + year -- part year + """ + self._artist = kw.get("artist") + self._album = kw.get("album") + self._year = kw.get("year") +searchAlbumsRequest.typecode.pyclass = searchAlbumsRequest + +_searchAlbumsResponseTypecode = Struct(pname=("urn:LyricWiki","searchAlbumsResponse"), ofwhat=[ns0.AlbumResultArray_Def(pname="return", aname="_return", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class searchAlbumsResponse: + typecode = _searchAlbumsResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + return -- part return + """ + self._return = kw.get("return") +searchAlbumsResponse.typecode.pyclass = searchAlbumsResponse + +_searchSongsRequestTypecode = Struct(pname=("urn:LyricWiki","searchSongs"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class searchSongsRequest: + typecode = _searchSongsRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + song -- part song + """ + self._artist = kw.get("artist") + self._song = kw.get("song") +searchSongsRequest.typecode.pyclass = searchSongsRequest + +_searchSongsResponseTypecode = Struct(pname=("urn:LyricWiki","searchSongsResponse"), ofwhat=[ns0.SongResult_Def(pname="return", aname="_return", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class searchSongsResponse: + typecode = _searchSongsResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + return -- part return + """ + self._return = kw.get("return") +searchSongsResponse.typecode.pyclass = searchSongsResponse + +_getSOTDRequestTypecode = Struct(pname=("urn:LyricWiki","getSOTD"), ofwhat=[], pyclass=None, encoded="urn:LyricWiki") +class getSOTDRequest: + typecode = _getSOTDRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + """ +getSOTDRequest.typecode.pyclass = getSOTDRequest + +_getSOTDResponseTypecode = Struct(pname=("urn:LyricWiki","getSOTDResponse"), ofwhat=[ns0.SOTDResult_Def(pname="return", aname="_return", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getSOTDResponse: + typecode = _getSOTDResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + return -- part return + """ + self._return = kw.get("return") +getSOTDResponse.typecode.pyclass = getSOTDResponse + +_getSongRequestTypecode = Struct(pname=("urn:LyricWiki","getSong"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getSongRequest: + typecode = _getSongRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + song -- part song + """ + self._artist = kw.get("artist") + self._song = kw.get("song") +getSongRequest.typecode.pyclass = getSongRequest + +_getSongResponseTypecode = Struct(pname=("urn:LyricWiki","getSongResponse"), ofwhat=[ns0.LyricsResult_Def(pname="return", aname="_return", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getSongResponse: + typecode = _getSongResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + return -- part return + """ + self._return = kw.get("return") +getSongResponse.typecode.pyclass = getSongResponse + +_getSongResultRequestTypecode = Struct(pname=("urn:LyricWiki","getSongResult"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getSongResultRequest: + typecode = _getSongResultRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + song -- part song + """ + self._artist = kw.get("artist") + self._song = kw.get("song") +getSongResultRequest.typecode.pyclass = getSongResultRequest + +_getSongResultResponseTypecode = Struct(pname=("urn:LyricWiki","getSongResultResponse"), ofwhat=[ns0.LyricsResult_Def(pname="songResult", aname="_songResult", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getSongResultResponse: + typecode = _getSongResultResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + songResult -- part songResult + """ + self._songResult = kw.get("songResult") +getSongResultResponse.typecode.pyclass = getSongResultResponse + +_getArtistRequestTypecode = Struct(pname=("urn:LyricWiki","getArtist"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getArtistRequest: + typecode = _getArtistRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + """ + self._artist = kw.get("artist") +getArtistRequest.typecode.pyclass = getArtistRequest + +_getArtistResponseTypecode = Struct(pname=("urn:LyricWiki","getArtistResponse"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ns0.AlbumDataArray_Def(pname="albums", aname="_albums", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getArtistResponse: + typecode = _getArtistResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + albums -- part albums + """ + self._artist = kw.get("artist") + self._albums = kw.get("albums") +getArtistResponse.typecode.pyclass = getArtistResponse + +_getAlbumRequestTypecode = Struct(pname=("urn:LyricWiki","getAlbum"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="album", aname="_album", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TCnumbers.Iint(pname="year", aname="_year", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getAlbumRequest: + typecode = _getAlbumRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + album -- part album + year -- part year + """ + self._artist = kw.get("artist") + self._album = kw.get("album") + self._year = kw.get("year") +getAlbumRequest.typecode.pyclass = getAlbumRequest + +_getAlbumResponseTypecode = Struct(pname=("urn:LyricWiki","getAlbumResponse"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="album", aname="_album", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TCnumbers.Iint(pname="year", aname="_year", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="amazonLink", aname="_amazonLink", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ns0.ArrayOfstring_Def(pname="songs", aname="_songs", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getAlbumResponse: + typecode = _getAlbumResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + album -- part album + year -- part year + amazonLink -- part amazonLink + songs -- part songs + """ + self._artist = kw.get("artist") + self._album = kw.get("album") + self._year = kw.get("year") + self._amazonLink = kw.get("amazonLink") + self._songs = kw.get("songs") +getAlbumResponse.typecode.pyclass = getAlbumResponse + +_getHometownRequestTypecode = Struct(pname=("urn:LyricWiki","getHometown"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getHometownRequest: + typecode = _getHometownRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + """ + self._artist = kw.get("artist") +getHometownRequest.typecode.pyclass = getHometownRequest + +_getHometownResponseTypecode = Struct(pname=("urn:LyricWiki","getHometownResponse"), ofwhat=[ZSI.TC.String(pname="country", aname="_country", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="state", aname="_state", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="hometown", aname="_hometown", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class getHometownResponse: + typecode = _getHometownResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + country -- part country + state -- part state + hometown -- part hometown + """ + self._country = kw.get("country") + self._state = kw.get("state") + self._hometown = kw.get("hometown") +getHometownResponse.typecode.pyclass = getHometownResponse + +_postArtistRequestTypecode = Struct(pname=("urn:LyricWiki","postArtist"), ofwhat=[ZSI.TC.Boolean(pname="overwriteIfExists", aname="_overwriteIfExists", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ns0.AlbumDataArray_Def(pname="albums", aname="_albums", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postArtistRequest: + typecode = _postArtistRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + overwriteIfExists -- part overwriteIfExists + artist -- part artist + albums -- part albums + """ + self._overwriteIfExists = kw.get("overwriteIfExists") + self._artist = kw.get("artist") + self._albums = kw.get("albums") +postArtistRequest.typecode.pyclass = postArtistRequest + +_postArtistResponseTypecode = Struct(pname=("urn:LyricWiki","postArtistResponse"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.Boolean(pname="dataUsed", aname="_dataUsed", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="message", aname="_message", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postArtistResponse: + typecode = _postArtistResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + dataUsed -- part dataUsed + message -- part message + """ + self._artist = kw.get("artist") + self._dataUsed = kw.get("dataUsed") + self._message = kw.get("message") +postArtistResponse.typecode.pyclass = postArtistResponse + +_postAlbumRequestTypecode = Struct(pname=("urn:LyricWiki","postAlbum"), ofwhat=[ZSI.TC.Boolean(pname="overwriteIfExists", aname="_overwriteIfExists", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="album", aname="_album", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TCnumbers.Iint(pname="year", aname="_year", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="asin", aname="_asin", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ns0.ArrayOfstring_Def(pname="songs", aname="_songs", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postAlbumRequest: + typecode = _postAlbumRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + overwriteIfExists -- part overwriteIfExists + artist -- part artist + album -- part album + year -- part year + asin -- part asin + songs -- part songs + """ + self._overwriteIfExists = kw.get("overwriteIfExists") + self._artist = kw.get("artist") + self._album = kw.get("album") + self._year = kw.get("year") + self._asin = kw.get("asin") + self._songs = kw.get("songs") +postAlbumRequest.typecode.pyclass = postAlbumRequest + +_postAlbumResponseTypecode = Struct(pname=("urn:LyricWiki","postAlbumResponse"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="album", aname="_album", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TCnumbers.Iint(pname="year", aname="_year", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.Boolean(pname="dataUsed", aname="_dataUsed", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="message", aname="_message", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postAlbumResponse: + typecode = _postAlbumResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + album -- part album + year -- part year + dataUsed -- part dataUsed + message -- part message + """ + self._artist = kw.get("artist") + self._album = kw.get("album") + self._year = kw.get("year") + self._dataUsed = kw.get("dataUsed") + self._message = kw.get("message") +postAlbumResponse.typecode.pyclass = postAlbumResponse + +_postSongRequestTypecode = Struct(pname=("urn:LyricWiki","postSong"), ofwhat=[ZSI.TC.Boolean(pname="overwriteIfExists", aname="_overwriteIfExists", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="lyrics", aname="_lyrics", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ns0.AlbumResultArray_Def(pname="onAlbums", aname="_onAlbums", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postSongRequest: + typecode = _postSongRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + overwriteIfExists -- part overwriteIfExists + artist -- part artist + song -- part song + lyrics -- part lyrics + onAlbums -- part onAlbums + """ + self._overwriteIfExists = kw.get("overwriteIfExists") + self._artist = kw.get("artist") + self._song = kw.get("song") + self._lyrics = kw.get("lyrics") + self._onAlbums = kw.get("onAlbums") +postSongRequest.typecode.pyclass = postSongRequest + +_postSongResponseTypecode = Struct(pname=("urn:LyricWiki","postSongResponse"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.Boolean(pname="dataUsed", aname="_dataUsed", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="message", aname="_message", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postSongResponse: + typecode = _postSongResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + song -- part song + dataUsed -- part dataUsed + message -- part message + """ + self._artist = kw.get("artist") + self._song = kw.get("song") + self._dataUsed = kw.get("dataUsed") + self._message = kw.get("message") +postSongResponse.typecode.pyclass = postSongResponse + +_postSong_flagsRequestTypecode = Struct(pname=("urn:LyricWiki","postSong_flags"), ofwhat=[ZSI.TC.Boolean(pname="overwriteIfExists", aname="_overwriteIfExists", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="lyrics", aname="_lyrics", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ns0.AlbumResultArray_Def(pname="onAlbums", aname="_onAlbums", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="flags", aname="_flags", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postSong_flagsRequest: + typecode = _postSong_flagsRequestTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + overwriteIfExists -- part overwriteIfExists + artist -- part artist + song -- part song + lyrics -- part lyrics + onAlbums -- part onAlbums + flags -- part flags + """ + self._overwriteIfExists = kw.get("overwriteIfExists") + self._artist = kw.get("artist") + self._song = kw.get("song") + self._lyrics = kw.get("lyrics") + self._onAlbums = kw.get("onAlbums") + self._flags = kw.get("flags") +postSong_flagsRequest.typecode.pyclass = postSong_flagsRequest + +_postSong_flagsResponseTypecode = Struct(pname=("urn:LyricWiki","postSong_flagsResponse"), ofwhat=[ZSI.TC.String(pname="artist", aname="_artist", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="song", aname="_song", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.Boolean(pname="dataUsed", aname="_dataUsed", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True), ZSI.TC.String(pname="message", aname="_message", typed=False, encoded=None, minOccurs=1, maxOccurs=1, nillable=True)], pyclass=None, encoded="urn:LyricWiki") +class postSong_flagsResponse: + typecode = _postSong_flagsResponseTypecode + __metaclass__ = pyclass_type + def __init__(self, **kw): + """Keyword parameters: + artist -- part artist + song -- part song + dataUsed -- part dataUsed + message -- part message + """ + self._artist = kw.get("artist") + self._song = kw.get("song") + self._dataUsed = kw.get("dataUsed") + self._message = kw.get("message") +postSong_flagsResponse.typecode.pyclass = postSong_flagsResponse diff --git a/nephilim/LyricWiki_types.py b/nephilim/LyricWiki_types.py new file mode 100644 index 0000000..d27cb1c --- /dev/null +++ b/nephilim/LyricWiki_types.py @@ -0,0 +1,162 @@ +################################################## +# file: LyricWiki_types.py +# +# schema types generated by "ZSI.generate.wsdl2python.WriteServiceModule" +# /usr/bin/wsdl2py --complexType http://lyricwiki.org/server.php?wsdl +# +################################################## + +import ZSI +import ZSI.TCcompound +from ZSI.schema import LocalElementDeclaration, ElementDeclaration, TypeDefinition, GTD, GED +from ZSI.generate.pyclass import pyclass_type + +############################## +# targetNamespace +# urn:LyricWiki +############################## + +class ns0: + targetNamespace = "urn:LyricWiki" + + class ArrayOfstring_Def(ZSI.TC.Array, TypeDefinition): + #complexType/complexContent base="SOAP-ENC:Array" + schema = "urn:LyricWiki" + type = (schema, "ArrayOfstring") + def __init__(self, pname, ofwhat=(), extend=False, restrict=False, attributes=None, **kw): + ofwhat = ZSI.TC.String(None, typed=False) + atype = (u'http://www.w3.org/2001/XMLSchema', u'string[]') + ZSI.TCcompound.Array.__init__(self, atype, ofwhat, pname=pname, childnames='item', **kw) + + class AlbumResultArray_Def(ZSI.TC.Array, TypeDefinition): + #complexType/complexContent base="SOAP-ENC:Array" + schema = "urn:LyricWiki" + type = (schema, "AlbumResultArray") + def __init__(self, pname, ofwhat=(), extend=False, restrict=False, attributes=None, **kw): + ofwhat = ZSI.TC.AnyType(None, typed=False) + atype = (u'http://www.w3.org/2001/XMLSchema', u'AlbumResult[]') + ZSI.TCcompound.Array.__init__(self, atype, ofwhat, pname=pname, childnames='item', **kw) + + class AlbumResult_Def(ZSI.TCcompound.ComplexType, TypeDefinition): + schema = "urn:LyricWiki" + type = (schema, "AlbumResult") + def __init__(self, pname, ofwhat=(), attributes=None, extend=False, restrict=False, **kw): + ns = ns0.AlbumResult_Def.schema + TClist = [ZSI.TC.String(pname="artist", aname="_artist", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="album", aname="_album", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TCnumbers.Iint(pname="year", aname="_year", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded"))] + self.attribute_typecode_dict = attributes or {} + if extend: TClist += ofwhat + if restrict: TClist = ofwhat + ZSI.TCcompound.ComplexType.__init__(self, None, TClist, pname=pname, inorder=0, **kw) + class Holder: + __metaclass__ = pyclass_type + typecode = self + def __init__(self): + # pyclass + self._artist = None + self._album = None + self._year = None + return + Holder.__name__ = "AlbumResult_Holder" + self.pyclass = Holder + + class SongResult_Def(ZSI.TCcompound.ComplexType, TypeDefinition): + schema = "urn:LyricWiki" + type = (schema, "SongResult") + def __init__(self, pname, ofwhat=(), attributes=None, extend=False, restrict=False, **kw): + ns = ns0.SongResult_Def.schema + TClist = [ZSI.TC.String(pname="artist", aname="_artist", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="song", aname="_song", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded"))] + self.attribute_typecode_dict = attributes or {} + if extend: TClist += ofwhat + if restrict: TClist = ofwhat + ZSI.TCcompound.ComplexType.__init__(self, None, TClist, pname=pname, inorder=0, **kw) + class Holder: + __metaclass__ = pyclass_type + typecode = self + def __init__(self): + # pyclass + self._artist = None + self._song = None + return + Holder.__name__ = "SongResult_Holder" + self.pyclass = Holder + + class LyricsResult_Def(ZSI.TCcompound.ComplexType, TypeDefinition): + schema = "urn:LyricWiki" + type = (schema, "LyricsResult") + def __init__(self, pname, ofwhat=(), attributes=None, extend=False, restrict=False, **kw): + ns = ns0.LyricsResult_Def.schema + TClist = [ZSI.TC.String(pname="artist", aname="_artist", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="song", aname="_song", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="lyrics", aname="_lyrics", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="url", aname="_url", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded"))] + self.attribute_typecode_dict = attributes or {} + if extend: TClist += ofwhat + if restrict: TClist = ofwhat + ZSI.TCcompound.ComplexType.__init__(self, None, TClist, pname=pname, inorder=0, **kw) + class Holder: + __metaclass__ = pyclass_type + typecode = self + def __init__(self): + # pyclass + self._artist = None + self._song = None + self._lyrics = None + self._url = None + return + Holder.__name__ = "LyricsResult_Holder" + self.pyclass = Holder + + class SOTDResult_Def(ZSI.TCcompound.ComplexType, TypeDefinition): + schema = "urn:LyricWiki" + type = (schema, "SOTDResult") + def __init__(self, pname, ofwhat=(), attributes=None, extend=False, restrict=False, **kw): + ns = ns0.SOTDResult_Def.schema + TClist = [ZSI.TC.String(pname="artist", aname="_artist", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="song", aname="_song", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="nominatedBy", aname="_nominatedBy", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="reason", aname="_reason", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="lyrics", aname="_lyrics", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded"))] + self.attribute_typecode_dict = attributes or {} + if extend: TClist += ofwhat + if restrict: TClist = ofwhat + ZSI.TCcompound.ComplexType.__init__(self, None, TClist, pname=pname, inorder=0, **kw) + class Holder: + __metaclass__ = pyclass_type + typecode = self + def __init__(self): + # pyclass + self._artist = None + self._song = None + self._nominatedBy = None + self._reason = None + self._lyrics = None + return + Holder.__name__ = "SOTDResult_Holder" + self.pyclass = Holder + + class AlbumDataArray_Def(ZSI.TC.Array, TypeDefinition): + #complexType/complexContent base="SOAP-ENC:Array" + schema = "urn:LyricWiki" + type = (schema, "AlbumDataArray") + def __init__(self, pname, ofwhat=(), extend=False, restrict=False, attributes=None, **kw): + ofwhat = ZSI.TC.AnyType(None, typed=False) + atype = (u'http://www.w3.org/2001/XMLSchema', u'AlbumData[]') + ZSI.TCcompound.Array.__init__(self, atype, ofwhat, pname=pname, childnames='item', **kw) + + class AlbumData_Def(ZSI.TCcompound.ComplexType, TypeDefinition): + schema = "urn:LyricWiki" + type = (schema, "AlbumData") + def __init__(self, pname, ofwhat=(), attributes=None, extend=False, restrict=False, **kw): + ns = ns0.AlbumData_Def.schema + TClist = [ZSI.TC.String(pname="album", aname="_album", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TCnumbers.Iint(pname="year", aname="_year", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), ZSI.TC.String(pname="amazonLink", aname="_amazonLink", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")), GTD("urn:LyricWiki","ArrayOfstring",lazy=False)(pname="songs", aname="_songs", minOccurs=1, maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded"))] + self.attribute_typecode_dict = attributes or {} + if extend: TClist += ofwhat + if restrict: TClist = ofwhat + ZSI.TCcompound.ComplexType.__init__(self, None, TClist, pname=pname, inorder=0, **kw) + class Holder: + __metaclass__ = pyclass_type + typecode = self + def __init__(self): + # pyclass + self._album = None + self._year = None + self._amazonLink = None + self._songs = None + return + Holder.__name__ = "AlbumData_Holder" + self.pyclass = Holder + +# end class ns0 (tns: urn:LyricWiki) diff --git a/nephilim/clPlugin.py b/nephilim/clPlugin.py new file mode 100644 index 0000000..efbd502 --- /dev/null +++ b/nephilim/clPlugin.py @@ -0,0 +1,136 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +import logging + +import plugins +from misc import * + +class Plugin: + name = None + dockWidget = None + settings = None + winMain = None + loaded = None + listeners = None + mpclient = None + DEFAULTS = {} + + def __init__(self, winMain, name): + self.name = name + self.winMain = winMain + self.loaded = False + self.listeners = [] + self.mpclient = winMain.mpclient + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + + #init settings + self.settings.beginGroup(self.name) + for key in self.DEFAULTS: + if not self.settings.contains(key): + self.settings.setValue(key, QVariant(self.DEFAULTS[key])) + self.settings.endGroup() + + def getName(self): + 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 load(self): + logging.info("loading") + if len(self.listeners): + logging.debug("adding %s listeners"%(len(self.listeners))) + for listener in self.listeners: + self.mpclient.add_listener(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 not self.loaded: + return + logging.info("unloading") + if len(self.listeners): + logging.debug("removing %s listeners"%(len(self.listeners))) + for listener in self.listeners: + self.mpclient.removeListener(listener[0], listener[1]) + + self._unload() + dock_widget = self.getDockWidget() + if dock_widget: + self.winMain.removeDock(dock_widget) + self.dockWidget = None + self.settingsWidget = None + self.loaded = False + def isLoaded(self): + return self.loaded + + def addListener(self, event, callback): + self.listeners.append([event, callback]) + + def getDockWidget(self, opts=None): + self.dockWidget = self._getDockWidget() + if self.dockWidget and opts: + self.dockWidget.setFeatures(opts) + self.dockWidget.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + return self.dockWidget + + class SettingsWidget(QtGui.QWidget): + """ plugins should subclass this""" + plugin = None + settings = None + + def __init__(self, plugin): + QtGui.QWidget.__init__(self) + self.plugin = plugin + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + + def save_settings(self): + """ reimplement this""" + self.plugin.saveSettings() + + def _add_widget(self, widget, label = '', tooltip = ''): + """adds a widget with label""" + if not self.layout(): + logging.error('Attempted to call add_widget with no layout set.') + widget.setToolTip(tooltip) + layout = QtGui.QHBoxLayout() + layout.addWidget(QtGui.QLabel(label)) + layout.addWidget(widget) + self.layout().addLayout(layout) + + def get_settings_widget(self): + """Should return subclassed SettingsWidget.""" + return + + def resetSettingCache(self): + #self.settings=None + self.settingsWidget=None + + def _getPluginClassname(self, cl): + """Returns the name of a plugin (without 'plugin'-prefix)""" + return str(cl).split('.')[-1].lower()[len('plugin'):] + + def _getDockWidget(self): + """Override this one.""" + return None + 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/nephilim/clSong.py b/nephilim/clSong.py new file mode 100644 index 0000000..9b38076 --- /dev/null +++ b/nephilim/clSong.py @@ -0,0 +1,85 @@ +from PyQt4 import QtCore +from misc import ORGNAME, APPNAME, sec2min +import os + +# compare two songs with respect to their album +def isSameAlbum(song1, song2): + 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 + + 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('9'): + try: + self._data['track']=int(t[0:i]) + except TypeError: + self._data['track']=-1 + break + self._data['track']=int(self._data['track']) + + # ensure all string-values are utf-8 encoded + for tag in self._data.keys(): + if isinstance(self._data[tag], str): + self._data[tag]=unicode(self._data[tag], "utf-8") + if 'time' in self._data: + self._data['time'] = int(self._data['time']) + self._data['timems'] = '%i:%i'%(self._data['time'] / 60, self._data['time'] % 60) + self._data['length'] = sec2min(self._data['time']) + + 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 getTrack(self): + """Get the track.""" + return self.getTag('track') + + def getAlbum(self): + """Get the album.""" + return self.getTag('album') + + 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.__str__() in self.__str__().lower() + + 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__() + + return default + + def expand_tags(self, str): + """Expands tags in form $tag in str.""" + ret = str + for tag in self._data: + ret = ret.replace('$' + tag, unicode(self._data[tag])) + return ret + diff --git a/nephilim/default_layout b/nephilim/default_layout new file mode 100644 index 0000000..5755674 Binary files /dev/null and b/nephilim/default_layout differ diff --git a/nephilim/misc.py b/nephilim/misc.py new file mode 100644 index 0000000..b07360d --- /dev/null +++ b/nephilim/misc.py @@ -0,0 +1,85 @@ +from PyQt4 import QtCore, QtGui +from htmlentitydefs import name2codepoint as n2cp +import re +import urllib2, cookielib +import socket +import unicodedata + +import logging + +socket.setdefaulttimeout(8) + +appIcon = 'gfx/icon.png' +APPNAME = 'nephilim' +ORGNAME = 'nephilim' + +def doEvents(): + """Make some time for necessary events.""" + QtCore.QEventLoop().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) + +def numeric_compare(x, y): + 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))) + +def toAscii(ustr): + if type(ustr)==str: + return ustr + return unicodedata.normalize('NFKD', ustr).encode('ascii', 'ignore') + +def substEntity(match): + 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() + +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] + + +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) diff --git a/nephilim/mpclient.py b/nephilim/mpclient.py new file mode 100644 index 0000000..26b4d13 --- /dev/null +++ b/nephilim/mpclient.py @@ -0,0 +1,364 @@ +from PyQt4 import QtCore +from clSong import Song +from traceback import print_exc +from misc import * +import mpd +from threading import Thread + +class MPClient(QtCore.QObject): + """This 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 + + # objects used for comparison with previous value + _curSongID = None + _curTime = None + _curState = None + _curVolume = None + _updatings_db = None + _cur_plist_id = 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 + 'onPlaylistChange' : '', + } + + 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=[] + self._cur_plist_id = -1 + + 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 = mpd.MPDClient() + self._client.connect(host, port) + except IOError: + 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 listPlaylist(self): + """Returns the current playlist.""" + if not self.isConnected(): + return [] + return self._curPlaylist + + def listLibrary(self): + """Returns the library.""" + if not self.isConnected(): + return [] + return self._curLib + + def getCurrentSong(self): + """Returns the current playing song.""" + if self.isConnected()==False: + return None + return self._curSong + + def updateDB(self, paths = None): + if not paths: + return self._client.update() + 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 + else: + ret['length'] = 0 + ret['time'] = 0 + return ret + except Exception, d: + print_exc() + return None + + def get_outputs(self): + """returns audio outputs""" + if self.isConnected(): + return self._retrieve(self._client.outputs) + else: + return [] + def set_output(self, output_id, state): + """set audio output output_id to state""" + if state: + self._client.enableoutput(output_id) + else: + self._client.disableoutput(output_id) + + def urlhandlers(self): + if not self.isConnected(): + return [] + else: + return self._client.urlhandlers() + + def repeat(self, val): + if isinstance(val, bool): + val = 1 if val else 0 + self._client.repeat(val) + def random(self,val): + if isinstance(val, bool): + val = 1 if val else 0 + 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 + + self._retrMutex.unlock() + return ret + + def isPlaying(self): + return self.getStatus()['state'] == 'play' + + def play(self, id): + """Play song with ID $id.""" + self._playCalled = True + if id: + 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.""" + if self._curSongID > 0: + 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 clear_playlist(self): + """Removes all songs from current playlist.""" + self._client.clear() + self._updatePlaylist() + + def addToPlaylist(self, paths): + """Add all files in $paths to the current playlist.""" + try: + self._client.command_list_ok_begin() + for path in paths: + self._client.addid(unicode(path)) + ret = self._client.command_list_end() + self._updatePlaylist() + if self._curState == 'stop': + self.play(ret[0]) + except mpd.CommandError: + logging.error('Cannot add some files, check permissions.') + + 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 add_listener(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.""" + song = self._retrieve(self._client.currentsong) + if not song: + self._curSong = None + else: + self._curSong = Song(song) + + 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) + + 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." + status = self.getStatus() + + if status == None: + self._client=None + self._raiseEvent('onDisconnect', None) + self.killTimer(self._timerID) + return + + self._updateCurrentSong() + " check if song has changed" + song = self._curSong + if song: + curID = song.getID() + else: + curID = -1 + + 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 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 + + if 'playlist' in status: + cur_plist_id = int(status['playlist']) + if cur_plist_id != self._cur_plist_id: + self._updatePlaylist() + self._raiseEvent('onPlaylistChange', None) + self._cur_plist_id = cur_plist_id + + " 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') diff --git a/nephilim/mpd.py b/nephilim/mpd.py new file mode 100644 index 0000000..2d14bb6 --- /dev/null +++ b/nephilim/mpd.py @@ -0,0 +1,360 @@ +# Python MPD client library +# Copyright (C) 2008 J. Alexander Treuman +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import socket + + +HELLO_PREFIX = "OK MPD " +ERROR_PREFIX = "ACK " +SUCCESS = "OK" +NEXT = "list_OK" + + +class MPDError(Exception): + pass + +class ConnectionError(MPDError): + pass + +class ProtocolError(MPDError): + pass + +class CommandError(MPDError): + pass + +class CommandListError(MPDError): + pass + + +class _NotConnected(object): + def __getattr__(self, attr): + return self._dummy + + def _dummy(*args): + raise ConnectionError("Not connected") + +class MPDClient(object): + def __init__(self): + self.iterate = False + self._reset() + self._commands = { + # Admin Commands + "disableoutput": self._getnone, + "enableoutput": self._getnone, + "kill": None, + "update": self._getitem, + # Informational Commands + "status": self._getobject, + "stats": self._getobject, + "outputs": self._getoutputs, + "commands": self._getlist, + "notcommands": self._getlist, + "tagtypes": self._getlist, + "urlhandlers": self._getlist, + # Database Commands + "find": self._getsongs, + "list": self._getlist, + "listall": self._getdatabase, + "listallinfo": self._getdatabase, + "lsinfo": self._getdatabase, + "search": self._getsongs, + "count": self._getobject, + # Playlist Commands + "add": self._getnone, + "addid": self._getitem, + "clear": self._getnone, + "currentsong": self._getobject, + "delete": self._getnone, + "deleteid": self._getnone, + "load": self._getnone, + "rename": self._getnone, + "move": self._getnone, + "moveid": self._getnone, + "playlist": self._getplaylist, + "playlistinfo": self._getsongs, + "playlistid": self._getsongs, + "plchanges": self._getsongs, + "plchangesposid": self._getchanges, + "rm": self._getnone, + "save": self._getnone, + "shuffle": self._getnone, + "swap": self._getnone, + "swapid": self._getnone, + "listplaylist": self._getlist, + "listplaylistinfo": self._getsongs, + "playlistadd": self._getnone, + "playlistclear": self._getnone, + "playlistdelete": self._getnone, + "playlistmove": self._getnone, + "playlistfind": self._getsongs, + "playlistsearch": self._getsongs, + # Playback Commands + "crossfade": self._getnone, + "next": self._getnone, + "pause": self._getnone, + "play": self._getnone, + "playid": self._getnone, + "previous": self._getnone, + "random": self._getnone, + "repeat": self._getnone, + "seek": self._getnone, + "seekid": self._getnone, + "setvol": self._getnone, + "stop": self._getnone, + "volume": self._getnone, + # Miscellaneous Commands + "clearerror": self._getnone, + "close": None, + "password": self._getnone, + "ping": self._getnone, + } + + def __getattr__(self, attr): + try: + retval = self._commands[attr] + except KeyError: + raise AttributeError("'%s' object has no attribute '%s'" % + (self.__class__.__name__, attr)) + return lambda *args: self._docommand(attr, args, retval) + + def _docommand(self, command, args, retval): + if self._commandlist is not None and not callable(retval): + raise CommandListError("%s not allowed in command list" % command) + self._writecommand(command, args) + if self._commandlist is None: + if callable(retval): + return retval() + return retval + self._commandlist.append(retval) + + def _writeline(self, line): + self._wfile.write(("%s\n" % line).encode('utf-8')) + self._wfile.flush() + + 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)) + self._writeline(u" ".join(parts)) + + def _readline(self): + line = self._rfile.readline() + if not line.endswith("\n"): + raise ConnectionError("Connection lost while reading line") + line = line.rstrip("\n") + if line.startswith(ERROR_PREFIX): + error = line[len(ERROR_PREFIX):].strip() + raise CommandError(error) + if self._commandlist is not None: + if line == NEXT: + return + if line == SUCCESS: + raise ProtocolError("Got unexpected '%s'" % SUCCESS) + elif line == SUCCESS: + return + return line + + def _readitem(self, separator): + line = self._readline() + if line is None: + return + item = line.split(separator, 1) + if len(item) < 2: + raise ProtocolError("Could not parse item: '%s'" % line) + return item + + def _readitems(self, separator=": "): + item = self._readitem(separator) + while item: + yield item + item = self._readitem(separator) + raise StopIteration + + def _readlist(self): + seen = None + for key, value in self._readitems(): + if key != seen: + if seen is not None: + raise ProtocolError("Expected key '%s', got '%s'" % + (seen, key)) + seen = key + yield value + raise StopIteration + + def _readplaylist(self): + for key, value in self._readitems(":"): + yield value + raise StopIteration + + def _readobjects(self, delimiters=[]): + obj = {} + for key, value in self._readitems(): + key = key.lower() + if obj: + if key in delimiters: + yield obj + obj = {} + elif obj.has_key(key): + if not isinstance(obj[key], list): + obj[key] = [obj[key], value] + else: + obj[key].append(value) + continue + obj[key] = value + if obj: + yield obj + raise StopIteration + + def _readcommandlist(self): + for retval in self._commandlist: + yield retval() + self._commandlist = None + self._getnone() + raise StopIteration + + def _wrapiterator(self, iterator): + if not self.iterate: + return list(iterator) + return iterator + + def _getnone(self): + line = self._readline() + if line is not None: + raise ProtocolError("Got unexpected return value: '%s'" % line) + + def _getitem(self): + items = list(self._readitems()) + if len(items) != 1: + return + return items[0][1] + + def _getlist(self): + return self._wrapiterator(self._readlist()) + + def _getplaylist(self): + return self._wrapiterator(self._readplaylist()) + + def _getobject(self): + objs = list(self._readobjects()) + if not objs: + return {} + return objs[0] + + def _getobjects(self, delimiters): + return self._wrapiterator(self._readobjects(delimiters)) + + def _getsongs(self): + return self._getobjects(["file"]) + + def _getdatabase(self): + return self._getobjects(["file", "directory", "playlist"]) + + def _getoutputs(self): + return self._getobjects(["outputid"]) + + def _getchanges(self): + return self._getobjects(["cpos"]) + + def _getcommandlist(self): + try: + return self._wrapiterator(self._readcommandlist()) + except CommandError: + self._commandlist = None + raise + + def _hello(self): + line = self._rfile.readline() + if not line.endswith("\n"): + raise ConnectionError("Connection lost while reading MPD hello") + line = line.rstrip("\n") + if not line.startswith(HELLO_PREFIX): + raise ProtocolError("Got invalid MPD hello: '%s'" % line) + self.mpd_version = line[len(HELLO_PREFIX):].strip() + + def _reset(self): + self.mpd_version = None + self._commandlist = None + self._sock = None + self._rfile = _NotConnected() + self._wfile = _NotConnected() + + def connect(self, host, port): + if self._sock: + raise ConnectionError("Already connected") + msg = "getaddrinfo returns an empty list" + try: + flags = socket.AI_ADDRCONFIG + except AttributeError: + flags = 0 + if port == None: #assume Unix domain socket + try: + self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._sock.connect(host) + except socket.error, msg: + if self._sock: + self._sock.close() + self._sock = None + else: + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, socket.IPPROTO_TCP, + flags): + af, socktype, proto, canonname, sa = res + try: + self._sock = socket.socket(af, socktype, proto) + self._sock.connect(sa) + except socket.error, msg: + if self._sock: + self._sock.close() + self._sock = None + continue + break + if not self._sock: + raise socket.error(msg) + self._rfile = self._sock.makefile("rb") + self._wfile = self._sock.makefile("wb") + try: + self._hello() + except: + self.disconnect() + raise + + def disconnect(self): + self._rfile.close() + self._wfile.close() + self._sock.close() + self._reset() + + def command_list_ok_begin(self): + if self._commandlist is not None: + raise CommandListError("Already in command list") + self._writecommand("command_list_ok_begin") + self._commandlist = [] + + def command_list_end(self): + if self._commandlist is None: + raise CommandListError("Not in command list") + self._writecommand("command_list_end") + return self._getcommandlist() + + +def escape(text): + return text.replace("\\", "\\\\").replace('"', '\\"') + + +# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79: 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"([^<]+)") + 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('%s\n
%s
'\ + '
\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() diff --git a/nephilim/winConnect.py b/nephilim/winConnect.py new file mode 100644 index 0000000..45789ac --- /dev/null +++ b/nephilim/winConnect.py @@ -0,0 +1,88 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +import time +from misc import * +from traceback import print_exc + +class winConnect(QtGui.QWidget): + txtHost=None + txtPort=None + lblInfo=None + _timerID=None + mpclient = None + settings = None + + + def __init__(self,parent): + QtGui.QWidget.__init__(self, parent) + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + self.txtHost = QtGui.QLineEdit(self.settings.value('MPD/host', QVariant('localhost')).toString()) + self.txtPort = QtGui.QLineEdit(self.settings.value('MPD/port', QVariant('6600')).toString()) + self.txtPort.setValidator(QtGui.QIntValidator(1, 65535, self.txtPort)) + self.lblInfo = QtGui.QLabel("connecting ...") + self.mpclient = parent.mpclient + + frame=QtGui.QVBoxLayout() + inputs=QtGui.QHBoxLayout() + + frame.addLayout(inputs) + frame.addWidget(self.lblInfo) + + inputs.addWidget(self.txtHost) + inputs.addWidget(self.txtPort) + + self.setWindowIcon(QtGui.QIcon(appIcon)) + self.setWindowTitle('Connect to mpd') + self.setLayout(frame) + self.resize(200,80) + self.center() + doEvents() + + self.mpclient.add_listener('onReady', self.onReady) + self.mpclient.add_listener('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 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() + self.settings.setValue('MPD/host', QVariant(self.txtHost.text())) + self.settings.setValue('MPD/port', QVariant(self.txtPort.text())) + self.txtHost.setEnabled(False) + self.txtPort.setEnabled(False) + doEvents() + + def onReady(self, params): + self.hide() + + def timerEvent(self, event): + host = str(self.txtHost.text()) + port = int(self.txtPort.text()) if self.txtPort.text() else None + + self.lblInfo.setText('Trying to connect to '+host+':'+str(port)+' ...') + doEvents() + self.mpclient.connect(host, port) + doEvents() + + def windowActivationChange(self, bool): + self.activateWindow() + self.raise_() + doEvents() diff --git a/nephilim/winMain.py b/nephilim/winMain.py new file mode 100644 index 0000000..accf864 --- /dev/null +++ b/nephilim/winMain.py @@ -0,0 +1,285 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +from traceback import print_exc + +from misc import * +from mpclient import MPClient + +import plugins + +from winConnect import winConnect +from winSettings import winSettings +import logging + +DEFAULT_LAYOUT_FILE = 'default_layout' + +class winMain(QtGui.QMainWindow): + """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 + + " MPD object" + mpclient = None + + " Statusbar objects" + statuslabel = None + time_slider = None + time_label = None + + settings = None + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + self.mpclient = MPClient() + + self.wConnect=winConnect(self) + + # statusbar + self.statusBar() + self.statuslabel = QtGui.QLabel() + self.time_slider = QtGui.QSlider(QtCore.Qt.Horizontal, self) + self.time_slider.setMaximumWidth(self.width()/4) + self.connect(self.time_slider, QtCore.SIGNAL('sliderReleased()'), self.on_time_slider_change) + self.time_label = QtGui.QLabel() + self.time_label.duration = '0:00' + + self.statusBar().addWidget(self.statuslabel) + self.statusBar().addPermanentWidget(self.time_label) + self.statusBar().addPermanentWidget(self.time_slider) + + mBar = QtGui.QMenuBar() # create a menubar + # File menu + m = mBar.addMenu("File") + m.setTearOffEnabled(True) + # connect + self.mConnect=m.addAction('Connect ...', self.wConnect.monitor) + self.mConnect.setIcon(QtGui.QIcon(appIcon)) + # disconnect + self.mDisconnect=m.addAction('Disconnect', self.mpclient.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 layout + self.mLayout=mBar.addMenu("Layout") + self.mLayout.setTearOffEnabled(True) + + # create a toolbar for the main menu + menu_toolbar = QtGui.QToolBar() + menu_toolbar.addWidget(mBar) + self.addToolBar(QtCore.Qt.TopToolBarArea, menu_toolbar) + + showWinSettings = 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: + if self.settings.value(plugin.getName() + '/load') == None: + showWinSettings = True + if self.settings.value(plugin.getName() + '/load', QVariant(True)).toBool(): + # load new plugins by default + try: + plugin.load() + except Exception, e: + plugins.setPluginMessage(plugin.getName(), "Exception while loading %s: %s"%(plugin.getName(), str(e))) + showWinSettings=True + + self.updateLayoutMenu() + self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks \ + |QtGui.QMainWindow.AllowTabbedDocks \ + |QtGui.QMainWindow.VerticalTabs) + self.setDockNestingEnabled(True) + self.restoreGeometry(self.settings.value('geometry').toByteArray()) + self.restoreLayout() + + " add event handlers" + self.mpclient.add_listener('onReady', self.onReady) + self.mpclient.add_listener('onConnect', self.onConnect) + self.mpclient.add_listener('onDisconnect', self.onDisconnect) + self.mpclient.add_listener('onUpdateDBStart', self.onUpdateDBStart) + self.mpclient.add_listener('onUpdateDBFinish', self.onUpdateDBFinish) + self.mpclient.add_listener('onSongChange', self.on_song_change) + self.mpclient.add_listener('onStateChange', self.update_state_messages) + self.mpclient.add_listener('onTimeChange', self.on_time_change) + + self.enableAll(True) + self.setWindowIcon(QtGui.QIcon(appIcon)) + # set icon in system tray + self.wConnect.monitor() + + self.update_state_messages() + self.show() + if showWinSettings: + 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() + + self.settings.setValue('geometry', QVariant(self.saveGeometry())) + self.settings.sync() + 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(self.settings.value('show_titlebars', QVariant(True)).toBool()) + 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: + self.settings.setValue('show_titlebars', QVariant(True)) + else: + self.settings.setValue('show_titlebars', QVariant(False)) + 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: + if dock in self.docks: + 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() + menu = QtGui.QMainWindow.createPopupMenu(self) + if menu: + actions = menu.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, 5000) + logging.info(status) + + def saveLayout(self): + self.settings.setValue('layout', QVariant(self.saveState())) + def restoreLayout(self): + layout = self.settings.value('layout').toByteArray() + if not layout: + try: + layout = open(DEFAULT_LAYOUT_FILE, 'rb').read() + except IOError: + logging.error("Error reading default layout.") + return + self.restoreState(layout) + + def showWinSettings(self): + if not self.wSettings: + self.wSettings=winSettings(self) + self.wSettings.show() + self.wSettings.raise_() + + def onReady(self, params): + self.initialiseData() + + def onConnect(self, params): + logging.info("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 initialiseData(self): + self.enableAll(True) + self.setStatus("") + doEvents + + def onDisconnect(self, params): + logging.info("Disconnected from MPD") + self.mDisconnect.setEnabled(False) + self.mConnect.setEnabled(True) + self.enableAll(False) + self.setStatus("You are disconnected. Choose File->Connect to reconnect!") + + def onUpdateDBFinish(self, params): + self.setStatus('') + def onUpdateDBStart(self, params): + self.setStatus('Updating the database. Please wait ...') + + def update_state_messages(self, params = None): + song = self.mpclient.getCurrentSong() + if song and self.mpclient.isPlaying(): + self.setWindowTitle(song.getTitle() + " by " + song.getArtist()) + self.statuslabel.setText("Now playing " + song.getTitle() + " by " + song.getArtist() + " on " + song.getAlbum()) + else: + self.setWindowTitle(APPNAME) + self.statuslabel.setText("") + + def on_time_slider_change(self): + self.mpclient.seek(self.time_slider.value()) + + def on_song_change(self, params): + status = self.mpclient.getStatus() + self.time_slider.setMaximum(status['length']) + self.time_slider.setEnabled(True) + self.time_label.duration = sec2min(status['length']) + self.update_state_messages(params) + + def on_time_change(self, params): + if not self.time_slider.isSliderDown(): + self.time_slider.setValue(params['newTime']) + self.time_label.setText(sec2min(params['newTime']) + '/' + self.time_label.duration) diff --git a/nephilim/winSettings.py b/nephilim/winSettings.py new file mode 100644 index 0000000..08873df --- /dev/null +++ b/nephilim/winSettings.py @@ -0,0 +1,176 @@ +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import QVariant +import os + +from misc import ORGNAME, APPNAME, Button, appIcon, doEvents +import plugins + + +class winSettings(QtGui.QWidget): + btnSave=None + btnClose=None + lstPlugins=None + + winMain=None + settings = None + settings_wg = [] + + class SettingsWidgetMPD(QtGui.QWidget): + mpclient = None + settings = None + host_txt = None + port_txt = None + lib_txt = None + update = None + outputs = None + + def __init__(self, mpclient): + QtGui.QWidget.__init__(self) + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + self.mpclient = mpclient + + self.settings.beginGroup('MPD') + self.host_txt = QtGui.QLineEdit(self.settings.value('host', QVariant('localhost')).toString()) + self.port_txt = QtGui.QLineEdit(self.settings.value('port', QVariant('6600')).toString()) + self.lib_txt = QtGui.QLineEdit(self.settings.value('music_dir', QVariant(os.path.expanduser('~/music/'))).toString()) + self.settings.endGroup() + + self.update = QtGui.QPushButton('Update MPD database') + self.connect(self.update, QtCore.SIGNAL('clicked()'), self.update_db) + + self.outputs = QtGui.QGroupBox('Audio outputs') + self.outputs.setLayout(QtGui.QVBoxLayout()) + class Output(QtGui.QCheckBox): + id = None + mpclient = None + def change_state(self, state): + self.mpclient.set_output(self.id, state) + + for output in self.mpclient.get_outputs(): + box = Output(output['outputname']) + if output['outputenabled'] == '1': + box.setChecked(True) + else: + box.setChecked(False) + box.id = int(output['outputid']) + box.mpclient = self.mpclient + self.connect(box, QtCore.SIGNAL('stateChanged(int)'), box.change_state) + self.outputs.layout().addWidget(box) + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().addWidget(self.host_txt) + self.layout().addWidget(self.port_txt) + self.layout().addWidget(self.lib_txt) + self.layout().addWidget(self.update) + self.layout().addWidget(self.outputs) + + def save_settings(self): + self.settings.beginGroup('MPD') + self.settings.setValue('host', QVariant(self.host_txt.text())) + self.settings.setValue('port', QVariant(self.port_txt.text())) + self.settings.setValue('music_dir', QVariant(self.lib_txt.text())) + self.settings.endGroup() + + def update_db(self): + self.mpclient.updateDB() + + def __init__(self, winMain, parent=None): + QtGui.QWidget.__init__(self, parent) + self.settings = QtCore.QSettings(ORGNAME, APPNAME) + self.winMain = winMain + + self.btnSave = Button('save all', self.onBtnSaveClick) + self.btnClose = Button('close', self.onBtnCloseClick) + + tabWidget = QtGui.QTabWidget(parent) + self.settings_wg.append(self.SettingsWidgetMPD(self.winMain.mpclient)) + tabWidget.addTab(self.settings_wg[-1], 'MPD settings') + + self.lstPlugins = QtGui.QListWidget(self) + tabWidget.addTab(self.lstPlugins, 'plugins') + for k,entry in plugins.listPlugins().iteritems(): + plugin=entry[plugins.PLUGIN_INSTANCE] + if plugin: + wg = plugin.get_settings_widget() + if wg: + self.settings_wg.append(wg) + tabWidget.addTab(self.settings_wg[-1], plugin.getName()) + self.fillList() + + self.setLayout(QtGui.QVBoxLayout()) + self.layout().addWidget(tabWidget) + + layoutButtons = QtGui.QHBoxLayout() + layoutButtons.addStretch() + layoutButtons.addWidget(self.btnSave) + layoutButtons.addWidget(self.btnClose) + self.layout().addLayout(layoutButtons) + + self.connect(self.lstPlugins, QtCore.SIGNAL('itemChanged (QListWidgetItem*)'), self.onlstPluginItemChanged) + + self.setWindowIcon(QtGui.QIcon(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: + if entry[plugins.PLUGIN_MSG]: + item=QtGui.QListWidgetItem("%s\t%s"%(entry[plugins.PLUGIN_CLASS], entry[plugins.PLUGIN_MSG])) + item.setCheckState(QtCore.Qt.Unchecked) + item.setTextColor(QtCore.Qt.red) + else: + 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 self.settings.value(plugin.getName() + '/load') == None: + # load new plugins by default + item.setTextColor(QtCore.Qt.blue) + self.settings.setValue(plugin.getName() + '/load', QtCore.QVariant(True)) + + 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): + for wg in self.settings_wg: + wg.save_settings() + + def onBtnCloseClick(self): + self.close() + def onlstPluginItemChanged(self, item): + # check here if we have to load or unload the plugin! + toload = (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() + self.winMain.restoreLayout() + else: + plugin=plugins.getPlugin(className) + if plugin: + plugin.unload() + if plugin: + self.settings.setValue(plugin.getName() + '/load', QtCore.QVariant(toload)) + def closeEvent(self, event): + map(lambda entry: entry[plugins.PLUGIN_INSTANCE] and entry[plugins.PLUGIN_INSTANCE].resetSettingCache(), plugins.listPlugins().values()) + self.settings_wg = None + self.winMain.wSettings=None -- cgit v1.2.3