1
# =============================================================================
3
# Remuco - A remote control system for media players.
4
# Copyright (C) 2006-2009 Oben Sonne <obensonne@googlemail.com>
6
# This file is part of Remuco.
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.
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.
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/>.
21
# =============================================================================
26
from dbus.exceptions import DBusException
29
from remuco.adapter import PlayerAdapter, ItemAction
30
from remuco.defs import *
31
from remuco import log
33
# =============================================================================
35
# =============================================================================
42
CAN_PROVIDE_METADATA = 1 << 5
43
CAN_HAS_TRACKLIST = 1 << 6
49
MINFO_KEY_RATING = "rating"
51
# =============================================================================
53
# =============================================================================
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)
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, )
63
# =============================================================================
65
# =============================================================================
67
class MPRISAdapter(PlayerAdapter):
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):
73
display_name = display_name or name
80
all_file_actions = FILE_ACTIONS + tuple(extra_file_actions or ())
82
PlayerAdapter.__init__(self, display_name,
83
max_rating=max_rating,
89
file_actions=all_file_actions,
90
mime_types=mime_types)
92
self.__playlist_actions = (PLAYLIST_ACTIONS +
93
tuple(extra_playlist_actions or ()))
97
self.__dbus_signal_handler = ()
100
self._shuffle = False
101
self._playing = PLAYBACK_STOP
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
112
log.debug("init done")
116
PlayerAdapter.start(self)
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)
128
self.__dbus_signal_handler = (
129
self._mp_p.connect_to_signal("TrackChange",
131
self._mp_p.connect_to_signal("StatusChange",
132
self._notify_status),
133
self._mp_p.connect_to_signal("CapsChange",
135
self._mp_t.connect_to_signal("TrackListChange",
136
self._notify_tracklist_change),
138
except DBusException, e:
139
raise StandardError("dbus error: %s" % e)
142
self._mp_p.GetStatus(reply_handler=self._notify_status,
143
error_handler=self._dbus_error)
145
self._mp_p.GetMetadata(reply_handler=self._notify_track,
146
error_handler=self._dbus_error)
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)
156
PlayerAdapter.stop(self)
158
for handler in self.__dbus_signal_handler:
161
self.__dbus_signal_handler = ()
169
self._poll_progress()
171
# =========================================================================
173
# =========================================================================
175
def ctrl_toggle_playing(self):
178
if self._playing == PLAYBACK_STOP:
179
self._mp_p.Play(reply_handler=self._dbus_ignore,
180
error_handler=self._dbus_error)
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)
187
def ctrl_toggle_repeat(self):
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)
196
def ctrl_toggle_shuffle(self):
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)
207
if not self.__can_next:
208
log.debug("go to next item is currently not possible")
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)
217
def ctrl_previous(self):
219
if not self.__can_prev:
220
log.debug("go to previous is currently not possible")
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)
229
def ctrl_volume(self, direction):
234
volume = self.__volume + 5 * direction
235
volume = min(volume, 100)
236
volume = max(volume, 0)
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)
244
gobject.idle_add(self._poll_volume)
246
def ctrl_seek(self, direction):
248
if not self.__can_seek:
249
log.debug("seeking is currently not possible")
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)
256
log.debug("new progress: %d" % self.__progress_now)
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)
265
gobject.idle_add(self._poll_progress)
267
# =========================================================================
269
# =========================================================================
271
def action_files(self, action_id, files, uris):
273
if action_id == IA_APPEND.id or action_id == IA_APPEND_PLAY.id:
276
self._mp_t.AddTrack(uris[0], action_id == IA_APPEND_PLAY.id)
278
self._mp_t.AddTrack(uri, False)
279
except DBusException, e:
280
log.warning("dbus error: %s" % e)
284
log.error("** BUG ** unexpected action: %d" % action_id)
286
def action_playlist_item(self, action_id, positions, ids):
288
if action_id == IA_REMOVE.id:
293
for pos in positions:
294
self._mp_t.DelTrack(pos)
295
except DBusException, e:
296
log.warning("dbus error: %s" % e)
299
elif action_id == IA_JUMP.id:
301
self.__jump_to(positions[0])
304
log.error("** BUG ** unexpected action: %d" % action_id)
306
# =========================================================================
308
# =========================================================================
310
def request_playlist(self, client):
312
if not self.__can_tracklist:
313
self.reply_playlist_request(client, [], [])
316
tracks = self.__get_tracklist()
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)
328
self.reply_playlist_request(client, ids, names,
329
item_actions=self.__playlist_actions)
331
# =========================================================================
332
# internal methods (may be overridden by subclasses to fix MPRIS issues)
333
# =========================================================================
335
def _poll_status(self):
336
"""Poll player status information.
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.
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)
348
def _poll_volume(self):
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)
356
def _poll_progress(self):
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)
364
def _notify_track(self, track):
366
log.debug("track: %s" % str(track))
368
id, info = self.__track2info(track)
370
self.__progress_max = info.get(INFO_LENGTH, 0) # for update_progress()
372
img = track.get("arturl")
373
if not img or not img.startswith("file:"):
374
img = self.find_image(id)
376
self.update_item(id, info, img)
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)
384
def _notify_status(self, status):
386
log.debug("status: %s " % str(status))
388
if status[0] == STATUS_PLAYING:
389
self._playing = PLAYBACK_PLAY
390
elif status[0] == STATUS_PAUSED:
391
self._playing = PLAYBACK_PAUSE
393
self._playing = PLAYBACK_STOP
394
self.update_playback(self._playing)
396
self._shuffle = status[1] > 0 # remember for toggle_shuffle()
397
self.update_shuffle(self._shuffle)
399
self._repeat = status[2] > 0 or status[3] > 0 # for toggle_repeat()
400
self.update_repeat(self._repeat)
402
def _notify_tracklist_change(self, new_len):
404
log.debug("tracklist change")
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)
411
def _notify_position(self, position):
413
log.debug("tracklist pos: %d" % position)
415
self.update_position(position)
417
def _notify_volume(self, volume):
419
self.__volume = volume # remember for ctrl_volume()
420
self.update_volume(volume)
422
def _notify_progress(self, progress):
424
self.__progress_now = progress // 1000 # remember for ctrl_seek()
425
self.update_progress(self.__progress_now, self.__progress_max)
427
def _notify_caps(self, caps):
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
436
# =========================================================================
437
# internal methods (private)
438
# =========================================================================
440
def __get_tracklist(self):
441
"""Get a list of track dicts of all tracks in the tracklist."""
444
len = self._mp_t.GetLength()
445
except DBusException, e:
446
log.warning("dbus error: %s" % e)
453
for i in range(0, len):
455
tracks.append(self._mp_t.GetMetadata(i))
456
except DBusException, e:
457
log.warning("dbus error: %s" % e)
462
def __track2info(self, track):
463
"""Convert an MPRIS meta data dict to a Remuco info dict."""
465
id = track.get("location", "None")
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)
480
def __jump_to(self, position):
481
"""Jump to a position in the tracklist.
483
MPRIS has no such method and this a workaround. Unfortunately if
484
behaves not as expected on dynamic playlists.
487
tracks = self.__get_tracklist()
489
if position >= len(tracks):
493
for track in tracks[position:]:
494
uris.append(track.get("location", "there must be a location"))
496
positions = range(position, len(tracks))
498
self.action_playlist_item(IA_REMOVE.id, positions, uris)
500
self.action_files(IA_APPEND_PLAY.id, [], uris)
502
# =========================================================================
503
# dbus reply handler (may be reused by subclasses)
504
# =========================================================================
506
def _dbus_error(self, error):
507
""" DBus error handler."""
509
if self._mp_p is None:
510
return # do not log errors when not stopped already
512
log.warning("DBus error: %s" % error)
514
def _dbus_ignore(self):
515
""" DBus reply handler for methods without reply."""