1# Copyright 2010,2012 Christoph Reiter <reiter.christoph@gmail.com> 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7 8import time 9import tempfile 10 11import dbus 12import dbus.service 13from senf import fsn2uri 14 15from quodlibet import app 16from quodlibet.util.dbusutils import DBusIntrospectable, DBusProperty 17from quodlibet.util.dbusutils import dbus_unicode_validate as unival 18 19from .util import MPRISObject 20 21# TODO: OpenUri, CanXYZ 22# Date parsing (util?) 23 24 25# http://www.mpris.org/2.0/spec/ 26class MPRIS2(DBusProperty, DBusIntrospectable, MPRISObject): 27 28 BUS_NAME = "org.mpris.MediaPlayer2.quodlibet" 29 PATH = "/org/mpris/MediaPlayer2" 30 31 ROOT_IFACE = "org.mpris.MediaPlayer2" 32 33 ROOT_ISPEC = """ 34<method name="Raise"/> 35<method name="Quit"/>""" 36 37 ROOT_PROPS = """ 38<property name="CanQuit" type="b" access="read"/> 39<property name="CanRaise" type="b" access="read"/> 40<property name="CanSetFullscreen" type="b" access="read"/> 41<property name="HasTrackList" type="b" access="read"/> 42<property name="Identity" type="s" access="read"/> 43<property name="DesktopEntry" type="s" access="read"/> 44<property name="SupportedUriSchemes" type="as" access="read"/> 45<property name="SupportedMimeTypes" type="as" access="read"/>""" 46 47 PLAYER_IFACE = "org.mpris.MediaPlayer2.Player" 48 49 PLAYER_ISPEC = """ 50<method name="Next"/> 51<method name="Previous"/> 52<method name="Pause"/> 53<method name="PlayPause"/> 54<method name="Stop"/> 55<method name="Play"/> 56<method name="Seek"> 57 <arg direction="in" name="Offset" type="x"/> 58</method> 59<method name="SetPosition"> 60 <arg direction="in" name="TrackId" type="o"/> 61 <arg direction="in" name="Position" type="x"/> 62</method> 63<method name="OpenUri"> 64 <arg direction="in" name="Uri" type="s"/> 65</method> 66<signal name="Seeked"> 67 <arg name="Position" type="x"/> 68</signal>""" 69 70 PLAYER_PROPS = """ 71<property name="PlaybackStatus" type="s" access="read"/> 72<property name="LoopStatus" type="s" access="readwrite"/> 73<property name="Rate" type="d" access="readwrite"/> 74<property name="Shuffle" type="b" access="readwrite"/> 75<property name="Metadata" type="a{sv}" access="read"/> 76<property name="Volume" type="d" access="readwrite"/> 77<property name="Position" type="x" access="read"> 78 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" \ 79value="false"/> 80</property> 81<property name="MinimumRate" type="d" access="read"/> 82<property name="MaximumRate" type="d" access="read"/> 83<property name="CanGoNext" type="b" access="read"/> 84<property name="CanGoPrevious" type="b" access="read"/> 85<property name="CanPlay" type="b" access="read"/> 86<property name="CanPause" type="b" access="read"/> 87<property name="CanSeek" type="b" access="read"/> 88<property name="CanControl" type="b" access="read"> 89 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" \ 90value="false"/> 91</property>""" 92 93 def __init__(self): 94 DBusIntrospectable.__init__(self) 95 DBusProperty.__init__(self) 96 97 self.set_introspection(MPRIS2.ROOT_IFACE, MPRIS2.ROOT_ISPEC) 98 self.set_properties(MPRIS2.ROOT_IFACE, MPRIS2.ROOT_PROPS) 99 100 self.set_introspection(MPRIS2.PLAYER_IFACE, MPRIS2.PLAYER_ISPEC) 101 self.set_properties(MPRIS2.PLAYER_IFACE, MPRIS2.PLAYER_PROPS) 102 103 bus = dbus.SessionBus() 104 name = dbus.service.BusName(self.BUS_NAME, bus) 105 MPRISObject.__init__(self, bus, self.PATH, name) 106 107 self.__metadata = None 108 self.__cover = None 109 player_options = app.player_options 110 self.__repeat_id = player_options.connect( 111 "notify::repeat", self.__repeat_changed) 112 self.__random_id = player_options.connect( 113 "notify::shuffle", self.__shuffle_changed) 114 self.__single_id = player_options.connect( 115 "notify::single", self.__single_changed) 116 117 self.__lsig = app.librarian.connect("changed", self.__library_changed) 118 self.__vsig = app.player.connect("notify::volume", 119 self.__volume_changed) 120 self.__seek_sig = app.player.connect("seek", self.__seeked) 121 122 def remove_from_connection(self, *arg, **kwargs): 123 super(MPRIS2, self).remove_from_connection(*arg, **kwargs) 124 125 player_options = app.player_options 126 player_options.disconnect(self.__repeat_id) 127 player_options.disconnect(self.__random_id) 128 player_options.disconnect(self.__single_id) 129 app.librarian.disconnect(self.__lsig) 130 app.player.disconnect(self.__vsig) 131 app.player.disconnect(self.__seek_sig) 132 133 if self.__cover is not None: 134 self.__cover.close() 135 self.__cover = None 136 self.__invalidate_metadata() 137 138 def __volume_changed(self, *args): 139 self.emit_properties_changed(self.PLAYER_IFACE, ["Volume"]) 140 141 def __repeat_changed(self, *args): 142 self.emit_properties_changed(self.PLAYER_IFACE, ["LoopStatus"]) 143 144 def __shuffle_changed(self, *args): 145 self.emit_properties_changed(self.PLAYER_IFACE, ["Shuffle"]) 146 147 def __single_changed(self, *args): 148 self.emit_properties_changed(self.PLAYER_IFACE, ["LoopStatus"]) 149 150 def __seeked(self, player, song, ms): 151 self.Seeked(ms * 1000) 152 153 def __library_changed(self, library, songs): 154 self.__invalidate_metadata() 155 if not songs or app.player.info not in songs: 156 return 157 self.emit_properties_changed(self.PLAYER_IFACE, ["Metadata"]) 158 159 @dbus.service.method(ROOT_IFACE) 160 def Raise(self): 161 app.present() 162 163 @dbus.service.method(ROOT_IFACE) 164 def Quit(self): 165 app.quit() 166 167 @dbus.service.signal(PLAYER_IFACE, signature="x") 168 def Seeked(self, position): 169 pass 170 171 @dbus.service.method(PLAYER_IFACE) 172 def Next(self): 173 player = app.player 174 paused = player.paused 175 player.next() 176 player.paused = paused 177 178 @dbus.service.method(PLAYER_IFACE) 179 def Previous(self): 180 player = app.player 181 paused = player.paused 182 player.previous() 183 player.paused = paused 184 185 @dbus.service.method(PLAYER_IFACE) 186 def Pause(self): 187 app.player.paused = True 188 189 @dbus.service.method(PLAYER_IFACE) 190 def Play(self): 191 app.player.play() 192 193 @dbus.service.method(PLAYER_IFACE) 194 def PlayPause(self): 195 app.player.playpause() 196 197 @dbus.service.method(PLAYER_IFACE) 198 def Stop(self): 199 app.player.stop() 200 201 @dbus.service.method(PLAYER_IFACE, in_signature="x") 202 def Seek(self, offset): 203 new_pos = app.player.get_position() + offset / 1000 204 app.player.seek(new_pos) 205 206 @dbus.service.method(PLAYER_IFACE, in_signature="ox") 207 def SetPosition(self, track_id, position): 208 if track_id == self.__get_current_track_id(): 209 app.player.seek(position / 1000) 210 211 def paused(self): 212 self.emit_properties_changed(self.PLAYER_IFACE, ["PlaybackStatus"]) 213 unpaused = paused 214 215 def song_started(self, song): 216 self.__invalidate_metadata() 217 218 # so the position in clients gets updated faster 219 self.Seeked(0) 220 221 self.emit_properties_changed(self.PLAYER_IFACE, 222 ["PlaybackStatus", "Metadata"]) 223 224 def __get_current_track_id(self): 225 path = "/net/sacredchao/QuodLibet" 226 if not app.player.info: 227 return dbus.ObjectPath(path + "/" + "NoTrack") 228 return dbus.ObjectPath(path + "/" + str(id(app.player.info))) 229 230 def __invalidate_metadata(self): 231 self.__metadata = None 232 233 def __get_metadata(self): 234 if self.__metadata is None: 235 self.__metadata = self.__get_metadata_real() 236 assert self.__metadata is not None 237 return self.__metadata 238 239 def __get_metadata_real(self): 240 """ 241 https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ 242 """ 243 244 metadata = {} 245 metadata["mpris:trackid"] = self.__get_current_track_id() 246 247 def ignore_overflow(dbus_type, value): 248 try: 249 return dbus_type(value) 250 except OverflowError: 251 return 0 252 253 song = app.player.info 254 if not song: 255 return metadata 256 257 metadata["mpris:length"] = ignore_overflow( 258 dbus.Int64, song("~#length") * 10 ** 6) 259 260 if self.__cover is not None: 261 self.__cover.close() 262 self.__cover = None 263 264 cover = app.cover_manager.get_cover(song) 265 if cover: 266 is_temp = cover.name.startswith(tempfile.gettempdir()) 267 if is_temp: 268 self.__cover = cover 269 metadata["mpris:artUrl"] = fsn2uri(cover.name) 270 271 # All list values 272 list_val = {"artist": "artist", "albumArtist": "albumartist", 273 "comment": "comment", "composer": "composer", "genre": "genre", 274 "lyricist": "lyricist"} 275 for xesam, tag in list_val.items(): 276 vals = song.list(tag) 277 if vals: 278 metadata["xesam:" + xesam] = list(map(unival, vals)) 279 280 # All single values 281 sing_val = {"album": "album", "title": "title", "asText": "~lyrics"} 282 for xesam, tag in sing_val.items(): 283 vals = song.comma(tag) 284 if vals: 285 metadata["xesam:" + xesam] = unival(vals) 286 287 # URI 288 metadata["xesam:url"] = song("~uri") 289 290 # Integers 291 num_val = {"audioBPM": "bpm", "discNumber": "disc", 292 "trackNumber": "track", "useCount": "playcount"} 293 294 for xesam, tag in num_val.items(): 295 val = song("~#" + tag, None) 296 if val is not None: 297 metadata["xesam:" + xesam] = ignore_overflow(dbus.Int32, val) 298 299 # Rating 300 metadata["xesam:userRating"] = ignore_overflow( 301 dbus.Double, song("~#rating")) 302 303 # Dates 304 ISO_8601_format = "%Y-%m-%dT%H:%M:%S" 305 tuple_time = time.gmtime(song("~#lastplayed")) 306 iso_time = time.strftime(ISO_8601_format, tuple_time) 307 metadata["xesam:lastUsed"] = iso_time 308 309 year = song("~year") 310 if year: 311 try: 312 tuple_time = time.strptime(year, "%Y") 313 iso_time = time.strftime(ISO_8601_format, tuple_time) 314 except ValueError: 315 pass 316 else: 317 metadata["xesam:contentCreated"] = iso_time 318 319 return metadata 320 321 def set_property(self, interface, name, value): 322 player = app.player 323 player_options = app.player_options 324 325 if interface == self.PLAYER_IFACE: 326 if name == "LoopStatus": 327 if value == "Playlist": 328 player_options.repeat = True 329 player_options.single = False 330 elif value == "Track": 331 player_options.repeat = True 332 player_options.single = True 333 elif value == "None": 334 player_options.repeat = False 335 player_options.single = False 336 elif name == "Rate": 337 pass 338 elif name == "Shuffle": 339 player_options.shuffle = value 340 elif name == "Volume": 341 player.volume = value 342 343 def get_property(self, interface, name): 344 player = app.player 345 player_options = app.player_options 346 347 if interface == self.ROOT_IFACE: 348 if name == "CanQuit": 349 return True 350 elif name == "CanRaise": 351 return True 352 elif name == "CanSetFullscreen": 353 return False 354 elif name == "HasTrackList": 355 return False 356 elif name == "Identity": 357 return app.name 358 elif name == "DesktopEntry": 359 return "io.github.quodlibet.QuodLibet" 360 elif name == "SupportedUriSchemes": 361 # TODO: enable once OpenUri is done 362 can = lambda s: False 363 #can = lambda s: app.player.can_play_uri("%s://fake" % s) 364 schemes = ["http", "https", "ftp", "file", "mms"] 365 return filter(can, schemes) 366 elif name == "SupportedMimeTypes": 367 from quodlibet import formats 368 return formats.mimes 369 elif interface == self.PLAYER_IFACE: 370 if name == "PlaybackStatus": 371 if not player.song: 372 return "Stopped" 373 return ("Playing", "Paused")[int(player.paused)] 374 elif name == "LoopStatus": 375 if not player_options.repeat: 376 return "None" 377 else: 378 if player_options.single: 379 return "Track" 380 return "Playlist" 381 elif name == "Rate": 382 return 1.0 383 elif name == "Shuffle": 384 return player_options.shuffle 385 elif name == "Metadata": 386 return self.__get_metadata() 387 elif name == "Volume": 388 # https://gitlab.freedesktop.org/mpris/mpris-spec/issues/8 389 return player.volume 390 elif name == "Position": 391 return player.get_position() * 1000 392 elif name == "MinimumRate": 393 return 1.0 394 elif name == "MaximumRate": 395 return 1.0 396 elif name == "CanGoNext": 397 return True 398 elif name == "CanGoPrevious": 399 return True 400 elif name == "CanPlay": 401 return True 402 elif name == "CanPause": 403 return True 404 elif name == "CanSeek": 405 return True 406 elif name == "CanControl": 407 return True 408