1
# Copyright (C) 2006-2007 Aren Olson
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)
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.
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.
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
25
from daap import DAAPClient
30
#detect authoriztion support in python-daap
34
tmp.connect("spam","eggs","sausage") #dummy login
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."""
53
PLUGIN_ENABLED = False
54
PLUGIN_ICON = gtk.Button().render_icon('gtk-network', gtk.ICON_SIZE_MENU)
60
AVAHI_INTERFACE = None
63
# Have to use a string since we only have one file.
64
GLADE_XML_STRING = None
68
Called by Exaile to load the data from the zipfile
70
global GLADE_XML_STRING
72
GLADE_XML_STRING = zip.get_data('gui.glade')
74
class DaapAvahiInterface: #derived from python-daap/examples
76
Handles detection of DAAP shares via Avahi.
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))
82
Called when a new share is found.
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()
90
def remove_service(self, interface, protocol, name, type, domain, flags):
92
Called when the connection to a share is lost.
94
# print "DAAP: Lost %s." % name
95
nstr = '%s%s%s%s%s' % (interface, protocol, name, type, domain)
97
self.panel.update_connections()
99
def __init__(self, panel):
101
Sets up the avahi listener.
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)
116
class DaapConnection(object):
118
A connection to a DAAP share.
120
def __init__(self, name, server, port):
121
self.all = library.TrackData()
122
self.connected = False
130
def connect(self, password = None):
132
Connect, login, and retrieve the track list.
135
client = DAAPClient()
136
if AUTH and password:
137
client.connect(self.server, self.port, password)
139
client.connect(self.server, self.port)
140
self.session = client.login()
141
self.connected = True
142
except daap.DAAPError:
144
self.connected = False
147
def disconnect(self):
149
Disconnect, clean up.
152
self.session.logout()
158
self.all = library.TrackData()
159
self.connected = False
163
Reload the tracks from the server
165
APP.status.set_first(_("Retrieving track list from server..."))
168
self.all = library.TrackData()
174
def get_database(self):
176
Get a DAAP database and its track list.
179
self.database = self.session.library()
182
def get_tracks(self, reset = 0):
184
Get the track list from a DAAP database
186
if reset or self.tracks == None:
187
self.tracks = self.database.tracks()
190
def convert_list(self):
192
Converts the DAAP track database into DaapTracks.
195
while i < len(self.tracks):
199
# Convert DAAPTrack's attributes to media.Track's.
200
eqiv = {'title':'minm','artist':'asar','album':'asal',
201
'genre':'asgn','track':'astn','enc':'asfm',
203
for field in eqiv.keys():
205
setattr(temp, field, u'%s'%tr.atom.getAtom(eqiv[field]))
206
if getattr(temp, field) == "None":
207
setattr(temp, field, "Unknown")
209
setattr(temp, field, "Unknown")
211
#TODO: convert year (asyr) here as well, what's the formula?
213
setattr(temp, '_len', tr.atom.getAtom('astm') / 1000)
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)
227
APP.plugin_tracks[plugins.name(__file__)] = self.all
230
def get_track(self, track_id, filename):
232
Save the track with track_id to filename
234
for t in self.tracks:
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."""))
247
class NetworkPanel(collection.CollectionPanel):
249
A panel that displays the available DAAP shares and their contents.
252
def __init__(self, exaile, xml):
254
Expects the main exaile object, and a glade xml object.
260
self.connected = False
263
self.connection_list = []
265
self.transfer_queue = None
266
self.transferring = False
270
self.track_cache = dict()
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' %
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' %
282
self.genre_image = gtk.gdk.pixbuf_new_from_file('images%sgenre.png' %
287
self.xml.get_widget('shares_combo_box').connect('changed',
290
def switch_share(self, widget=None):
292
Change the active share
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...'):
298
dialog = common.TextEntryDialog(self.exaile.window,
299
_("Enter IP address and port for share"),
300
_("Enter IP address and port."))
302
if resp == gtk.RESPONSE_OK:
303
loc = dialog.get_value()
304
address = loc.split(':')[0]
306
port = loc.split(':')[1]
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>"""))
315
self.active_share = DaapConnection("Custom: ", address, port)
317
elif resp == gtk.RESPONSE_CANCEL:
318
shares_box.set_active(-1)
319
self.active_share = None
322
self.active_share = self.connection_list[shares_box.get_active()]
324
if self.active_share:
325
if not self.active_share.connected:
327
self.active_share.connect()
328
except daap.DAAPError:
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)
336
if resp == gtk.RESPONSE_OK:
337
password = dialog.get_value()
339
self.active_share.connect(password)
341
except daap.DAAPError:
343
elif resp == gtk.RESPONSE_CANCEL:
344
shares_box.set_active(-1)
345
self.active_share = None
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()
354
shares_box.set_active(-1)
355
self.active_share = None
357
if self.active_share and self.active_share.all:
358
self.all = self.active_share.all
367
def search_tracks(self, keyword, all):
369
Search the tracks for keyword.
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:
383
return library.lstrip_special(library.the_cutter(field))
385
slstrip = library.lstrip_special
387
if self.choice.get_active() == 2:
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:
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:
397
new = [(slstrip(a.album), a.track,
398
slstrip(a.title), a ) for a in check]
401
return library.TrackData([a[n] for a in new])
403
def update_connections(self):
404
sbox = self.xml.get_widget('shares_combo_box')
406
while n < 100: #not good, how can we find the # of entries?
407
sbox.remove_text(0) #No exception when nothing to remove?!
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...'))
416
def create_popup(self):
418
Creates the context menu.
421
self.append = menu.append(_("Append to Current"),
422
self.append_to_playlist)
424
self.queue_item = menu.append(_("Queue Items"),
425
self.append_to_playlist)
426
menu.append_separator()
428
self.save_item = menu.append(_("Save Items"),
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
444
collection.CollectionPanel.load_tree(self, event)
446
def save_selected(self, widget=None, event=None):
448
Save the selected tracks to disk.
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()
459
if result == gtk.RESPONSE_OK:
460
folder = dialog.get_current_folder()
461
self.save_items(items, folder)
464
def save_items(self, items, folder):
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)
476
Adds 'Network' tab to side panel, sets up Avahi.
478
global APP, TAB_PANE, GLADE_XML_STRING, PANEL, CONNECTIONS
481
raise plugins.PluginInitException("python-daap is not available, "
482
"disabling Music Sharing plugin.")
486
raise plugins.PluginInitException("Avahi is not available, "
487
"disabling Music Sharing plugin.")
490
xml = gtk.glade.xml_new_from_buffer(GLADE_XML_STRING,
491
len(GLADE_XML_STRING))
493
TAB_PANE = xml.get_widget('network_box')
494
PANEL = NetworkPanel(APP, xml)
495
AVAHI_INTERFACE = DaapAvahiInterface(PANEL)
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)
505
PANEL.update_connections()
512
Removes 'Network' tab, disconnects from shares.
516
AVAHI_INTERFACE = None
517
for s in CONNECTIONS.values():
518
#disconnect required for servers that limit connections, eg. itunes.
520
# print "DAAP: Disconnected from %s." % s.name
523
if APP.plugin_tracks.has_key(plugins.name(__file__)):
524
del APP.plugin_tracks[plugins.name(__file__)]