~ubuntu-branches/ubuntu/wily/remuco-server/wily

« back to all changes in this revision

Viewing changes to base/module/remuco/mpris.py

  • Committer: Bazaar Package Importer
  • Author(s): Chow Loong Jin
  • Date: 2009-03-30 00:59:36 UTC
  • Revision ID: james.westby@ubuntu.com-20090330005936-hkxki384hm0d33gj
Tags: upstream-0.8.2.1
ImportĀ upstreamĀ versionĀ 0.8.2.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# =============================================================================
 
2
#
 
3
#    Remuco - A remote control system for media players.
 
4
#    Copyright (C) 2006-2009 Oben Sonne <obensonne@googlemail.com>
 
5
#
 
6
#    This file is part of Remuco.
 
7
#
 
8
#    Remuco is free software: you can redistribute it and/or modify
 
9
#    it under the terms of the GNU General Public License as published by
 
10
#    the Free Software Foundation, either version 3 of the License, or
 
11
#    (at your option) any later version.
 
12
#
 
13
#    Remuco is distributed in the hope that it will be useful,
 
14
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
#    GNU General Public License for more details.
 
17
#
 
18
#    You should have received a copy of the GNU General Public License
 
19
#    along with Remuco.  If not, see <http://www.gnu.org/licenses/>.
 
20
#
 
21
# =============================================================================
 
22
 
 
23
import os.path
 
24
 
 
25
import dbus
 
26
from dbus.exceptions import DBusException
 
27
import gobject
 
28
 
 
29
from remuco.adapter import PlayerAdapter, ItemAction
 
30
from remuco.defs import *
 
31
from remuco import log
 
32
 
 
33
# =============================================================================
 
34
# MPRIS constants
 
35
# =============================================================================
 
36
 
 
37
CAN_GO_NEXT          = 1 << 0
 
38
CAN_GO_PREV          = 1 << 1
 
39
CAN_PAUSE            = 1 << 2
 
40
CAN_PLAY             = 1 << 3
 
41
CAN_SEEK             = 1 << 4
 
42
CAN_PROVIDE_METADATA = 1 << 5
 
43
CAN_HAS_TRACKLIST    = 1 << 6
 
44
 
 
45
STATUS_PLAYING = 0
 
46
STATUS_PAUSED  = 1
 
47
STATUS_STOPPED = 2
 
48
 
 
49
MINFO_KEY_RATING = "rating"
 
50
 
 
51
# =============================================================================
 
52
# actions
 
53
# =============================================================================
 
54
 
 
55
IA_APPEND = ItemAction("Append", multiple=True)
 
56
IA_APPEND_PLAY = ItemAction("Append and play", multiple=True)
 
57
FILE_ACTIONS = (IA_APPEND, IA_APPEND_PLAY)
 
58
 
 
59
IA_JUMP = ItemAction("Jump to") # __jump_to() is ambiguous on dynamic playlists  
 
60
IA_REMOVE = ItemAction("Remove", multiple=True)
 
61
PLAYLIST_ACTIONS = (IA_REMOVE, )
 
62
 
 
63
# =============================================================================
 
64
# player adapter
 
65
# =============================================================================
 
66
 
 
67
class MPRISAdapter(PlayerAdapter):
 
68
    
 
69
    def __init__(self, name, display_name=None, poll=2.5, mime_types=None,
 
70
                 rating=False, extra_file_actions=None,
 
71
                 extra_playlist_actions=None):
 
72
        
 
73
        display_name = display_name or name
 
74
            
 
75
        if rating:
 
76
            max_rating = 5
 
77
        else:
 
78
            max_rating = 0
 
79
        
 
80
        all_file_actions = FILE_ACTIONS + tuple(extra_file_actions or ())
 
81
            
 
82
        PlayerAdapter.__init__(self, display_name,
 
83
                               max_rating=max_rating,
 
84
                               playback_known=True,
 
85
                               volume_known=True,
 
86
                               repeat_known=True,
 
87
                               shuffle_known=True,
 
88
                               progress_known=True,
 
89
                               file_actions=all_file_actions,
 
90
                               mime_types=mime_types)
 
91
        
 
92
        self.__playlist_actions = (PLAYLIST_ACTIONS +
 
93
                                   tuple(extra_playlist_actions or ()))
 
