~m-buck/+junk/gtk-desktop-info

« back to all changes in this revision

Viewing changes to plugin_rhythmbox.py

  • Committer: Mark Buck (Kaivalagi)
  • Date: 2009-06-19 17:13:00 UTC
  • Revision ID: m_buck@hotmail.com-20090619171300-5cbhr90xwg62z27y
Added --backgroundblend and --backgroundcolour options for visual seperation of output from wallpaper if required, Fixed song length output in the rhythmbox plugin when songs are an hour long or more, Added copy option to right click, enabling the copying of html content to the clipboard for testing, Moved common functions into a plugin_common module

Show diffs side-by-side

added added

removed removed

Lines of Context:
6
6
#
7
7
#  Author: Kaivalagi
8
8
# Created: 23/11/2008
9
 
from htmlentitydefs import name2codepoint, codepoint2name
10
9
from optparse import OptionParser
11
 
import traceback
 
10
from plugin_common import getHTMLText, getTypedValue, getFormattedDuration
12
11
import codecs
 
12
import fileinput
 
13
import logging
 
14
import os
 
15
import re
13
16
import shutil
14
 
import os
 
17
import traceback
15
18
import urllib
16
 
import re
17
 
import fileinput
18
 
import logging
19
19
 
20
20
try:
21
21
    import dbus
51
51
        self.tracknumber = tracknumber
52
52
        self.genre = genre
53
53
        self.year = year
54
 
        self.filename = filename        
 
54
        self.filename = filename
55
55
        self.current_position_percent = current_position_percent
56
56
        self.current_position = current_position
57
57
        self.rating = rating
59
59
 
60
60
class RhythmboxConfig:
61
61
    HEADERTEMPLATE = None
62
 
    TEMPLATE = None        
 
62
    TEMPLATE = None
63
63
    STATUSTEXT = "Playing,Paused,Stopped"
64
64
    NOUNKNOWNOUTPUT = False
65
 
    
 
65
 
66
66
class Output:
67
 
    
 
67
 
68
68
    options = None
69
69
    output = u""
70
70
    error = u""
76
76
        self.loadConfigData()
77
77
 
78
78
    def loadConfigData(self):
79
 
        try:         
 
79
        try:
80
80
 
81
81
            self.config = RhythmboxConfig()
82
 
            
 
82
 
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
86
86
            else:
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")
89
 
                            
 
89
 
90
90
            if os.path.exists(configfilepath):
91
 
                
 
91
 
92
92
                self.logger.info("Loading config settings from \"%s\""%configfilepath)
93
93
 
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
97
 
    
 
97
 
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)
100
 
                                                     
 
100
 
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")
110
110
                            else:
111
 
                                self.logger.error("Unknown option in config file: " + name)               
 
111
                                self.logger.error("Unknown option in config file: " + name)
112
112
            else:
113
113
                self.logger.info("Config data file %s not found, using defaults and setting up config file for next time" % configfilepath)
114
 
                
 
114
 
115
115
                userconfigpath = os.path.join(os.path.expanduser('~'), ".config/"+app_name+"/")
116
116
                configsource = os.path.join(app_path, "config/"+module_name+".config")
117
 
                
 
117
 
118
118
                if os.path.exists(userconfigpath) == False:
119
119
                    os.makedirs(userconfigpath)
120
120
 
124
124
            self.logger.error(e.__str__()+"\n"+traceback.format_exc())
125
125
 
126
126
    def getTypedValue(self, value, expectedtype):
127
 
        
 
127
 
128
128
        try:
129
129
            if len(value.strip(" ")) == 0:
130
130
                return None
131
 
            
 
131
 
132
132
            elif value.lower() == "true":
133
133
                if expectedtype == "boolean":
134
134
                    return True
135
135
                else:
136
136
                    self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
137
 
                    
 
137
 
138
138
            elif value.lower() == "false":
139
139
                if expectedtype == "boolean":
140
140
                    return False
141
141
                else:
142
142
                    self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
143
 
                    
 
143
 
144
144
            elif self.isNumeric(value) == True:
145
145
                if expectedtype == "integer":
146
146
                    return int(value)
147
147
                else:
148
148
                    self.logger.error("Expected type was '%s', but the value '%s' was given"%(expectedtype, value))
149
 
                    
 
149
 
150
150
            else:
