~qioeujqioejqioe-deactivatedaccount/exaile/missing-signals

« back to all changes in this revision

Viewing changes to plugins/daap-share/daap-share.py

  • Committer: Adam Olsen
  • Date: 2007-08-31 19:36:41 UTC
  • Revision ID: arolsen@gmail.com-20070831193641-isghtp1fq2433m2m
Moving the plugins directory into the source directory

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2007 Aren Olson
 
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, gtk, dbus
 
18
import xl.plugins as plugins
 
19
from xl import panels, media, xlmisc, library, common
 
20
from gettext import gettext as _
 
21
from xl.panels import collection
 
22
 
 
23
try:
 
24
    import daap
 
25
    from daap import DAAPClient
 
26
    DAAP = True
 
27
except:
 
28
    DAAP = False
 
29
 
 
30
#detect authoriztion support in python-daap
 
31
if DAAP:
 
32
    try:
 
33
        tmp = DAAPClient()
 
34
        tmp.connect("spam","eggs","sausage") #dummy login
 
35
        del tmp
 
36
    except TypeError:
 
37
        AUTH = False
 
38
    except:
 
39
        AUTH = True
 
40
 
 
41
try:
 
42
    import avahi
 
43
    AVAHI = True
 
44
except:
 
45
    AVAHI = False
 
46
 
 
47
PLUGIN_NAME = "Music Sharing"
 
48
PLUGIN_AUTHORS = ['Aren Olson <reacocard@gmail.com>']
 
49
PLUGIN_VERSION = '0.7.6'
 
50
PLUGIN_DESCRIPTION = r"""Allows playing of DAAP music shares.
 
51
\n\nDepends: python-daap, python-avahi."""
 
52
 
 
53
PLUGIN_ENABLED = False
 
54
PLUGIN_ICON = gtk.Button().render_icon('gtk-network', gtk.ICON_SIZE_MENU)
 
55
 
 
56
APP = None
 
57
SETTINGS = None
 
58
TAB_PANE = None
 
59
PANEL = None
 
60
AVAHI_INTERFACE = None
 
61
CONNECTIONS = {}
 
62
 
 
63
# Have to use a string since we only have one file.
 
64
GLADE_XML_STRING = None
 
65
 
 
66
def load_data(zip):
 
67
    """
 
68
        Called by Exaile to load the data from the zipfile
 
69
    """
 
70
    global GLADE_XML_STRING
 
71
 
 
72
    GLADE_XML_STRING = zip.get_data('gui.glade')
 
73
 
 
74
class DaapAvahiInterface: #derived from python-daap/examples
 
75
    """
 
76
        Handles detection of DAAP shares via Avahi.
 
77
    """
 
78
 
 
79
    def new_service(self, interface, protocol, name, type, domain, flags):
 
80
        interface, protocol, name, type, domain, host, aprotocol, address, port, txt, flags = self.server.ResolveService(interface, protocol, name, type, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0))
 
81
        """
 
82
            Called when a new share is found.
 
83
        """
 
84
#        print "DAAP: Found %s." % name
 
85
        #Use all available info in key to avoid name conflicts.
 
86
        nstr = '%s%s%s%s%s' % (interface, protocol, name, type, domain)
 
87
        CONNECTIONS[nstr] = DaapConnection(name, address, port)
 
88
        self.panel.update_connections()
 
89
 
 
90
    def remove_service(self, interface, protocol, name, type, domain, flags):
 
91
        """
 
92
            Called when the connection to a share is lost.
 
93
        """
 
94
#        print "DAAP: Lost %s." % name
 
95
        nstr = '%s%s%s%s%s' % (interface, protocol, name, type, domain)
 
96
        del CONNECTIONS[nstr]
 
97
        self.panel.update_connections()
 
98
 
 
99
    def __init__(self, panel):
 
100
        """
 
101
            Sets up the avahi listener.
 
102
        """
 
103
        self.bus = dbus.SystemBus()
 
104
        self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME,
 
105
            avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)
 
106
        self.stype = '_daap._tcp'
 
107
        self.domain = 'local'
 
108
        self.browser = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME,
 
109
            self.server.ServiceBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC,
 
110
            self.stype, self.domain, dbus.UInt32(0))),
 