94
         
 
95
        self.__name = name
 
96
        
 
97
        self.__dbus_signal_handler = ()
 
98
        
 
99
        self._repeat = False
 
100
        self._shuffle = False
 
101
        self._playing = PLAYBACK_STOP
 
102
        self.__volume = 0
 
103
        self.__progress_now = 0
 
104
        self.__progress_max = 0
 
105
        self.__can_pause = False
 
106
        self.__can_play = False
 
107
        self.__can_seek = False
 
108
        self.__can_next = False
 
109
        self.__can_prev = False
 
110
        self.__can_tracklist = False
 
111
        
 
112
        log.debug("init done")
 
113
 
 
114
    def start(self):
 
115
        
 
116
        PlayerAdapter.start(self)
 
117
        
 
118
        try:
 
119
            bus = dbus.SessionBus()
 
120
            proxy = bus.get_object("org.mpris.%s" % self.__name, "/Player")
 
121
            self._mp_p = dbus.Interface(proxy, "org.freedesktop.MediaPlayer")
 
122
            proxy = bus.get_object("org.mpris.%s" % self.__name, "/TrackList")
 
123
            self._mp_t = dbus.Interface(proxy, "org.freedesktop.MediaPlayer")
 
124
        except DBusException, e:
 
125
            raise StandardError("dbus error: %s" % e)
 
126
 
 
127
        try:
 
128
            self.__dbus_signal_handler = (
 
129
                self._mp_p.connect_to_signal("TrackChange",
 
130
                                             self._notify_track),
 
131
                self._mp_p.connect_to_signal("StatusChange",
 
132
                                             self._notify_status),
 
133
                self._mp_p.connect_to_signal("CapsChange",
 
134
                                             self._notify_caps),
 
135
                self._mp_t.connect_to_signal("TrackListChange",
 
136
                                             self._notify_tracklist_change),
 
137
            )
 
138
        except DBusException, e:
 
139
            raise StandardError("dbus error: %s" % e)
 
140
 
 
141
        try:
 
142
            self._mp_p.GetStatus(reply_handler=self._notify_status,
 
143
                                 error_handler=self._dbus_error)
 
144
            
 
145
            self._mp_p.GetMetadata(reply_handler=self._notify_track,
 
146
                                   error_handler=self._dbus_error)
 
147
    
 
148
            self._mp_p.GetCaps(reply_handler=self._notify_caps,
 
149
                               error_handler=self._dbus_error)
 
150
        except DBusException, e:
 
151
            # this is not necessarily a fatal error
 
152
            log.warning("dbus error: %s" % e)
 
153
        
 
154
    def stop(self):
 
155
        
 
156
        PlayerAdapter.stop(self)
 
157
        
 
158
        for handler in self.__dbus_signal_handler:
 
159
            handler.remove()
 
160
            
 
161
        self.__dbus_signal_handler = ()
 
162
        
 
163
        self._mp_p = None
 
164
        self._mp_t = None
 
165
        
 
166
    def poll(self):
 
167
        
 
168
        self._poll_volume()
 
169
        self._poll_progress()
 
170
        
 
171
    # =========================================================================
 
172
    # control interface 
 
173
    # =========================================================================
 
174
    
 
175
    def ctrl_toggle_playing(self):
 
176
        
 
177
        try:
 
178
            if self._playing == PLAYBACK_STOP:
 
179
                self._mp_p.Play(reply_handler=self._dbus_ignore,
 
180
                                error_handler=self._dbus_error)
 
181
            else:
 
182
                self._mp_p.Pause(reply_handler=self._dbus_ignore,
 
183
                                 error_handler=self._dbus_error)
 
184
        except DBusException, e:
 
185
            log.warning("dbus error: %s" % e)
 
186
    
 
187
    def ctrl_toggle_repeat(self):
 
188
        
 
189
        try:
 
190
            self._mp_t.SetLoop(not self._repeat,
 
191
                               reply_handler=self._dbus_ignore,
 
192
                               error_handler=self._dbus_error)
 
193
        except DBusException, e:
 
194
            log.warning("dbus error: %s" % e)
 
195
    
 
196
    def ctrl_toggle_shuffle(self):
 