151
151
                return value
152
152
 
153
153
        except (TypeError, ValueError):
154
154
            self.logger.error("Cannot convert '%s' to expected type of '%s'"%(value,expectedtype))
155
155
            return value
156
 
                
 
156
 
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
162
 
        
 
162
 
163
163
    def getOutputData(self, datatype, statustext, nounknownoutput):
164
164
        output = u""
165
 
        
 
165
 
166
166
        if DBUS_AVAIL == True:
167
 
            
 
167
 
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"))
180
 
            
 
180
 
181
181
            try:
182
 
                    
 
182
 
183
183
                bus = dbus.SessionBus()
184
184
                if self.musicData == None:
185
 
                    
 
185
 
186
186
                    if self.testDBus(bus, 'org.gnome.Rhythmbox'):
187
 
                    
 
187
 
188
188
                        self.logger.info("Calling dbus interface for music data")
189
 
        
 
189
 
190
190
                        try:
191
191
                            self.logger.info("Setting up dbus interface")
192
 
                            
 
192
 
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')
198
 
                                
 
198
 
199
199
                            self.logger.info("Calling dbus interface for music data")
200
 
                            
 
200
 
201
201
                            # prepare song properties for data retrieval
202
 
                            
 
202
 
203
203
                            volume = str(int(100*iface_player.getVolume()))
204
 
                            
 
204
 
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)
209
209
                            else:
210
210
                                status = self.getStatusText("playing", statustext)
211
 
                                
 
211
 
212
212
                                props = iface_shell.getSongProperties(uri)
213
 
        
 
213
 
214
214
                                # grab the data into variables
215
215
                                location = props["location"]
216
 
                                
 
216
 
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
222
222
                                else:
223
223
                                    filename = ""
224
 
                                
 
224
 
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"])
231
 
    
 
231
 
232
232
                                if year == "0": year = "?"
233
233
                                if tracknumber == "0": tracknumber = "?"
234
 
                                
 
234
 
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"]
238
 
                                    
 
238
 
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"))
246
246
                                else:
247
247
                                    # default coverart image for this plugin if none found
248
 
                                    coverart = unknown_coverart                           
249
 
                                                                
250
 
                                # common details                 
251
 
                                genre = props["genre"]                            
 
248
                                    coverart = unknown_coverart
 
249
 
 
250
                                # common details
 
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")
255
 
                                
 
255
 
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))
259
259
                                else:
260
260
                                    length = "?:??"
261
261
                                    current_position_percent = "?"
262
 
                                    
 
262
 
263
263
                                rating = str(int(props["rating"]))
264
 
                                    
 
264
 
265
265
                                volume = str(int(100*iface_player.getVolume()))
266
 
                                                    
 
266
 
267
267
                                self.musicData = MusicData(status,coverart,title,album,length,artist,tracknumber,genre,year,filename,current_position_percent,current_position,rating,volume)
268
 
                                
 
268
 
269
269
                        except Exception, e:
270
270
                            self.logger.info("Issue calling the dbus service:"+e.__str__()+"\n"+traceback.format_exc())
271
 
        
 
271
 
272
272
                if self.musicData != None:
273
 
                    
 
273
 
274
274
                    self.logger.info("Preparing output for datatype:"+datatype)
275
 
    
 
275
 
276
276
                    if datatype == "ST": #status
277
277
                        if self.musicData.status == None or len(self.musicData.status) == 0:
278
278
                            output = None
279
279
                        else:
280
 
                            output = self.getHTMLText(self.musicData.status)
281
 
    
 
280
                            output = getHTMLText(self.musicData.status)
 
281
 
282
282
                    elif datatype == "CA": #coverart
283
283
                        if self.musicData.coverart == None or len(self.musicData.coverart) == 0:
284
284
                            output = None
285
285
                        else:
286
286
                            output = self.musicData.coverart
287
 
                                                    
 
287
 
288
288
                    elif datatype == "TI": #title
289
289
                        if self.musicData.title == None or len(self.musicData.title) == 0:
290
290
                            output = None
291
291
                        else:
292
 
                            output = self.getHTMLText(self.musicData.title)
293
 
                            
 
292
                            output = getHTMLText(self.musicData.title)
 
293
 
294
294
                    elif datatype == "AL": #album
