=== modified file 'plugins/cd/__init__.py'
--- plugins/cd/__init__.py 2012-10-03 04:43:15 +0000
+++ plugins/cd/__init__.py 2012-11-09 04:25:00 +0000
@@ -27,12 +27,12 @@
from xl.nls import gettext as _
from xl import providers, event
-from xl.hal import Handler
+from xl.hal import Handler, UDisksProvider
from xl.devices import Device
import logging
logger = logging.getLogger(__name__)
-PROVIDER = None
+PROVIDER = PROVIDER_UDISKS = None
import dbus, threading, os, struct
from fcntl import ioctl
@@ -61,15 +61,17 @@
CDROM_DATA_TRACK = 0x04
def enable(exaile):
- global PROVIDER
- PROVIDER = CDHandler()
- providers.register("hal", PROVIDER)
-
+ global PROVIDER, PROVIDER_UDISKS
+ #~ PROVIDER = CDHandler()
+ #~ providers.register("hal", PROVIDER)
+ PROVIDER_UDISKS = UDisksCdProvider()
+ providers.register("udisks", PROVIDER_UDISKS)
def disable(exaile):
- global PROVIDER
+ global PROVIDER, PROVIDER_UDISKS
providers.unregister("hal", PROVIDER)
- PROVIDER = None
+ providers.unregister("udisks", PROVIDER_UDISKS)
+ PROVIDER = PROVIDER_UDISKS = None
class CDTocParser(object):
#based on code from http://carey.geek.nz/code/python-cdrom/cdtoc.py
@@ -186,7 +188,7 @@
tr.set_tag_raw('genre',
info['DGENRE'])
- self._set_name(title[1].decode('iso-8859-15', 'replace'))
+ self.name = title[1].decode('iso-8859-15', 'replace')
event.log_event('cddb_info_retrieved', self, True)
class CDDevice(Device):
@@ -244,8 +246,25 @@
return cddev
+class UDisksCdProvider(UDisksProvider):
+ name = 'cd'
+ PRIORITY = UDisksProvider.NORMAL
+
+ def get_priority(self, obj):
+ props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties')
+ iface = 'org.freedesktop.UDisks.Device'
+ # DeviceChanged is called before and after tracks are read. We only want
+ # the second case, so use number of audio tracks to identify supported
+ # media. As a bonus, this means we never have to care about the type of
+ # disc (CD, DVD, etc.).
+ ntracks = props.Get(iface, 'OpticalDiscNumAudioTracks')
+ return self.PRIORITY if ntracks > 0 else None
+
+ def get_device(self, obj):
+ # TODO: If this is the same disc, return old device object.
+ props = dbus.Interface(obj, 'org.freedesktop.DBus.Properties')
+ iface = 'org.freedesktop.UDisks.Device'
+ return CDDevice(dev=str(props.Get(iface, 'DeviceFile')))
+
# vim: et sts=4 sw=4
-
-
-
=== modified file 'xl/hal.py'
--- xl/hal.py 2012-10-03 04:43:15 +0000
+++ xl/hal.py 2012-11-09 04:25:00 +0000
@@ -24,7 +24,7 @@
# do so. If you do not wish to do so, delete this exception statement
# from your version.
-import logging
+import logging, threading, time
import dbus
from xl import common, providers, event, devices, settings
@@ -32,6 +32,183 @@
logger = logging.getLogger(__name__)
+class UDisks(providers.ProviderHandler):
+ """
+ Provides support for UDisks devices.
+
+ If the D-Bus connection fails, this object will grow a "failed"
+ attribute with True as the value. Plugins should check for this when
+ registering if they want to provide HAL fallback. FIXME: There's a race
+ condition here.
+ """
+
+ # States: start -> init -> addremove <-> listening -> end.
+ # The addremove state acts as a lock against concurrent changes.
+
+ def __init__(self, devicemanager):
+ self._lock = lock = threading.Lock()
+ self._state = 'init'
+ logger.debug("UDisks: state = init")
+
+ providers.ProviderHandler.__init__(self, 'udisks')
+ self.devicemanager = devicemanager
+
+ self.bus = self.obj = self.iface = None
+ self.devices = {}
+ self.providers = {}
+
+ #~ @common.threaded
+ def connect(self):
+ assert self._state == 'init'
+ logger.debug("Connecting to UDisks")
+ try:
+ self.bus = bus = dbus.SystemBus()
+ self.obj = obj = bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks')
+ self.iface = iface = dbus.Interface(obj, 'org.freedesktop.UDisks')
+ iface.connect_to_signal('DeviceAdded', self._udisks_device_added)
+ iface.connect_to_signal('DeviceRemoved', self._udisks_device_removed)
+ iface.connect_to_signal('DeviceChanged', self._udisks_device_added)
+ logger.info("Connected to UDisks")
+ event.log_event("hal_connected", self, None)
+ except Exception:
+ logger.warning("Failed to connect to UDisks, " \
+ "autodetection of devices will be disabled.")
+ common.log_exception()
+ self._state = 'listening'
+ logger.debug("UDisks: state = listening")
+ self.failed = True
+ return
+ self._state = 'addremove'
+ logger.debug("UDisks: state = addremove")
+ self._add_all()
+ self._state = 'listening'
+ logger.debug("UDisks: state = listening")
+
+ def _add_all(self):
+ assert self._state == 'addremove'
+ for path in self.iface.EnumerateDevices():
+ self._add_device(path)
+
+ def _add_device(self, path=None, obj=None):
+ """
+ Call with either path or obj (obj gets priority). Not thread-safe.
+ """
+ assert self._state == 'addremove'
+ if obj is None:
+ obj = self.bus.get_object('org.freedesktop.UDisks', path)
+
+ # In the following code, `old` and `new` are providers, while
+ # `self.devices[path]` and `device` are old/new devices. There are
+ # several possible code paths that should be correctly handled:
+ # - No old nor new provider for this path.
+ # - Provider changes (nothing to something, something to nothing,
+ # something to something else); obviously device changes as well.
+ # - Provider stays the same, but device changes (i.e. instant media-
+ # swapping; not sure it can happen).
+ # - Provider and device stay the same.
+ old, new = self._get_provider_for(obj)
+ if new is None:
+ if old is not None:
+ self.devicemanager.remove_device(self.devices[path])
+ del self.devices[path]
+ return
+ device = new.get_device(obj)
+ if new is old and device is self.devices[path]:
+ return # Exactly the same device
+ if old is not None:
+ self.devicemanager.remove_device(self.devices[path])
+ if new is None:
+ return
+ try:
+ device.autoconnect()
+ except:
+ logger.exception("Failed autoconnecting device " + str(device))
+ else:
+ self.devicemanager.add_device(device)
+ self.providers[path] = new
+ self.devices[path] = device
+
+ def _get_provider_for(self, obj):
+ """
+ Return (old_provider, old_priority), (new_provider, new_priority).
+ Not thread-safe.
+ """
+ assert self._state == 'addremove'
+ highest_prio = -1
+ highest = None
+ old = self.providers.get(obj.object_path)
+ for provider in self.get_providers():
+ priority = provider.get_priority(obj)
+ if priority is None: continue
+ # Find highest priority, preferring old provider.
+ if priority > highest_prio or \
+ (priority == highest_prio and provider is old):
+ highest_prio = priority
+ highest = provider
+ return old, highest
+
+ def _remove_device(self, path):
+ assert self._state == 'addremove'
+ try:
+ self.devicemanager.remove_device(self.devices[path])
+ del self.devices[path]
+ except KeyError:
+ logger.warning("UDisks: Can't remove device (not found): " + path)
+
+ def _udisks_device_added(self, path):
+ logger.debug("UDisks: Device added: " + str(path))
+ if self._addremove():
+ self._add_device(path)
+ self._state = 'listening'
+ logger.debug("UDisks: state = listening (_device_added)")
+
+ def _udisks_device_removed(self, path):
+ if self._addremove():
+ try:
+ self._remove_device(path)
+ logger.debug("UDisks: Device removed: " + str(path))
+ except KeyError: # Not ours
+ pass
+ self._state = 'listening'
+ logger.debug("UDisks: state = listening")
+
+ # FIXME: Handle provider add/remove (following code unused & untested).
+
+ def on_provider_added(self, provider):
+ if self._addremove():
+ self._connect_all()
+ self._state = 'listening'
+ logger.debug("UDisks: state = listening")
+
+ def on_provider_removed(self, provider):
+ if self._addremove():
+ for path, provider_ in self.providers.iteritems():
+ if provider_ is provider:
+ self._remove_device(path)
+ self._state = 'listening'
+ logger.debug("UDisks: state = listening")
+
+ def _addremove(self):
+ """
+ Helper to transition safely from listening to addremove state.
+
+ Returns whether the transition happens.
+ """
+ i = 0
+ while True:
+ with self._lock:
+ if self._state == 'listening':
+ self._state = 'addremove'
+ logger.debug("UDisks: state = addremove")
+ return True
+ # If active state is init, we sleep and try again a few times.
+ # TODO: Whose thread is this we are blocking?
+ if i == 5:
+ logger.error("UDisks: Failed to acquire lock. Ignoring device event.")
+ return False
+ i += 1
+ time.sleep(1)
+
class HAL(providers.ProviderHandler):
"""
HAL interface
@@ -144,6 +321,12 @@
def device_from_udi(self, hal, udi):
pass
+class UDisksProvider:
+ VERY_LOW, LOW, NORMAL, HIGH, VERY_HIGH = range(0, 101, 25)
+ def get_priority(self, obj):
+ pass # return: int [0..100] or None
+ def get_device(self, obj):
+ pass # return: xl.devices.Device
+
# vim: et sts=4 sw=4
-
=== modified file 'xl/main.py'
--- xl/main.py 2012-10-24 04:33:53 +0000
+++ xl/main.py 2012-11-09 04:25:00 +0000
@@ -254,6 +254,8 @@
# Initialize HAL
if self.options.Hal:
from xl import hal
+ self.udisks = hal.UDisks(self.devices)
+ self.udisks.connect()
self.hal = hal.HAL(self.devices)
self.hal.connect()
else:
=== modified file 'xl/trax/util.py'
--- xl/trax/util.py 2012-10-03 04:43:15 +0000
+++ xl/trax/util.py 2012-11-09 04:25:00 +0000
@@ -65,14 +65,17 @@
tracks = []
gloc = gio.File(uri)
+
# don't do advanced checking on streaming-type uris as it can fail or
# otherwise be terribly slow.
# TODO: move uri definition somewhere more common for easy reuse?
-
if gloc.get_uri_scheme() in ('http', 'mms', 'cdda'):
return [Track(uri)]
- file_type = gloc.query_info("standard::type").get_file_type()
+ try:
+ file_type = gloc.query_info("standard::type").get_file_type()
+ except gio.Error: # E.g. cdda
+ file_type = None
if file_type == gio.FILE_TYPE_DIRECTORY:
# TODO: refactor Library so we dont need the collection obj
from xl.collection import Library, Collection
=== modified file 'xlgui/panel/flatplaylist.py'
--- xlgui/panel/flatplaylist.py 2012-10-03 04:43:15 +0000
+++ xlgui/panel/flatplaylist.py 2012-11-09 04:25:00 +0000
@@ -121,8 +121,7 @@
def set_playlist(self, playlist):
self.model.clear()
- tracks = [track for track in playlist]
- self.tracks = tracks
+ self.tracks = tracks = list(playlist)
for i, track in enumerate(tracks):
self.model.append([i + 1, track.get_tag_display("title"), track])