197
        
 
198
        try:
 
199
            self._mp_t.SetRandom(not self._shuffle,
 
200
                                 reply_handler=self._dbus_ignore,
 
201
                                 error_handler=self._dbus_error)
 
202
        except DBusException, e:
 
203
            log.warning("dbus error: %s" % e)
 
204
        
 
205
    def ctrl_next(self):
 
206
        
 
207
        if not self.__can_next:
 
208
            log.debug("go to next item is currently not possible")
 
209
            return
 
210
        
 
211
        try:
 
212
            self._mp_p.Next(reply_handler=self._dbus_ignore,
 
213
                            error_handler=self._dbus_error)
 
214
        except DBusException, e:
 
215
            log.warning("dbus error: %s" % e)
 
216
    
 
217
    def ctrl_previous(self):
 
218
 
 
219
        if not self.__can_prev:
 
220
            log.debug("go to previous is currently not possible")
 
221
            return
 
222
        
 
223
        try:
 
224
            self._mp_p.Prev(reply_handler=self._dbus_ignore,
 
225
                            error_handler=self._dbus_error)
 
226
        except DBusException, e:
 
227
            log.warning("dbus error: %s" % e)
 
228
        
 
229
    def ctrl_volume(self, direction):
 
230
        
 
231
        if direction == 0:
 
232
            volume = 0
 
233
        else:
 
234
            volume = self.__volume + 5 * direction
 
235
            volume = min(volume, 100)
 
236
            volume = max(volume, 0)
 
237
            
 
238
        try:
 
239
            self._mp_p.VolumeSet(volume, reply_handler=self._dbus_ignore,
 
240
                                 error_handler=self._dbus_error)
 
241
        except DBusException, e:
 
242
            log.warning("dbus error: %s" % e)
 
243
        
 
244
        gobject.idle_add(self._poll_volume)
 
245
        
 
246
    def ctrl_seek(self, direction):
 
247
        
 
248
        if not self.__can_seek:
 
249
            log.debug("seeking is currently not possible")
 
250
            return
 
251
        
 
252
        self.__progress_now += 5 * direction
 
253
        self.__progress_now = min(self.__progress_now, self.__progress_max)
 
254
        self.__progress_now = max(self.__progress_now, 0)
 
255
        
 
256
        log.debug("new progress: %d" % self.__progress_now)
 
257
        
 
258
        try:
 
259
            self._mp_p.PositionSet(self.__progress_now * 1000,
 
260
                                   reply_handler=self._dbus_ignore,
 
261
                                   error_handler=self._dbus_error)
 
262
        except DBusException, e:
 
263
            log.warning("dbus error: %s" % e)
 
264
        
 
265
        gobject.idle_add(self._poll_progress)
 
266
 
 
267
    # =========================================================================
 
268
    # actions interface
 
269
    # =========================================================================
 
270
    
 
271
    def action_files(self, action_id, files, uris):
 
272
        
 
273
        if action_id == IA_APPEND.id or action_id == IA_APPEND_PLAY.id:
 
274
            
 
275
            try:
 
276
                self._mp_t.AddTrack(uris[0], action_id == IA_APPEND_PLAY.id)
 
277
                for uri in uris[1:]:
 
278
                    self._mp_t.AddTrack(uri, False)
 
279
            except DBusException, e:
 
280
                log.warning("dbus error: %s" % e)
 
281
                return
 
282
        
 
283
        else:
 
284
            log.error("** BUG ** unexpected action: %d" % action_id)
 
285
 
 
286
    def action_playlist_item(self, action_id, positions, ids):
 
287
        
 
288
        if action_id == IA_REMOVE.id:
 
289
            
 
290
            positions.sort()
 
291
            positions.reverse()
 
292
            try:
 
293
                for pos in positions:
 
294
                    self._mp_t.DelTrack(pos)
 
295
            except DBusException, e:
 
296
                log.warning("dbus error: %s" % e)
 
297
                return
 
298
        
 
299
        elif action_id == IA_JUMP.id:
 
300
            
 
301
            self.__jump_to(positions[0])
 
302
        
 
303
        else:
 
304
            log.error("** BUG ** unexpected action: %d" % action_id)
 
305
    
 
306
    # =========================================================================
 
