summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjerous <jerous@gmail.com>2008-08-23 17:49:06 +0200
committerjerous <jerous@gmail.com>2008-08-23 17:49:06 +0200
commitaafd9acd552099623204b2249e2c70b3cf27f95a (patch)
tree318edf6b078a8f9978028b494cfd25ebbf9798e0
parent01f47fffef0ac6bfcf3ab064a6bd3cf26b4f9448 (diff)
parent49e765eadb497e0fd2d72b6e3c74edb921b072a8 (diff)
fix wrong things in git
-rw-r--r--.gitignore1
-rw-r--r--clMonty.py21
-rw-r--r--clPlugin.py29
-rw-r--r--clSong.py11
-rw-r--r--jmpc.py98
-rw-r--r--jmpd.py219
-rw-r--r--misc.py105
-rw-r--r--montypc.py8
-rw-r--r--mpdconf3
-rw-r--r--plugins/AlbumCover.py20
-rw-r--r--plugins/Library.py6
-rw-r--r--plugins/Lyrics.py69
-rw-r--r--plugins/Notify.py145
-rw-r--r--plugins/PlayControl.py125
-rw-r--r--plugins/Playlist.py5
-rw-r--r--plugins/Shortcuts.py48
-rw-r--r--plugins/SongStatus.py22
-rw-r--r--plugins/Systray.py7
-rw-r--r--plugins/Tabs.py161
-rw-r--r--plugins/__init__.py12
-rw-r--r--plugins/amazon_cover_fetcher.py (renamed from amazon_cover_fetcher.py)0
-rw-r--r--test.py19
-rw-r--r--wgPlaylist.py8
-rw-r--r--wgSongList.py21
-rw-r--r--winMain.py88
-rw-r--r--winSettings.py34
26 files changed, 1155 insertions, 130 deletions
diff --git a/.gitignore b/.gitignore
index d08a050..65d9461 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
.vimsession
settings.txt
layout
+*.cover
diff --git a/clMonty.py b/clMonty.py
index db28f59..e6c57e4 100644
--- a/clMonty.py
+++ b/clMonty.py
@@ -3,7 +3,7 @@ from traceback import *
from clSong import Song
from traceback import print_exc
from misc import *
-import mpd
+import jmpc
from threading import Thread
class Monty(QtCore.QObject):
@@ -26,6 +26,7 @@ class Monty(QtCore.QObject):
_timerID=None
events={
+ 'beforeSongChange':'curSongID',
'onSongChange':'oldSongID, newSongID',
'onTimeChange':'oldTime, newTime',
'onStateChange':'oldState, newState',
@@ -69,7 +70,7 @@ class Monty(QtCore.QObject):
self._updateLib()
self._updatePlaylist()
self._updateCurrentSong()
- self._timerID=self.startTimer(300)
+ self._timerID=self.startTimer(500)
except Exception:
print_exc()
self._raiseEvent('onStateChange', {'oldState':'stop', 'newState':self.getStatus()['state']})
@@ -129,6 +130,11 @@ class Monty(QtCore.QObject):
print_exc()
return None
+ def repeat(self,val):
+ self._client.repeat(val)
+ def random(self,val):
+ self._client.random(val)
+
_retrMutex=QtCore.QMutex()
def _retrieve(self, method):
"""Makes sure only one call is made at a time to MPD."""
@@ -144,6 +150,7 @@ class Monty(QtCore.QObject):
def play(self, id):
"""Play song with ID $id."""
+ self._playCalled=True
if id!=None:
self._client.playid(id)
else:
@@ -157,7 +164,13 @@ class Monty(QtCore.QObject):
self._client.pause(0)
def next(self):
"""Move on to the next song in the playlist."""
- self._client.next()
+ 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()
@@ -269,6 +282,8 @@ class Monty(QtCore.QObject):
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:
diff --git a/clPlugin.py b/clPlugin.py
index 75e2b44..d750bfe 100644
--- a/clPlugin.py
+++ b/clPlugin.py
@@ -10,12 +10,16 @@ class Plugin:
settingsWidget=None
settings=None
winMain=None
+ loaded=None
def __init__(self, winMain, name):
self.name=name
self.winMain=winMain
+ self.loaded=False
- def getName(self):
+ def getName(self, lower=False):
+ if lower:
+ return self.name.lower()
return self.name
def getInfo(self):
return ''
@@ -26,7 +30,21 @@ class Plugin:
def setStatus(self, status):
self.winMain.setStatus(status)
- def getDockWidget(self, opts):
+ def load(self):
+ self._load()
+ opts=QtGui.QDockWidget.DockWidgetClosable|QtGui.QDockWidget.DockWidgetMovable
+ self.winMain.addDock(self.getDockWidget(opts))
+ self.loaded=True
+ def unload(self):
+ self._unload()
+ self.winMain.removeDock(self.getDockWidget())
+ self.dockWidget=None
+ self.settingsWidget=None
+ self.loaded=False
+ def isLoaded(self):
+ return self.loaded
+
+ def getDockWidget(self, opts=None):
try:
if not self.dockWidget:
self.dockWidget=self._getDockWidget()
@@ -103,5 +121,10 @@ class Plugin:
dock.setWidget(widget)
return dock
-
+ def _load(self):
+ """Override this one."""
+ return
+ def _unload(self):
+ """Override this one."""
+ return
diff --git a/clSong.py b/clSong.py
index d2111d7..971245a 100644
--- a/clSong.py
+++ b/clSong.py
@@ -6,6 +6,17 @@ class Song:
self._data=data
if 'id' in self._data:
self._data['id']=int(self._data['id'])
+ if 'track' in self._data:
+ # make sure the track is a valid number!
+ t=self._data['track']
+ for i in xrange(len(t)):
+ if ord(t[i])<ord('0') or ord(t[i])>ord('9'):
+ try:
+ self._data['track']=int(t[0:i])
+ except:
+ self._data['track']=-1
+ break
+ self._data['track']=int(self._data['track'])
def getID(self):
"""Get the ID."""
diff --git a/jmpc.py b/jmpc.py
new file mode 100644
index 0000000..31d2190
--- /dev/null
+++ b/jmpc.py
@@ -0,0 +1,98 @@
+import socket
+
+class MPDError(Exception):
+ pass
+class ConnectionError(MPDError):
+ pass
+class ProtocolError(MPDError):
+ pass
+class CommandError(MPDError):
+ pass
+class CommandListError(MPDError):
+ pass
+
+
+class client:
+ _csock=None
+ _rfile=None
+ _wfile=None
+ def __init__(self):
+ pass
+
+ def connect(self, host, port, type=socket.SOCK_STREAM):
+ if self._csock:
+ raise ConnectionError("Already connected")
+
+ self._csock=socket.socket(socket.AF_INET, type)
+ self._csock.connect((host, port))
+ self._file = self._csock.makefile("rwb")
+ handshake=self._read()
+ if handshake[0:len("hello jmpd")]!="hello jmpd":
+ print("Failed to handshake: %s" % (handshake))
+ self.disconnect()
+ return
+ self._write("ack")
+
+
+ def _read(self):
+ return self._file.readline().strip()
+ def _write(self, line):
+ self._file.write("%s\n" % (line))
+
+ def disconnect(self):
+ self._csock.close()
+ self._csock=None
+
+ def command_list_ok_begin(self):
+ pass
+
+ def command_list_end(self):
+ pass
+
+ def update(self):
+ pass
+
+ def status(self):
+ pass
+
+ def playid(id):
+ pass
+ def stop(self):
+ pass
+
+ def pause(self):
+ pass
+ def resume(self):
+ pass
+
+ def next(self):
+ pass
+ def previous(self):
+ pass
+
+ def seekid(time):
+ pass
+
+ # Playlist management
+ def add(path):
+ pass
+ def deleteid(path):
+ pass
+
+ # volume
+ def setvol(newvolume):
+ pass
+
+ # info
+ def listallinfo(self):
+ pass
+ def listplaylistinfo(self):
+ pass
+ def currentsong(self):
+ pass
+
+
+from test import port
+client=client()
+client.connect('localhost', port)
+client.disconnect()
diff --git a/jmpd.py b/jmpd.py
new file mode 100644
index 0000000..0d36ca0
--- /dev/null
+++ b/jmpd.py
@@ -0,0 +1,219 @@
+import socket
+from thread import start_new_thread
+from traceback import print_exc
+import codecs
+import os
+
+from mutagen.easyid3 import EasyID3
+
+from clSettings import Settings
+
+VERSION='0.1'
+MUSIC_DIRECTORY="music_directory"
+ENCODING="UTF-8"
+
+class server:
+ ssocket=None
+ _sockets=[]
+
+ def __init__(self):
+ self._jmpd=jmpd()
+ self._sockets=[]
+
+ def start(self, port, type=socket.SOCK_STREAM):
+ try:
+ self.ssocket=socket.socket(socket.AF_INET, type)
+ self._start_server(port)
+ except:
+ print_exc()
+ return
+
+ def _start_server(self, port):
+ self.ssocket.bind(('localhost', port))
+ self.ssocket.listen(5)
+ self.ssocket.settimeout(None)
+
+ print("Started jmpd v%s" % (VERSION))
+ while True:
+ csock,caddr=self.ssocket.accept()
+ self._sockets.append(csock)
+ #start_new_thread(self.handle, (csock, caddr))
+ self.handle(csock, caddr)
+
+ def _read(self, file):
+ line=""
+ while line=="":
+ line=file.readline()
+ return line.strip()
+ def _write(self, file, line):
+ file.write("%s\n" % (line))
+
+ def handle(self, csocket, caddr):
+ print("Incoming connection from %s" % (str(caddr)))
+ file=csocket.makefile('rw', 0)
+ self._write(file, 'hello jmpd %s'%(VERSION))
+
+ try:
+ while True:
+ line=self._read(file)
+ print line
+ except:
+ print_exc()
+
+ def closeAll(self):
+ for socket in self._sockets:
+ socket.close()
+
+def _getName(line):
+ return line[0:line.find(': ')]
+def _getValue(line):
+ return line[line.find(': ')+2:]
+
+tags=['title', 'artist', 'album', 'tracknumber', 'genre']
+class jmpd:
+ _lib=None # library
+ _settings=None
+
+ def __init__(self):
+ self._lib=[]
+ self._settings=Settings("mpdconf")
+ self.readLib()
+
+ def writeLib(self):
+ all_tags=tags
+ encoding=ENCODING
+ tags.append('filename')
+
+ fout=codecs.open(self._settings.get("db_file"), mode='w', encoding=encoding)
+ fout.write("# jmpd\n")
+ fout.write("begin info\n")
+ fout.write("encoding: %s\n" % (encoding))
+ fout.write("jmpd: %s\n" % (VERSION))
+ fout.write("end info\n")
+ fout.write("\n")
+
+ fout.write("begin library\n")
+ cur_path=""
+ for entry in self._lib:
+ if entry['path']!=cur_path:
+ fout.write("set path: %s\n" % entry['path'])
+ cur_path=entry['path']
+
+ fout.write("begin file\n")
+ for tag in all_tags:
+ if tag in entry:
+ fout.write("%s: %s\n" % (tag, entry[tag]))
+ fout.write("end file\n")
+
+ fout.write("end library\n")
+ fout.close()
+
+ def readLib(self):
+ """Restore the library from file"""
+ db_file=self._settings.get("db_file")
+ encoding=ENCODING
+ if os.path.exists(db_file)==False:
+ self._lib=[]
+ return
+
+ # we first open the file in ASCII-mode, and read on till the real encoding
+ # then we reopen it in that encoding
+ fin=open(db_file, 'r')
+ fin.close()
+ fin=codecs.open(db_file, mode='r', encoding=encoding)
+
+ self._readLine(fin) # begin info
+ encoding=_getValue(self._readLine(fin))
+ version=_getValue(self._readLine(fin))
+ if version!=VERSION:
+ raise Exception("Invalid database file")
+ self._readNext(fin, 'end info')
+
+ self._readNext(fin, 'begin library')
+ cur_path=""
+ while True:
+ line=self._readLine(fin)
+ if line=="end library":
+ break
+ if _getName(line)=="set path: ":
+ cur_path=_getValue(line)
+ elif line=="begin file":
+ self._readFile(fin, cur_path, encoding)
+
+ fin.close()
+
+ def _readFile(self, fin, cur_path, encoding):
+ """Read the data for a file"""
+ entry={'filepath': cur_path}
+ line=""
+ while line!="end file":
+ line=self._readLine(fin)
+ if line.find(': ')>0:
+ entry[_getName(line)]=_getValue(line)
+
+ self._lib.append(entry)
+ def _readEnd(self, fin, section):
+ """Read the next line containing an end section"""
+ line=""
+ while line!="end %s"%(section):
+ line=self._readLine(fin)
+ return line
+ def _readBegin(self, fin, section):
+ """Read the next line containing a begin section"""
+ line=""
+ while line!="begin %s"%(section):
+ line=self._readLine(fin)
+ return line
+
+ def _readNext(self, fin, find):
+ line=""
+ while line!=find:
+ line=self._readLine(fin)
+ return line
+ def _readLine(self, fin):
+ """Reads the next line having characters and not being a commented line"""
+ ret="\n"
+ try:
+ while ret=="\n" or ret[0]=='#':
+ ret=fin.readline()
+ except:
+ pass
+ if len(ret)==0:
+ raise Exception("EOF")
+ return ret.strip()
+
+
+ def update(self):
+ """Look for songs in the music_directory"""
+ self._lib=[]
+ for dir in self._settings.get(MUSIC_DIRECTORY).split(":"):
+ self._update_rec(dir)
+
+ self.writeLib()
+
+ def _update_rec(self, path):
+ subdirs=[]
+ for filename in os.listdir(path):
+ filepath="%s/%s"%(path,filename)
+ if os.path.isdir(filepath):
+ subdirs.append(filepath)
+ if filename[-4:]==".mp3":
+ audio=EasyID3(filepath)
+ entry = {'path': path.decode(ENCODING)
+ , 'filename': filename.decode(ENCODING)
+ }
+ for tag in tags:
+ try:
+ value=str(audio[tag])
+ entry[tag]=value[3:-2]
+ except:
+ entry[tag]=''
+ self._lib.append(entry)
+
+ for subdir in subdirs:
+ self._update_rec(subdir)
+
+from test import port
+s=server()
+s.start(port)
+
diff --git a/misc.py b/misc.py
index c5b0c2e..8b93326 100644
--- a/misc.py
+++ b/misc.py
@@ -1,4 +1,8 @@
from PyQt4 import QtCore, QtGui
+import re
+import urllib2, httplib, cookielib
+import socket
+socket.setdefaulttimeout(8)
appIcon=QtGui.QIcon('gfx/icon.png')
@@ -14,11 +18,112 @@ def sec2min(secs):
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 format(string, song=None, xtra_tags={}):
+ """Replace all tags in $str with their respective value."""
+ # what tags are available?
+ if song:
+ tags=song._data
+ else:
+ tags={}
+ for tag in xtra_tags:
+ tags[tag]=xtra_tags[tag]
+
+ ret=string
+ # first perform some functions: $name(value)
+ func=re.compile('\$[a-z]{2}\(', re.MULTILINE|re.IGNORECASE)
+ start=0
+ loops=20 # my stupidity is endless, so we better make sure to never always loop!
+ while True and loops>0:
+ loops-=1
+ match=func.search(ret[start:])
+ if not match:
+ break
+ # we've found a match!
+ # look for matching parenthesis!
+ start+=match.start() # we have to add, because we start search from $start!
+ end=ret.find('(', start)+1
+ balance=1
+ while balance>0 and end<len(ret):
+ if ret[end]==')':balance-=1
+ if ret[end]=='(':balance+=1
+ end+=1 # can do better
+ # whole function is in ret[start:end]
+ # find function-name
+ name=ret[start+1:ret.find('(', start)]
+ result=None # result of this function
+ if name=='if':
+ # $if(param,if-true)
+ comma=ret.find(',',start)
+ param1=ret[start+len('$if('):comma]
+ param2=ret[comma+1:end-1]
+ result=''
+ if param1[1:] in tags:
+ result=param2
+ else:
+ start+=1
+ if result!=None:
+ ret=("%s%s%s")%(ret[0:start], result, ret[end:])
+
+ # perform all replacements!
+ for tag in tags:
+ ret=ret.replace('$%s'%(tag), str(tags[tag]))
+ return ret
+
+def fetch(SE, sites, song=None, xtra_tags={}):
+ """Returns None when nothing found, or [site,source-url]."""
+ url=format(SE, song, xtra_tags)
+ url=url.replace(' ', '+')
+
+ request=urllib2.Request(url)
+ request.add_header('User-Agent', 'montypc')
+ opener=urllib2.build_opener()
+ data=opener.open(request).read()
+
+ # look for urls on the search page!
+ regex=re.compile('<a href="(.*?)".*?>.*?<\/a>')
+ urls=regex.findall(data)
+
+ # look for predefined urls, which are good lyrics-sites
+ # we assume they are in order of importance; the first one matching
+ # is taken
+ finalURL=None
+ finalRegex=None
+ for url in urls:
+ if finalURL:
+ break
+ for site in sites:
+ if url.find(site)>=0:
+ finalURL=url
+ finalRegex=sites[site]
+ break
+
+ match=None
+ if finalURL:
+ cj = cookielib.CookieJar()
+ opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
+ r = opener.open(finalURL)
+ data=r.read()
+ regex=re.compile(finalRegex, re.IGNORECASE|re.MULTILINE|re.DOTALL)
+ match=regex.search(data)
+ if match:
+ data=match.group(1)
+ data=data.replace('<br>', '<br />')
+ data=data.replace('<br />', '<br />')
+ data=data.strip()
+ return [data,finalURL]
+ return None
+
class Button(QtGui.QPushButton):
iconSize=32
"""A simple Button class which calls $onClick when clicked."""
diff --git a/montypc.py b/montypc.py
index 5b043dd..724401b 100644
--- a/montypc.py
+++ b/montypc.py
@@ -8,19 +8,11 @@ from winMain import winMain
from winSettings import winSettings
from clMonty import monty
from traceback import print_exc
-#import hotshot, hotshot.stats
try:
wMain = winMain()
wMain.show()
app.exec_()
- #prof=hotshot.Profile("profile")
- #prof.runcall(app.exec_)
- #prof.close()
- #stats=hotshot.stats.load("profile")
- #stats.strip_dirs()
- #stats.sort_stats('time','calls')
- #stats.print_stats(20)
except Exception, e:
print_exc()
diff --git a/mpdconf b/mpdconf
new file mode 100644
index 0000000..d719eeb
--- /dev/null
+++ b/mpdconf
@@ -0,0 +1,3 @@
+music_directory /media/maxtor/music/library/Alberto Alcalá
+#music_directory /media/maxtor/music/new/Isaac Hayes:/media/maxtor/music/new/Mule Variations
+db_file db.txt
diff --git a/plugins/AlbumCover.py b/plugins/AlbumCover.py
index e30d1e6..512302e 100644
--- a/plugins/AlbumCover.py
+++ b/plugins/AlbumCover.py
@@ -19,21 +19,27 @@ AC_DEFAULT_FILES='cover,album'
class wgAlbumCover(QtGui.QWidget):
" container for the image"
img=None
+ imgLoaded=False
def __init__(self,parent=None):
QtGui.QWidget.__init__(self,parent)
+ self.img=QtGui.QImage()
monty.addListener('onSongChange', self.onSongChange)
monty.addListener('onReady', self.onReady)
monty.addListener('onDisconnect', self.onDisconnect)
+ monty.addListener('onStateChange', self.onSongChange)
self.setMinimumSize(64,64)
def mousePressEvent(self, event):
self.onSongChange(None)
+ def getIMG(self):
+ if self.imgLoaded:
+ return self.img
+ return None
+
def paintEvent(self, event):
- if not self.img:
- return
l=min(self.width(),self.height())
p=QtGui.QPainter(self)
rect=QtCore.QRectF((self.width()-l)/2,0,l,l)
@@ -51,6 +57,7 @@ class wgAlbumCover(QtGui.QWidget):
def fetchCover(self, song):
# set default cover, in case all else fails!
+ self.imgLoaded=False
self.img.load('gfx/no-cd-cover.png')
for i in xrange(3):
src=settings.get('albumcover.fetch%i'%(i), str(i))
@@ -60,6 +67,7 @@ class wgAlbumCover(QtGui.QWidget):
# (download and) load in self.img from a source!
if self.fetchCoverSrc(song, src):
# ACK!
+ self.imgLoaded=True
break
self.update()
@@ -141,13 +149,11 @@ class wgAlbumCover(QtGui.QWidget):
return False
def onReady(self, params):
- self.img=QtGui.QImage()
-
self.onSongChange(None)
self.update()
def onDisconnect(self, params):
- self.img=None
+ self.img.load('')
self.update()
@@ -155,6 +161,7 @@ class pluginAlbumCover(Plugin):
o=None
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'AlbumCover')
+ def _load(self):
self.o=wgAlbumCover(None)
def getInfo(self):
return "Display the album cover of the currently playing album."
@@ -171,6 +178,9 @@ class pluginAlbumCover(Plugin):
" albumcover.files: comma separated list of filenames (without extension)to be considered an album cover. The extensions from albumcover.gfx.ext are used.\n" \
" albumcover.gfx.ext: comma separated list of gfx-file extensions.\n"
+ def getWidget(self):
+ return self.o
+
def _getDockWidget(self):
return self._createDock(self.o)
diff --git a/plugins/Library.py b/plugins/Library.py
index 4f82c2e..c4e9dc7 100644
--- a/plugins/Library.py
+++ b/plugins/Library.py
@@ -4,15 +4,15 @@ from clPlugin import *
from misc import *
from wgPlaylist import Playlist
from wgSongList import clrRowSel
+from clSettings import settings
class pluginLibrary(Plugin):
o=None
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'Library')
- self.o=Playlist(winMain, self, ['song'], 'The Library'
+ def _load(self):
+ self.o=Playlist(self.winMain, self, ['song'], 'Library'
, self.onDoubleClick, self.onKeyPress)
- self.o.showColumn(0,False)
- self.o.setMode('library', 'artist/album')
def getInfo(self):
return "List showing all the songs allowing filtering and grouping."
diff --git a/plugins/Lyrics.py b/plugins/Lyrics.py
index 895a401..9cb0b4e 100644
--- a/plugins/Lyrics.py
+++ b/plugins/Lyrics.py
@@ -1,8 +1,5 @@
from PyQt4 import QtGui,QtCore
-import urllib2, httplib, cookielib
-import socket
-socket.setdefaulttimeout(8)
import re
from thread import start_new_thread
from traceback import print_exc
@@ -104,64 +101,31 @@ class wgLyrics(QtGui.QWidget):
if line.strip():
sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip()
# construct URL to search!
- url=settings.get('lyrics.engine', LY_DEFAULT_ENGINE)
- url=url.replace('$artist', song.getArtist())
- url=url.replace('$title', song.getTitle()).replace('$album', song.getAlbum())
- url=url.replace(' ', '+')
+ SE=settings.get('lyrics.engine', LY_DEFAULT_ENGINE)
try:
- request=urllib2.Request(url)
- request.add_header('User-Agent', 'montypc')
- opener=urllib2.build_opener()
- data=opener.open(request).read()
-
- # look for urls!
- regex=re.compile('<a href="(.*?)".*?>.*?<\/a>')
- urls=regex.findall(data)
-
- # look for predefined urls, which are good lyrics-sites
- finalURL=None
- finalRegex=None
- for url in urls:
- if finalURL:
- break
- for site in sites:
- if url.find(site)>=0:
- finalURL=url
- finalRegex=sites[site]
- break
-
- match=None
- QtCore.QCoreApplication.postEvent(self, ResetEvent(song))
- #print finalURL
- if finalURL:
- cj = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
- r = opener.open(finalURL)
- data=r.read()
- regex=re.compile(finalRegex, re.IGNORECASE|re.MULTILINE|re.DOTALL)
- match=regex.search(data)
- #print match
- if match:
- data=match.group(1)
- data=data.replace('<br>', '<br />')
- data=data.replace('<br />', '<br />')
- data=data.strip()
+ ret=fetch(SE, sites, song, {})
+ if ret:
+ txt='%s<br /><br /><a href="%s">%s</a>'%(ret[0],ret[1],ret[1])
# save for later use!
if lyFName:
# we can't save if the path isn't correct
- file=open(lyFName, 'w')
- file.write(data)
- file.close()
- data='%s<br /><br />(source: <a href="%s">%s</a>)'%(data,finalURL,finalURL)
+ try:
+ file=open(lyFName, 'w')
+ file.write(ret[0])
+ file.close()
+ except:
+ # probably a wrong path!
+ pass
else:
- data='Lyrics not found :\'('
-
- QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(data))
+ txt="No lyrics found :'("
+ QtCore.QCoreApplication.postEvent(self, ResetEvent(song))
+ QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(txt))
except:
print_exc()
QtCore.QCoreApplication.postEvent(self, ResetEvent(song))
- QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Oh noes, an error occured while fetching lyrics!'))
+ QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, site unavailable!'\
+ '<br />You have an internet connection?'))
self._fetchCnt=0
def onDisconnect(self, params):
@@ -178,6 +142,7 @@ class pluginLyrics(Plugin):
o=None
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'Lyrics')
+ def _load(self):
self.o=wgLyrics(None)
def getInfo(self):
return "Show (and fetch) the lyrics of the currently playing song."
diff --git a/plugins/Notify.py b/plugins/Notify.py
new file mode 100644
index 0000000..d995d1c
--- /dev/null
+++ b/plugins/Notify.py
@@ -0,0 +1,145 @@
+from PyQt4 import QtGui,QtCore
+from thread import start_new_thread
+from traceback import print_exc
+
+from misc import *
+from clSettings import settings,mpdSettings
+from clMonty import monty
+from clPlugin import *
+
+class winNotify(QtGui.QWidget):
+ _timerID=None
+ resizeWindow=True
+ winMain=None
+
+ # data used for showing off
+ timer=None
+ msg=None
+ song=None
+ xtra_tags=None
+
+ def __init__(self, winMain, parent=None):
+ QtGui.QWidget.__init__(self, parent)
+ self.winMain=winMain
+
+ self.setWindowFlags(QtCore.Qt.ToolTip)
+ self.setWindowOpacity(0.7)
+
+ font=QtGui.QFont()
+ font.setPixelSize(30)
+ font.setFamily('Comic Sans Ms')
+ self.setFont(font)
+
+ monty.addListener('onSongChange', self.onSongChange)
+ monty.addListener('onReady', self.onReady)
+ monty.addListener('onDisconnect', self.onDisconnect)
+ monty.addListener('onStateChange', self.onStateChange)
+ monty.addListener('onVolumeChange', self.onVolumeChange)
+
+ def onSongChange(self, params):
+ self.show('$if($artist,$artist$if($album, [$album #$track])\n$title ($length))', monty.getCurrentSong())
+
+ def onReady(self, params):
+ self.show('montypc loaded!')
+
+ def onDisconnect(self, params):
+ self.show('Disconnected!')
+
+ def onStateChange(self, params):
+ self.show(params['newState'])
+
+ def onVolumeChange(self, params):
+ self.show('Volume: %i%%'%(params['newVolume']))
+
+ def mousePressEvent(self, event):
+ self.hide()
+
+ def show(self, msg, song=None, xtra_tags={}):
+ if not self.isVisible():
+ self.setVisible(True)
+ self.resizeWindow=True
+ self.msg=msg
+ self.song=song
+ self.xtra_tags=xtra_tags
+ if self._timerID:
+ self.killTimer(self._timerID)
+ self._timerID=self.startTimer(200)
+ self.timer=11
+ self.raise_()
+ self.timerEvent(None)
+
+ def hide(self):
+ if self._timerID:
+ self.killTimer(self._timerID)
+ self._timerID=None
+ self.setHidden(True)
+
+
+ def centerH(self):
+ screen = QtGui.QDesktopWidget().screenGeometry()
+ size = self.geometry()
+ self.move((screen.width()-size.width())/2, 100)
+ def paintEvent(self, event):
+ p=QtGui.QPainter(self)
+ margin=3 # margin in pixels
+ spacing=10 # space between album cover and text
+
+ # determine the Rect our message must fit in
+ txt="%s"%(format(self.msg, self.song, self.xtra_tags))
+ rect=p.boundingRect(0,0,1,1, QtCore.Qt.AlignHCenter, txt)
+
+ # check if 1/ albumcover plugin is loaded, and 2/ there is an
+ # album cover
+ width=0
+ try:
+ cover=self.winMain.getPlugin('albumcover')
+ img=cover.getWidget().getIMG()
+ if img:
+ width=128
+ else:
+ spacing=0
+ except:
+ img=None
+ pass
+
+ # do we have to resize?
+ if self.resizeWindow:
+ self.resizeWindow=False
+ self.resize(rect.width()+width+margin*3+spacing, max(width,rect.height())+margin*2)
+ self.centerH()
+
+ # fill up with a nice color :)
+ p.fillRect(QtCore.QRect(0,0,self.width(),self.height()), QtGui.QBrush(QtGui.QColor(230,230,255)))
+
+ # draw album cover if necessary
+ if img:
+ rImg=QtCore.QRectF(margin,margin,width,width)
+ p.drawImage(rImg, img)
+
+ # Pen In Black ...
+ p.setPen(QtCore.Qt.black)
+ rect=p.boundingRect(width+margin+spacing,margin,
+ rect.width(),self.height(), QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter, txt)
+ p.drawText(rect, QtCore.Qt.AlignHCenter, txt)
+
+ def timerEvent(self, event):
+ self.timer-=1
+ if self.timer<=0:
+ self.hide()
+ self.update()
+
+
+
+class pluginNotify(Plugin):
+ o=None
+ def __init__(self, winMain):
+ Plugin.__init__(self, winMain, 'Notify')
+ def _load(self):
+ self.o=winNotify(self.winMain)
+ def getInfo(self):
+ return "Show interesting events in a popup window."
+
+ def _getSettings(self):
+ return []
+ def afterSaveSettings(self):
+ self.o.onSongChange(None)
diff --git a/plugins/PlayControl.py b/plugins/PlayControl.py
index 06952e6..abe4547 100644
--- a/plugins/PlayControl.py
+++ b/plugins/PlayControl.py
@@ -3,6 +3,22 @@ from PyQt4 import QtGui, QtSvg, QtCore
from misc import *
from clMonty import monty
from clPlugin import *
+from thread import start_new_thread
+from random import randint
+
+# Some predefined constants.
+# Note that REPEAT precedes RANDOM. E.g. if repeat
+# is ALBUM, and random is SONG, then it'll take a random song
+# from the current album ...
+PC_RANDOM_NO=0 # no randomness
+PC_RANDOM_SONG=1 # choose a song random from the playlist
+PC_RANDOM_ALBUM=2 # choose a random album, and play that fully
+
+PC_REPEAT_NO=0 # no repeat
+PC_REPEAT_SONG=1 # repeat current song
+PC_REPEAT_ALBUM=2 # repeat current album
+PC_REPEAT_PLAYLIST=3 # repeat playlist
+
class wgPlayControl(QtGui.QWidget):
"""Displays controls for interacting with playing, like play, volume ..."""
@@ -20,6 +36,12 @@ class wgPlayControl(QtGui.QWidget):
" all objects in this widget"
objects=None
+ cmbRepeat=None
+ cmbShuffle=None
+
+ " contains the songs of the album the current song is playing. None, if the album is not set"
+ curAlbumSongs=None
+
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
@@ -56,11 +78,28 @@ class wgPlayControl(QtGui.QWidget):
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.cmbShuffle=QtGui.QComboBox(self)
+ self.cmbShuffle.addItem('Don\'t play dices')
+ self.cmbShuffle.addItem('Random song')
+ self.cmbShuffle.addItem('Random album')
+ self.cmbShuffle.setCurrentIndex(int(settings.get('playcontrol.shuffle', PC_RANDOM_SONG)))
+
+ self.cmbRepeat=QtGui.QComboBox(self)
+ self.cmbRepeat.addItem('No repeat')
+ self.cmbRepeat.addItem('Repeat current song')
+ self.cmbRepeat.addItem('Repeat album')
+ self.cmbRepeat.addItem('Playlist')
+ self.cmbRepeat.setCurrentIndex(int(settings.get('playcontrol.repeat', PC_REPEAT_PLAYLIST)))
+
self.objects=[self.slrVolume, self.slrTime,
self.btnStop, self.btnNext, self.btnPrevious]
layout=QtGui.QHBoxLayout(parent)
- self.setLayout(layout)
+ layout2=QtGui.QHBoxLayout(parent)
+ layoutWidget=QtGui.QVBoxLayout(parent)
+ layoutWidget.addLayout(layout)
+ layoutWidget.addLayout(layout2)
+ self.setLayout(layoutWidget)
layout.addWidget(self.btnPrevious)
layout.addWidget(self.btnPlayPause)
@@ -69,12 +108,18 @@ class wgPlayControl(QtGui.QWidget):
layout.addWidget(self.slrTime)
layout.addWidget(self.slrVolume)
layout.addWidget(self.svgVolume)
+
+ layout2.addWidget(self.cmbRepeat)
+ layout2.addWidget(self.cmbShuffle)
self.connect(self.slrVolume, QtCore.SIGNAL('valueChanged(int)'),self.onVolumeSliderChange)
self.connect(self.slrTime, QtCore.SIGNAL('sliderReleased()'),self.onTimeSliderChange)
+ self.connect(self.cmbRepeat, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbRepeatChanged)
+ self.connect(self.cmbShuffle, QtCore.SIGNAL('currentIndexChanged(int)'),self.onCmbShuffleChanged)
+
monty.addListener('onStateChange', self.onStateChange)
- monty.addListener('onSongChange', self.onSongChange)
+ monty.addListener('beforeSongChange', self.beforeSongChange)
monty.addListener('onSongChange', self.onSongChange)
monty.addListener('onVolumeChange', self.onVolumeChange)
monty.addListener('onReady', self.onStateChange)
@@ -105,7 +150,68 @@ class wgPlayControl(QtGui.QWidget):
self.slrTime.setEnabled(True)
except:
pass
+ # look in another thread for the songs in the current album!
+ params=()
+ start_new_thread(self.findAlbumSongs, params)
+ def beforeSongChange(self, params):
+ nextID=None
+ song=monty.getCurrentSong()
+ # decide here what next song to play!
+ repeat=self.cmbRepeat.currentIndex()
+ random=self.cmbShuffle.currentIndex()
+ # is the current song the last of the album?
+ eofAlbum=int(song.getTrack())==int(self.curAlbumSongs[-1].getTrack())
+ if repeat==PC_REPEAT_NO:
+ # no repeat, nothing to see here! Pass on!
+ pass
+ elif repeat==PC_REPEAT_SONG:
+ # we must repeat the previous song!
+ nextID=params['curSongID']
+ elif repeat==PC_REPEAT_ALBUM:
+ # check if we are at the last track, if it is, we must start a new!
+ if eofAlbum:
+ nextID=self.curAlbumSongs[0].getID()
+ elif repeat==PC_REPEAT_PLAYLIST:
+ # repeating the playlist is handled by monty itself;
+ # it is set in onCmbRepeatChanged.
+ pass
+
+ if random==PC_RANDOM_NO:
+ # just follow our leader Monty.
+ pass
+ elif random==PC_RANDOM_SONG:
+ # pick a random song! This depends on what repeat-mode we're in.
+ if repeat==PC_REPEAT_NO or repeat==PC_REPEAT_PLAYLIST:
+ # we don't repeat anything, so we can just let monty pick the
+ # next random one!
+ pass
+ elif repeat==PC_REPEAT_SONG:
+ # song repeat; don't choose the next one!
+ nextID=params['curSongID']
+ elif repeat==PC_REPEAT_ALBUM and self.curAlbumSongs:
+ # pick random song from current album
+ nextID=self.curAlbumSongs[randint(0,len(self.curAlbumSongs)-1)].getID()
+ elif random==PC_RANDOM_ALBUM:
+ # pick a random album! This means, we pick the first song of a random
+ # album.
+ if eofAlbum and (repeat==PC_REPEAT_PLAYLIST or repeat==PC_REPEAT_NO):
+ # all first songs of an album
+ albums=filter(lambda s: s.getAlbum() and s.getTrack()==1, monty.listPlaylist())
+ nextID=albums[randint(0,len(albums)-1)].getID()
+
+ if nextID!=None:
+ monty.play(nextID)
+
+ def findAlbumSongs(self):
+ """This method looks for the songs in the album of current playing song."""
+ song=monty.getCurrentSong()
+ self.curAlbumSongs=None
+ if not song or not song.getAlbum():
+ return
+ self.curAlbumSongs=filter(lambda s: s.getArtist()==song.getArtist()
+ and s.getAlbum()==song.getAlbum(), monty.listPlaylist())
+ self.curAlbumSongs.sort(numeric_compare)
def onBtnPlayPauseClick(self):
status=monty.getStatus()
@@ -135,13 +241,26 @@ class wgPlayControl(QtGui.QWidget):
else:
mode=('0', 'min', 'med', 'max')[int(3*v/100)]
self.svgVolume.load('gfx/stock_volume-%s.svg'%(mode))
-
+
+ def onCmbRepeatChanged(self, newval):
+ settings.set('playcontrol.repeat', newval)
+ if newval==PC_REPEAT_PLAYLIST:
+ monty.repeat(1)
+ else:
+ monty.repeat(0)
+ def onCmbShuffleChanged(self, newval):
+ settings.set('playcontrol.shuffle', newval)
+ if newval==PC_RANDOM_SONG:
+ monty.random(1)
+ else:
+ monty.random(0)
class pluginPlayControl(Plugin):
o=None
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'PlayControl')
+ def _load(self):
self.o=wgPlayControl(None)
def getInfo(self):
return "Have total control over the playing!"
diff --git a/plugins/Playlist.py b/plugins/Playlist.py
index 1a13545..81c92ad 100644
--- a/plugins/Playlist.py
+++ b/plugins/Playlist.py
@@ -4,14 +4,15 @@ from clPlugin import *
from misc import *
from wgPlaylist import Playlist
from wgSongList import clrRowSel
+from clSettings import settings
class pluginPlaylist(Plugin):
o=None
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'Playlist')
- self.o=Playlist(winMain, self, ['artist', 'title', 'album'], 'The Playlist'
+ def _load(self):
+ self.o=Playlist(self.winMain, self, ['artist', 'title', 'track', 'album'], 'Playlist'
, self.onDoubleClick, self.onKeyPress)
- self.o.setMode('playlist')
monty.addListener('onSongChange', self.onSongChange)
def getInfo(self):
return "The playlist showing the songs that will be played."
diff --git a/plugins/Shortcuts.py b/plugins/Shortcuts.py
new file mode 100644
index 0000000..f8956f5
--- /dev/null
+++ b/plugins/Shortcuts.py
@@ -0,0 +1,48 @@
+from PyQt4 import QtGui, QtCore
+
+from Xlib.display import Display
+from Xlib import X
+from Xlib.XK import *
+from thread import start_new_thread
+
+from clPlugin import Plugin
+from clSettings import settings
+
+
+class pluginShortcuts(Plugin):
+ display=None
+ root=None
+ def __init__(self, winMain):
+ global keycodes
+ Plugin.__init__(self, winMain, 'Shortcuts')
+
+ def _load(self):
+ return
+ self.disp=Display()
+ self.root=self.disp.screen().root
+ self.root.change_attributes(event_mask=X.KeyPressMask)
+ #for keycode,name in enumerate(keycodes):
+ #print keycode
+ #self.root.grab_key(keycode, X.AnyModifier, 0, X.GrabModeAsync, X.GrabModeAsync)
+ start_new_thread(self.keyHandler, (None,))
+
+ def err(self,param=None,param2=None):
+ print param
+ print param2
+
+ def getInfo(self):
+ return "Control playing using shortcuts."
+
+ def _getSettings(self):
+ return []
+ return [
+ ['shortcuts.playpauze', 'Play/pauze', 'Key to start playing/pauze.', QtGui.QLineEdit(settings.get('shortcuts.playpauze', 'Win+Home'))],
+ ]
+ def afterSaveSettings(self):
+ pass
+
+ def keyHandler(self, emptyParam):
+ while 1:
+ event=self.root.display.next_event()
+ keycode=event.detail
+ print keycode
diff --git a/plugins/SongStatus.py b/plugins/SongStatus.py
index 65e1866..a03476e 100644
--- a/plugins/SongStatus.py
+++ b/plugins/SongStatus.py
@@ -4,10 +4,10 @@ from clPlugin import *
from traceback import print_exc
SS_DEFAULT_FORMAT='<font size="4">now $state</font>'\
-'<font size="8" color="blue">$title</font>'\
+'$if($title,<font size="8" color="blue">$title</font>'\
'<br />by <font size="8" color="green">$artist</font>'\
-'<br /><font size="5" color="red">[$album # $track]</font>'\
-'<br /><font size="4">$time/$length</font>'
+'<br /><font size="5" color="red">[$album # $track]</font>)'\
+'$if($length,<br /><font size="4">$time/$length</font>)'
class wgSongStatus(QtGui.QWidget):
"""Displays the status of the current song, if playing."""
@@ -34,21 +34,16 @@ class wgSongStatus(QtGui.QWidget):
status=monty.getStatus()
song=monty.getCurrentSong()
- values={'state':'', 'time':'', 'length':'', 'title':'', 'artist':'', 'album':'', 'track':''}
+ values={'state':''}
try:
values['state']={'play':'playing', 'stop':'stopped', 'pause':'paused'}[status['state']]
- values['time']=sec2min(status['time'])
- values['length']=sec2min(status['length'])
- values['title']=song.getTitle()
- values['artist']=song.getArtist()
- values['album']=song.getAlbum()
- values['track']=song.getTrack()
+ if 'time' in status:
+ values['length']=sec2min(status['length'])
+ values['time']=sec2min(status['time'])
except:
pass
- txt=settings.get('songstatus.format', SS_DEFAULT_FORMAT)
- for key in values:
- txt=txt.replace('$%s'%(key), values[key])
+ txt=format(settings.get('songstatus.format', SS_DEFAULT_FORMAT), song, values)
self.lblInfo.setText(txt)
def text(self):
@@ -58,6 +53,7 @@ class pluginSongStatus(Plugin):
o=None
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'SongStatus')
+ def _load(self):
self.o=wgSongStatus(None)
def getInfo(self):
return "Show information about the current song."
diff --git a/plugins/Systray.py b/plugins/Systray.py
index 5f0f38f..3352585 100644
--- a/plugins/Systray.py
+++ b/plugins/Systray.py
@@ -8,10 +8,13 @@ class pluginSystray(Plugin):
def __init__(self, winMain):
Plugin.__init__(self, winMain, 'Systray')
- self.o=QtGui.QSystemTrayIcon(appIcon, winMain)
+ def _load(self):
+ self.o=QtGui.QSystemTrayIcon(appIcon, self.winMain)
self.o.show()
- winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)')
+ self.winMain.connect(self.o, QtCore.SIGNAL('activated (QSystemTrayIcon::ActivationReason)')
, self.onSysTrayClick)
+ def _unload(self):
+ self.o.hide()
def getInfo(self):
return "Display the montypc icon in the systray."
diff --git a/plugins/Tabs.py b/plugins/Tabs.py
new file mode 100644
index 0000000..12ae0e7
--- /dev/null
+++ b/plugins/Tabs.py
@@ -0,0 +1,161 @@
+from PyQt4 import QtGui,QtCore
+
+import re
+from thread import start_new_thread
+from traceback import print_exc
+
+from misc import *
+from clSettings import settings,mpdSettings
+from clMonty import monty
+from clPlugin import *
+
+class ResetEvent(QtCore.QEvent):
+ song=None
+ def __init__(self, song=None):
+ QtCore.QEvent.__init__(self,QtCore.QEvent.User)
+ self.song=song
+class AddHtmlEvent(QtCore.QEvent):
+ html=None
+ def __init__(self,html):
+ QtCore.QEvent.__init__(self,QtCore.QEvent.User)
+ self.html=html
+
+TABS_DEFAULT_ENGINE='http://www.google.com/search?q=tabs|chords+"$artist"+"$title"'
+TABS_DEFAULT_SITES='azchords.com <pre>(.*?)</pre>\n'\
+ 'fretplay.com <P CLASS="tabs">(.*?)</P>\n'\
+ 'guitaretab.com <pre style="COLOR: #000000; FONT-SIZE: 11px;">(.*)?</pre>'\
+
+class wgTabs(QtGui.QWidget):
+ " contains the tabs"
+ txt=None
+ def __init__(self, parent=None):
+ QtGui.QWidget.__init__(self, parent)
+ self.txt=QtGui.QTextEdit(parent)
+ self.txt.setReadOnly(True)
+
+ layout=QtGui.QVBoxLayout()
+ layout.addWidget(self.txt)
+ self.setLayout(layout)
+
+ monty.addListener('onSongChange', self.onSongChange)
+ monty.addListener('onReady', self.onReady)
+ monty.addListener('onDisconnect', self.onDisconnect)
+
+ def onSongChange(self, params):
+ song=monty.getCurrentSong()
+ try:
+ song._data['file']
+ except:
+ self.resetTxt()
+ return
+
+ self.resetTxt(song)
+ start_new_thread(self.fetchTabs, (song,))
+
+ def customEvent(self, event):
+ if isinstance(event,ResetEvent):
+ self.resetTxt(event.song)
+ elif isinstance(event,AddHtmlEvent):
+ self.txt.insertHtml(event.html)
+
+ def onReady(self, params):
+ self.onSongChange(None)
+
+ _mutex=QtCore.QMutex()
+ _fetchCnt=0
+ def fetchTabs(self, song):
+ # only allow 1 instance to look tabs!
+ self._mutex.lock()
+ if self._fetchCnt:
+ self._mutex.unlock()
+ return
+ self._fetchCnt=1
+ self._mutex.unlock()
+
+ QtCore.QCoreApplication.postEvent(self, ResetEvent(song))
+
+ # save the data to file!
+ save_dir=settings.get('tabs.dir', '/holy_grail')
+ fInfo=QtCore.QFileInfo(save_dir)
+ if fInfo.isDir():
+ tabsFName='%s/%s - %s.txt'%(save_dir,song.getArtist(),song.getTitle())
+ else:
+ tabsFName=None
+ # does the file exist? if yes, read that one!
+ try:
+ # we have it: load, and return!
+ file=open(tabsFName, 'r')
+ QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(file.read()))
+ file.close()
+ self._fetchCnt=0
+ return
+ except:
+ pass
+
+ # fetch from inet
+ QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('<i>Searching tabs ...</i>'))
+
+ lines=settings.get('tabs.sites', TABS_DEFAULT_SITES).split('\n')
+ sites={}
+ for line in lines:
+ if line.strip():
+ sites[line[0:line.find('\t')]]=line[line.find('\t'):].strip()
+ # construct URL to search!
+ SE=settings.get('tabs.engine', TABS_DEFAULT_ENGINE)
+ try:
+ ret=fetch(SE, sites, song, {})
+ if ret:
+ txt='<pre>%s<br /><br /><a href="%s">%s</a></pre>'%(ret[0],ret[1],ret[1])
+ # save for later use!
+ if tabsFName:
+ # we can't save if the path isn't correct
+ try:
+ file=open(tabsFName, 'w')
+ file.write(ret[0])
+ file.close()
+ except:
+ pass
+ else:
+ txt="No tabs found :'("
+
+ QtCore.QCoreApplication.postEvent(self, ResetEvent(song))
+ QtCore.QCoreApplication.postEvent(self, AddHtmlEvent(txt))
+ except:
+ print_exc()
+ QtCore.QCoreApplication.postEvent(self, ResetEvent(song))
+ QtCore.QCoreApplication.postEvent(self, AddHtmlEvent('Woops, site unavailable!'\
+ '<br />You have an internet connection?'))
+ self._fetchCnt=0
+
+ def onDisconnect(self, params):
+ self.resetTxt()
+
+ def resetTxt(self, song=None):
+ self.txt.clear()
+ if song:
+ self.txt.insertHtml('<b>%s</b>\n<br /><u>%s</u><br />'\
+ '<br />\n\n'%(song.getTitle(), song.getArtist()))
+
+
+class pluginTabs(Plugin):
+ o=None
+ def __init__(self, winMain):
+ Plugin.__init__(self, winMain, 'Tabs')
+ def _load(self):
+ self.o=wgTabs(None)
+ def getInfo(self):
+ return "Show (and fetch) the tabs of the currently playing song."
+
+ def _getDockWidget(self):
+ return self._createDock(self.o)
+
+ def _getSettings(self):
+ sites=QtGui.QTextEdit()
+ sites.insertPlainText(settings.get('tabs.sites', TABS_DEFAULT_SITES))
+ return [
+ ['tabs.engine', 'Search engine', 'The URL that is used to search. $artist, $title and $album are replaced in the URL.', QtGui.QLineEdit(settings.get('tabs.engine', TABS_DEFAULT_ENGINE))],
+ ['tabs.sites', 'Sites & regexes', 'This field contains all sites, together with the regex needed to fetch the tabs.\nEvery line must look like this: $domain $regex-start(.*?)$regex-end\n$domain is the domain of the tabs website, $regex-start is the regex indicating the start of the tabs, $regex-end indicates the end. E.g. footabs.org <tabs>(.*?)</tabs>', sites],
+ ['tabs.dir', 'Tabs directory', 'Directory where tabs should be stored and retrieved.', QtGui.QLineEdit(settings.get('tabs.dir'))],
+ ]
+ def afterSaveSettings(self):
+ self.o.onSongChange(None)
diff --git a/plugins/__init__.py b/plugins/__init__.py
index c0db019..d83b441 100644
--- a/plugins/__init__.py
+++ b/plugins/__init__.py
@@ -9,5 +9,13 @@ for file in os.listdir('plugins'):
name=file[:-3] # name without ext
pkg='plugins.%s'%(name) # pkg name
pluginName='plugin%s'%(name) # classname
- __import__(pkg, globals(), locals(), pluginName, -1)
- _plugins.append([pkg, pluginName])
+
+ module=__import__(pkg, globals(), locals(), pluginName, -1)
+ try:
+ # check here if the file contains an entity (let's hope
+ # a class!) with $pluginName!
+ module.__dict__[pluginName]
+ _plugins.append([pkg, pluginName])
+ except:
+ #print "Failed to load plugin "+pluginName
+ pass
diff --git a/amazon_cover_fetcher.py b/plugins/amazon_cover_fetcher.py
index 2de0181..2de0181 100644
--- a/amazon_cover_fetcher.py
+++ b/plugins/amazon_cover_fetcher.py
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..7c82438
--- /dev/null
+++ b/test.py
@@ -0,0 +1,19 @@
+from thread import start_new_thread
+from traceback import print_exc
+
+#import jmpd
+#import jmpc
+
+port=6600
+
+#server=jmpd.server()
+#start_new_thread(server.start, (port,))
+
+#try:
+ #client=jmpc.client()
+ #client.connect('localhost', port)
+#except:
+ #print_exc()
+
+#server.closeAll()
+#client.close()
diff --git a/wgPlaylist.py b/wgPlaylist.py
index 0b670e4..a6771c0 100644
--- a/wgPlaylist.py
+++ b/wgPlaylist.py
@@ -3,6 +3,7 @@ from PyQt4 import QtGui, QtCore
from wgSongList import SongList
from misc import *
+from clSettings import settings
class Playlist(QtGui.QWidget):
@@ -33,7 +34,11 @@ class Playlist(QtGui.QWidget):
self.cmbMode.addItem('genre')
self.cmbMode.addItem('genre/artist')
self.cmbMode.addItem('genre/artist/album')
- self.lstSongs=SongList(parent, headers, onDoubleClick)
+ self.lstSongs=SongList(parent, self.name, headers, onDoubleClick)
+
+ i=int(settings.get("l%s.mode"%(self.name), 0))
+ self.cmbMode.setCurrentIndex(i)
+ self.onModeChangeChange(str(self.cmbMode.itemText(i)))
frame=QtGui.QVBoxLayout()
frame.addWidget(self.cmbMode)
@@ -97,6 +102,7 @@ class Playlist(QtGui.QWidget):
self.lstSongs.setMode('playlist')
else:
self.lstSongs.setMode('library', str(value))
+ settings.set("l%s.mode"%(self.name), self.cmbMode.currentIndex())
self.filter()
def showColumn(self, column, show=True):
diff --git a/wgSongList.py b/wgSongList.py
index bf9d197..cac8b2c 100644
--- a/wgSongList.py
+++ b/wgSongList.py
@@ -5,6 +5,7 @@ from traceback import print_exc
from misc import *
from clSong import Song
+from clSettings import settings
# constants used for fSongs
LIB_ROW=0
@@ -79,13 +80,16 @@ class SongList(QtGui.QWidget):
wgGfxArtist=QtSvg.QSvgRenderer('gfx/user_icon.svg')
- def __init__(self, parent, headers, onDoubleClick):
+ def __init__(self, parent, name, headers, onDoubleClick):
QtGui.QWidget.__init__(self, parent)
self.onDoubleClick=onDoubleClick
# we receive an array of strings; we convert that to an array of (header, width)
- self.headers=map(lambda h: [h, 250, True], headers)
- self.headers.insert(0, ['id', 30, True])
+ self.name=name
+ # load the headers, and fetch from the settings the width and visibility
+ self.headers=map(lambda h: [h, int(settings.get('l%s.%s.width'%(self.name,h),250))
+ , settings.get('l%s.%s.visible'%(self.name,h),'1')=='1'], headers)
+ self.headers.insert(0, ['id', 30, settings.get('l%s.%s.visible'%(self.name,'id'),'0')=='1'])
self.songs=None
self.numSongs=None
self.fSongs=None
@@ -130,6 +134,9 @@ class SongList(QtGui.QWidget):
def sizeHint(self):
return QtCore.QSize(10000,10000)
+ def getSongs(self):
+ return self.songs
+
def setMode(self, mode, groupBy=''):
self.selRows=[]
self.selIDs=[]
@@ -555,10 +562,13 @@ class SongList(QtGui.QWidget):
self.update()
elif self.resizeCol!=None:
+ # store this new width!
+ self.saveColumnWidth(self.resizeCol)
# we're not resizing anymore!
self.resizeCol=None
self.update()
-
+ def saveColumnWidth(self, col):
+ settings.set('l%s.%s.width'%(self.name,self.headers[col][0]), self.headers[col][1])
def mouseDoubleClickEvent(self, event):
pos=event.pos()
row=self._pos2row(pos)
@@ -599,6 +609,7 @@ class SongList(QtGui.QWidget):
rect=p.boundingRect(10,10,1,1, QtCore.Qt.AlignLeft, str(song.getTag(hdr)))
w=max(rect.width(), w)
self.headers[self.resizeColumn][1]=w+2*margin
+ self.saveColumnWidth(self.resizeColumn)
self.resizeColumn=None
self.resizeEvent(None)
if self.redrawID!=None:
@@ -849,7 +860,7 @@ class SongList(QtGui.QWidget):
p.drawRect(QtCore.QRect(1,1,self.width()-3,self.height()-3))
self._paintCnt=0
-
+
#text='%s - %s' % (self.selMiscs, '')
#text='%s - %s' % (str(self.selRows), str(self.selIDs))
#r=QtCore.QRect(10,self.height()-40,self.width()-20,20)
diff --git a/winMain.py b/winMain.py
index b34d069..d4cb3bf 100644
--- a/winMain.py
+++ b/winMain.py
@@ -23,10 +23,12 @@ class winMain(QtGui.QMainWindow):
"""The winMain class is mpc's main window, showing the playlists and control-interface"""
" list of plugins that are loaded. name=>Plugin"
plugins={}
+ docks=[]
" menus"
mConnect=None
mDisconnect=None
+ mLayout=None
" connection window"
wConnect=None
@@ -40,19 +42,8 @@ class winMain(QtGui.QMainWindow):
QtGui.QWidget.__init__(self, parent)
self.setWindowTitle("montypc - An MPD client")
- self.plugins={}
- opts=QtGui.QDockWidget.DockWidgetClosable|QtGui.QDockWidget.DockWidgetMovable
- for p in plugins.listPlugins():
- pkg,name=p[0],p[1]
- # WARNING - dirty hack ahead!
- plugin=eval('%s.%s'%(pkg,name))(self)
- self.plugins[plugin.getName().lower()]=plugin
- dock=plugin.getDockWidget(opts)
- if dock:
- self.addDockWidget(QtCore.Qt.TopDockWidgetArea, plugin.getDockWidget(opts))
-
self.wConnect=winConnect()
-
+
self.statusBar() # create a statusbar
mBar=self.menuBar() # create a menubar
# menu file
@@ -76,16 +67,22 @@ class winMain(QtGui.QMainWindow):
m.addAction("Settings", self.showWinSettings).setIcon(QtGui.QIcon('gfx/gtk-preferences.svg'))
# menu layout
- m=mBar.addMenu("Layout")
- m.setTearOffEnabled(True)
- m.addAction('Save layout', self.saveLayout)
- m.addAction('Restore layout', self.restoreLayout)
- m.addSeparator()
- # can not use iterators, as that gives some creepy error 'bout c++
- actions=self.createPopupMenu().actions()
- for i in xrange(len(actions)):
- m.addAction(actions[i])
+ self.mLayout=mBar.addMenu("Layout")
+ self.mLayout.setTearOffEnabled(True)
+
+ self.plugins={}
+ newPlugins=False # are there new plugins?
+ for p in plugins.listPlugins():
+ pkg,name=p[0],p[1]
+ # WARNING - dirty hack ahead!
+ plugin=eval('%s.%s'%(pkg,name))(self)
+ newPlugins=newPlugins|(settings.get('%s.load'%(plugin.getName(True)), None)==None)
+ self.plugins[plugin.getName().lower()]=plugin
+ if settings.get('%s.load'%(plugin.getName(True)), '1')=='1':
+ # load new plugins by default
+ plugin.load()
+ self.updateLayoutMenu()
self.setDockOptions(QtGui.QMainWindow.AllowNestedDocks \
|QtGui.QMainWindow.AllowTabbedDocks \
|QtGui.QMainWindow.VerticalTabs)
@@ -113,9 +110,55 @@ class winMain(QtGui.QMainWindow):
self.wConnect.monitor()
self.show()
- #self.showWinSettings()
+ if newPlugins:
+ self.showWinSettings()
doEvents()
+ def updateLayoutMenu(self):
+ self.mLayout.clear()
+ self.mLayout.addAction('Save layout', self.saveLayout)
+ self.mLayout.addAction('Restore layout', self.restoreLayout)
+ self.mLayout.addSeparator()
+ # create checkable menu
+ a=QtGui.QAction('Show titlebars', self)
+ a.setCheckable(True)
+ a.setChecked(settings.get('winMain.show.titlebars', '1')=='1')
+ self.toggleTitleBars(a.isChecked())
+ self.connect(a, QtCore.SIGNAL('toggled(bool)'), self.toggleTitleBars)
+ self.mLayout.addAction(a)
+ self.mLayout.addSeparator()
+ # can not use iterators, as that gives some creepy error 'bout c++
+ actions=self.createPopupMenu().actions()
+ for i in xrange(len(actions)):
+ self.mLayout.addAction(actions[i])
+
+ def toggleTitleBars(self, val):
+ if val:
+ settings.set('winMain.show.titlebars', '1')
+ else:
+ settings.set('winMain.show.titlebars', '0')
+ for dock in self.docks:
+ if val:
+ dock.setTitleBarWidget(None)
+ else:
+ dock.setTitleBarWidget(QtGui.QWidget())
+
+ def getPlugin(self, name):
+ try:
+ return self.plugins[name.lower()]
+ except:
+ return None
+
+ def addDock(self, dock):
+ if dock:
+ self.docks.append(dock)
+ self.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock)
+ self.updateLayoutMenu()
+ def removeDock(self, dock):
+ if dock:
+ self.docks.remove(dock)
+ self.removeDockWidget(dock)
+ self.updateLayoutMenu()
mMenuVisible=None
def createPopupMenu(self):
@@ -156,6 +199,7 @@ class winMain(QtGui.QMainWindow):
if not self.wSettings:
self.wSettings=winSettings(self)
self.wSettings.show()
+ self.wSettings.raise_()
def onReady(self, params):
self.initialiseData()
diff --git a/winSettings.py b/winSettings.py
index 7951da0..310673f 100644
--- a/winSettings.py
+++ b/winSettings.py
@@ -2,11 +2,13 @@ from PyQt4 import QtGui, QtCore
from misc import *
from clMonty import monty
+from clSettings import settings
class winSettings(QtGui.QWidget):
btnSave=None
btnClose=None
+ lstPlugins=None
winMain=None
@@ -17,20 +19,28 @@ class winSettings(QtGui.QWidget):
self.btnSave=Button('save all', self.onBtnSaveClick)
self.btnClose=Button('close', self.onBtnCloseClick)
- tabPlugins=QtGui.QTextEdit(self)
- tabPlugins.setReadOnly(True)
+ self.lstPlugins=QtGui.QListWidget(self)
layoutWin=QtGui.QVBoxLayout()
layoutButtons=QtGui.QHBoxLayout()
tabWidget=QtGui.QTabWidget(parent)
self.setLayout(layoutWin)
- tabWidget.addTab(tabPlugins, "plugins")
+ tabWidget.addTab(self.lstPlugins, "plugins")
for p in self.winMain.plugins:
plugin=self.winMain.plugins[p]
- tabPlugins.insertHtml("%s: <i>%s</i><br />"%(plugin.getName(), plugin.getInfo()))
+ item=QtGui.QListWidgetItem("%s\t%s"%(plugin.getName(), plugin.getInfo()))
+ if plugin.isLoaded():
+ item.setCheckState(QtCore.Qt.Checked)
+ else:
+ item.setCheckState(QtCore.Qt.Unchecked)
+
+ if settings.get('%s.load'%(plugin.getName(True)),None)==None:
+ # load new plugins by default
+ item.setTextColor(QtCore.Qt.blue)
+ settings.set('%s.load'%(plugin.getName(True)), 1)
+ self.lstPlugins.addItem(item)
tabWidget.addTab(plugin.getSettingsWidget(), plugin.getName())
-
layoutWin.addWidget(tabWidget)
layoutWin.addLayout(layoutButtons)
@@ -39,6 +49,8 @@ class winSettings(QtGui.QWidget):
layoutButtons.addWidget(self.btnSave)
layoutButtons.addWidget(self.btnClose)
+ self.connect(self.lstPlugins, QtCore.SIGNAL('itemChanged (QListWidgetItem*)'), self.onlstPluginItemChanged)
+
self.setWindowIcon(appIcon)
self.setWindowTitle('Settings')
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
@@ -57,7 +69,17 @@ class winSettings(QtGui.QWidget):
plugin.saveSettings()
def onBtnCloseClick(self):
self.close()
-
+ def onlstPluginItemChanged(self, item):
+ # check here if we have to load or unload the plugin!
+ pluginName=str(item.text()[0:str(item.text()).find('\t')])
+ plugin=self.winMain.getPlugin(pluginName)
+ loaded=int(item.checkState()==QtCore.Qt.Checked)
+ settings.set('%s.load'%(plugin.getName(True)), loaded)
+ if loaded:
+ plugin.load()
+ else:
+ plugin.unload()
+ return
def closeEvent(self, event):
for p in self.winMain.plugins:
plugin=self.winMain.plugins[p]