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
=== added directory 'plugins/mpris'
=== added file 'plugins/mpris/PLUGININFO'
--- plugins/mpris/PLUGININFO 1970-01-01 00:00:00 +0000
+++ plugins/mpris/PLUGININFO 2009-04-05 03:55:27 +0000
@@ -0,0 +1,4 @@
1Version="0.0.1"
2Authors=["Abhishek Mukherjee <abhishek.mukher.g@gmail.com"]
3Name="MPRIS"
4Description="Creates an MPRIS D-Bus object to control Exaile"
05
=== added file 'plugins/mpris/__init__.py'
--- plugins/mpris/__init__.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/__init__.py 2009-04-05 21:01:49 +0000
@@ -0,0 +1,20 @@
1__all__ = ["exaile_mpris"]
2
3import exaile_mpris
4import logging
5
6LOG = logging.getLogger("exaile.plugins.mpris")
7
8_MPRIS = None
9
10def enable(exaile):
11 LOG.debug("Enabling MPRIS")
12 if _MPRIS is None:
13 global _MPRIS
14 _MPRIS = exaile_mpris.ExaileMpris(exaile)
15 _MPRIS.exaile = exaile
16 _MPRIS.acquire()
17
18def disable(exaile):
19 LOG.debug("Disabling MPRIS")
20 _MPRIS.release()
021
=== added file 'plugins/mpris/exaile_mpris.py'
--- plugins/mpris/exaile_mpris.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/exaile_mpris.py 2009-04-05 21:15:43 +0000
@@ -0,0 +1,72 @@
1"""
2An implementation of the MPRIS D-Bus protocol for use with Exaile
3"""
4
5import dbus
6import dbus.service
7import logging
8
9import mpris_root
10import mpris_tracklist
11import mpris_player
12
13LOG = logging.getLogger("exaile.plugins.mpris.exaile_mpris")
14
15OBJECT_NAME = 'org.mpris.exaile'
16
17class ExaileMpris(object):
18
19 """
20 Controller for various MPRIS objects.
21 """
22
23 def __init__(self, exaile=None):
24 """
25 Constructs an MPRIS controller. Note, you must call acquire()
26 """
27 self.exaile = exaile
28 self.mpris_root = None
29 self.mpris_tracklist = None
30 self.mpris_player = None
31 self.bus = None
32
33 def release(self):
34 """
35 Releases all objects from D-Bus and unregisters the bus
36 """
37 for obj in (self.mpris_root, self.mpris_tracklist, self.mpris_player):
38 if obj is not None:
39 obj.remove_from_connection()
40 self.mpris_root = None
41 self.mpris_tracklist = None
42 self.mpris_player = None
43 if self.bus is not None:
44 self.bus.get_bus().release_name(self.bus.get_name())
45
46 def acquire(self):
47 """
48 Connects to D-Bus and registers all components
49 """
50 self._acquire_bus()
51 self._add_interfaces()
52
53 def _acquire_bus(self):
54 """
55 Connect to D-Bus and set self.bus to be a valid connection
56 """
57 if self.bus is not None:
58 self.bus.get_bus().request_name(OBJECT_NAME)
59 else:
60 self.bus = dbus.service.BusName(OBJECT_NAME, bus=dbus.SessionBus())
61
62 def _add_interfaces(self):
63 """
64 Connects all interfaces to D-Bus
65 """
66 self.mpris_root = mpris_root.ExaileMprisRoot(self.exaile, self.bus)
67 self.mpris_tracklist = mpris_tracklist.ExaileMprisTrackList(
68 self.exaile,
69 self.bus)
70 self.mpris_player = mpris_player.ExaileMprisPlayer(
71 self.exaile,
72 self.bus)
073
=== added file 'plugins/mpris/mpris_player.py'
--- plugins/mpris/mpris_player.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/mpris_player.py 2009-04-08 05:37:35 +0000
@@ -0,0 +1,227 @@
1from __future__ import division
2
3import dbus
4import dbus.service
5
6import xl.event
7
8import mpris_tag_converter
9
10INTERFACE_NAME = 'org.freedesktop.MediaPlayer'
11
12class MprisCaps(object):
13 """
14 Specification for the capabilities field in MPRIS
15 """
16 NONE = 0
17 CAN_GO_NEXT = 1 << 0
18 CAN_GO_PREV = 1 << 1
19 CAN_PAUSE = 1 << 2
20 CAN_PLAY = 1 << 3
21 CAN_SEEK = 1 << 4
22 CAN_PROVIDE_METADATA = 1 << 5
23 CAN_HAS_TRACKLIST = 1 << 6
24
25EXAILE_CAPS = (MprisCaps.CAN_GO_NEXT
26 | MprisCaps.CAN_GO_PREV
27 | MprisCaps.CAN_PAUSE
28 | MprisCaps.CAN_PLAY
29 | MprisCaps.CAN_SEEK
30 | MprisCaps.CAN_PROVIDE_METADATA
31 | MprisCaps.CAN_HAS_TRACKLIST)
32
33class ExaileMprisPlayer(dbus.service.Object):
34
35 """
36 /Player (Root) object methods
37 """
38
39 def __init__(self, exaile, bus):
40 dbus.service.Object.__init__(self, bus, '/Player')
41 self.exaile = exaile
42 self._tag_converter = mpris_tag_converter.ExaileTagConverter(exaile)
43 xl.event.add_callback(self.track_change_cb, 'playback_start')
44 # FIXME: Does not watch for shuffle, repeat
45 # TODO: playback_start does not distinguish if play button was pressed
46 # or we simply moved to a new track
47 for event in ('stop_track', 'playback_start', 'playback_toggle_pause'):
48 xl.event.add_callback(self.status_change_cb, event)
49
50
51 @dbus.service.method(INTERFACE_NAME)
52 def Next(self):
53 """
54 Goes to the next element
55 """
56 self.exaile.queue.next()
57
58 @dbus.service.method(INTERFACE_NAME)
59 def Prev(self):
60 """
61 Goes to the previous element
62 """
63 self.exaile.queue.prev()
64
65 @dbus.service.method(INTERFACE_NAME)
66 def Pause(self):
67 """
68 If playing, pause. If paused, unpause.
69 """
70 self.exaile.player.toggle_pause()
71
72 @dbus.service.method(INTERFACE_NAME)
73 def Stop(self):
74 """
75 Stop playing
76 """
77 self.exaile.player.stop()
78
79 @dbus.service.method(INTERFACE_NAME)
80 def Play(self):
81 """
82 If Playing, rewind to the beginning of the current track, else.
83 start playing
84 """
85 if self.exaile.player.is_playing():
86 self.exaile.player.play(self.exaile.player.current)
87 else:
88 self.exaile.queue.play()
89
90 @dbus.service.method(INTERFACE_NAME, in_signature="b")
91 def Repeat(self, repeat):
92 """
93 Toggle the current track repeat
94 """
95 pass
96
97 @dbus.service.method(INTERFACE_NAME, out_signature="(iiii)")
98 def GetStatus(self):
99 """
100 Return the status of "Media Player" as a struct of 4 ints:
101 * First integer: 0 = Playing, 1 = Paused, 2 = Stopped.
102 * Second interger: 0 = Playing linearly , 1 = Playing randomly.
103 * Third integer: 0 = Go to the next element once the current has
104 finished playing , 1 = Repeat the current element
105 * Fourth integer: 0 = Stop playing once the last element has been
106 played, 1 = Never give up playing
107 """
108 if self.exaile.player.is_playing():
109 playing = 0
110 elif self.exaile.player.is_paused():
111 playing = 1
112 else:
113 playing = 2
114
115 if not self.exaile.queue.current_playlist.random_enabled:
116 random = 0
117 else:
118 random = 1
119
120 go_to_next = 0 # Do not have ability to repeat single track
121
122 if not self.exaile.queue.current_playlist.repeat_enabled:
123 repeat = 0
124 else:
125 repeat = 1
126
127 return (playing, random, go_to_next, repeat)
128
129 @dbus.service.method(INTERFACE_NAME, out_signature="a{sv}")
130 def GetMetadata(self):
131 """
132 Gives all meta data available for the currently played element.
133 """
134 if self.exaile.player.current is None:
135 return []
136 return self._tag_converter.get_metadata(self.exaile.player.current)
137
138 @dbus.service.method(INTERFACE_NAME, out_signature="i")
139 def GetCaps(self):
140 """
141 Returns the "Media player"'s current capabilities, see MprisCaps
142 """
143 return EXAILE_CAPS
144
145 @dbus.service.method(INTERFACE_NAME, in_signature="i")
146 def VolumeSet(self, volume):
147 """
148 Sets the volume, arument in the range [0, 100]
149 """
150 if volume < 0 or volume > 100:
151 pass
152
153 self.exaile.settings['player/volume'] = volume / 100
154
155 @dbus.service.method(INTERFACE_NAME, out_signature="i")
156 def VolumeGet(self):
157 """
158 Returns the current volume (must be in [0;100])
159 """
160 return self.exaile.settings['player/volume'] * 100
161
162 @dbus.service.method(INTERFACE_NAME, in_signature="i")
163 def PositionSet(self, millisec):
164 """
165 Sets the playing position (argument must be in [0, <track_length>]
166 in milliseconds)
167 """
168 if millisec > self.exaile.player.current.tags['length'] * 1000 \
169 or millisec < 0:
170 return
171 self.exaile.player.seek(millisec / 1000)
172
173 @dbus.service.method(INTERFACE_NAME, out_signature="i")
174 def PositionGet(self):
175 """
176 Returns the playing position (will be [0, track_length] in
177 milliseconds)
178 """
179 return int(self.exaile.player.get_position() / 1000000)
180
181 def track_change_cb(self, type, object, data):
182 """
183 Callback will emit the dbus signal TrackChange with the current
184 songs metadata
185 """
186 metadata = self.GetMetadata()
187 self.TrackChange(metadata)
188
189 def status_change_cb(self, type, object, data):
190 """
191 Callback will emit the dbus signal StatusChange with the current
192 status
193 """
194 struct = self.GetStatus()
195 self.StatusChange(struct)
196
197 def caps_change_cb(self, type, object, data):
198 """
199 Callback will emit the dbus signal CapsChange with the current Caps
200 """
201 caps = self.GetCaps()
202 self.CapsChange(caps)
203
204 @dbus.service.signal(INTERFACE_NAME, signature="a{sv}")
205 def TrackChange(self, metadata):
206 """
207 Signal is emitted when the "Media Player" plays another "Track".
208 Argument of the signal is the metadata attached to the new "Track"
209 """
210 pass
211
212 @dbus.service.signal(INTERFACE_NAME, signature="(iiii)")
213 def StatusChange(self, struct):
214 """
215 Signal is emitted when the status of the "Media Player" change. The
216 argument has the same meaning as the value returned by GetStatus.
217 """
218 pass
219
220 @dbus.service.signal(INTERFACE_NAME)
221 def CapsChange(self):
222 """
223 Signal is emitted when the "Media Player" changes capabilities, see
224 GetCaps method.
225 """
226 pass
227
0228
=== added file 'plugins/mpris/mpris_root.py'
--- plugins/mpris/mpris_root.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/mpris_root.py 2009-04-05 21:01:49 +0000
@@ -0,0 +1,38 @@
1import dbus
2import dbus.service
3
4from xl.nls import gettext as _
5
6INTERFACE_NAME = 'org.freedesktop.MediaPlayer'
7
8class ExaileMprisRoot(dbus.service.Object):
9
10 """
11 / (Root) object methods
12 """
13
14 def __init__(self, exaile, bus):
15 dbus.service.Object.__init__(self, bus, '/')
16 self.exaile = exaile
17
18 @dbus.service.method(INTERFACE_NAME, out_signature="s")
19 def Identity(self):
20 """
21 Identify the "media player"
22 """
23 return _("Exaile %(version)s") % {'version': self.exaile.get_version()}
24
25 @dbus.service.method(INTERFACE_NAME)
26 def Quit(self):
27 """
28 Makes the "Media Player" exit.
29 """
30 self.exaile.quit()
31
32 @dbus.service.method(INTERFACE_NAME, out_signature="(qq)")
33 def MprisVersion(self):
34 """
35 Makes the "Media Player" exit.
36 """
37 return (1, 0)
38
039
=== added file 'plugins/mpris/mpris_tag_converter.py'
--- plugins/mpris/mpris_tag_converter.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/mpris_tag_converter.py 2009-04-07 23:55:02 +0000
@@ -0,0 +1,176 @@
1"""
2A converter utility to convert from exaile tags to mpris Metadata
3"""
4
5import logging
6_LOG = logging.getLogger('exaile.plugins.mpris.mpris_tag_converter')
7
8# Dictionary to map MPRIS tags to Exaile Tags
9# Each key is the mpris tag, each value is a dictionary with possible keys:
10# * out_type: REQUIRED, function that will convert to the MPRIS type
11# * exaile_tag: the name of the tag in exaile, defaults to the mpris tag
12# * conv: a conversion function to call on the exaile metadata, defaults to
13# lambda x: x
14# * desc: a description of what's in the tag
15# * constructor: a function to call that takes (exaile, track) and returns
16# the value for the key. If it returns None, the tag is not
17# set
18MPRIS_TAG_INFORMATION = {
19 'location' : {'out_type' : unicode,
20 'exaile_tag': 'loc',
21 'conv' : lambda x: "file://" + x,
22 'desc' : 'Name',
23 },
24 'title' : {'out_type' : unicode,
25 'desc' : 'Name of artist or band',
26 },
27 'album' : {'out_type' : unicode,
28 'desc' : 'Name of compilation',
29 },
30 'tracknumber': {'out_type' : unicode,
31 'desc' : 'The position in album',
32 },
33 'time' : {'out_type' : int,
34 'exaile_tag': 'length',
35 'desc' : 'The duration in seconds',
36 },
37 'mtime' : {'out_type' : int,
38 'desc' : 'The duration in milliseconds',
39 },
40 'genre' : {'out_type' : unicode,
41 'desc' : 'The genre',
42 },
43 'comment' : {'out_type' : unicode,
44 'desc' : 'A comment about the work',
45 },
46 'rating' : {'out_type' : int,
47 'desc' : 'A "taste" rate value, out of 5',
48 },
49 'year' : {'out_type' : int,
50 'exaile_tag': 'date',
51 'conv' : lambda x: x.split('-')[0],
52 'desc' : 'The year of performing',
53 },
54 'date' : {'out_type' : int,
55 'exaile_tag': None,
56 'desc' : 'When the performing was realized, '
57 'since epoch',
58 },
59 'arturl' : {'out_type' : unicode,
60 'desc' : 'an URI to an image',
61 },
62 'audio-bitrate': {'out_type': int,
63 'exaile_tag': 'bitrate',
64 'desc' : 'The number of bits per second',
65 },
66 'audio-samplerate': {'out_type': int,
67 'desc' : 'The number of samples per second',
68 },
69 }
70EXAILE_TAG_INFORMATION = {}
71def __fill_exaile_tag_information():
72 """
73 Fille EXAILE_TAG_INFORMATION with the exaile_tag: mpris_tag, the
74 inverse of MPRIS_TAG_INFORMATION
75 """
76 for mpris_tag in MPRIS_TAG_INFORMATION:
77 if 'exaile_tag' in MPRIS_TAG_INFORMATION[mpris_tag]:
78 exaile_tag = MPRIS_TAG_INFORMATION[mpris_tag]['exaile_tag']
79 else:
80 exaile_tag = mpris_tag
81 if exaile_tag is None:
82 continue
83 EXAILE_TAG_INFORMATION[exaile_tag] = mpris_tag
84__fill_exaile_tag_information()
85
86class OutputTypeMismatchException(Exception):
87 def __init__(self, exaile_tag, mpris_tag, val):
88 Exception.__init__(self,
89 "Could not convert tag exaile:'%s' to mpris:'%s':"
90 "Error converting '%s' to type '%s"
91 % (exaile_tag, mpris_tag, val,
92 MPRIS_TAG_INFORMATION[mpris_tag]['out_type']))
93
94class ExaileTagConverter(object):
95
96 """
97 Class to convert tags from Exaile to Metadata for MPRIS
98 """
99
100 def __init__(self, exaile):
101 self.exaile = exaile
102
103 def get_metadata(self, track):
104 """
105 Returns the Metadata for track as defined by MPRIS standard
106 """
107 metadata = {}
108 for exaile_tag in track.tags:
109 if exaile_tag not in EXAILE_TAG_INFORMATION:
110 continue
111 val = ExaileTagConverter.__get_first_item(track.tags[exaile_tag])
112 try:
113 mpris_tag, mpris_val = ExaileTagConverter.convert_tag(
114 exaile_tag, val)
115 except OutputTypeMismatchException, e:
116 _LOG.exception(e)
117 continue
118 if mpris_tag is None:
119 continue
120 metadata[mpris_tag] = mpris_val
121
122 for mpris_tag in MPRIS_TAG_INFORMATION:
123 if 'constructor' in MPRIS_TAG_INFORMATION[mpris_tag]:
124 val = MPRIS_TAG_INFORMATION[mpris_tag]['constructor'](
125 self.exaile,
126 track
127 )
128 if val is not None:
129 try:
130 metadata[mpris_tag] = \
131 MPRIS_TAG_INFORMATION[mpris_tag]['out_type'](val)
132 except ValueError:
133 raise OutputTypeMismatchException(exaile_tag,
134 mpris_tag,
135 val,
136 )
137
138
139 return metadata
140
141 @staticmethod
142 def __get_first_item(value):
143 """
144 Unlists lists and returns the first value, if not a lists,
145 returns value
146 """
147 if not isinstance(value, basestring) and hasattr(value, "__getitem__"):
148 if len(value):
149 return value[0]
150 return None
151 return value
152
153 @staticmethod
154 def convert_tag(exaile_tag, exaile_value):
155 """
156 Converts a single tag into MPRIS form, return a 2-tuple of
157 (mpris_tag, mpris_val). Returns (None, None) if there is no
158 translation
159 """
160 if exaile_tag not in EXAILE_TAG_INFORMATION:
161 return (None, None)
162 mpris_tag = EXAILE_TAG_INFORMATION[exaile_tag]
163 info = MPRIS_TAG_INFORMATION[mpris_tag]
164 if 'conv' in info:
165 mpris_value = info['conv'](exaile_value)
166 else:
167 mpris_value = exaile_value
168 try:
169 mpris_value = info['out_type'](mpris_value)
170 except ValueError:
171 raise OutputTypeMismatchException(exaile_tag,
172 mpris_tag,
173 exaile_value,
174 )
175 return (mpris_tag, mpris_value)
176
0177
=== added file 'plugins/mpris/mpris_tracklist.py'
--- plugins/mpris/mpris_tracklist.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/mpris_tracklist.py 2009-04-07 23:55:02 +0000
@@ -0,0 +1,122 @@
1import dbus
2import dbus.service
3
4import xl.track
5import xl.event
6
7import mpris_tag_converter
8
9INTERFACE_NAME = 'org.freedesktop.MediaPlayer'
10
11class ExaileMprisTrackList(dbus.service.Object):
12
13 """
14 /TrackList object methods
15 """
16
17 def __init__(self, exaile, bus):
18 dbus.service.Object.__init__(self, bus, '/TrackList')
19 self.exaile = exaile
20 self.tag_converter = mpris_tag_converter.ExaileTagConverter(exaile)
21 for event in ('tracks_removed', 'tracks_added'):
22 xl.event.add_callback(self.tracklist_change_cb, event)
23
24 def __get_playlist(self):
25 """
26 Returns the list of tracks in the current playlist
27 """
28 return self.exaile.queue.current_playlist.get_ordered_tracks()
29
30 @dbus.service.method(INTERFACE_NAME,
31 in_signature="i", out_signature="a{sv}")
32 def GetMetadata(self, pos):
33 """
34 Gives all meta data available for element at given position in the
35 TrackList, counting from 0
36
37 Each dict entry is organized as follows
38 * string: Metadata item name
39 * variant: Metadata value
40 """
41 track = self.__get_playlist()[pos]
42 return self.tag_converter.get_metadata(track)
43
44 @dbus.service.method(INTERFACE_NAME, out_signature="i")
45 def GetCurrentTrack(self):
46 """
47 Return the position of current URI in the TrackList The return
48 value is zero-based, so the position of the first URI in the
49 TrackList is 0. The behavior of this method is unspecified if
50 there are zero elements in the TrackList.
51 """
52 try:
53 return self.exaile.queue.current_playlist.index(
54 self.exaile.player.current)
55 except ValueError:
56 return -1
57
58 @dbus.service.method(INTERFACE_NAME, out_signature="i")
59 def GetLength(self):
60 """
61 Number of elements in the TrackList
62 """
63 return len(self.exaile.queue.current_playlist)
64
65 @dbus.service.method(INTERFACE_NAME,
66 in_signature="sb", out_signature="i")
67 def AddTrack(self, uri, play_immediately):
68 """
69 Appends an URI in the TrackList.
70 """
71 if not uri.startswith("file://"):
72 return -1
73 uri = uri[7:]
74 track = self.exaile.collection.get_track_by_loc(unicode(uri))
75 if track is None:
76 track = xl.track.Track(uri)
77 self.exaile.queue.current_playlist.add(track)
78 if play_immediately:
79 self.exaile.queue.play(track)
80 return 0
81
82 @dbus.service.method(INTERFACE_NAME, in_signature="i")
83 def DelTrack(self, pos):
84 """
85 Appends an URI in the TrackList.
86 """
87 self.exaile.queue.current_playlist.remove(pos)
88
89 @dbus.service.method(INTERFACE_NAME, in_signature="b")
90 def SetLoop(self, loop):
91 """
92 Sets the player's "repeat" or "loop" setting
93 """
94 self.exaile.queue.current_playlist.set_repeat(loop)
95
96 @dbus.service.method(INTERFACE_NAME, in_signature="b")
97 def SetRandom(self, random):
98 """
99 Sets the player's "random" setting
100 """
101 self.exaile.queue.current_playlist.set_random(random)
102
103 def tracklist_change_cb(self, type, object, data):
104 """
105 Callback for a track list change
106 """
107 len = self.GetLength()
108 self.TrackListChange(len)
109
110 @dbus.service.signal(INTERFACE_NAME, signature="i")
111 def TrackListChange(self, num_of_elements):
112 """
113 Signal is emitted when the "TrackList" content has changed:
114 * When one or more elements have been added
115 * When one or more elements have been removed
116 * When the ordering of elements has changed
117
118 The argument is the number of elements in the TrackList after the
119 change happened.
120 """
121 pass
122
0123
=== added file 'plugins/mpris/test.py'
--- plugins/mpris/test.py 1970-01-01 00:00:00 +0000
+++ plugins/mpris/test.py 2009-04-08 05:37:12 +0000
@@ -0,0 +1,305 @@
1"""
2Simple test case for MPRIS. Make sure you set the global variable FILE with an
3absolute path to a valid playable, local music piece before running this test
4"""
5import unittest
6import dbus
7import os
8import time
9
10OBJECT_NAME = 'org.mpris.exaile'
11INTERFACE = 'org.freedesktop.MediaPlayer'
12FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir, os.path.pardir,
13 'tests', 'data', 'music', 'delerium', 'chimera',
14 '05 - Truly.mp3')
15assert os.path.isfile(FILE), FILE + " must be a valid musical piece"
16FILE = "file://" + FILE
17print "Test file will be: " + FILE
18
19class TestExaileMpris(unittest.TestCase):
20
21 """
22 Tests Exaile MPRIS plugin
23 """
24
25 def setUp(self):
26 """
27 Simple setUp that makes dbus connections and assigns them to
28 self.player, self.track_list, and self.root. Also begins playing
29 a song so every test case can assume that a song is playing
30 """
31 bus = dbus.SessionBus()
32 objects = {'root': '/',
33 'player': '/Player',
34 'track_list': '/TrackList',
35 }
36 intfs = {}
37 for key in objects:
38 object = bus.get_object(OBJECT_NAME, objects[key])
39 intfs[key] = dbus.Interface(object, INTERFACE)
40 self.root = intfs['root']
41 self.player = intfs['player']
42 self.track_list = intfs['track_list']
43 self.player.Play()
44 time.sleep(0.5)
45
46 def __wait_after(function):
47 """
48 Decorator to add a delay after a function call
49 """
50 def inner(*args, **kwargs):
51 function(*args, **kwargs)
52 time.sleep(0.5)
53 return inner
54
55 @__wait_after
56 def _stop(self):
57 """
58 Stops playing w/ delay
59 """
60 self.player.Stop()
61
62 @__wait_after
63 def _play(self):
64 """
65 Starts playing w/ delay
66 """
67 self.player.Play()
68
69 @__wait_after
70 def _pause(self):
71 """
72 Pauses playing w/ delay
73 """
74 self.player.Pause()
75
76
77class TestMprisRoot(TestExaileMpris):
78
79 """
80 Check / (Root) object functions for MPRIS. Does not check Quit
81 """
82
83 def testIdentity(self):
84 """
85 Make sure we output Exaile with our identity
86 """
87 id = self.root.Identity()
88 self.assertEqual(id, self.root.Identity())
89 self.assertTrue(id.startswith("Exaile"))
90
91 def testMprisVersion(self):
92 """
93 Checks that we are using MPRIS version 1.0
94 """
95 version = self.root.MprisVersion()
96 self.assertEqual(dbus.UInt16(1), version[0])
97 self.assertEqual(dbus.UInt16(0), version[1])
98
99class TestTrackList(TestExaileMpris):
100
101 """
102 Tests the /TrackList object for MPRIS
103 """
104
105 def testGetMetadata(self):
106 """
107 Make sure we can get metadata. Also makes sure that locations will
108 not change randomly
109 """
110 md = self.track_list.GetMetadata(0)
111 md_2 = self.track_list.GetMetadata(0)
112 self.assertEqual(md, md_2)
113
114 def testAppendDelWithoutPlay(self):
115 """
116 Tests appending and deleting songs from the playlist without
117 playing them
118 """
119 cur_track = self.track_list.GetCurrentTrack()
120 len = self.track_list.GetLength()
121
122 self.assertEqual(0, self.track_list.AddTrack(FILE, False))
123 self.assertEqual(len + 1, self.track_list.GetLength())
124 self.assertEqual(cur_track, self.track_list.GetCurrentTrack())
125
126 md = self.track_list.GetMetadata(len)
127 self.assertEqual(FILE, md['location'])
128
129 self.track_list.DelTrack(len)
130 self.assertEqual(len, self.track_list.GetLength())
131 self.assertEqual(cur_track, self.track_list.GetCurrentTrack())
132
133 def testAppendDelWithPlay(self):
134 """
135 Tests appending songs into the playlist with playing the songs
136 """
137 cur_track = self.track_list.GetCurrentTrack()
138 cur_md = self.track_list.GetMetadata(cur_track)
139 len = self.track_list.GetLength()
140
141 self.assertEqual(0, self.track_list.AddTrack(FILE, True))
142 self.assertEqual(len + 1, self.track_list.GetLength())
143
144 md = self.track_list.GetMetadata(len)
145 self.assertEqual(FILE, md['location'])
146 self.assertEqual(len, self.track_list.GetCurrentTrack())
147
148 self.track_list.DelTrack(len)
149 self.assertEqual(len, self.track_list.GetLength())
150
151 self.track_list.AddTrack(cur_md['location'], True)
152
153 def testGetCurrentTrack(self):
154 """
155 Check the GetCurrentTrack information
156 """
157 cur_track = self.track_list.GetCurrentTrack()
158 self.assertTrue(cur_track >= 0, "Tests start with playing music")
159
160 self._stop()
161 self.assertEqual(dbus.Int32(-1), self.track_list.GetCurrentTrack(),
162 "Our implementation returns -1 if no tracks are playing")
163
164 self._play()
165 self.assertEqual(cur_track, self.track_list.GetCurrentTrack(),
166 "After a stop and play, we should be at the same track")
167
168 def __test_bools(self, getter, setter):
169 """
170 Generic function for checking that a boolean value changes
171 """
172 cur_val = getter()
173 if cur_val == dbus.Int32(0):
174 val = False
175 elif cur_val == dbus.Int32(1):
176 val = True
177 else:
178 self.fail("Got an invalid value from status")
179
180 setter(False)
181 status = getter()
182 self.assertEqual(dbus.Int32(0), status)
183
184 setter(True)
185 status = getter()
186 self.assertEqual(dbus.Int32(1), status)
187
188 setter(val)
189 self.track_list.SetLoop(val)
190
191 def testLoop(self):
192 """
193 Tests that you can change the loop settings
194 """
195 self.__test_bools(lambda: self.player.GetStatus()[3],
196 lambda x: self.track_list.SetLoop(x))
197
198 def testRandom(self):
199 """
200 Tests that you can change the random settings
201 """
202 self.__test_bools(lambda: self.player.GetStatus()[1],
203 lambda x: self.track_list.SetRandom(x))
204
205class TestPlayer(TestExaileMpris):
206
207 """
208 Tests the /Player object for MPRIS
209 """
210
211 def testNextPrev(self):
212 """
213 Make sure you can skip back and forward
214 """
215 cur_track = self.track_list.GetCurrentTrack()
216 self.player.Next()
217 new_track = self.track_list.GetCurrentTrack()
218 self.assertNotEqual(cur_track, new_track)
219 self.player.Prev()
220 self.assertEqual(cur_track, self.track_list.GetCurrentTrack())
221
222 def testStopPlayPause(self):
223 """
224 Make sure play, pause, and stop behaive as designed
225 """
226 self._stop()
227 self.assertEqual(dbus.Int32(2), self.player.GetStatus()[0])
228
229 self._play()
230 self.assertEqual(dbus.Int32(0), self.player.GetStatus()[0])
231 self._play()
232 self.assertEqual(dbus.Int32(0), self.player.GetStatus()[0])
233
234 self._pause()
235 self.assertEqual(dbus.Int32(1), self.player.GetStatus()[0])
236 self._pause()
237 self.assertEqual(dbus.Int32(0), self.player.GetStatus()[0])
238
239 self._stop()
240 self.assertEqual(dbus.Int32(2), self.player.GetStatus()[0])
241 self._pause()
242 self.assertEqual(dbus.Int32(2), self.player.GetStatus()[0])
243
244 def testVolume(self):
245 """
246 Test to make sure volumes are set happily
247 """
248 vol = self.player.VolumeGet()
249 self.player.VolumeSet(1 - vol)
250 self.assertEqual(1 - vol, self.player.VolumeGet())
251 self.player.VolumeSet(vol)
252 self.assertEqual(vol, self.player.VolumeGet())
253
254 def testPosition(self):
255 """
256 Test the PositionGet and PositionSet functions. Unfortuantely this
257 is very time sensitive and thus has about a 10 second sleep in the
258 function
259 """
260 time.sleep(3)
261 self._pause()
262
263 pos = self.player.PositionGet()
264 time.sleep(1)
265 self.assertEqual(pos, self.player.PositionGet(),
266 "Position shouldn't move while paused")
267
268 self._pause()
269 time.sleep(4)
270 last_pos = self.player.PositionGet()
271 self.assertTrue(pos < last_pos,
272 "Position shouldn't advance while paused: %d >= %d" %
273 (pos, last_pos))
274
275 self.player.PositionSet(pos)
276 time.sleep(2)
277 self._pause()
278 mid_pos = self.player.PositionGet(),
279 self.assertTrue(mid_pos[0] < last_pos,
280 "Resetting to position %d, %d should be between that at %d"
281 % (pos, mid_pos[0], last_pos))
282
283 self._pause()
284 time.sleep(0.5)
285 self.assertTrue(pos < self.player.PositionGet(),
286 "Make sure it still advances")
287
288 self.player.PositionSet(-1)
289 self.assertTrue(pos < self.player.PositionGet(),
290 "Don't move to invalid position")
291
292
293def suite():
294 sub_test = [TestMprisRoot, TestTrackList]
295
296 suites = []
297
298 for test in sub_test:
299 suites.append(unittest.defaultTestLoader.loadTestsFromTestCase(test))
300
301 return unittest.TestSuite(suites)
302
303if __name__ == "__main__":
304 unittest.main()
305
0306
=== modified file 'plugins/notify/__init__.py'
--- plugins/notify/__init__.py 2008-10-17 04:04:50 +0000
+++ plugins/notify/__init__.py 2009-04-05 05:48:03 +0000
@@ -4,27 +4,41 @@
44
5pynotify.init('exailenotify')5pynotify.init('exailenotify')
66
7def on_play(type, player, track):7class ExaileNotification(object):
8 title = " / ".join(track['title'] or _("Unknown"))8 def __init__(self):
9 artist = " / ".join(track['artist'] or "")9 self.notification = pynotify.Notification("Exaile")
10 album = " / ".join(track['album'] or "")10 self.exaile = None
11 summary = cgi.escape(title)11
12 if artist and album:12 def on_play(self, type, player, track):
13 body = _("by %(artist)s\nfrom <i>%(album)s</i>") % {13 title = " / ".join(track['title'] or _("Unknown"))
14 'artist' : cgi.escape(artist), 14 artist = " / ".join(track['artist'] or "")
15 'album' : cgi.escape(album)}15 album = " / ".join(track['album'] or "")
16 elif artist:16 summary = cgi.escape(title)
17 body = _("by %(artist)s") % {'artist' : cgi.escape(artist)}17 if artist and album:
18 elif album:18 body = _("by %(artist)s\nfrom <i>%(album)s</i>") % {
19 body = _("from %(album)s") % {'album' : cgi.escape(album)}19 'artist' : cgi.escape(artist),
20 else:20 'album' : cgi.escape(album)}
21 body = ""21 elif artist:
22 notify = pynotify.Notification(summary, body)22 body = _("by %(artist)s") % {'artist' : cgi.escape(artist)}
2323 elif album:
24 notify.show()24 body = _("from %(album)s") % {'album' : cgi.escape(album)}
25 else:
26 body = ""
27 self.notification.update(summary, body)
28 item = track.get_album_tuple()
29 image = None
30 if all(item) and hasattr(self.exaile, 'covers'):
31 image = self.exaile.covers.coverdb.get_cover(*item)
32 if image is None:
33 image = 'exaile'
34 self.notification.set_property('icon-name', image)
35 self.notification.show()
36
37EXAILE_NOTIFICATION = ExaileNotification()
2538
26def enable(exaile):39def enable(exaile):
27 event.add_callback(on_play, 'playback_start')40 EXAILE_NOTIFICATION.exaile = exaile
41 event.add_callback(EXAILE_NOTIFICATION.on_play, 'playback_start')
2842
29def disable(exaile):43def disable(exaile):
30 event.remove_callback(on_play, 'playback_start')44 event.remove_callback(EXAILE_NOTIFICATION.on_play, 'playback_start')
3145
=== modified file 'xlgui/plugins.py'
--- xlgui/plugins.py 2009-01-05 23:30:31 +0000
+++ xlgui/plugins.py 2009-04-04 21:10:41 +0000
@@ -106,18 +106,26 @@
106 (model, iter) = self.list.get_selection().get_selected()106 (model, iter) = self.list.get_selection().get_selected()
107 if not iter: return107 if not iter: return
108108
109 pluginname = model.get_value(iter, 2)109 pluginname = model.get_value(iter, 2)[0]
110 if not pluginname in self.plugins.enabled_plugins:110 if not self.__configure_available(pluginname):
111 return
112
113 plugin = self.plugins.enabled_plugins[pluginname]
114 if not hasattr(plugin, 'get_prefs_pane'):
115 commondialogs.error(self.parent, _("The selected " 111 commondialogs.error(self.parent, _("The selected "
116 "plugin doesn't have any configuration options"))112 "plugin doesn't have any configuration options"))
117 return113 return
118114
119 self.guimain.show_preferences(plugin_page=pluginname)115 self.guimain.show_preferences(plugin_page=pluginname)
120116
117 def __configure_available(self, pluginname):
118 """
119 Returns if a plugin given by pluginname has the ability to open a
120 configure dialog
121 """
122 if pluginname not in self.plugins.enabled_plugins:
123 return False
124 plugin = self.plugins.enabled_plugins[pluginname]
125 if not hasattr(plugin, 'get_prefs_pane'):
126 return False
127 return True
128
121 def row_selected(self, selection, user_data=None):129 def row_selected(self, selection, user_data=None):
122 """130 """
123 Called when a row is selected131 Called when a row is selected
@@ -132,6 +140,12 @@
132 self.description.get_buffer().set_text(140 self.description.get_buffer().set_text(
133 info['Description'].replace(r'\n', "\n"))141 info['Description'].replace(r'\n', "\n"))
134 self.name_label.set_markup("<b>%s</b>" % info['Name'])142 self.name_label.set_markup("<b>%s</b>" % info['Name'])
143 (model, iter) = selection.get_selected()
144 pluginname = model.get(iter, 2)[0]
145 if self.__configure_available(pluginname):
146 self.configure_button.set_sensitive(True)
147 else:
148 self.configure_button.set_sensitive(False)
135149
136 def toggle_cb(self, cell, path, model):150 def toggle_cb(self, cell, path, model):
137 """151 """
@@ -150,8 +164,8 @@
150 commondialogs.error(self.parent, _('Could '164 commondialogs.error(self.parent, _('Could '
151 'not disable plugin.'))165 'not disable plugin.'))
152 return166 return
153
154 model[path][1] = enable167 model[path][1] = enable
168 self.row_selected(self.list.get_selection())
155169
156 def destroy(self, *e):170 def destroy(self, *e):
157 self.dialog.destroy()171 self.dialog.destroy()