307
    # request interface 
 
308
    # =========================================================================
 
309
    
 
310
    def request_playlist(self, client):
 
311
        
 
312
        if not self.__can_tracklist:
 
313
            self.reply_playlist_request(client, [], [])
 
314
            return
 
315
        
 
316
        tracks = self.__get_tracklist()
 
317
 
 
318
        ids = []
 
319
        names = []
 
320
        for track in tracks:
 
321
            id, info = self.__track2info(track)
 
322
            artist = info.get(INFO_ARTIST) or "??"
 
323
            title = info.get(INFO_TITLE) or "??"
 
324
            name = "%s - %s" % (artist, title)
 
325
            ids.append(id)
 
326
            names.append(name)
 
327
        
 
328
        self.reply_playlist_request(client, ids, names,
 
329
                                    item_actions=self.__playlist_actions)
 
330
 
 
331
    # =========================================================================
 
332
    # internal methods (may be overridden by subclasses to fix MPRIS issues) 
 
333
    # =========================================================================
 
334
    
 
335
    def _poll_status(self):
 
336
        """Poll player status information.
 
337
        
 
338
        Some MPRIS players do not notify about all status changes, so that
 
339
        status must be polled. Subclasses may call this method for that purpose.
 
340
        
 
341
        """
 
342
        try:
 
343
            self._mp_p.GetStatus(reply_handler=self._notify_status,
 
344
                                 error_handler=self._dbus_error)
 
345
        except DBusException, e:
 
346
            log.warning("dbus error: %s" % e)
 
347
        
 
348
    def _poll_volume(self):
 
349
        
 
350
        try:
 
351
            self._mp_p.VolumeGet(reply_handler=self._notify_volume,
 
352
                                 error_handler=self._dbus_error)
 
353
        except DBusException, e:
 
354
            log.warning("dbus error: %s" % e)
 
355
        
 
356
    def _poll_progress(self):
 
357
        
 
358
        try:
 
359
            self._mp_p.PositionGet(reply_handler=self._notify_progress,
 
360
                                   error_handler=self._dbus_error)
 
361
        except DBusException, e:
 
362
            log.warning("dbus error: %s" % e)
 
363
            
 
364
    def _notify_track(self, track):
 
365
        
 
366
        log.debug("track: %s" % str(track))
 
367
    
 
368
        id, info = self.__track2info(track)
 
369
        
 
370
        self.__progress_max = info.get(INFO_LENGTH, 0) # for update_progress()
 
371
    
 
372
        img = track.get("arturl")
 
373
        if not img or not img.startswith("file:"):
 
374
            img = self.find_image(id)
 
375
    
 
376
        self.update_item(id, info, img)
 
377
        
 
378
        try:
 
379
            self._mp_t.GetCurrentTrack(reply_handler=self._notify_position,
 
380
                                       error_handler=self._dbus_error)
 
381
        except DBusException, e:
 
382
            log.warning("dbus error: %s" % e)
 
383
            
 
384
    def _notify_status(self, status):
 
385
        
 
386
        log.debug("status: %s " % str(status))
 
387
        
 
388
        if status[0] == STATUS_PLAYING:
 
389
            self._playing = PLAYBACK_PLAY
 
390
        elif status[0] == STATUS_PAUSED:
 
391
            self._playing = PLAYBACK_PAUSE
 
392
        else:
 
393
            self._playing = PLAYBACK_STOP
 
394
        self.update_playback(self._playing)
 
395
        
 
396
        self._shuffle = status[1] > 0 # remember for toggle_shuffle()
 
397
        self.update_shuffle(self._shuffle)
 
398
        
 
399
        self._repeat = status[2] > 0 or status[3] > 0 # for toggle_repeat()
 
400
        self.update_repeat(self._repeat)
 
401
        
 
402
    def _notify_tracklist_change(self, new_len):
 
403
        
 
404
        log.debug("tracklist change")
 
405
        try:
 
406
            self._mp_t.GetCurrentTrack(reply_handler=self._notify_position,
 
407
                                       error_handler=self._dbus_error)
 
408
        except DBusException, e:
 
409
            log.warning("dbus error: %s" % e)
 
410
        
 
411
    def _notify_position(self, position):
 