295
295
                        if self.musicData.album == None or len(self.musicData.album) == 0:
296
296
                            output = None
297
297
                        else:
298
 
                            output = self.getHTMLText(self.musicData.album)
299
 
                            
 
298
                            output = getHTMLText(self.musicData.album)
 
299
 
300
300
                    elif datatype == "AR": #artist
301
301
                        if self.musicData.artist == None or len(self.musicData.artist) == 0:
302
302
                            output = None
303
303
                        else:
304
 
                            output = self.getHTMLText(self.musicData.artist)
 
304
                            output = getHTMLText(self.musicData.artist)
305
305
 
306
306
                    elif datatype == "TN": #tracknumber
307
307
                        if self.musicData.tracknumber == None or len(self.musicData.tracknumber) == 0:
308
308
                            output = None
309
309
                        else:
310
310
                            output = self.musicData.tracknumber
311
 
                            
 
311
 
312
312
                    elif datatype == "GE": #genre
313
313
                        if self.musicData.title == genre or len(self.musicData.genre) == 0:
314
314
                            output = None
315
315
                        else:
316
 
                            output = self.getHTMLText(self.musicData.genre)
317
 
                            
 
316
                            output = getHTMLText(self.musicData.genre)
 
317
 
318
318
                    elif datatype == "YR": #year
319
319
                        if self.musicData.year == None or len(self.musicData.year) == 0:
320
320
                            output = None
321
321
                        else:
322
322
                            output = self.musicData.year
323
 
                                                    
 
323
 
324
324
                    elif datatype == "FN": #filename
325
325
                        if self.musicData.filename == None or len(self.musicData.filename) == 0:
326
326
                            output = None
327
327
                        else:
328
 
                            output = self.getHTMLText(self.musicData.filename)
329
 
    
 
328
                            output = getHTMLText(self.musicData.filename)
 
329
 
330
330
                    elif datatype == "LE": # length
331
331
                        if self.musicData.length == None or len(self.musicData.length) == 0:
332
332
                            output = None
333
333
                        else:
334
334
                            output = self.musicData.length
335
 
                            
 
335
 
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:
338
338
                            output = None
339
339
                        else:
340
340
                            output = self.musicData.current_position_percent
341
 
                            
 
341
 
342
342
                    elif datatype == "PT": #current position in time
343
343
                        if self.musicData.current_position == None or len(self.musicData.current_position) == 0:
344
344
                            output = None
345
345
                        else:
346
346
                            output = self.musicData.current_position
347
 
                            
 
347
 
348
348
                    elif datatype == "VO": #volume
349
349
                        if self.musicData.volume == None or len(self.musicData.volume) == 0:
350
350
                            output = None
351
351
                        else:
352
352
                            output = self.musicData.volume
353
 
                            
 
353
 
354
354
                    elif datatype == "RT": #rating
355
355
                        if self.musicData.rating == None or self.isNumeric(self.musicData.rating) == False:
356
356
                            output = None
378
378
                    output = unknown_rating
379
379
                else:
380
380
                    output = unknown_string
381
 
            
 
381
 
382
382
            return output
383
 
        
 
383
 
384
384
    def getStatusText(self, status, statustext):
385
 
        
386
 
        if status != None:        
 
385
 
 
386
        if status != None:
387
387
            statustextparts = statustext.split(",")
388
 
            
 
388
 
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]
395
 
            
 
395
 
396
396
        else:
397
397
            return status
398
 
        
 
398
 
399
399
    def getTemplateItemOutput(self, template_text):
400
 
        
 
400
 
401
401
        # keys to template data
402
402
        DATATYPE_KEY = "datatype"
403
403
        STATUSTEXT_KEY = "statustext"
404
404
        NOUNKNOWNOUTPUT_KEY = "nounknownoutput"
405
 
        
 
405
 
406
406
        datatype = None
407
407
        statustext = self.config.STATUSTEXT #default to command line option
408
408
        nounknownoutput = self.config.NOUNKNOWNOUTPUT #default to command line option
409
 
        
 
409
 
410
410
        for option in template_text.split('--'):
411
411
            if len(option) == 0 or option.isspace():
412
412
                continue
413
 
            
 
413
 
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('=')
416
416
            if (x != -1):
421
421
            else:
422
422
                key = option.strip()
423
423
                value = None
424
 
            
 