111
            avahi.DBUS_INTERFACE_SERVICE_BROWSER)
 
112
        self.browser.connect_to_signal('ItemNew', self.new_service)
 
113
        self.browser.connect_to_signal('ItemRemove', self.remove_service)
 
114
        self.panel = panel
 
115
 
 
116
class DaapConnection(object):
 
117
    """
 
118
        A connection to a DAAP share.
 
119
    """
 
120
    def __init__(self, name, server, port):
 
121
        self.all = library.TrackData()
 
122
        self.connected = False
 
123
        self.tracks = None
 
124
        self.server = server
 
125
        self.port = port
 
126
        self.name = name
 
127
        self.auth = False
 
128
        self.password = None
 
129
 
 
130
    def connect(self, password = None):
 
131
        """
 
132
            Connect, login, and retrieve the track list.
 
133
        """
 
134
        try:
 
135
            client = DAAPClient()
 
136
            if AUTH and password:
 
137
                client.connect(self.server, self.port, password)
 
138
            else:
 
139
                client.connect(self.server, self.port)
 
140
            self.session = client.login()
 
141
            self.connected = True
 
142
        except daap.DAAPError:
 
143
            self.auth = True
 
144
            self.connected = False
 
145
            raise daap.DAAPError
 
146
 
 
147
    def disconnect(self):
 
148
        """
 
149
            Disconnect, clean up.
 
150
        """
 
151
        try:
 
152
            self.session.logout()
 
153
        except:
 
154
            pass
 
155
        self.session = None
 
156
        self.tracks = None
 
157
        self.database = None
 
158
        self.all = library.TrackData()
 
159
        self.connected = False
 
160
 
 
161
    def reload(self):
 
162
        """
 
163
            Reload the tracks from the server
 
164
        """
 
165
        APP.status.set_first(_("Retrieving track list from server..."))
 
166
        self.tracks = None
 
167
        self.database = None
 
168
        self.all = library.TrackData()
 
169
        self.get_database()
 
170
        self.convert_list()
 
171
        APP.status.clear()
 
172
 
 
173
 
 
174
    def get_database(self):
 
175
        """
 
176
            Get a DAAP database and its track list.
 
177
        """
 
178
        if self.session:
 
179
            self.database = self.session.library()
 
180
            self.get_tracks(1)
 
181
 
 
182
    def get_tracks(self, reset = 0):
 
183
        """
 
184
            Get the track list from a DAAP database
 
185
        """
 
186
        if reset or self.tracks == None:
 
187
            self.tracks = self.database.tracks()
 
188
            return self.tracks
 
189
 
 
190
    def convert_list(self):
 
191
        """
 
192
            Converts the DAAP track database into DaapTracks.
 
193
        """
 
194
        i = 0
 
195
        while i < len(self.tracks):
 
196
            tr = self.tracks[i]
 
197
            if tr:
 
198
                temp = media.Track()
 
199
                # Convert DAAPTrack's attributes to media.Track's.
 
200
                eqiv = {'title':'minm','artist':'asar','album':'asal',
 
201
                    'genre':'asgn','track':'astn','enc':'asfm',
 
202
                    'bitrate':'asbr'}
 
203
                for field in eqiv.keys():
 
204
                    try:
 
205
                        setattr(temp, field, u'%s'%tr.atom.getAtom(eqiv[field]))
 
206
                        if getattr(temp, field) == "None":
 
207
                            setattr(temp, field, "Unknown")
 
208
                    except:
 
209
                        setattr(temp, field, "Unknown")
 
210
 
 
211
                #TODO: convert year (asyr) here as well, what's the formula?
 
212
                try:
 
213
                    setattr(temp, '_len', tr.atom.getAtom('astm') / 1000)
 
214
                except:
 
215
                    setattr(temp, '_len', 0)
 
216
                temp.type = getattr(tr, 'type')
 
217
                temp.daapid = getattr(tr, 'id')
 
218
                temp.connection = self
 
219
    #http://<server>:<port>/databases/<dbid>/items/<id>.<type>?session-id=<sessionid>
 
220
                temp.loc = "http://%s:%s/databases/%s/items/%s.%s?session-id=%s" % \
 
