Merge lp:~linkinpark342/exaile/exaile-mpris into lp:exaile/0.3.3

Proposed by Abhishek Mukherjee
Status: Merged
Merged at revision: 1832
Proposed branch: lp:~linkinpark342/exaile/exaile-mpris
Merge into: lp:exaile/0.3.3
Diff against target: None lines
To merge this branch: bzr merge lp:~linkinpark342/exaile/exaile-mpris
Reviewer Review Type Date Requested Status
Johannes Sasongko Needs Resubmitting
Review via email: mp+5335@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Abhishek Mukherjee (linkinpark342) wrote :

This is a plug-in to support the MPRIS D-Bus specification, defined at http://wiki.xmms2.xmms.se/wiki/MPRIS . Many other fancy media players are following this spec (re: VLC, Amarok2), so why not us. This would also allow us to interact with the all of one Ubuntu package (from my quick perusal of apt-cache search) which depends on this for it's sole functionality.

All functions/signals are implemented with the exception of setting repeat on a single track, which I believe we do not support yet. GUI controls for Volume, Random, and Shuffle do not update with the backend, however the calls do effect the player. See bug 355749

Revision history for this message
Abhishek Mukherjee (linkinpark342) wrote :

I forgot to mention this branch also has
a) Notification plugin update. fixes bug 355470
  i) Cover are shown
  ii) Replaces stale Notifications
b) Plugin interface update
  i) Configure is only clickable for plugins that allow it

Revision history for this message
Johannes Sasongko (sjohannes) wrote :

Thanks, but there are some problems with the proposed merge.

1. Could you submit a clean patch (against trunk) with just the plugin, please. The other things you included in the patch should be reported in other bug reports so they can be reviewed separately. You should also create a new branch from trunk, so the commit history for those other files are not changed.

2. Another thing, please add copyright/licence headers to the source files, similar to the ones in most of Exaile's source code. (I think the preferred licence is GPL version "2 or later".) I know some of the other plugins are missing these headers, but I consider it a bug. This will make it possible for us to contact individual authors if there is ever any licence issues.

review: Needs Resubmitting
Revision history for this message
reacocard (reacocard) wrote :

Actually for 0.3 the preferred license is GPLv3 or later.

1832. By Abhishek Mukherjee

Added MPRIS Method calling syntax. Still need to add signals

1833. By Abhishek Mukherjee

Added signal declarations. Need to connect to player.

1834. By Abhishek Mukherjee

Added signal handling

1835. By Abhishek Mukherjee

Added tag error handling

1836. By Abhishek Mukherjee

Added ability to enable-disable MPRIS

1837. By Abhishek Mukherjee

Added Docstrings to exaile_mpris

1838. By Abhishek Mukherjee

Added what we learned from some unit testing

1839. By Abhishek Mukherjee

Added the unit test itself

1840. By Abhishek Mukherjee

plugins/mpris/test.py: Added docstrings, fixed testPosition

1841. By Abhishek Mukherjee

plugins/mpris/mpris_player.py: Added sanity check on PositionSet

1842. By Abhishek Mukherjee

Added docstrings, fixed some style. Added GPLv1 headers

1843. By Abhishek Mukherjee

Upgraded to GPLv3, fixed one of the copyright headers

Revision history for this message
Abhishek Mukherjee (linkinpark342) wrote :

> Thanks, but there are some problems with the proposed merge.
>
> 1. Could you submit a clean patch (against trunk) with just the plugin,
> please. The other things you included in the patch should be reported in other
> bug reports so they can be reviewed separately. You should also create a new
> branch from trunk, so the commit history for those other files are not
> changed.
Heh, i tried to sneak them in without someone noticing. not really. I just forgot to branch properly when i first got the repo. I overwrote the entire history with a new branch without any of the various other fixes.
>
> 2. Another thing, please add copyright/licence headers to the source files,
> similar to the ones in most of Exaile's source code. (I think the preferred
> licence is GPL version "2 or later".) I know some of the other plugins are
> missing these headers, but I consider it a bug. This will make it possible for
> us to contact individual authors if there is ever any licence issues.
I had thought I put them there. I did however upgrade to v3 as Adam mentioned below.

1844. By Abhishek Mukherjee

Added warning on the test

1845. By Abhishek Mukherjee