412
        
 
413
        log.debug("tracklist pos: %d" % position)
 
414
        
 
415
        self.update_position(position)
 
416
    
 
417
    def _notify_volume(self, volume):
 
418
        
 
419
        self.__volume = volume # remember for ctrl_volume()
 
420
        self.update_volume(volume)
 
421
        
 
422
    def _notify_progress(self, progress):
 
423
        
 
424
        self.__progress_now = progress // 1000 # remember for ctrl_seek()
 
425
        self.update_progress(self.__progress_now, self.__progress_max)
 
426
    
 
427
    def _notify_caps(self, caps):
 
428
        
 
429
        self.__can_play = caps & CAN_PLAY != 0
 
430
        self.__can_pause = caps & CAN_PAUSE != 0
 
431
        self.__can_next = caps & CAN_GO_NEXT != 0
 
432
        self.__can_prev = caps & CAN_GO_PREV != 0
 
433
        self.__can_seek = caps & CAN_SEEK != 0
 
434
        self.__can_tracklist = caps & CAN_HAS_TRACKLIST != 0
 
435
    
 
436
    # =========================================================================
 
437
    # internal methods (private) 
 
438
    # =========================================================================
 
439
    
 
440
    def __get_tracklist(self):
 
441
        """Get a list of track dicts of all tracks in the tracklist."""
 
442
        
 
443
        try:
 
444
            len = self._mp_t.GetLength()
 
445
        except DBusException, e:
 
446
            log.warning("dbus error: %s" % e)
 
447
            len = 0
 
448
        
 
449
        if len == 0:
 
450
            return []
 
451
        
 
452
        tracks = []
 
453
        for i in range(0, len):
 
454
            try:
 
455
                tracks.append(self._mp_t.GetMetadata(i))
 
456
            except DBusException, e:
 
457
                log.warning("dbus error: %s" % e)
 
458
                return []
 
459
            
 
460
        return tracks
 
461
 
 
462
    def __track2info(self, track):
 
463
        """Convert an MPRIS meta data dict to a Remuco info dict."""
 
464
        
 
465
        id = track.get("location", "None")
 
466
        
 
467
        info = {}
 
468
        title_alt = os.path.basename(id)
 
469
        title_alt = os.path.splitext(title_alt)[0]
 
470
        info[INFO_TITLE] = track.get("title", title_alt)
 
471
        info[INFO_ARTIST] = track.get("artist", "")
 
472
        info[INFO_ALBUM] = track.get("album", "")
 
473
        info[INFO_GENRE] = track.get("genre", "")
 
474
        info[INFO_YEAR] = track.get("year", "")
 
475
        info[INFO_LENGTH] = track.get("time", track.get("mtime", 0) // 1000)
 
476
        info[INFO_RATING] = track.get("rating", 0)
 
477
        
 
478
        return (id, info)
 
479
    
 
480
    def __jump_to(self, position):
 
481
        """Jump to a position in the tracklist.
 
482
        
 
483
        MPRIS has no such method and this a workaround. Unfortunately if
 
484
        behaves not as expected on dynamic playlists.
 
485
        
 
486
        """
 
487
        tracks = self.__get_tracklist()
 
488
        
 
489
        if position >= len(tracks):
 
490
            return
 
491
        
 
492
        uris = []
 
493
        for track in tracks[position:]:
 
494
            uris.append(track.get("location", "there must be a location"))
 
495
        
 
496
        positions = range(position, len(tracks))
 
497
        
 
498
        self.action_playlist_item(IA_REMOVE.id, positions, uris)
 
499
        
 
500
        self.action_files(IA_APPEND_PLAY.id, [], uris)
 
501
    
 
502
    # =========================================================================
 
503
    # dbus reply handler (may be reused by subclasses) 
 
504
    # =========================================================================
 
505
    
 
506
    def _dbus_error(self, error):
 
507
        """ DBus error handler."""
 
508
        
 
509
        if self._mp_p is None:
 
510
            return # do not log errors when not stopped already
 
511
        
 
512
        log.warning("DBus error: %s" % error)
 
513
        
 
514
    def _dbus_ignore(self):
 
515
        """ DBus reply handler for methods without reply."""
 
516
        
 
517
        pass
 
518
        
 
519