221
                    (self.server, self.port, self.database.id, temp.daapid,
 
222
                    temp.type, self.session.sessionid)
 
223
                temp._rating = 2 #bad, but it works.
 
224
                self.all.append(temp)
 
225
            i = i + 1
 
226
 
 
227
        APP.plugin_tracks[plugins.name(__file__)] = self.all
 
228
 
 
229
    #@common.threaded
 
230
    def get_track(self, track_id, filename):
 
231
        """
 
232
            Save the track with track_id to filename
 
233
        """
 
234
        for t in self.tracks:
 
235
            if t.id == track_id:
 
236
                try:
 
237
                    t.save(filename)
 
238
                except CannotSendRequest:
 
239
                    dialog = gtk.MessageDialog(APP.window,
 
240
                        gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
 
241
                        _("""This server does not support multiple connections.
 
242
You must stop playback before downloading songs."""))
 
243
 
 
244
 
 
245
 
 
246
 
 
247
class NetworkPanel(collection.CollectionPanel):
 
248
    """
 
249
        A panel that displays the available DAAP shares and their contents.
 
250
    """
 
251
    name = 'network'
 
252
    def __init__(self, exaile, xml):
 
253
        """
 
254
            Expects the main exaile object, and a glade xml object.
 
255
        """
 
256
        self.xml = xml
 
257
        self.exaile = exaile
 
258
 
 
259
        self.db = exaile.db
 
260
        self.connected = False
 
261
        self.all = {}
 
262
 
 
263
        self.connection_list = []
 
264
 
 
265
        self.transfer_queue = None
 
266
        self.transferring = False
 
267
        self.queue = None
 
268
 
 
269
        self.keyword = None
 
270
        self.track_cache = dict()
 
271
        self.start_count = 0
 
272
        self.tree = None
 
273
        self.connect_id = None
 
274
        self.separator_image = self.exaile.window.render_icon('gtk-remove', 
 
275
            gtk.ICON_SIZE_SMALL_TOOLBAR)
 
276
        self.artist_image = gtk.gdk.pixbuf_new_from_file('images%sartist.png' %
 
277
            os.sep)
 
278
        self.album_image = self.exaile.window.render_icon('gtk-cdrom',
 
279
            gtk.ICON_SIZE_SMALL_TOOLBAR)
 
280
        self.track_image = gtk.gdk.pixbuf_new_from_file('images%strack.png' %
 
281
            os.sep)
 
282
        self.genre_image = gtk.gdk.pixbuf_new_from_file('images%sgenre.png' %
 
283
            os.sep)
 
284
 
 
285
        self.setup_widgets()
 
286
 
 
287
        self.xml.get_widget('shares_combo_box').connect('changed',
 
288
            self.switch_share)
 
289
 
 
290
    def switch_share(self, widget=None):
 
291
        """
 
292
            Change the active share
 
293
        """
 
294
        shares_box = self.xml.get_widget('shares_combo_box')
 
295
        if shares_box.get_active() != -1:
 
296
            if shares_box.get_active_text() == _('Custom location...'):
 
297
                while 1:
 
298
                    dialog = common.TextEntryDialog(self.exaile.window,
 
299
                        _("Enter IP address and port for share"),
 
300
                        _("Enter IP address and port."))
 
301
                    resp = dialog.run()
 
302
                    if resp == gtk.RESPONSE_OK:
 
303
                        loc = dialog.get_value()
 
304
                        address = loc.split(':')[0]
 
305
                        try:
 
306
                            port = loc.split(':')[1]
 
307
                        except IndexError:
 
308
                            dialog = gtk.MessageDialog(APP.window,
 
309
                            gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
 
310
                            _("""You must supply an IP address and port number, in the format
 
311
<ip address>:<port>"""))
 
312
                            dialog.run()
 
313
                            dialog.destroy()
 
314
                            continue
 
315
                        self.active_share = DaapConnection("Custom: ", address, port)
 
316
                        break
 
317
                    elif resp == gtk.RESPONSE_CANCEL:
 
318
                        shares_box.set_active(-1)
 
319
                        self.active_share = None
 
320
                        break
 
321
            else:
 
322
                self.active_share = self.connection_list[shares_box.get_active()]
 
323
 
 
324
            if self.active_share:
 