Added conversion for artist tag

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'plugins/mpris'
2=== added file 'plugins/mpris/PLUGININFO'
3--- plugins/mpris/PLUGININFO 1970-01-01 00:00:00 +0000
4+++ plugins/mpris/PLUGININFO 2009-04-05 03:55:27 +0000
5@@ -0,0 +1,4 @@
6+Version="0.0.1"
7+Authors=["Abhishek Mukherjee <abhishek.mukher.g@gmail.com"]
8+Name="MPRIS"
9+Description="Creates an MPRIS D-Bus object to control Exaile"
10
11=== added file 'plugins/mpris/__init__.py'
12--- plugins/mpris/__init__.py 1970-01-01 00:00:00 +0000
13+++ plugins/mpris/__init__.py 2009-04-05 21:01:49 +0000
14@@ -0,0 +1,20 @@
15+__all__ = ["exaile_mpris"]
16+
17+import exaile_mpris
18+import logging
19+
20+LOG = logging.getLogger("exaile.plugins.mpris")
21+
22+_MPRIS = None
23+
24+def enable(exaile):
25+ LOG.debug("Enabling MPRIS")
26+ if _MPRIS is None:
27+ global _MPRIS
28+ _MPRIS = exaile_mpris.ExaileMpris(exaile)
29+ _MPRIS.exaile = exaile
30+ _MPRIS.acquire()
31+
32+def disable(exaile):
33+ LOG.debug("Disabling MPRIS")
34+ _MPRIS.release()
35
36=== added file 'plugins/mpris/exaile_mpris.py'
37--- plugins/mpris/exaile_mpris.py 1970-01-01 00:00:00 +0000
38+++ plugins/mpris/exaile_mpris.py 2009-04-05 21:15:43 +0000
39@@ -0,0 +1,72 @@
40+"""
41+An implementation of the MPRIS D-Bus protocol for use with Exaile
42+"""
43+
44+import dbus
45+import dbus.service
46+import logging
47+
48+import mpris_root
49+import mpris_tracklist
50+import mpris_player
51+
52+LOG = logging.getLogger("exaile.plugins.mpris.exaile_mpris")
53+
54+OBJECT_NAME = 'org.mpris.exaile'
55+
56+class ExaileMpris(object):
57+
58+ """
59+ Controller for various MPRIS objects.
60+ """
61+
62+ def __init__(self, exaile=None):
63+ """
64+ Constructs an MPRIS controller. Note, you must call acquire()
65+ """
66+ self.exaile = exaile
67+ self.mpris_root = None
68+ self.mpris_tracklist = None
69+ self.mpris_player = None
70+ self.bus = None
71+
72+ def release(self):
73+ """
74+ Releases all objects from D-Bus and unregisters the bus
75+ """
76+ for obj in (self.mpris_root, self.mpris_tracklist, self.mpris_player):
77+ if obj is not None:
78+ obj.remove_from_connection()
79+ self.mpris_root = None
80+ self.mpris_tracklist = None
81+ self.mpris_player = None
82+ if self.bus is not None:
83+ self.bus.get_bus().release_name(self.bus.get_name())
84+
85+ def acquire(self):
86+ """
87+ Connects to D-Bus and registers all components
88+ """
89+ self._acquire_bus()
90+ self._add_interfaces()
91+
92+ def _acquire_bus(self):
93+ """
94+ Connect to D-Bus and set self.bus to be a valid connection
95+ """
96+ if self.bus is not None:
97+ self.bus.get_bus().request_name(OBJECT_NAME)
98+ else:
99+ self.bus = dbus.service.BusName(OBJECT_NAME, bus=dbus.SessionBus())
100+
101+ def _add_interfaces(self):
102+ """
103+ Connects all interfaces to D-Bus
104+ """
105+ self.mpris_root = mpris_root.ExaileMprisRoot(self.exaile, self.bus)
106+ self.mpris_tracklist = mpris_tracklist.ExaileMprisTrackList(
107+ self.exaile,
108+ self.bus)
109+ self.mpris_player = mpris_player.ExaileMprisPlayer(
110+ self.exaile,
111+ self.bus)
112
113=== added file 'plugins/mpris/mpris_player.py'
114--- plugins/mpris/mpris_player.py 1970-01-01 00:00:00 +0000
115+++ plugins/mpris/mpris_player.py 2009-04-08 05:37:35 +0000
116@@ -0,0 +1,227 @@
117+from __future__ import division
118+
119+import dbus
120+import dbus.service
121+
122+import xl.event
123+
124+import mpris_tag_converter
125+
126+INTERFACE_NAME = 'org.freedesktop.MediaPlayer'
127+
128+class MprisCaps(object):
129+ """
130+ Specification for the capabilities field in MPRIS
131+ """
132+ NONE = 0
133+ CAN_GO_NEXT = 1 << 0
134+ CAN_GO_PREV = 1 << 1
135+ CAN_PAUSE = 1 << 2
136+ CAN_PLAY = 1 << 3
137+ CAN_SEEK = 1 << 4
138+ CAN_PROVIDE_METADATA = 1 << 5
139+ CAN_HAS_TRACKLIST = 1 << 6
140+
141+EXAILE_CAPS = (MprisCaps.CAN_GO_NEXT
142+ | MprisCaps.CAN_GO_PREV
143+ | MprisCaps.CAN_PAUSE
144+ | MprisCaps.CAN_PLAY
145+ | MprisCaps.CAN_SEEK
146+ | MprisCaps.CAN_PROVIDE_METADATA
147+ | MprisCaps.CAN_HAS_TRACKLIST)
148+
149+class ExaileMprisPlayer(dbus.service.Object):
150+
151+ """
152+ /Player (Root) object methods
153+ """
154+
155+ def __init__(self, exaile, bus):
156+ dbus.service.Object.__init__(self, bus, '/Player')
157+ self.exaile = exaile
158+ self._tag_converter = mpris_tag_converter.ExaileTagConverter(exaile)
159+ xl.event.add_callback(self.track_change_cb, 'playback_start')
160+ # FIXME: Does not watch for shuffle, repeat
161+ # TODO: playback_start does not distinguish if play button was pressed
162+ # or we simply moved to a new track
163+ for event in ('stop_track', 'playback_start', 'playback_toggle_pause'):
164+ xl.event.add_callback(self.status_change_cb, event)
165+
166+
167+ @dbus.service.method(INTERFACE_NAME)
168+ def Next(self):
169+ """
170+ Goes to the next element
171+ """
172+ self.exaile.queue.next()
173+
174+ @dbus.service.method(INTERFACE_NAME)
175+ def Prev(self):
176+ """
177+ Goes to the previous element
178+ """
179+ self.exaile.queue.prev()
180+
181+ @dbus.service.method(INTERFACE_NAME)
182+ def Pause(self):
183+ """
184+ If playing, pause. If paused, unpause.
185+ """
186+ self.exaile.player.toggle_pause()
187+
188+ @dbus.service.method(INTERFACE_NAME)
189+ def Stop(self):
190+ """
191+ Stop playing
192+ """
193+ self.exaile.player.stop()
194+
195+ @dbus.service.method(INTERFACE_NAME)
196+ def Play(self):
197+ """
198+ If Playing, rewind to the beginning of the current track, else.
199+ start playing
200+ """
201+ if self.exaile.player.is_playing():
202+ self.exaile.player.play(self.exaile.player.current)
203+ else:
204+ self.exaile.queue.play()
205+
206+ @dbus.service.method(INTERFACE_NAME, in_signature="b")
207+ def Repeat(self, repeat):
208+ """
209+ Toggle the current track repeat
210+ """
211+ pass
212+
213+ @dbus.service.method(INTERFACE_NAME, out_signature="(iiii)")
214+ def GetStatus(self):
215+ """
216+ Return the status of "Media Player" as a struct of 4 ints:
217+ * First integer: 0 = Playing, 1 = Paused, 2 = Stopped.
218+ * Second interger: 0 = Playing linearly , 1 = Playing randomly.
219+ * Third integer: 0 = Go to the next element once the current has
220+ finished playing , 1 = Repeat the current element
221+ * Fourth integer: 0 = Stop playing once the last element has been
222+ played, 1 = Never give up playing
223+ """
224+ if self.exaile.player.is_playing():
225+ playing = 0
226+ elif self.exaile.player.is_paused():
227+ playing = 1
228+ else:
229+ playing = 2
230+
231+ if not self.exaile.queue.current_playlist.random_enabled:
232+ random = 0
233+ else:
234+ random = 1
235+
236+ go_to_next = 0 # Do not have ability to repeat single track
237+
238+ if not self.exaile.queue.current_playlist.repeat_enabled:
239+ repeat = 0
240+ else:
241+ repeat = 1
242+
243+ return (playing, random, go_to_next, repeat)
244+
245+ @dbus.service.method(INTERFACE_NAME, out_signature="a{sv}")
246+ def GetMetadata(self):
247+ """
248+ Gives all meta data available for the currently played element.
249+ """
250+ if self.exaile.player.current is None:
251+ return []
252+ return self._tag_converter.get_metadata(self.exaile.player.current)
253+
254+ @dbus.service.method(INTERFACE_NAME, out_signature="i")
255+ def GetCaps(self):
256+ """
257+ Returns the "Media player"'s current capabilities, see MprisCaps
258+ """
259+ return EXAILE_CAPS
260+
261+ @dbus.service.method(INTERFACE_NAME, in_signature="i")
262+ def VolumeSet(self, volume):
263+ """
264+ Sets the volume, arument in the range [0, 100]
265+ """
266+ if volume < 0 or volume > 100:
267+ pass
268+
269+ self.exaile.settings['player/volume'] = volume / 100
270+
271+ @dbus.service.method(INTERFACE_NAME, out_signature="i")
272+ def VolumeGet(self):
273+ """
274+ Returns the current volume (must be in [0;100])
275+ """
276+ return self.exaile.settings['player/volume'] * 100
277+
278+ @dbus.service.method(INTERFACE_NAME, in_signature="i")
279+ def PositionSet(self, millisec):
280+ """
281+ Sets the playing position (argument must be in [0, <track_length>]
282+ in milliseconds)
283+ """
284+ if millisec > self.exaile.player.current.tags['length'] * 1000 \
285+ or millisec < 0:
286+ return
287+ self.exaile.player.seek(millisec / 1000)
288+
289+ @dbus.service.method(INTERFACE_NAME, out_signature="i")
290+ def PositionGet(self):
291+ """
292+ Returns the playing position (will be [0, track_length] in
293+ milliseconds)
294+ """
295+ return int(self.exaile.player.get_position() / 1000000)
296+
297+ def track_change_cb(self, type, object, data):
298+ """
299+ Callback will emit the dbus signal TrackChange with the current
300+ songs metadata
301+ """
302+ metadata = self.GetMetadata()
303+ self.TrackChange(metadata)
304+
305+ def status_change_cb(self, type, object, data):
306+ """
307+ Callback will emit the dbus signal StatusChange with the current
308+ status
309+ """
310+ struct = self.GetStatus()
311+ self.StatusChange(struct)
312+
313+ def caps_change_cb(self, type, object, data):
314+ """
315+ Callback will emit the dbus signal CapsChange with the current Caps
316+ """
317+ caps = self.GetCaps()
318+ self.CapsChange(caps)
319+
320+ @dbus.service.signal(INTERFACE_NAME, signature="a{sv}")
321+ def TrackChange(self, metadata):
322+ """
323+ Signal is emitted when the "Media Player" plays another "Track".
324+ Argument of the signal is the metadata attached to the new "Track"
325+ """
326+ pass
327+
328+ @dbus.service.signal(INTERFACE_NAME, signature="(iiii)")
329+ def StatusChange(self, struct):
330+ """
331+ Signal is emitted when the status of the "Media Player" change. The
332+ argument has the same meaning as the value returned by GetStatus.
333+ """
334+ pass
335+
336+ @dbus.service.signal(INTERFACE_NAME)
337+ def CapsChange(self):
338+ """
339+ Signal is emitted when the "Media Player" changes capabilities, see
340+ GetCaps method.
341+ """
342+ pass
343+
344
345=== added file 'plugins/mpris/mpris_root.py'
346--- plugins/mpris/mpris_root.py 1970-01-01 00:00:00 +0000
347+++ plugins/mpris/mpris_root.py 2009-04-05 21:01:49 +0000
348@@ -0,0 +1,38 @@
349+import dbus
350+import dbus.service
351+
352+from xl.nls import gettext as _
353+
354+INTERFACE_NAME = 'org.freedesktop.MediaPlayer'
355+
356+class ExaileMprisRoot(dbus.service.Object):
357+
358+ """
359+ / (Root) object methods
360+ """
361+
362+ def __init__(self, exaile, bus):
363+ dbus.service.Object.__init__(self, bus, '/')
364+ self.exaile = exaile
365+
366+ @dbus.service.method(INTERFACE_NAME, out_signature="s")
367+ def Identity(self):
368+ """
369+ Identify the "media player"
370+ """
371+ return _("Exaile %(version)s") % {'version': self.exaile.get_version()}
372+
373+ @dbus.service.method(INTERFACE_NAME)
374+ def Quit(self):
375+ """
376+ Makes the "Media Player" exit.
377+ """
378+ self.exaile.quit()
379+
380+ @dbus.service.method(INTERFACE_NAME, out_signature="(qq)")
381+ def MprisVersion(self):
382+ """
383+ Makes the "Media Player" exit.
384+ """
385+ return (1, 0)
386+
387
388=== added file 'plugins/mpris/mpris_tag_converter.py'
389--- plugins/mpris/mpris_tag_converter.py 1970-01-01 00:00:00 +0000
390+++ plugins/mpris/mpris_tag_converter.py 2009-04-07 23:55:02 +0000
391@@ -0,0 +1,176 @@
392+"""
393+A converter utility to convert from exaile tags to mpris Metadata
394+"""
395+
396+import logging
397+_LOG = logging.getLogger('exaile.plugins.mpris.mpris_tag_converter')
398+
399+# Dictionary to map MPRIS tags to Exaile Tags
400+# Each key is the mpris tag, each value is a dictionary with possible keys:
401+# * out_type: REQUIRED, function that will convert to the MPRIS type
402+# * exaile_tag: the name of the tag in exaile, defaults to the mpris tag
403+# * conv: a conversion function to call on the exaile metadata, defaults to
404+# lambda x: x
405+# * desc: a description of what's in the tag
406+# * constructor: a function to call that takes (exaile, track) and returns
407+# the value for the key. If it returns None, the tag is not
408+# set
409+MPRIS_TAG_INFORMATION = {
410+ 'location' : {'out_type' : unicode,
411+ 'exaile_tag': 'loc',
412+ 'conv' : lambda x: "file://" + x,
413+ 'desc' : 'Name',
414+ },
415+ 'title' : {'out_type' : unicode,
416+ 'desc' : 'Name of artist or band',
417+ },
418+ 'album' : {'out_type' : unicode,
419+ 'desc' : 'Name of compilation',
420+ },
421+ 'tracknumber': {'out_type' : unicode,
422+ 'desc' : 'The position in album',
423+ },
424+ 'time' : {'out_type' : int,
425+ 'exaile_tag': 'length',
426+ 'desc' : 'The duration in seconds',
427+ },
428+ 'mtime' : {'out_type' : int,
429+ 'desc' : 'The duration in milliseconds',
430+ },
431+ 'genre' : {'out_type' : unicode,
432+ 'desc' : 'The genre',
433+ },
434+ 'comment' : {'out_type' : unicode,
435+ 'desc' : 'A comment about the work',
436+ },
437+ 'rating' : {'out_type' : int,
438+ 'desc' : 'A "taste" rate value, out of 5',
439+ },
440+ 'year' : {'out_type' : int,
441+ 'exaile_tag': 'date',
442+ 'conv' : lambda x: x.split('-')[0],
443+ 'desc' : 'The year of performing',
444+ },
445+ 'date' : {'out_type' : int,
446+ 'exaile_tag': None,
447+ 'desc' : 'When the performing was realized, '
448+ 'since epoch',
449+ },
450+ 'arturl' : {'out_type' : unicode,
451+ 'desc' : 'an URI to an image',
452+ },
453+ 'audio-bitrate': {'out_type': int,
454+ 'exaile_tag': 'bitrate',
455+ 'desc' : 'The number of bits per second',
456+ },
457+ 'audio-samplerate': {'out_type': int,
458+ 'desc' : 'The number of samples per second',
459+ },
460+ }
461+EXAILE_TAG_INFORMATION = {}
462+def __fill_exaile_tag_information():
463+ """
464+ Fille EXAILE_TAG_INFORMATION with the exaile_tag: mpris_tag, the
465+ inverse of MPRIS_TAG_INFORMATION
466+ """
467+ for mpris_tag in MPRIS_TAG_INFORMATION:
468+ if 'exaile_tag' in MPRIS_TAG_INFORMATION[mpris_tag]:
469+ exaile_tag = MPRIS_TAG_INFORMATION[mpris_tag]['exaile_tag']
470+ else:
471+ exaile_tag = mpris_tag
472+ if exaile_tag is None:
473+ continue
474+ EXAILE_TAG_INFORMATION[exaile_tag] = mpris_tag
475+__fill_exaile_tag_information()
476+
477+class OutputTypeMismatchException(Exception):
478+ def __init__(self, exaile_tag, mpris_tag, val):
479+ Exception.__init__(self,
480+ "Could not convert tag exaile:'%s' to mpris:'%s':"
481+ "Error converting '%s' to type '%s"
482+ % (exaile_tag, mpris_tag, val,
483+ MPRIS_TAG_INFORMATION[mpris_tag]['out_type']))
484+
485+class ExaileTagConverter(object):
486+
487+ """
488+ Class to convert tags from Exaile to Metadata for MPRIS
489+ """
490+
491+ def __init__(self, exaile):
492+ self.exaile = exaile
493+
494+ def get_metadata(self, track):
495+ """
496+ Returns the Metadata for track as defined by MPRIS standard
497+ """
498+ metadata = {}
499+ for exaile_tag in track.tags:
500+ if exaile_tag not in EXAILE_TAG_INFORMATION:
501+ continue
502+ val = ExaileTagConverter.__get_first_item(track.tags[exaile_tag])
503+ try:
504+ mpris_tag, mpris_val = ExaileTagConverter.convert_tag(
505+ exaile_tag, val)
506+ except OutputTypeMismatchException, e:
507+ _LOG.exception(e)
508+ continue
509+ if mpris_tag is None:
510+ continue
511+ metadata[mpris_tag] = mpris_val
512+
513+ for mpris_tag in MPRIS_TAG_INFORMATION:
514+ if 'constructor' in MPRIS_TAG_INFORMATION[mpris_tag]:
515+ val = MPRIS_TAG_INFORMATION[mpris_tag]['constructor'](
516+ self.exaile,
517+ track
518+ )
519+ if val is not None:
520+ try:
521+ metadata[mpris_tag] = \
522+ MPRIS_TAG_INFORMATION[mpris_tag]['out_type'](val)
523+ except ValueError:
524+ raise OutputTypeMismatchException(exaile_tag,
525+ mpris_tag,
526+ val,
527+ )
528+
529+
530+ return metadata
531+
532+ @staticmethod
533+ def __get_first_item(value):
534+ """
535+ Unlists lists and returns the first value, if not a lists,
536+ returns value
537+ """
538+ if not isinstance(value, basestring) and hasattr(value, "__getitem__"):
539+ if len(value):
540+ return value[0]
541+ return None
542+ return value
543+
544+ @staticmethod
545+ def convert_tag(exaile_tag, exaile_value):
546+ """
547+ Converts a single tag into MPRIS form, return a 2-tuple of
548+ (mpris_tag, mpris_val). Returns (None, None) if there is no
549+ translation
550+ """
551+ if exaile_tag not in EXAILE_TAG_INFORMATION:
552+ return (None, None)
553+ mpris_tag = EXAILE_TAG_INFORMATION[exaile_tag]
554+ info = MPRIS_TAG_INFORMATION[mpris_tag]
555+ if 'conv' in info:
556+ mpris_value = info['conv'](exaile_value)
557+ else:
558+ mpris_value = exaile_value
559+ try:
560+ mpris_value = info['out_type'](mpris_value)
561+ except ValueError:
562+ raise OutputTypeMismatchException(exaile_tag,
563+ mpris_tag,
564+ exaile_value,
565+ )
566+ return (mpris_tag, mpris_value)
567+
568
569=== added file 'plugins/mpris/mpris_tracklist.py'
570--- plugins/mpris/mpris_tracklist.py 1970-01-01 00:00:00 +0000
571+++ plugins/mpris/mpris_tracklist.py 2009-04-07 23:55:02 +0000
572@@ -0,0 +1,122 @@
573+import dbus
574+import dbus.service
575+
576+import xl.track
577+import xl.event
578+
579+import mpris_tag_converter
580+
581+INTERFACE_NAME = 'org.freedesktop.MediaPlayer'
582+
583+class ExaileMprisTrackList(dbus.service.Object):
584+
585+ """
586+ /TrackList object methods
587+ """
588+
589+ def __init__(self, exaile, bus):
590+ dbus.service.Object.__init__(self, bus, '/TrackList')
591+ self.exaile = exaile
592+ self.tag_converter = mpris_tag_converter.ExaileTagConverter(exaile)
593+ for event in ('tracks_removed', 'tracks_added'):
594+ xl.event.add_callback(self.tracklist_change_cb, event)
595+
596+ def __get_playlist(self):
597+ """
598+ Returns the list of tracks in the current playlist
599+ """
600+ return self.exaile.queue.current_playlist.get_ordered_tracks()
601+
602+ @dbus.service.method(INTERFACE_NAME,
603+ in_signature="i", out_signature="a{sv}")
604+ def GetMetadata(self, pos):
605+ """
606+ Gives all meta data available for element at given position in the
607+ TrackList, counting from 0
608+
609+ Each dict entry is organized as follows
610+ * string: Metadata item name
611+ * variant: Metadata value
612+ """
613+ track = self.__get_playlist()[pos]
614+ return self.tag_converter.get_metadata(track)
615+
616+ @dbus.service.method(INTERFACE_NAME, out_signature="i")
617+ def GetCurrentTrack(self):
618+ """
619+ Return the position of current URI in the TrackList The return
620+ value is zero-based, so the position of the first URI in the
621+ TrackList is 0. The behavior of this method is unspecified if
622+ there are zero elements in the TrackList.
623+ """
624+ try:
625+ return self.exaile.queue.current_playlist.index(
626+ self.exaile.player.current)
627+ except ValueError:
628+ return -1
629+
630+ @dbus.service.method(INTERFACE_NAME, out_signature="i")
631+ def GetLength(self):
632+ """
633+ Number of elements in the TrackList
634+ """
635+ return len(self.exaile.queue.current_playlist)
636+
637+ @dbus.service.method(INTERFACE_NAME,
638+ in_signature="sb", out_signature="i")
639+ def AddTrack(self, uri, play_immediately):
640+ """
641+ Appends an URI in the TrackList.
642+ """
643+ if not uri.startswith("file://"):
644+ return -1
645+ uri = uri[7:]
646+ track = self.exaile.collection.get_track_by_loc(unicode(uri))
647+ if track is None:
648+ track = xl.track.Track(uri)
649+ self.exaile.queue.current_playlist.add(track)
650+ if play_immediately:
651+ self.exaile.queue.play(track)
652+ return 0
653+
654+ @dbus.service.method(INTERFACE_NAME, in_signature="i")
655+ def DelTrack(self, pos):
656+ """
657+ Appends an URI in the TrackList.
658+ """
659+ self.exaile.queue.current_playlist.remove(pos)
660+
661+ @dbus.service.method(INTERFACE_NAME, in_signature="b")
662+ def SetLoop(self, loop):
663+ """
664+ Sets the player's "repeat" or "loop" setting
665+ """
666+ self.exaile.queue.current_playlist.set_repeat(loop)
667+
668+ @dbus.service.method(INTERFACE_NAME, in_signature="b")
669+ def SetRandom(self, random):
670+ """
671+ Sets the player's "random" setting
672+ """
673+ self.exaile.queue.current_playlist.set_random(random)
674+
675+ def tracklist_change_cb(self, type, object, data):
676+ """
677+ Callback for a track list change
678+ """
679+ len = self.GetLength()
680+ self.TrackListChange(len)
681+
682+ @dbus.service.signal(INTERFACE_NAME, signature="i")
683+ def TrackListChange(self, num_of_elements):
684+ """
685+ Signal is emitted when the "TrackList" content has changed:
686+ * When one or more elements have been added
687+ * When one or more elements have been removed
688+ * When the ordering of elements has changed
689+
690+ The argument is the number of elements in the TrackList after the
691+ change happened.
692+ """
693+ pass
694+
695
696=== added file 'plugins/mpris/test.py'
697--- plugins/mpris/test.py 1970-01-01 00:00:00 +0000
698+++ plugins/mpris/test.py 2009-04-08 05:37:12 +0000
699@@ -0,0 +1,305 @@
700+"""
701+Simple test case for MPRIS. Make sure you set the global variable FILE with an
702+absolute path to a valid playable, local music piece before running this test
703+"""
704+import unittest
705+import dbus
706+import os
707+import time
708+
709+OBJECT_NAME = 'org.mpris.exaile'
710+INTERFACE = 'org.freedesktop.MediaPlayer'
711+FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir, os.path.pardir,
712+ 'tests', 'data', 'music', 'delerium', 'chimera',
713+ '05 - Truly.mp3')
714+assert os.path.isfile(FILE), FILE + " must be a valid musical piece"
715+FILE = "file://" + FILE
716+print "Test file will be: " + FILE
717+
718+class TestExaileMpris(unittest.TestCase):
719+
720+ """
721+ Tests Exaile MPRIS plugin
722+ """
723+
724+ def setUp(self):
725+ """
726+ Simple setUp that makes dbus connections and assigns them to
727+ self.player, self.track_list, and self.root. Also begins playing
728+ a song so every test case can assume that a song is playing
729+ """
730+ bus = dbus.SessionBus()
731+ objects = {'root': '/',
732+ 'player': '/Player',
733+ 'track_list': '/TrackList',
734+ }
735+ intfs = {}
736+ for key in objects:
737+ object = bus.get_object(OBJECT_NAME, objects[key])
738+ intfs[key] = dbus.Interface(object, INTERFACE)
739+ self.root = intfs['root']
740+ self.player = intfs['player']
741+ self.track_list = intfs['track_list']
742+ self.player.Play()
743+ time.sleep(0.5)
744+
745+ def __wait_after(function):
746+ """
747+ Decorator to add a delay after a function call
748+ """
749+ def inner(*args, **kwargs):
750+ function(*args, **kwargs)
751+ time.sleep(0.5)
752+ return inner
753+
754+ @__wait_after
755+ def _stop(self):
756+ """
757+ Stops playing w/ delay
758+ """
759+ self.player.Stop()
760+
761+ @__wait_after
762+ def _play(self):
763+ """
764+ Starts playing w/ delay
765+ """
766+ self.player.Play()
767+
768+ @__wait_after
769+ def _pause(self):
770+ """
771+ Pauses playing w/ delay
772+ """
773+ self.player.Pause()
774+
775+
776+class TestMprisRoot(TestExaileMpris):
777+
778+ """
779+ Check / (Root) object functions for MPRIS. Does not check Quit
780+ """
781+
782+ def testIdentity(self):
783+ """
784+ Make sure we output Exaile with our identity
785+ """
786+ id = self.root.Identity()
787+ self.assertEqual(id, self.root.Identity())
788+ self.assertTrue(id.startswith("Exaile"))
789+
790+ def testMprisVersion(self):
791+ """
792+ Checks that we are using MPRIS version 1.0
793+ """
794+ version = self.root.MprisVersion()
795+ self.assertEqual(dbus.UInt16(1), version[0])
796+ self.assertEqual(dbus.UInt16(0), version[1])
797+
798+class TestTrackList(TestExaileMpris):
799+
800+ """
801+ Tests the /TrackList object for MPRIS
802+ """
803+
804+ def testGetMetadata(self):
805+ """
806+ Make sure we can get metadata. Also makes sure that locations will
807+ not change randomly
808+ """
809+ md = self.track_list.GetMetadata(0)
810+ md_2 = self.track_list.GetMetadata(0)
811+ self.assertEqual(md, md_2)
812+
813+ def testAppendDelWithoutPlay(self):
814+ """
815+ Tests appending and deleting songs from the playlist without
816+ playing them
817+ """
818+ cur_track = self.track_list.GetCurrentTrack()
819+ len = self.track_list.GetLength()
820+
821+ self.assertEqual(0, self.track_list.AddTrack(FILE, False))
822+ self.assertEqual(len + 1, self.track_list.GetLength())
823+ self.assertEqual(cur_track, self.track_list.GetCurrentTrack())
824+
825+ md = self.track_list.GetMetadata(len)
826+ self.assertEqual(FILE, md['location'])
827+
828+ self.track_list.DelTrack(len)
829+ self.assertEqual(len, self.track_list.GetLength())
830+ self.assertEqual(cur_track, self.track_list.GetCurrentTrack())
831+
832+ def testAppendDelWithPlay(self):
833+ """
834+ Tests appending songs into the playlist with playing the songs
835+ """
836+ cur_track = self.track_list.GetCurrentTrack()
837+ cur_md = self.track_list.GetMetadata(cur_track)
838+ len = self.track_list.GetLength()
839+
840+ self.assertEqual(0, self.track_list.AddTrack(FILE, True))
841+ self.assertEqual(len + 1, self.track_list.GetLength())
842+
843+ md = self.track_list.GetMetadata(len)
844+ self.assertEqual(FILE, md['location'])
845+ self.assertEqual(len, self.track_list.GetCurrentTrack())
846+
847+ self.track_list.DelTrack(len)
848+ self.assertEqual(len, self.track_list.GetLength())
849+
850+ self.track_list.AddTrack(cur_md['location'], True)
851+
852+ def testGetCurrentTrack(self):
853+ """
854+ Check the GetCurrentTrack information
855+ """
856+ cur_track = self.track_list.GetCurrentTrack()
857+ self.assertTrue(cur_track >= 0, "Tests start with playing music")
858+
859+ self._stop()
860+ self.assertEqual(dbus.Int32(-1), self.track_list.GetCurrentTrack(),
861+ "Our implementation returns -1 if no tracks are playing")
862+
863+ self._play()
864+ self.assertEqual(cur_track, self.track_list.GetCurrentTrack(),
865+ "After a stop and play, we should be at the same track")
866+
867+ def __test_bools(self, getter, setter):
868+ """
869+ Generic function for checking that a boolean value changes
870+ """
871+ cur_val = getter()
872+ if cur_val == dbus.Int32(0):
873+ val = False
874+ elif cur_val == dbus.Int32(1):
875+ val = True
876+ else:
877+ self.fail("Got an invalid value from status")
878+
879+ setter(False)
880+ status = getter()
881+ self.assertEqual(dbus.Int32(0), status)
882+
883+ setter(True)
884+ status = getter()
885+ self.assertEqual(dbus.Int32(1), status)
886+
887+ setter(val)
888+ self.track_list.SetLoop(val)
889+
890+ def testLoop(self):
891+ """
892+ Tests that you can change the loop settings
893+ """
894+ self.__test_bools(lambda: self.player.GetStatus()[3],
895+ lambda x: self.track_list.SetLoop(x))
896+
897+ def testRandom(self):
898+ """
899+ Tests that you can change the random settings
900+ """
901+ self.__test_bools(lambda: self.player.GetStatus()[1],
902+ lambda x: self.track_list.SetRandom(x))
903+
904+class TestPlayer(TestExaileMpris):
905+
906+ """
907+ Tests the /Player object for MPRIS
908+ """
909+
910+ def testNextPrev(self):
911+ """
912+ Make sure you can skip back and forward
913+ """
914+ cur_track = self.track_list.GetCurrentTrack()
915+ self.player.Next()
916+ new_track = self.track_list.GetCurrentTrack()
917+ self.assertNotEqual(cur_track, new_track)
918+ self.player.Prev()
919+ self.assertEqual(cur_track, self.track_list.GetCurrentTrack())
920+
921+ def testStopPlayPause(self):
922+ """
923+ Make sure play, pause, and stop behaive as designed
924+ """
925+ self._stop()
926+ self.assertEqual(dbus.Int32(2), self.player.GetStatus()[0])
927+
928+ self._play()
929+ self.assertEqual(dbus.Int32(0), self.player.GetStatus()[0])
930+ self._play()
931+ self.assertEqual(dbus.Int32(0), self.player.GetStatus()[0])
932+
933+ self._pause()
934+ self.assertEqual(dbus.Int32(1), self.player.GetStatus()[0])
935+ self._pause()
936+ self.assertEqual(dbus.Int32(0), self.player.GetStatus()[0])
937+
938+ self._stop()
939+ self.assertEqual(dbus.Int32(2), self.player.GetStatus()[0])
940+ self._pause()
941+ self.assertEqual(dbus.Int32(2), self.player.GetStatus()[0])
942+
943+ def testVolume(self):
944+ """
945+ Test to make sure volumes are set happily
946+ """
947+ vol = self.player.VolumeGet()
948+ self.player.VolumeSet(1 - vol)
949+ self.assertEqual(1 - vol, self.player.VolumeGet())
950+ self.player.VolumeSet(vol)
951+ self.assertEqual(vol, self.player.VolumeGet())
952+
953+ def testPosition(self):
954+ """
955+ Test the PositionGet and PositionSet functions. Unfortuantely this
956+ is very time sensitive and thus has about a 10 second sleep in the
957+ function
958+ """
959+ time.sleep(3)
960+ self._pause()
961+
962+ pos = self.player.PositionGet()
963+ time.sleep(1)
964+ self.assertEqual(pos, self.player.PositionGet(),
965+ "Position shouldn't move while paused")
966+
967+ self._pause()
968+ time.sleep(4)
969+ last_pos = self.player.PositionGet()
970+ self.assertTrue(pos < last_pos,
971+ "Position shouldn't advance while paused: %d >= %d" %
972+ (pos, last_pos))
973+
974+ self.player.PositionSet(pos)
975+ time.sleep(2)
976+ self._pause()
977+ mid_pos = self.player.PositionGet(),
978+ self.assertTrue(mid_pos[0] < last_pos,
979+ "Resetting to position %d, %d should be between that at %d"
980+ % (pos, mid_pos[0], last_pos))
981+
982+ self._pause()
983+ time.sleep(0.5)
984+ self.assertTrue(pos < self.player.PositionGet(),
985+ "Make sure it still advances")
986+
987+ self.player.PositionSet(-1)
988+ self.assertTrue(pos < self.player.PositionGet(),
989+ "Don't move to invalid position")
990+
991+
992+def suite():
993+ sub_test = [TestMprisRoot, TestTrackList]
994+
995+ suites = []
996+
997+ for test in sub_test:
998+ suites.append(unittest.defaultTestLoader.loadTestsFromTestCase(test))
999+
1000+ return unittest.TestSuite(suites)
1001+
1002+if __name__ == "__main__":
1003+ unittest.main()
1004+
1005
1006=== modified file 'plugins/notify/__init__.py'
1007--- plugins/notify/__init__.py 2008-10-17 04:04:50 +0000
1008+++ plugins/notify/__init__.py 2009-04-05 05:48:03 +0000
1009@@ -4,27 +4,41 @@
1010
1011 pynotify.init('exailenotify')
1012
1013-def on_play(type, player, track):
1014- title = " / ".join(track['title'] or _("Unknown"))
1015- artist = " / ".join(track['artist'] or "")
1016- album = " / ".join(track['album'] or "")
1017- summary = cgi.escape(title)
1018- if artist and album:
1019- body = _("by %(artist)s\nfrom <i>%(album)s</i>") % {
1020- 'artist' : cgi.escape(artist),
1021- 'album' : cgi.escape(album)}
1022- elif artist:
1023- body = _("by %(artist)s") % {'artist' : cgi.escape(artist)}
1024- elif album:
1025- body = _("from %(album)s") % {'album' : cgi.escape(album)}
1026- else:
1027- body = ""
1028- notify = pynotify.Notification(summary, body)
1029-
1030- notify.show()
1031+class ExaileNotification(object):
1032+ def __init__(self):
1033+ self.notification = pynotify.Notification("Exaile")
1034+ self.exaile = None
1035+
1036+ def on_play(self, type, player, track):
1037+ title = " / ".join(track['title'] or _("Unknown"))
1038+ artist = " / ".join(track['artist'] or "")
1039+ album = " / ".join(track['album'] or "")
1040+ summary = cgi.escape(title)
1041+ if artist and album:
1042+ body = _("by %(artist)s\nfrom <i>%(album)s</i>") % {
1043+ 'artist' : cgi.escape(artist),
1044+ 'album' : cgi.escape(album)}
1045+ elif artist:
1046+ body = _("by %(artist)s") % {'artist' : cgi.escape(artist)}
1047+ elif album:
1048+ body = _("from %(album)s") % {'album' : cgi.escape(album)}
1049+ else:
1050+ body = ""
1051+ self.notification.update(summary, body)
1052+ item = track.get_album_tuple()
1053+ image = None
1054+ if all(item) and hasattr(self.exaile, 'covers'):
1055+ image = self.exaile.covers.coverdb.get_cover(*item)
1056+ if image is None:
1057+ image = 'exaile'
1058+ self.notification.set_property('icon-name', image)
1059+ self.notification.show()
1060+
1061+EXAILE_NOTIFICATION = ExaileNotification()
1062
1063 def enable(exaile):
1064- event.add_callback(on_play, 'playback_start')
1065+ EXAILE_NOTIFICATION.exaile = exaile
1066+ event.add_callback(EXAILE_NOTIFICATION.on_play, 'playback_start')
1067
1068 def disable(exaile):
1069- event.remove_callback(on_play, 'playback_start')
1070+ event.remove_callback(EXAILE_NOTIFICATION.on_play, 'playback_start')
1071
1072=== modified file 'xlgui/plugins.py'
1073--- xlgui/plugins.py 2009-01-05 23:30:31 +0000
1074+++ xlgui/plugins.py 2009-04-04 21:10:41 +0000
1075@@ -106,18 +106,26 @@
1076 (model, iter) = self.list.get_selection().get_selected()
1077 if not iter: return
1078
1079- pluginname = model.get_value(iter, 2)
1080- if not pluginname in self.plugins.enabled_plugins:
1081- return
1082-
1083- plugin = self.plugins.enabled_plugins[pluginname]
1084- if not hasattr(plugin, 'get_prefs_pane'):
1085+ pluginname = model.get_value(iter, 2)[0]
1086+ if not self.__configure_available(pluginname):
1087 commondialogs.error(self.parent, _("The selected "
1088 "plugin doesn't have any configuration options"))
1089 return
1090
1091 self.guimain.show_preferences(plugin_page=pluginname)
1092
1093+ def __configure_available(self, pluginname):
1094+ """
1095+ Returns if a plugin given by pluginname has the ability to open a
1096+ configure dialog
1097+ """
1098+ if pluginname not in self.plugins.enabled_plugins:
1099+ return False
1100+ plugin = self.plugins.enabled_plugins[pluginname]
1101+ if not hasattr(plugin, 'get_prefs_pane'):
1102+ return False
1103+ return True
1104+
1105 def row_selected(self, selection, user_data=None):
1106 """
1107 Called when a row is selected
1108@@ -132,6 +140,12 @@
1109 self.description.get_buffer().set_text(
1110 info['Description'].replace(r'\n', "\n"))
1111 self.name_label.set_markup("<b>%s</b>" % info['Name'])
1112+ (model, iter) = selection.get_selected()
1113+ pluginname = model.get(iter, 2)[0]
1114+ if self.__configure_available(pluginname):
1115+ self.configure_button.set_sensitive(True)
1116+ else:
1117+ self.configure_button.set_sensitive(False)
1118
1119 def toggle_cb(self, cell, path, model):
1120 """
1121@@ -150,8 +164,8 @@
1122 commondialogs.error(self.parent, _('Could '
1123 'not disable plugin.'))
1124 return
1125-
1126 model[path][1] = enable
1127+ self.row_selected(self.list.get_selection())
1128
1129 def destroy(self, *e):
1130 self.dialog.destroy()