76
76
self.loadConfigData()
78
78
def loadConfigData(self):
81
81
self.config = RhythmboxConfig()
83
83
if self.options.config != None:
84
84
# load the config based on options passed in from the main app
85
85
configfilepath = self.options.config
87
87
# load plugin config from home directory of the user
88
88
configfilepath = os.path.join(os.path.expanduser('~'), ".config/"+app_name+"/"+module_name+".config")
90
90
if os.path.exists(configfilepath):
92
92
self.logger.info("Loading config settings from \"%s\""%configfilepath)
94
94
for line in fileinput.input(os.path.expanduser(configfilepath)):
95
95
line = line.strip()
96
96
if len(line) > 0 and line[0:1] != "#": # ignore commented lines or empty ones
98
98
name = line.split("=")[0].strip().upper() # config setting name on the left of =
99
99
value = line.split("=")[1].split("#")[0].strip() # config value on the right of = (minus any trailing comments)
101
101
if len(value) > 0:
102
102
if name == "HEADERTEMPLATE":
103
self.config.HEADERTEMPLATE = self.getTypedValue(value, "string")
103
self.config.HEADERTEMPLATE = getTypedValue(value, "string")
104
104
elif name == "TEMPLATE":
105
self.config.TEMPLATE = self.getTypedValue(value, "string")
105
self.config.TEMPLATE = getTypedValue(value, "string")
106
106
elif name == "STATUSTEXT":
107
self.config.STATUSTEXT = self.getTypedValue(value, "string")
107
self.config.STATUSTEXT = getTypedValue(value, "string")
108
108
elif name == "NOUNKNOWNOUTPUT":
109
self.config.NOUNKNOWNOUTPUT = self.getTypedValue(value, "boolean")
109
self.config.NOUNKNOWNOUTPUT = getTypedValue(value, "boolean")
111
self.logger.error("Unknown option in config file: " + name)
111
self.logger.error("Unknown option in config file: " + name)
113
113
self.logger.info("Config data file %s not found, using defaults and setting up config file for next time" % configfilepath)
115
115
userconfigpath = os.path.join(os.path.expanduser('~'), ".config/"+app_name+"/")
116
116
configsource = os.path.join(app_path, "config/"+module_name+".config")
118
118
if os.path.exists(userconfigpath) == False:
119
119
os.makedirs(userconfigpath)
124
124
self.logger.error(e.__str__()+"\n"+traceback.format_exc())
126
126
def getTypedValue(self, value, expectedtype):
129
129
if len(value.strip(" ")) == 0:
132
132
elif value.lower() == "true":
133
133
if expectedtype == "boolean":
136
136
self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
138
138
elif value.lower() == "false":
139
139
if expectedtype == "boolean":
142
142
self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
144
144
elif self.isNumeric(value) == True:
145
145
if expectedtype == "integer":
146
146
return int(value)
148
148
self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
153
153
except (TypeError, ValueError):
154
154
self.logger.error("Cannot convert '%s' to expected type of '%s'"%(value,expectedtype))
157
157
def testDBus(self, bus, interface):
158
158
obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
159
159
dbus_iface = dbus.Interface(obj, 'org.freedesktop.DBus')
160
160
avail = dbus_iface.ListNames()
161
161
return interface in avail
163
163
def getOutputData(self, datatype, statustext, nounknownoutput):
166
166
if DBUS_AVAIL == True:
168
168
if nounknownoutput == True:
169
169
unknown_time = ""
170
170
unknown_number = ""
177
177
unknown_string = "Unknown"
178
178
unknown_coverart = "file://"+urllib.quote(os.path.join(app_path,"images/"+module_name+".png"))
179
179
unknown_rating = "file://"+urllib.quote(os.path.join(app_path,"images/ratingicons/0.png"))
183
183
bus = dbus.SessionBus()
184
184
if self.musicData == None:
186
186
if self.testDBus(bus, 'org.gnome.Rhythmbox'):
188
188
self.logger.info("Calling dbus interface for music data")
191
191
self.logger.info("Setting up dbus interface")
193
193
# setup dbus hooks
194
194
remote_object_shell = bus.get_object('org.gnome.Rhythmbox', '/org/gnome/Rhythmbox/Shell')
195
195
iface_shell = dbus.Interface(remote_object_shell, 'org.gnome.Rhythmbox.Shell')
196
196
remote_object_player = bus.get_object('org.gnome.Rhythmbox', '/org/gnome/Rhythmbox/Player')
197
197
iface_player = dbus.Interface(remote_object_player, 'org.gnome.Rhythmbox.Player')
199
199
self.logger.info("Calling dbus interface for music data")
201
201
# prepare song properties for data retrieval
203
203
volume = str(int(100*iface_player.getVolume()))
205
205
uri = iface_player.getPlayingUri()
206
206
if len(uri) == 0:
207
207
status = self.getStatusText("stopped", statustext)
208
208
self.musicData = MusicData(status,None,None,None,None,None,None,None,None,None,None,None,None,volume)
210
210
status = self.getStatusText("playing", statustext)
212
212
props = iface_shell.getSongProperties(uri)
214
214
# grab the data into variables
215
215
location = props["location"]
217
217
# handle a file or stream differently for filename
218
218
if location.find("file://") != -1:
219
filename = location[location.rfind("/")+1:]
219
filename = location[location.rfind("/")+1:]
220
220
elif len(location) > 0:
221
221
filename = location
225
225
# try to get all the normal stuff...the props return an empty string if nothing is available
226
226
title = props["title"]
227
227
album = props["album"]
228
228
artist = props["artist"]
229
229
year = str(props["year"])
230
230
tracknumber = str(props["track-number"])
232
232
if year == "0": year = "?"
233
233
if tracknumber == "0": tracknumber = "?"
235
235
# if no title and a stream song title then use that instead (internet radio)
236
236
if len(title) == 0 and "rb:stream-song-title" in props:
237
237
title = props["rb:stream-song-title"]
239
239
# get coverart url or file link
240
240
if "rb:coverArt-uri" in props:
241
241
coverart = props["rb:coverArt-uri"]
245
245
coverart = "file://"+urllib.quote(coverart.encode("utf-8"))
247
247
# default coverart image for this plugin if none found
248
coverart = unknown_coverart
251
genre = props["genre"]
248
coverart = unknown_coverart
251
genre = props["genre"]
252
252
length_seconds = int(props["duration"])
253
253
current_seconds = int(iface_player.getElapsed())
254
254
current_position = str(int(current_seconds/60%60)).rjust(1,"0")+":"+str(int(current_seconds%60)).rjust(2,"0")
256
256
if length_seconds > 0:
257
length = str(length_seconds/60%60).rjust(1,"0")+":"+str(length_seconds%60).rjust(2,"0")
258
current_position_percent = str(int((float(current_seconds) / float(props["duration"]))*100))
257
length = getFormattedDuration(length_seconds)
258
current_position_percent = str(int((float(current_seconds) / float(props["duration"]))*100))
261
261
current_position_percent = "?"
263
263
rating = str(int(props["rating"]))
265
265
volume = str(int(100*iface_player.getVolume()))
267
267
self.musicData = MusicData(status,coverart,title,album,length,artist,tracknumber,genre,year,filename,current_position_percent,current_position,rating,volume)
269
269
except Exception, e:
270
270
self.logger.info("Issue calling the dbus service:"+e.__str__()+"\n"+traceback.format_exc())
272
272
if self.musicData != None:
274
274
self.logger.info("Preparing output for datatype:"+datatype)
276
276
if datatype == "ST": #status
277
277
if self.musicData.status == None or len(self.musicData.status) == 0:
280
output = self.getHTMLText(self.musicData.status)
280
output = getHTMLText(self.musicData.status)
282
282
elif datatype == "CA": #coverart
283
283
if self.musicData.coverart == None or len(self.musicData.coverart) == 0:
286
286
output = self.musicData.coverart
288
288
elif datatype == "TI": #title
289
289
if self.musicData.title == None or len(self.musicData.title) == 0:
292
output = self.getHTMLText(self.musicData.title)
292
output = getHTMLText(self.musicData.title)
294
294
elif datatype == "AL": #album
295
295
if self.musicData.album == None or len(self.musicData.album) == 0:
298
output = self.getHTMLText(self.musicData.album)
298
output = getHTMLText(self.musicData.album)
300
300
elif datatype == "AR": #artist
301
301
if self.musicData.artist == None or len(self.musicData.artist) == 0:
304
output = self.getHTMLText(self.musicData.artist)
304
output = getHTMLText(self.musicData.artist)
306
306
elif datatype == "TN": #tracknumber
307
307
if self.musicData.tracknumber == None or len(self.musicData.tracknumber) == 0:
310
310
output = self.musicData.tracknumber
312
312
elif datatype == "GE": #genre
313
313
if self.musicData.title == genre or len(self.musicData.genre) == 0:
316
output = self.getHTMLText(self.musicData.genre)
316
output = getHTMLText(self.musicData.genre)
318
318
elif datatype == "YR": #year
319
319
if self.musicData.year == None or len(self.musicData.year) == 0:
322
322
output = self.musicData.year
324
324
elif datatype == "FN": #filename
325
325
if self.musicData.filename == None or len(self.musicData.filename) == 0:
328
output = self.getHTMLText(self.musicData.filename)
328
output = getHTMLText(self.musicData.filename)
330
330
elif datatype == "LE": # length
331
331
if self.musicData.length == None or len(self.musicData.length) == 0:
334
334
output = self.musicData.length
336
336
elif datatype == "PP": #current position in percent
337
337
if self.musicData.current_position_percent == None or len(self.musicData.current_position_percent) == 0:
340
340
output = self.musicData.current_position_percent
342
342
elif datatype == "PT": #current position in time
343
343
if self.musicData.current_position == None or len(self.musicData.current_position) == 0:
346
346
output = self.musicData.current_position
348
348
elif datatype == "VO": #volume
349
349
if self.musicData.volume == None or len(self.musicData.volume) == 0:
352
352
output = self.musicData.volume
354
354
elif datatype == "RT": #rating
355
355
if self.musicData.rating == None or self.isNumeric(self.musicData.rating) == False:
378
378
output = unknown_rating
380
380
output = unknown_string
384
384
def getStatusText(self, status, statustext):
387
387
statustextparts = statustext.split(",")
389
389
if status == "playing":
390
390
return statustextparts[0]
391
391
elif status == "paused":
392
392
return statustextparts[1]
393
393
elif status == "stopped":
394
394
return statustextparts[2]
399
399
def getTemplateItemOutput(self, template_text):
401
401
# keys to template data
402
402
DATATYPE_KEY = "datatype"
403
403
STATUSTEXT_KEY = "statustext"
404
404
NOUNKNOWNOUTPUT_KEY = "nounknownoutput"
407
407
statustext = self.config.STATUSTEXT #default to command line option
408
408
nounknownoutput = self.config.NOUNKNOWNOUTPUT #default to command line option
410
410
for option in template_text.split('--'):
411
411
if len(option) == 0 or option.isspace():
414
414
# not using split here...it can't assign both key and value in one call, this should be faster
415
415
x = option.find('=')
422
422
key = option.strip()
426
426
if key == DATATYPE_KEY:
427
datatype = self.getTypedValue(value, "string")
427
datatype = getTypedValue(value, "string")
428
428
elif key == STATUSTEXT_KEY:
429
statustext = self.getTypedValue(value, "string")
429
statustext = getTypedValue(value, "string")
430
430
elif key == NOUNKNOWNOUTPUT_KEY:
431
431
nounknownoutput = True
433
433
self.logger.info("Unknown template option: " + option)
435
435
except (TypeError, ValueError):
436
436
self.logger.info("Cannot convert option argument to number: " + option)
439
439
if datatype != None:
440
440
return self.getOutputData(datatype, statustext, nounknownoutput)
442
442
self.logger.info("Template item does not have datatype defined")
446
446
def getOutputFromTemplate(self, template):
451
451
# a and b are indexes in the template string
452
452
# moving from left to right the string is processed
453
453
# b is index of the opening bracket and a of the closing bracket
454
454
# everything between b and a is a template that needs to be parsed
456
456
b = template.find('[', a)
459
459
b = len(template)
462
462
# if there is something between a and b, append it straight to output
464
464
output += template[a : b]
536
536
output = output + self.getOutputFromTemplate(template)
538
538
return output.encode("utf-8")
540
def isNumeric(self,value):
547
def getHTMLText(self,text):
550
for char in text: #html:
552
htmlentities.append(char)
554
htmlentities.append('&%s;' % codepoint2name[ord(char)])
555
html = "".join(htmlentities)
557
html = html.replace("\n","<br>\n") # switch out new line for html breaks
562
def getCleanText(self,html):
565
text = text.replace("\n","") # remove new lines from html
566
text = text.replace("'","'") # workaround for shitty xml codes not compliant with html
567
text = text.replace("<br>","\n") # switch out html breaks for new line
568
text = re.sub('<(.|\n)+?>','',text) # remove any html tags
569
text = re.sub('&(%s);' % '|'.join(name2codepoint), lambda m: chr(name2codepoint[m.group(1)]), text)
574
540
def getHTML(options):
575
541
output = Output(options)
576
542
html = output.getOutput()
580
546
# to enable testing in isolation
581
547
if __name__ == "__main__":
583
549
parser = OptionParser()
584
parser.add_option("--noheader", dest="noheader", default=False, action="store_true", help=u"Turn off header output. This will override any header template setting to be nothing")
550
parser.add_option("--noheader", dest="noheader", default=False, action="store_true", help=u"Turn off header output. This will override any header template setting to be nothing")
585
551
parser.add_option("--headertemplate", dest="headertemplate", type="string", metavar="FILE", help=u"Override the header template for the plugin, default or config based template ignored.")
586
552
parser.add_option("--template", dest="template", type="string", metavar="FILE", help=u"Override the template for the plugin, default or config based template ignored.")
587
553
parser.add_option("--verbose", dest="verbose", default=False, action="store_true", help=u"Outputs verbose info to the terminal")
588
554
parser.add_option("--version", dest="version", default=False, action="store_true", help=u"Displays the version of the script.")
589
parser.add_option("--logfile", dest="logfile", type="string", metavar="FILE", help=u"If a filepath is set, the script logs to the filepath.")
555
parser.add_option("--logfile", dest="logfile", type="string", metavar="FILE", help=u"If a filepath is set, the script logs to the filepath.")
591
557
(options, args) = parser.parse_args()
593
559
output = Output(options)
594
560
html = output.getOutput()