~ubuntu-branches/ubuntu/natty/exaile/natty

« back to all changes in this revision

Viewing changes to plugins/lastfmproxy/lastfmproxy.py

  • Committer: Bazaar Package Importer
  • Author(s): Nick Ellery
  • Date: 2008-11-04 18:33:00 UTC
  • mfrom: (1.1.7 upstream)
  • Revision ID: james.westby@ubuntu.com-20081104183300-e4t9seztl35bdb24
Tags: 0.2.14-0ubuntu1
* New upstream release (LP: #280287)
* debian/control:
  - tighten dependency on python-gtk2 to (>= 2.10)
  - added python-sexy to recommends to add a clear button to filters
  - added python-gnome2-extras to recommends for lyrics, better tray icon,
    etc.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 Adam Olsen 
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 1, or (at your option)
 
6
# any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
16
 
 
17
import os, zipfile, md5, random, time, re, sys
 
18
import xl.plugins as plugins
 
19
from gettext import gettext as _
 
20
random.seed(time.time())
 
21
from xl import common, xlmisc
 
22
from xl.panels import radio
 
23
import xl.path
 
24
import xl.media as media
 
25
import time, gtk, gobject, gtk.glade
 
26
import urllib
 
27
 
 
28
PLUGIN_NAME = _("LastFM Radio")
 
29
PLUGIN_AUTHORS = ['Adam Olsen <arolsen@gmail.com>']
 
30
PLUGIN_VERSION = "0.2.7"
 
31
PLUGIN_DESCRIPTION = _(r"""Allows for streaming via lastfm proxy.\n\nThis
 
32
plugin is very beta and still doesn't work perfectly.""")
 
33
PLUGIN_ENABLED = False
 
34
PLUGIN_ICON = None
 
35
 
 
36
TMP_DIR = None
 
37
PROXY = None
 
38
PLUGIN = None
 
39
HTTP_CLIENT = None
 
40
BUTTONS = []
 
41
TIPS = gtk.Tooltips()
 
42
GLADE_XML_STRING = None
 
43
CONS = plugins.SignalContainer()
 
44
SHOW_COVER = False
 
45
OLD_COVER_FUNC = None
 
46
 
 
47
def lastfm_error(message):
 
48
    common.error(APP.window, message)
 
49
 
 
50
class LastFMDriver(radio.RadioDriver):
 
51
    def __init__(self, panel):
 
52
        self.model = panel.model
 
53
        self.folder_icon = panel.folder
 
54
        self.note = panel.track
 
55
        self.tree = panel.tree
 
56
        self.panel = panel
 
57
        self.exaile = APP
 
58
        self.last_node = None
 
59
        self.no_new_page = True
 
60
 
 
61
        self.custom_urls = self.exaile.settings.get_list('custom_urls',
 
62
            plugin=plugins.name(__file__))
 
63
 
 
64
    def command(self, command):
 
65
        self.lfmcommand = command
 
66
        self.exaile.status.set_first(_("Running command: %s...") %
 
67
            command, 3500) 
 
68
        self.do_command()
 
69
 
 
70
    @common.threaded
 
71
    def do_command(self):
 
72
        HTTP_CLIENT.req(self.lfmcommand)
 
73
 
 
74
    def get_menu(self, item, menu):
 
75
        menu.append_separator()
 
76
        menu.append(_("Add Station"), self.on_add, 'gtk-add')
 
77
        if isinstance(item, radio.RadioGenre) and item.custom:
 
78
            menu.append(_("Remove Station"), self.on_remove, 'gtk-delete')
 
79
            self.last_menu_item = item
 
80
        menu.append_separator()
 
81
        menu.append(_("Play 'Similar Artist' Radio"), lambda *e:
 
82
            self.play_radio(
 
83
                _('Similar Artist'), 
 
84
                _('Artist'),
 
85
                "lastfm://artist/%s/similarartists"),
 
86
            'gtk-media-play')
 
87
 
 
88
        menu.append(_("Play 'Neighbour' Radio"), lambda *e:
 
89
            self.play_radio(
 
90
                _('Neighbour'), 
 
91
                _('User'),
 
92
                "lastfm://user/%s/personal"),
 
93
            'gtk-media-play')
 
94
 
 
95
        menu.append(_("Play 'Group' Radio"), lambda *e:
 
96
            self.play_radio(
 
97
                _('Group'),
 
98
                _("Group"),
 
99
                "lastfm://group/%s"),
 
100
            'gtk-media-play')
 
101
 
 
102
        menu.append(_('Play URL'), lambda *e: 
 
103
            self.play_radio(
 
104
                '', 
 
105
                _("URL"),
 
106
                'lastfm://%s'),
 
107
            'gtk-media-play')
 
108
            
 
109
        return menu
 
110
 
 
111
    def play_radio(self, title, field, url):
 
112
        """
 
113
            Opens a dialog and asks the user for a field, and plays the
 
114
            station
 
115
        """
 
116
        dialog = common.MultiTextEntryDialog(APP.window, title)
 
117
        dialog.add_field("%s: " % field)
 
118
        result = dialog.run()
 
119
        dialog.hide()
 
120
 
 
121
        if result == gtk.RESPONSE_OK:
 
122
            values = dialog.get_values()
 
123
            display = values[0]
 
124
            value = urllib.quote(values[0].replace("lastfm://", ''))
 
125
 
 
126
            item = radio.RadioGenre("%s: %s" % (field, display))
 
127
            item.lastfm_url = url % value
 
128
            self.load_genre(item)
 
129
 
 
130
    def on_add(self, *e):
 
131
        """
 
132
            Called when the user wants to add a station
 
133
        """
 
134
        dialog = common.MultiTextEntryDialog(APP.window, _("Add Station"))
 
135
        dialog.add_field(_("Name"))
 
136
        dialog.add_field(_("URL"))
 
137
 
 
138
        result = dialog.run()
 
139
        values = dialog.get_values()
 
140
        dialog.hide()
 
141
        if result == gtk.RESPONSE_OK:
 
142
            if values[1].find("://") == -1:
 
143
                values[1] = "lastfm://%s" % values[1]
 
144
            self.custom_urls.append(values)
 
145
            self.exaile.settings.set_list('custom_urls',
 
146
                self.custom_urls, plugin=plugins.name(__file__))
 
147
 
 
148
            item = radio.RadioGenre(values[0], self)
 
149
            item.lastfm_url = values[1]
 
150
            item.custom = True
 
151
            node = self.model.append(self.last_node, [self.note, item])
 
152
            item.node = node
 
153
 
 
154
    def on_remove(self, *e):
 
155
        item = self.last_menu_item
 
156
        node = item.node
 
157
 
 
158
        result = common.yes_no_dialog(APP.window, _("Are you sure you "
 
159
            "want to remove this item?"))
 
160
        if result == gtk.RESPONSE_YES:
 
161
            self.model.remove(node)
 
162
            
 
163
            new_custom = []
 
164
            for station, url in self.custom_urls:
 
165
                if item.name == station and item.lastfm_url == url:
 
166
                    continue
 
167
                else:
 
168
                    new_custom.append([station, url])
 
169
 
 
170
            self.custom_urls = new_custom
 
171
            self.exaile.settings.set_list('custom_urls',
 
172
                self.custom_urls, plugin=plugins.name(__file__))
 
173
 
 
174
        return False
 
175
 
 
176
    def load_streams(self, node, load_node, use_cache=True):
 
177
        stations = (
 
178
            (_('Personal'), 'lastfm://user/%s/personal' % self.config.username),
 
179
            (_('Recommended'), 'lastfm://user/%s/recommended/100' %
 
180
                self.config.username),
 
181
            (_('Neighbourhood'), 'lastfm://user/%s/neighbours' %
 
182
                self.config.username),
 
183
            (_('Loved Tracks'), 'lastfm://user/%s/loved' % self.config.username),
 
184
        )
 
185
 
 
186
        for station, url in stations:
 
187
            item = radio.RadioGenre(station, self)
 
188
            item.lastfm_url = url
 
189
            item.custom = False
 
190
            n = self.model.append(node, [self.note, item])
 
191
            item.node = n
 
192
 
 
193
        # custom
 
194
        for station, url in self.custom_urls:
 
195
            item = radio.RadioGenre(station, self)
 
196
            item.lastfm_url = url
 
197
            item.custom = True
 
198
            n = self.model.append(node, [self.note, item])
 
199
            item.node = n
 
200
 
 
201
        self.model.remove(load_node)
 
202
        self.last_node = node
 
203
 
 
204
    def handles_url(self, url):
 
205
        """
 
206
            Check to see if this is a lastfm:// url, and if so, return True
 
207
        """
 
208
        if re.match(r"^lastfm://.*$", url):
 
209
            return True
 
210
 
 
211
    def handle_url(self, url):
 
212
        """
 
213
            Handle a url
 
214
        """
 
215
        url = "lastfm://%s" % urllib.quote(url.replace('lastfm://', ''))
 
216
        item = radio.RadioGenre(url, self)
 
217
        item.lastfm_url = url
 
218
        self.load_genre(item)
 
219
 
 
220
    def load_genre(self, genre, rel=False):
 
221
        if not PROXY.proxy_ready:
 
222
            common.error(APP.window, _("The Last.FM Proxy has "
 
223
                "not yet connected to last.fm.  Please wait "
 
224
                "a moment and try again"))
 
225
            return
 
226
        current = self.exaile.player.current
 
227
        if hasattr(current, 'lastfm_track'):
 
228
            current.album = "LastFM: %s" % str(genre)
 
229
            new_loc = 'http://localhost:%d/lastfm.mp3' % self.listenport
 
230
            self.exaile.tracks.refresh_row(current)
 
231
 
 
232
            self.exaile.status.set_first(_("Changing stations..."), 3500)
 
233
            if not current.loc == new_loc:
 
234
                current.loc = new_loc
 
235
                self.exaile.player.play_track(current)
 
236
            if not current in self.exaile.tracks.songs:
 
237
                self.exaile.tracks.append_song(current)
 
238
            self.command("/changestation/%s" % genre.lastfm_url)
 
239
            xlmisc.log("LastFM: playing %s" % genre.lastfm_url)
 
240
            return
 
241
        tr = media.Track()
 
242
        tr.type = 'stream'
 
243
        tr.artist = _('LastFM Radio!')
 
244
        tr.album = "LastFM: %s" % str(genre)
 
245
        tr.title = "LastFM: %s" % str(genre)
 
246
        tr.loc = "http://localhost:%d/lastfm.mp3" % self.listenport
 
247
        tr.lastfm_track = True
 
248
 
 
249
        self.exaile.tracks.append_song(tr)
 
250
        self.exaile.player.play_track(tr)
 
251
        xlmisc.log("LastFM: playing %s" % genre.lastfm_url)
 
252
        self.command('/changestation/%s' % genre.lastfm_url)
 
253
        
 
254
    def __str__(self):
 
255
        return _("LastFM Radio")
 
256
 
 
257
def unzip_file(file, odir):
 
258
    """
 
259
        Unzips "file" to "odir" (output directory)
 
260
    """
 
261
    z = zipfile.ZipFile(file)
 
262
    for name in z.namelist():
 
263
        m = re.match(r'^(.*)/([^/]*)$', name)
 
264
        if m:
 
265
            dir = m.group(1)
 
266
 
 
267
            if not os.path.isdir(os.path.join(odir, dir)):
 
268
                os.makedirs(os.path.join(odir, dir))
 
269
 
 
270
            if not m.group(2): continue
 
271
 
 
272
        h = open(os.path.join(odir, name), 'w')
 
273
        h.write(z.read(name))
 
274
        h.close()
 
275
 
 
276
def load_data(zip):
 
277
    """
 
278
        Loads the data from the zipfile
 
279
    """
 
280
    global TMP_DIR, PLUGIN_ICON, GLADE_XML_STRING
 
281
    if TMP_DIR: return
 
282
 
 
283
    fname = os.sep.join(str(__file__).split(os.path.sep)[0:-1])
 
284
    TMP_DIR = "/tmp/lfmdir%s" % md5.new(str(random.randrange(0,
 
285
        123471287348834))).hexdigest()
 
286
    os.mkdir(TMP_DIR)
 
287
    unzip_file(fname, TMP_DIR)
 
288
 
 
289
    GLADE_XML_STRING = zip.get_data('data/lastfmproxy.glade')
 
290
 
 
291
@common.threaded
 
292
def run_proxy(config):
 
293
    RUN_COUNT = 0
 
294
    while True:
 
295
        try:
 
296
            PLUGIN.listenport = config.listenport
 
297
            PROXY.run(config.bind_address, config.listenport)
 
298
            return
 
299
        except Exception, e:
 
300
            if RUN_COUNT >= 5:
 
301
                raise(e)
 
302
            if e.args[0] == 98:
 
303
                RUN_COUNT += 1
 
304
                xlmisc.log("LastFM Proxy: Port %d in use, trying %d" %
 
305
                    (config.listenport, config.listenport + 1))
 
306
                config.listenport += 1
 
307
            else:
 
308
                raise(e)
 
309
 
 
310
BUTTON_ITEMS = (
 
311
    (_('LastFM: Skip this track'), 'gtk-media-forward', '/skip'),
 
312
    (_('LastFM: Mark this track as loved'), 'gtk-add', '/love'),
 
313
    (_('LastFM: Ban this track'), 'gtk-delete', '/ban'),
 
314
)
 
315
 
 
316
@common.threaded
 
317
def set_cover(value):
 
318
    if not OLD_COVER_FUNC or not hasattr(APP.player.current,
 
319
        'lastfm_track'): return
 
320
    fname = "/tmp/lfmcover%s" % md5.new(str(random.randrange(0,
 
321
        23423423442))).hexdigest()
 
322
 
 
323
    value = value.replace('130x130', '300x300')
 
324
    data = urllib.urlopen(value).read()
 
325
    h = open(fname, 'w+')
 
326
    h.write(data)
 
327
    h.close()
 
328
    APP.player.current._tmp_cover = fname
 
329
 
 
330
    gobject.idle_add(APP.cover.set_lfmimage, fname)
 
331
 
 
332
def album_callback(value):
 
333
    if OLD_COVER_FUNC:
 
334
        set_cover(value)
 
335
    xlmisc.log("LastFM: Got cover: %s" % value)
 
336
 
 
337
def new_cover_func(self, *e):
 
338
    """
 
339
        Do absolutely nothing.  Srs.
 
340
    """
 
341
    pass
 
342
 
 
343
SIGNAL_ID = None
 
344
def play_track(exaile, track):
 
345
    global SIGNAL_ID, OLD_COVER_FUNC
 
346
    if not SHOW_COVER or not hasattr(APP.player.current,
 
347
        'lastfm_track'): return
 
348
    
 
349
    OLD_COVER_FUNC = APP.cover.set_image
 
350
    APP.cover.set_image = new_cover_func
 
351
    APP.cover.set_lfmimage = OLD_COVER_FUNC
 
352
 
 
353
    APP.cover.set_lfmimage(xl.path.get_data('images', 'nocover.png'))
 
354
    SIGNAL_ID = APP.connect('timer_update', lambda *e:
 
355
        HTTP_CLIENT.req('/np'))
 
356
 
 
357
def stop_track(exaile, track):
 
358
    global SIGNAL_ID, OLD_COVER_FUNC
 
359
    if not SHOW_COVER: return
 
360
    if SIGNAL_ID:
 
361
        APP.disconnect(SIGNAL_ID)
 
362
        SIGNAL_ID = None
 
363
    if OLD_COVER_FUNC:
 
364
        APP.cover.set_lfmimage(xl.path.get_data('images', 'nocover.png'))
 
365
        APP.cover.set_image = OLD_COVER_FUNC
 
366
        OLD_COVER_FUNC = None
 
367
 
 
368
def initialize():
 
369
    global PROXY, PLUGIN, HTTP_CLIENT, BUTTONS, SHOW_COVER
 
370
    if not TMP_DIR in sys.path: sys.path.append(TMP_DIR)
 
371
 
 
372
    import lastfmmain
 
373
    import config
 
374
    import httpclient
 
375
 
 
376
    settings = APP.settings
 
377
    SHOW_COVER = settings.get_boolean('ui/show_cover', True)
 
378
 
 
379
    port = settings.get_int('listenport', plugin=plugins.name(__file__),
 
380
        default=1881)
 
381
    config.listenport = port
 
382
#    config.username = settings.get_str('lastfmuser',
 
383
#        plugin=plugins.name(__file__),
 
384
#        default=settings.get_str('lastfm/user', ''))
 
385
#    config.password = settings.get_crypted('lastfmpass',
 
386
#        plugin=plugins.name(__file__),
 
387
#        default=settings.get_str('lastfm/pass', ''))
 
388
 
 
389
#    if settings.get_boolean('lastfm_use_main', default=True,
 
390
#        plugin=plugins.name(__file__)):
 
391
    config.username = settings.get_str('lastfm/user', '')
 
392
    config.password = settings.get_crypted('lastfm/pass', '') 
 
393
 
 
394
    if not PLUGIN:
 
395
        PLUGIN = LastFMDriver(APP.pradio_panel)
 
396
        PLUGIN.config = config
 
397
        APP.pradio_panel.add_driver(PLUGIN, plugins.name(__file__))
 
398
        HTTP_CLIENT = httpclient.httpclient('localhost', config.listenport)
 
399
        APP.add_urlhandler(PLUGIN)
 
400
 
 
401
    PROXY = lastfmmain.proxy(config.username, config.password)
 
402
    PROXY.np_image_func = album_callback
 
403
    PROXY.basedir = TMP_DIR
 
404
    run_proxy(config)
 
405
 
 
406
    if not CONS.dict:
 
407
        CONS.connect(APP.player, 'play-track', play_track)
 
408
        CONS.connect(APP.player, 'stop-track', stop_track)
 
409
 
 
410
    if not BUTTONS:
 
411
        for tooltip, icon, command in BUTTON_ITEMS:
 
412
            button = gtk.Button()
 
413
            button.connect('clicked', lambda w, command=command: PLUGIN.command(command))
 
414
            image = gtk.Image()
 
415
            image.set_from_stock(icon, gtk.ICON_SIZE_MENU)
 
416
            button.set_size_request(32, 32)
 
417
            button.set_image(image)
 
418
            TIPS.set_tip(button, tooltip)
 
419
            APP.xml.get_widget('rating_toolbar').pack_start(button)
 
420
            BUTTONS.append(button)
 
421
        for button in BUTTONS:
 
422
            button.show()
 
423
 
 
424
    return True
 
425
 
 
426
def destroy():
 
427
    global PLUGIN, MENU_ITEM, PROXY, BUTTONS, OLD_COVER_FUNC
 
428
    if TMP_DIR:
 
429
        sys.path.remove(TMP_DIR) 
 
430
 
 
431
    CONS.disconnect_all()
 
432
 
 
433
    if OLD_COVER_FUNC:
 
434
        APP.cover.set_image = OLD_COVER_FUNC
 
435
        OLD_COVER_FUNC = None
 
436
 
 
437
    if PLUGIN:
 
438
        APP.pradio_panel.remove_driver(PLUGIN)
 
439
        APP.remove_urlhandler(PLUGIN)
 
440
 
 
441
    if BUTTONS:
 
442
        for button in BUTTONS:
 
443
            button.hide()
 
444
            button.destroy()
 
445
 
 
446
        BUTTONS = []
 
447
    
 
448
    if PROXY:
 
449
        PROXY.np_image_func = None
 
450
        PROXY.quit = True
 
451
 
 
452
    PROXY = None
 
453
    MENU_ITEM = None
 
454
    PLUGIN = None
 
455
 
 
456
#def use_main_toggled(box, user, password):
 
457
#    active = not box.get_active()
 
458
#    user.set_sensitive(active)
 
459
#    password.set_sensitive(active)
 
460
 
 
461
def quick_init():
 
462
    """
 
463
        Runs initialize, but returns False so the timer doesn't continue to
 
464
        run
 
465
    """
 
466
    global PROXY
 
467
    initialize()
 
468
 
 
469
    return False
 
470
 
 
471
def configure():
 
472
    global PROXY
 
473
    exaile = APP
 
474
    settings = exaile.settings
 
475
 
 
476
    xml = gtk.glade.xml_new_from_buffer(GLADE_XML_STRING,
 
477
        len(GLADE_XML_STRING))
 
478
 
 
479
    dialog = xml.get_widget('ConfigurationDialog')
 
480
#    use_main = xml.get_widget('lastfm_use_main')
 
481
#    lastfm_user = xml.get_widget('lastfm_user')
 
482
#    lastfm_pass = xml.get_widget('lastfm_pass')
 
483
    lastfm_listen_port = xml.get_widget('lastfm_listen_port')
 
484
 
 
485
#    use_main.set_active(settings.get_boolean('use_main',
 
486
#        plugin=plugins.name(__file__), default=True))
 
487
 
 
488
#    lastfm_user.set_text(settings.get_str('lastfmuser',
 
489
#        plugin=plugins.name(__file__),
 
490
#        default=settings.get_str('lastfm/user', '')))
 
491
#    lastfm_pass.set_text(settings.get_crypted('lastfmpass',
 
492
#        plugin=plugins.name(__file__),
 
493
#        default=settings.get_str('lastfm/pass', '')))
 
494
 
 
495
    lastfm_listen_port.set_text(settings.get_str('listenport',
 
496
        plugin=plugins.name(__file__), default=1881))
 
497
 
 
498
#    use_main.connect('toggled', lambda *e: use_main_toggled(use_main,
 
499
#        lastfm_user, lastfm_pass))
 
500
#    use_main_toggled(use_main, lastfm_user, lastfm_pass)
 
501
 
 
502
    result = dialog.run()
 
503
    dialog.hide()
 
504
    if result == gtk.RESPONSE_OK:
 
505
        if PROXY: 
 
506
            PROXY.np_image_func = None
 
507
            PROXY.quit = True
 
508
            PROXY.stop = True
 
509
            exaile.player.stop()
 
510
#        settings.set_boolean('use_main', use_main.get_active(),
 
511
#            plugin=plugins.name(__file__))
 
512
#        settings.set_str('lastfm_user', lastfm_user.get_text(), 
 
513
#            plugin=plugins.name(__file__))
 
514
#        settings.set_crypted('lastfm_pass', lastfm_pass.get_text(),
 
515
#            plugin=plugins.name(__file__))
 
516
 
 
517
        settings.set_str('listenport', lastfm_listen_port.get_text(),
 
518
            plugin=plugins.name(__file__))
 
519
 
 
520
        if PLUGIN_ENABLED:
 
521
            gobject.timeout_add(5000, quick_init)
 
522
 
 
523
icon_data = ["16 16 72 1",
 
524
"       c None",
 
525
".      c #D20039",
 
526
"+      c #D71B4E",
 
527
"@      c #EE9EB4",
 
528
"#      c #F9DFE6",
 
529
"$      c #FAE5EB",
 
530
"%      c #F5C8D4",
 
531
"&      c #E05077",
 
532
"*      c #E04D75",
 
533
"=      c #F6CAD6",
 
534
"-      c #F7D3DD",
 
535
";      c #E87A98",
 
536
">      c #D71E50",
 
537
",      c #F8D8E1",
 
538
"'      c #F5C5D2",
 
539
")      c #E1547A",
 
540
"!      c #DD416B",
 
541
"~      c #EB8DA6",
 
542
"{      c #FDF5F7",
 
543
"]      c #E36084",
 
544
"^      c #D40A41",
 
545
"/      c #FAE0E7",
 
546
"(      c #E66F8F",
 
547
"_      c #E04F76",
 
548
":      c #EC94AC",
 
549
"<      c #D40D43",
 
550
"[      c #EFA2B7",
 
551
"}      c #F3BBCA",
 
552
"|      c #E25C80",
 
553
"1      c #DD3D68",
 
554
"2      c #DC3965",
 
555
"3      c #F8DAE2",
 
556
"4      c #E1577D",
 
557
"5      c #D3033B",
 
558
"6      c #F6CBD7",
 
559
"7      c #EA8AA4",
 
560
"8      c #E67191",
 
561
"9      c #FBEAEF",
 
562
"0      c #F3BAC9",
 
563
"a      c #E36184",
 
564
"b      c #D3083F",
 
565
"c      c #F9DCE4",
 
566
"d      c #E05279",
 
567
"e      c #FAE1E8",
 
568
"f      c #D40E44",
 
569
"g      c #D82253",
 
570
"h      c #E9829E",
 
571
"i      c #FBEBEF",
 
572
"j      c #ED9BB1",
 
573
"k      c #F0A9BC",
 
574
"l      c #F1B1C2",
 
575
"m      c #DB3562",
 
576
"n      c #F6CED9",
 
577
"o      c #E56B8C",
 
578
"p      c #E1557B",
 
579
"q      c #F9DDE5",
 
580
"r      c #D92657",
 
581
"s      c #FBE6EC",
 
582
"t      c #F2B4C5",
 
583
"u      c #DC3A66",
 
584
"v      c #D92959",
 
585
"w      c #E77997",
 
586
"x      c #FDF2F5",
 
587
"y      c #E15379",
 
588
"z      c #FBE8ED",
 
589
"A      c #DF4B73",
 
590
"B      c #DA2B5A",
 
591
"C      c #F2B8C8",
 
592
"D      c #D92A5A",
 
593
"E      c #FAE3E9",
 
594
"F      c #F7CFDA",
 
595
"G      c #E9839F",
 
596
" .............. ",
 
597
"................",
 
598
"................",
 
599
"................",
 
600
"..+@#$%&..*=-;..",
 
601
".>,')!~{]^/(_:<.",
 
602
".[}....|$1$2....",
 
603
".34....567890ab.",
 
604
".cd.....]efghij.",
 
605
".kl....mgno..pq.",
 
606
".rstuvwx2yzAB0C.",
 
607
"..Dl/EFa..G3clg.",
 
608
"................",
 
609
"................",
 
610
"................",
 
611
" .............. "]
 
612
PLUGIN_ICON = gtk.gdk.pixbuf_new_from_xpm_data(icon_data)