#!/usr/bin/python
# -*- coding: utf-8 -*-
###############################################################################
# conkyExaile.py is a simple python script to gather
# details of from exaile for use in conky.
#
# Author: Kaivalagi
# Created: 21/09/2008
# Modifications:
# 21/09/2008 Added version option
# 21/09/2008 Updated to handle nothing, will return "Unknown" or zero value output when nothing is available
# 21/09/2008 Added --nounknownoutput option to turn off "Unknown" and "0:00" as default output for the unknown
# 22/09/2008 Minor bug fixes for unknown output
# 23/09/2008 Updated to do all dbus data retrievial upfront and reuse data if still available in musicData class
# 25/09/2008 Updated to handle when no music is playing, no unnecessary dbus calls will be made
# 25/09/2008 Updated rating output generation to use ljust string function
# 02/10/2008 Updated script to now use "[" and "]" as template brackets rather than "{" and "}" so that the execp/execpi conky command can be used, this enables the use of $font, $color options in the template which conky will then make adjustments for in the output!
# 04/10/2008 Updated help for template option
# 10/11/2008 Added --errorlog and --infolog options to log data to a file, removed --enableerrors option as unnecessary
# 17/11/2008 Now loading the template file in unicode mode to allow for the extended character set
# 17/11/2008 Added datatypes: ST (status), GE (genre), YR (year), TN (track number), FN (file name)
# 17/11/2008 Added --statustext option to allow overridding of the standard status text
# 20/11/2008 Unrated rating now outputs nothing
# 21/11/2008 Fixed statustext to work inside template now
# 06/01/2009 Removed .encode("utf-8") from internal return to fix unicode output?...it works...
# 18/05/2009 Updated to expand ~ based template paths
from datetime import datetime
from optparse import OptionParser
import codecs
import sys
import traceback
import os
try:
import dbus
DBUS_AVAIL = True
except ImportError:
# Dummy D-Bus library
class _Connection:
get_object = lambda *a: object()
class _Interface:
__init__ = lambda *a: None
ListNames = lambda *a: []
class Dummy: pass
dbus = Dummy()
dbus.Interface = _Interface
dbus.service = Dummy()
dbus.service.method = lambda *a: lambda f: f
dbus.service.Object = object
dbus.SessionBus = _Connection
DBUS_AVAIL = False
class CommandLineParser:
parser = None
def __init__(self):
self.parser = OptionParser()
self.parser.add_option("-t", "--template", dest="template", type="string", metavar="FILE", help=u"define a template file to generate output in one call. A displayable item in the file is in the form [--datatype=TI]. The following are possible options within each item: --datatype,--ratingchar. Note that the short forms of the options are not currently supported! None of these options are applicable at command line when using templates.")
self.parser.add_option("-d", "--datatype", dest="datatype", default="TI", type="string", metavar="DATATYPE", help=u"[default: %default] The data type options are: ST (status), TI (title), AL (album), AR (artist), GE (genre), YR (year), TN (track number), FN (file name), LE (length), PP (current position in percent), PT (current position in time), VO (volume), RT (rating). Not applicable at command line when using templates.")
self.parser.add_option("-r", "--ratingchar", dest="ratingchar", default="*", type="string", metavar="CHAR", help=u"[default: %default] The output character for the ratings scale. Command line option overridden if used in templates.")
self.parser.add_option("-s", "--statustext", dest="statustext", default="Playing,Paused,Stopped", type="string", metavar="TEXT", help=u"[default: %default] The text must be comma delimited in the form 'A,B,C'. Command line option overridden if used in templates.")
self.parser.add_option("-n", "--nounknownoutput", dest="nounknownoutput", default=False, action="store_true", help=u"Turn off unknown output such as \"Unknown\" for title and \"0:00\" for length. Command line option overridden if used in templates.")
self.parser.add_option("-v", "--verbose", dest="verbose", default=False, action="store_true", help=u"Request verbose output, not a good idea when running through conky!")
self.parser.add_option("-V", "--version", dest="version", default=False, action="store_true", help=u"Displays the version of the script.")
self.parser.add_option("--errorlogfile", dest="errorlogfile", type="string", metavar="FILE", help=u"If a filepath is set, the script appends errors to the filepath.")
self.parser.add_option("--infologfile", dest="infologfile", type="string", metavar="FILE", help=u"If a filepath is set, the script appends info to the filepath.")
def parse_args(self):
(options, args) = self.parser.parse_args()
return (options, args)
def print_help(self):
return self.parser.print_help()
class MusicData:
def __init__(self,status,coverart,title,album,length,artist,tracknumber,genre,year,filename,current_position_percent,current_position,rating,volume):
self.status = status
self.coverart = coverart
self.title = title
self.album = album
self.length = length
self.artist = artist
self.tracknumber = tracknumber
self.genre = genre
self.year = year
self.filename = filename
self.current_position_percent = current_position_percent
self.current_position = current_position
self.rating = rating
self.volume = volume
class ExaileInfo:
error = u""
musicData = None
def __init__(self, options):
self.options = options
def testDBus(self, bus, interface):
obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
dbus_iface = dbus.Interface(obj, 'org.freedesktop.DBus')
avail = dbus_iface.ListNames()
return interface in avail
def getOutputData(self, datatype, ratingchar, statustext, nounknownoutput):
output = u""
if nounknownoutput == True:
unknown_time = ""
unknown_number = ""
unknown_string = ""
else:
unknown_time = "0:00"
unknown_number = "0"
unknown_string = "Unknown"
try:
bus = dbus.SessionBus()
if self.musicData == None:
if self.testDBus(bus, 'org.exaile.DBusInterface'):
try:
self.logInfo("Setting up dbus interface")
remote_object = bus.get_object("org.exaile.DBusInterface","/DBusInterfaceObject")
iface = dbus.Interface(remote_object, "org.exaile.DBusInterface")
self.logInfo("Calling dbus interface for music data")
# grab the data for use
status = self.getStatusText(iface.status(), statustext)
coverart = iface.get_cover_path()
# if cover art found then use it, otherwise use default coverart image for this plugin
if len(coverart) > 0 and coverart.find("nocover.png") == -1:
coverart = coverart.encode("utf-8")
#handle escape chars, are there anymore???
coverart = coverart.replace(" ", "\ ")
coverart = coverart.replace("(", "\(")
coverart = coverart.replace(")", "\)")
length = iface.get_length()
title = iface.get_title()
album = iface.get_album()
artist = iface.get_artist()
genre = iface.get_track_attr("genre")
year = iface.get_track_attr("year")
tracknumber = iface.get_track_attr("track")
filename = iface.get_track_attr("filename")
if len(length) > 0:
current_position_percent = str(int(iface.current_position()))
length_minutes, length_seconds = map(int,length.split(":"))
current_seconds = int((float(length_minutes)*60.0+float(length_seconds))*(float(current_position_percent)/100.0))
current_position = str(int(current_seconds/60%60)).rjust(1,"0")+":"+str(int(current_seconds%60)).rjust(2,"0")
else:
current_position_percent = "0"
current_position = "0:00"
volume = str(int(iface.get_volume().split(".")[0]))
rating = str(int(iface.get_rating()))
self.musicData = MusicData(status,coverart,title,album,length,artist,tracknumber,genre,year,filename,current_position_percent,current_position,rating,volume)
except Exception, e:
self.logError("Issue calling the dbus service:"+e.__str__())
if self.musicData != None:
self.logInfo("Preparing output for datatype:"+datatype)
if datatype == "ST": #status
if self.musicData.status == None or len(self.musicData.status) == 0:
output = None
else:
output = self.musicData.status
elif datatype == "CA": #coverart
if self.musicData.coverart == None or len(self.musicData.coverart) == 0:
output = None
else:
output = self.musicData.coverart
elif datatype == "TI": #title
if self.musicData.title == None or len(self.musicData.title) == 0:
output = None
else:
output = self.musicData.title
elif datatype == "AL": #album
if self.musicData.album == None or len(self.musicData.album) == 0:
output = None
else:
output = self.musicData.album
elif datatype == "AR": #artist
if self.musicData.artist == None or len(self.musicData.artist) == 0:
output = None
else:
output = self.musicData.artist
elif datatype == "GE": #genre
if self.musicData.title == genre or len(self.musicData.genre) == 0:
output = None
else:
output = self.musicData.genre
elif datatype == "YR": #year
if self.musicData.year == None or len(self.musicData.year) == 0:
output = None
else:
output = self.musicData.year
elif datatype == "TN": #tracknumber
if self.musicData.tracknumber == None or len(self.musicData.tracknumber) == 0:
output = None
else:
output = self.musicData.tracknumber
elif datatype == "FN": #filename
if self.musicData.filename == None or len(self.musicData.filename) == 0:
output = None
else:
output = self.musicData.filename
elif datatype == "LE": # length
if self.musicData.length == None or len(self.musicData.length) == 0:
output = None
else:
output = self.musicData.length
elif datatype == "PP": #current position in percent
if self.musicData.current_position_percent == None or len(self.musicData.current_position_percent) == 0:
output = None
else:
output = self.musicData.current_position_percent
elif datatype == "PT": #current position in time
if self.musicData.current_position == None or len(self.musicData.current_position) == 0:
output = None
else:
output = self.musicData.current_position
elif datatype == "VO": #volume
if self.musicData.volume == None or len(self.musicData.volume) == 0:
output = None
else:
output = self.musicData.volume
elif datatype == "RT": #rating
if self.musicData.rating == None or self.isNumeric(self.musicData.rating) == False:
output = None
else:
rating = int(self.musicData.rating)
if rating > 0:
output = u"".ljust(rating,ratingchar)
elif rating == 0:
output = u""
else:
output = None
else:
self.logError("Unknown datatype provided: " + datatype)
return u""
if output == None or self.musicData == None:
if datatype in ["LE","PT"]:
output = unknown_time
elif datatype in ["PP","VO","YR","TN"]:
output = unknown_number
elif datatype == "CA":
output = ""
else:
output = unknown_string
return output
except SystemExit:
self.logError("System Exit!")
return u""
except Exception, e:
traceback.print_exc()
self.logError("Unknown error when calling getOutputText:" + e.__str__())
return u""
def getStatusText(self, status, statustext):
if status != None:
statustextparts = statustext.split(",")
if status == "playing":
return statustextparts[0]
elif status == "paused":
return statustextparts[1]
elif status == "stopped":
return statustextparts[2]
else:
return status
def getTemplateItemOutput(self, template_text):
# keys to template data
DATATYPE_KEY = "datatype"
RATINGCHAR_KEY = "ratingchar"
STATUSTEXT_KEY = "statustext"
NOUNKNOWNOUTPUT_KEY = "nounknownoutput"
datatype = None
ratingchar = self.options.ratingchar #default to command line option
statustext = self.options.statustext #default to command line option
nounknownoutput = self.options.nounknownoutput #default to command line option
for option in template_text.split('--'):
if len(option) == 0 or option.isspace():
continue
# not using split here...it can't assign both key and value in one call, this should be faster
x = option.find('=')
if (x != -1):
key = option[:x].strip()
value = option[x + 1:].strip()
if value == "":
value = None
else:
key = option.strip()
value = None
try:
if key == DATATYPE_KEY:
datatype = value
elif key == RATINGCHAR_KEY:
ratingchar = value
elif key == STATUSTEXT_KEY:
statustext = value
elif key == NOUNKNOWNOUTPUT_KEY:
nounknownoutput = True
else:
self.logError("Unknown template option: " + option)
except (TypeError, ValueError):
self.logError("Cannot convert option argument to number: " + option)
return u""
if datatype != None:
return self.getOutputData(datatype, ratingchar, statustext, nounknownoutput)
else:
self.logError("Template item does not have datatype defined")
return u""
def getOutputFromTemplate(self, template):
output = u""
end = False
a = 0
# a and b are indexes in the template string
# moving from left to right the string is processed
# b is index of the opening bracket and a of the closing bracket
# everything between b and a is a template that needs to be parsed
while not end:
b = template.find('[', a)
if b == -1:
b = len(template)
end = True
# if there is something between a and b, append it straight to output
if b > a:
output += template[a : b]
# check for the escape char (if we are not at the end)
if template[b - 1] == '\\' and not end:
# if its there, replace it by the bracket
output = output[:-1] + '['
# skip the bracket in the input string and continue from the beginning
a = b + 1
continue
if end:
break
a = template.find(']', b)
if a == -1:
self.logError("Missing terminal bracket (]) for a template item")
return u""
# if there is some template text...
if a > b + 1:
output += self.getTemplateItemOutput(template[b + 1 : a])
a = a + 1
return output
def writeOutput(self):
if self.options.template != None:
#load the file
try:
fileinput = codecs.open(os.path.expanduser(self.options.template), encoding='utf-8')
template = fileinput.read()
fileinput.close()
except Exception, e:
self.logError("Error loading template file: " + e.__str__())
else:
output = self.getOutputFromTemplate(template)
else:
output = self.getOutputData(self.options.datatype, self.options.ratingchar, self.options.statustext, self.options.nounknownoutput)
print output.encode("utf-8")
def isNumeric(self,value):
try:
temp = int(value)
return True
except:
return False
def logInfo(self, text):
if self.options.verbose == True:
print >> sys.stdout, "INFO: " + text
if self.options.infologfile != None:
datetimestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
fileoutput = open(self.options.infologfile, "ab")
fileoutput.write(datetimestamp+" INFO: "+text+"\n")
fileoutput.close()
def logError(self, text):
print >> sys.stderr, "ERROR: " + text
if self.options.errorlogfile != None:
datetimestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
fileoutput = open(self.options.errorlogfile, "ab")
fileoutput.write(datetimestamp+" ERROR: "+text+"\n")
fileoutput.close()
def main():
parser = CommandLineParser()
(options, args) = parser.parse_args()
if options.version == True:
print >> sys.stdout,"conkyExaile v.2.04"
else:
if options.verbose == True:
print >> sys.stdout, "*** INITIAL OPTIONS:"
print >> sys.stdout, " datatype:", options.datatype
print >> sys.stdout, " template:", options.template
print >> sys.stdout, " ratingchar:", options.ratingchar
print >> sys.stdout, " nounknownoutput:", options.nounknownoutput
print >> sys.stdout, " verbose:", options.verbose
print >> sys.stdout, " errorlogfile:",options.errorlogfile
print >> sys.stdout, " infologfile:",options.infologfile
exaileinfo = ExaileInfo(options)
exaileinfo.writeOutput()
if __name__ == '__main__':
main()
sys.exit()