325
                if not self.active_share.connected:
 
326
                    try:
 
327
                        self.active_share.connect()
 
328
                    except daap.DAAPError:
 
329
                        while 1:
 
330
                            dialog = common.TextEntryDialog(self.exaile.window,
 
331
                                _("%s %s") % ("Enter password for",
 
332
                                 self.active_share.name ),
 
333
                                _("Password required."))
 
334
                            dialog.entry.set_visibility(False)
 
335
                            resp = dialog.run()
 
336
                            if resp == gtk.RESPONSE_OK:
 
337
                                password = dialog.get_value()
 
338
                                try:
 
339
                                    self.active_share.connect(password)
 
340
                                    break
 
341
                                except daap.DAAPError:
 
342
                                    continue
 
343
                            elif resp == gtk.RESPONSE_CANCEL:
 
344
                                shares_box.set_active(-1)
 
345
                                self.active_share = None
 
346
                                break
 
347
                    except:
 
348
                        dialog = gtk.MessageDialog(APP.window,
 
349
                            gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
 
350
                            _("Could not connect to server. Did you enter the correct address and port?"))
 
351
                        result = dialog.run()
 
352
                        dialog.destroy()
 
353
                        print result
 
354
                        shares_box.set_active(-1)
 
355
                        self.active_share = None
 
356
 
 
357
                if self.active_share and self.active_share.all:
 
358
                    self.all = self.active_share.all
 
359
                else:
 
360
                    self.all = {}
 
361
            else:
 
362
                self.all = {}
 
363
        else:
 
364
            self.all = {}
 
365
        self.load_tree(1)
 
366
 
 
367
    def search_tracks(self, keyword, all):
 
368
        """
 
369
            Search the tracks for keyword.
 
370
        """
 
371
        if keyword:
 
372
            check = []
 
373
            for track in self.all:
 
374
                for item in ('artist', 'album', 'title'):
 
375
                    attr = getattr(track, item)
 
376
                    if attr.lower().find(keyword.lower()) > -1:
 
377
                        check.append(track)
 
378
                        break
 
379
        else:
 
380
            check = self.all
 
381
 
 
382
        def stripit(field):
 
383
            return library.lstrip_special(library.the_cutter(field))
 
384
 
 
385
        slstrip = library.lstrip_special
 
386
 
 
387
        if self.choice.get_active() == 2:
 
388
            n = 5
 
389
            new = [(slstrip(a.genre), stripit(a.artist), slstrip(a.album),
 
390
                    a.track, slstrip(a.title), a) for a in check]
 
391
        elif self.choice.get_active() == 0:
 
392
            n = 4
 
393
            new = [(stripit(a.artist), slstrip(a.album), a.track, 
 
394
                    slstrip(a.title), a) for a in check]
 
395
        elif self.choice.get_active() == 1:
 
396
            n = 3
 
397
            new = [(slstrip(a.album), a.track, 
 
398
                    slstrip(a.title), a ) for a in check]
 
399
 
 
400
        new.sort()
 
401
        return library.TrackData([a[n] for a in new])
 
402
 
 
403
    def update_connections(self):
 
404
        sbox = self.xml.get_widget('shares_combo_box')
 
405
        n = 0
 
406
        while n < 100: #not good, how can we find the # of entries?
 
407
            sbox.remove_text(0) #No exception when nothing to remove?!
 
408
            n = n + 1
 
409
        self.connection_list = []
 
410
        for c in CONNECTIONS.values():
 
411
            self.connection_list.append(c)
 
412
        for c in self.connection_list:
 
413
            sbox.append_text('%s (%s:%s)' % (c.name, c.server, c.port))
 
414
        sbox.append_text(_('Custom location...'))
 
415
 
 
416
    def create_popup(self):
 
417
        """
 
418
            Creates the context menu.
 
419
        """
 
420
        menu = xlmisc.Menu()
 
421
        self.append = menu.append(_("Append to Current"),
 
422
            self.append_to_playlist)
 
423
 
 
424
        self.queue_item = menu.append(_("Queue Items"),
 
425
            self.append_to_playlist)
 
426
        menu.append_separator()
 
427
 
 
428
        self.save_item = menu.append(_("Save Items"),
 
429
            self.save_selected)
 
