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