424
 
425
425
            try:
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
432
432
                else:
433
433
                    self.logger.info("Unknown template option: " + option)
434
 
    
 
434
 
435
435
            except (TypeError, ValueError):
436
436
                self.logger.info("Cannot convert option argument to number: " + option)
437
437
                return u""
438
 
                
 
438
 
439
439
        if datatype != None:
440
440
            return self.getOutputData(datatype, statustext, nounknownoutput)
441
441
        else:
442
442
            self.logger.info("Template item does not have datatype defined")
443
443
            return u""
444
 
    
445
 
    
 
444
 
 
445
 
446
446
    def getOutputFromTemplate(self, template):
447
447
        output = u""
448
448
        end = False
449
449
        a = 0
450
 
        
 
450
 
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
455
455
        while not end:
456
456
            b = template.find('[', a)
457
 
            
 
457
 
458
458
            if b == -1:
459
459
                b = len(template)
460
460
                end = True
461
 
            
 
461
 
462
462
            # if there is something between a and b, append it straight to output
463
463
            if b > a:
464
464
                output += template[a : b]
469
469
                    # skip the bracket in the input string and continue from the beginning
470
470
                    a = b + 1
471
471
                    continue
472
 
                    
 
472
 
473
473
            if end:
474
474
                break
475
 
            
 
475
 
476
476
            a = template.find(']', b)
477
 
            
 
477
 
478
478
            if a == -1:
479
479
                self.logger.info("Missing terminal bracket (]) for a template item")
480
480
                return u""
481
 
            
 
481
 
482
482
            # if there is some template text...
483
483
            if a > b + 1:
484
484
                output += self.getTemplateItemOutput(template[b + 1 : a])
485
 
            
 
485
 
486
486
            a = a + 1
487
 
    
 
487
 
488
488
        return output
489
 
    
 
489
 
490
490
    def getOutput(self):
491
491
 
492
492
        if self.options.noheader == True:
511
511
            headertemplate = inputfile.read()
512
512
        finally:
513
513
            inputfile.close()
514
 
            
 
514
 
515
515
        if self.options.template != None:
516
516
            templatefilepath = self.options.template
517
517
            self.logger.info("Using custom template file '%s'"%templatefilepath)
521
521
        else:
522
522
            templatefilepath = app_path+"/templates/rhythmbox.template"
523
523
            self.logger.info("Using default template")
524
 
             
 
524
 
525
525
        #load the file
526
526
        try:
527
527
            inputfile = codecs.open(os.path.expanduser(templatefilepath), encoding='utf-8')
536
536
        output = output + self.getOutputFromTemplate(template)
537
537
 
538
538
        return output.encode("utf-8")
539
 
    
540
 
    def isNumeric(self,value):
541
 
        try:
542
 
            temp = int(value)
543
 
            return True
544
 
        except:
545
 
            return False
546
 
 
547
 
    def getHTMLText(self,text):
548
 
        try:
549
 
            htmlentities = []               
550
 
            for char in text: #html:
551
 
                if ord(char) < 128:
552
 
                    htmlentities.append(char)
553
 
                else:
554
 
                    htmlentities.append('&%s;' % codepoint2name[ord(char)])
555
 
            html = "".join(htmlentities)
556
 
            
557
 
            html = html.replace("\n","<br>\n") # switch out new line for html breaks
558
 
            return html            
559
 
        except:
560
 
            return text
561
 
 
562
 
    def getCleanText(self,html):
563
 
        try:
564
 
            text = str(html)
565
 
            text = text.replace("\n","") # remove new lines from html
566
 
            text = text.replace("&apos;","'") # 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)
570
 
            return text            
571
 
        except:
572
 
            return html
573
 
        
 
539
 
574
540
def getHTML(options):
575
541
    output = Output(options)
576
542
    html = output.getOutput()
579
545
 
580
546
# to enable testing in isolation
581
547
if __name__ == "__main__":
582
 
    
 
548
 
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.")                
590
 
    
 
555
    parser.add_option("--logfile", dest="logfile", type="string", metavar="FILE", help=u"If a filepath is set, the script logs to the filepath.")
 
556
 
591
557
    (options, args) = parser.parse_args()
592
 
        
 
558
 
593
559
    output = Output(options)
594
560
    html = output.getOutput()
595
561
    del output