430
 
 
431
        self.menu = menu
 
432
 
 
433
    @common.threaded
 
434
    def load_tree(self, event=None):
 
435
        if event and not isinstance(event, gtk.ComboBox):
 
436
            shares_box = self.xml.get_widget('shares_combo_box')
 
437
            if shares_box.get_active() != -1:
 
438
                #active_share = self.connection_list[shares_box.get_active()]
 
439
                self.active_share.reload()
 
440
                if self.active_share.all:
 
441
                    self.all = self.active_share.all
 
442
                else:
 
443
                    self.all = {}
 
444
        collection.CollectionPanel.load_tree(self, event)
 
445
 
 
446
    def save_selected(self, widget=None, event=None):
 
447
        """
 
448
            Save the selected tracks to disk.
 
449
        """
 
450
        items = self.get_selected_items()
 
451
        dialog = gtk.FileChooserDialog(_("Select a save location"),
 
452
            APP.window, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
 
453
            (_('Open'), gtk.RESPONSE_OK, _('Cancel'), gtk.RESPONSE_CANCEL))
 
454
        dialog.set_current_folder(APP.get_last_dir())
 
455
        dialog.set_select_multiple(False)
 
456
        result = dialog.run()
 
457
        dialog.hide()
 
458
 
 
459
        if result == gtk.RESPONSE_OK:
 
460
            folder = dialog.get_current_folder()
 
461
            self.save_items(items, folder)
 
462
 
 
463
    @common.threaded
 
464
    def save_items(self, items, folder):
 
465
        for i in items:
 
466
            tnum = i.get_track()
 
467
            if tnum < 10: tnum = "0%s"%tnum
 
468
            else: tnum = str(tnum)
 
469
            filename = "%s%s%s - %s.%s"%(folder, os.sep, tnum,
 
470
                i.get_title(), i.type)
 
471
            i.connection.get_track(i.daapid, filename)
 
472
#                print "DAAP: saving track %s to %s."%(i.daapid, filename)
 
473
 
 
474
def initialize():
 
475
    """
 
476
        Adds 'Network' tab to side panel, sets up Avahi.
 
477
    """
 
478
    global APP, TAB_PANE, GLADE_XML_STRING, PANEL, CONNECTIONS
 
479
 
 
480
    if not DAAP:
 
481
        raise plugins.PluginInitException("python-daap is not available, "
 
482
                    "disabling Music Sharing plugin.")
 
483
        return False
 
484
 
 
485
    if not AVAHI:
 
486
        raise plugins.PluginInitException("Avahi is not available, "
 
487
                    "disabling Music Sharing plugin.")
 
488
        return False
 
489
 
 
490
    xml = gtk.glade.xml_new_from_buffer(GLADE_XML_STRING,
 
491
        len(GLADE_XML_STRING))
 
492
 
 
493
    TAB_PANE = xml.get_widget('network_box')
 
494
    PANEL = NetworkPanel(APP, xml)
 
495
    AVAHI_INTERFACE = DaapAvahiInterface(PANEL)
 
496
 
 
497
    notebook = APP.xml.get_widget('side_notebook')
 
498
    tab_label = gtk.Label()
 
499
    tab_label.set_text(_('Network'))
 
500
    tab_label.set_angle(90)
 
501
    notebook.append_page(TAB_PANE, tab_label)
 
502
 
 
503
    TAB_PANE.show_all()
 
504
    PANEL.load_tree()
 
505
    PANEL.update_connections()
 
506
 
 
507
    return True
 
508
 
 
509
 
 
510
def destroy():
 
511
    """
 
512
        Removes 'Network' tab, disconnects from shares.
 
513
    """
 
514
    global CONNECTIONS
 
515
    TAB_PANE.destroy()
 
516
    AVAHI_INTERFACE = None
 
517
    for s in CONNECTIONS.values():
 
518
        #disconnect required for servers that limit connections, eg. itunes.
 
519
        s.disconnect()
 
520
#        print "DAAP: Disconnected from %s." % s.name
 
521
    CONNECTIONS = {}
 
522
    PANEL = None
 
523
    if APP.plugin_tracks.has_key(plugins.name(__file__)):
 
524
        del APP.plugin_tracks[plugins.name(__file__)]