1# Copyright (c) 2014-2021 Cedric Bellegarde <cedric.bellegarde@adishatz.org> 2# Copyright (c) 2016 Gaurav Narula 3# Copyright (c) 2016 Felipe Borges <felipeborges@gnome.org> 4# Copyright (c) 2013 Arnel A. Borja <kyoushuu@yahoo.com> 5# Copyright (c) 2013 Vadim Rutkovsky <vrutkovs@redhat.com> 6# This program is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17from gi.repository import Gio, Gst, GLib, Gtk 18 19from random import randint 20 21from lollypop.logger import Logger 22from lollypop.define import App, ArtSize, Repeat, Notifications 23from lollypop.objects_track import Track 24 25 26class Server: 27 28 def __init__(self, con, path): 29 method_outargs = {} 30 method_inargs = {} 31 for interface in Gio.DBusNodeInfo.new_for_xml(self.__doc__).interfaces: 32 33 for method in interface.methods: 34 method_outargs[method.name] = "(" + "".join( 35 [arg.signature for arg in method.out_args]) + ")" 36 method_inargs[method.name] = tuple( 37 arg.signature for arg in method.in_args) 38 39 con.register_object(object_path=path, 40 interface_info=interface, 41 method_call_closure=self.on_method_call) 42 43 self.method_inargs = method_inargs 44 self.method_outargs = method_outargs 45 46 def on_method_call(self, 47 connection, 48 sender, 49 object_path, 50 interface_name, 51 method_name, 52 parameters, 53 invocation): 54 55 args = list(parameters.unpack()) 56 for i, sig in enumerate(self.method_inargs[method_name]): 57 if sig == "h": 58 msg = invocation.get_message() 59 fd_list = msg.get_unix_fd_list() 60 args[i] = fd_list.get(args[i]) 61 62 try: 63 result = getattr(self, method_name)(*args) 64 65 # out_args is atleast (signature1). 66 # We therefore always wrap the result as a tuple. 67 # Refer to https://bugzilla.gnome.org/show_bug.cgi?id=765603 68 result = (result,) 69 70 out_args = self.method_outargs[method_name] 71 if out_args != "()": 72 variant = GLib.Variant(out_args, result) 73 invocation.return_value(variant) 74 else: 75 invocation.return_value(None) 76 except: 77 pass 78 79 80class MPRIS(Server): 81 """ 82 <!DOCTYPE node PUBLIC 83 "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" 84 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> 85 <node> 86 <interface name="org.freedesktop.DBus.Introspectable"> 87 <method name="Introspect"> 88 <arg name="data" direction="out" type="s"/> 89 </method> 90 </interface> 91 <interface name="org.freedesktop.DBus.Properties"> 92 <method name="Get"> 93 <arg name="interface" direction="in" type="s"/> 94 <arg name="property" direction="in" type="s"/> 95 <arg name="value" direction="out" type="v"/> 96 </method> 97 <method name="Set"> 98 <arg name="interface_name" direction="in" type="s"/> 99 <arg name="property_name" direction="in" type="s"/> 100 <arg name="value" direction="in" type="v"/> 101 </method> 102 <method name="GetAll"> 103 <arg name="interface" direction="in" type="s"/> 104 <arg name="properties" direction="out" type="a{sv}"/> 105 </method> 106 </interface> 107 <interface name="org.mpris.MediaPlayer2"> 108 <method name="Raise"> 109 </method> 110 <method name="Quit"> 111 </method> 112 <property name="CanQuit" type="b" access="read" /> 113 <property name="Fullscreen" type="b" access="readwrite" /> 114 <property name="CanRaise" type="b" access="read" /> 115 <property name="HasTrackList" type="b" access="read"/> 116 <property name="Identity" type="s" access="read"/> 117 <property name="DesktopEntry" type="s" access="read"/> 118 <property name="SupportedUriSchemes" type="as" access="read"/> 119 <property name="SupportedMimeTypes" type="as" access="read"/> 120 </interface> 121 <interface name="org.mpris.MediaPlayer2.Player"> 122 <method name="Next"/> 123 <method name="Previous"/> 124 <method name="Pause"/> 125 <method name="PlayPause"/> 126 <method name="Stop"/> 127 <method name="Play"/> 128 <method name="Seek"> 129 <arg direction="in" name="Offset" type="x"/> 130 </method> 131 <method name="SetPosition"> 132 <arg direction="in" name="TrackId" type="o"/> 133 <arg direction="in" name="Position" type="x"/> 134 </method> 135 <method name="OpenUri"> 136 <arg direction="in" name="Uri" type="s"/> 137 </method> 138 <signal name="Seeked"> 139 <arg name="Position" type="x"/> 140 </signal> 141 <property name="PlaybackStatus" type="s" access="read"/> 142 <property name="LoopStatus" type="s" access="readwrite"/> 143 <property name="Rate" type="d" access="readwrite"/> 144 <property name="Shuffle" type="b" access="readwrite"/> 145 <property name="Metadata" type="a{sv}" access="read"> 146 </property> 147 <property name="Volume" type="d" access="readwrite"/> 148 <property name="Position" type="x" access="read"/> 149 <property name="MinimumRate" type="d" access="read"/> 150 <property name="MaximumRate" type="d" access="read"/> 151 <property name="CanGoNext" type="b" access="read"/> 152 <property name="CanGoPrevious" type="b" access="read"/> 153 <property name="CanPlay" type="b" access="read"/> 154 <property name="CanPause" type="b" access="read"/> 155 <property name="CanSeek" type="b" access="read"/> 156 <property name="CanControl" type="b" access="read"/> 157 </interface> 158 <interface name="org.mpris.MediaPlayer2.ExtensionSetRatings"> 159 <method name="SetRating">\ 160 <arg direction="in" name="TrackId" type="o"/> 161 <arg direction="in" name="Rating" type="d"/>\ 162 </method>\ 163 <property name="HasRatingsExtension" type="b" access="read"/>\ 164 </interface> 165 </node> 166 """ 167 __MPRIS_IFACE = "org.mpris.MediaPlayer2" 168 __MPRIS_PLAYER_IFACE = "org.mpris.MediaPlayer2.Player" 169 __MPRIS_RATINGS_IFACE = "org.mpris.MediaPlayer2.ExtensionSetRatings" 170 __MPRIS_LOLLYPOP = "org.mpris.MediaPlayer2.Lollypop" 171 __MPRIS_PATH = "/org/mpris/MediaPlayer2" 172 173 def __init__(self, app): 174 self.__app = app 175 self.__rating = None 176 self.__lollypop_id = 0 177 self.__metadata = {"mpris:trackid": GLib.Variant( 178 "o", 179 "/org/mpris/MediaPlayer2/TrackList/NoTrack")} 180 self.__track_id = self.__get_media_id(0) 181 self.__bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) 182 Gio.bus_own_name_on_connection(self.__bus, 183 self.__MPRIS_LOLLYPOP, 184 Gio.BusNameOwnerFlags.NONE, 185 None, 186 None) 187 Server.__init__(self, self.__bus, self.__MPRIS_PATH) 188 App().player.connect("current-changed", self.__on_current_changed) 189 App().player.connect("seeked", self.__on_seeked) 190 App().player.connect("status-changed", self.__on_status_changed) 191 App().player.connect("volume-changed", self.__on_volume_changed) 192 App().player.connect("rate-changed", self.__on_rate_changed) 193 App().settings.connect("changed::shuffle", self.__on_shuffle_changed) 194 App().settings.connect("changed::repeat", self.__on_repeat_changed) 195 196 def Raise(self): 197 self.__app.window.present_with_time(Gtk.get_current_event_time()) 198 199 def Quit(self): 200 self.__app.quit() 201 202 def Next(self): 203 App().player.next() 204 if App().settings.get_enum("notifications") == Notifications.MPRIS: 205 App().notify.send_track(App().player.current_track) 206 207 def Previous(self): 208 App().player.prev() 209 if App().settings.get_enum("notifications") == Notifications.MPRIS: 210 App().notify.send_track(App().player.current_track) 211 212 def Pause(self): 213 App().player.pause() 214 215 def PlayPause(self): 216 App().player.play_pause() 217 218 def Stop(self): 219 App().player.stop() 220 221 def Play(self): 222 if App().player.current_track.id is None: 223 App().player.set_party(True) 224 else: 225 App().player.play() 226 227 def SetPosition(self, track_id, position): 228 App().player.seek(position / 1000) 229 230 def OpenUri(self, uri): 231 track_id = App().tracks.get_id_by_uri(uri) 232 if track_id: 233 App().player.load(Track(track_id)) 234 235 def Seek(self, offset): 236 position = App().player.position 237 App().player.seek(position + offset / 1000) 238 239 def Seeked(self, position): 240 self.__bus.emit_signal( 241 None, 242 self.__MPRIS_PATH, 243 self.__MPRIS_PLAYER_IFACE, 244 "Seeked", 245 GLib.Variant.new_tuple(GLib.Variant("x", position))) 246 247 def SetRating(self, track_id, rating): 248 # We don't currently care about the trackId since 249 # we have not yet implemented the TrackList interface. 250 App().player.current_track.set_rate(int(rating * 5)) 251 252 def Get(self, interface, property_name): 253 if property_name in ["CanQuit", "CanRaise", "CanSeek", 254 "CanControl", "HasRatingsExtension"]: 255 return GLib.Variant("b", True) 256 elif property_name == "HasTrackList": 257 return GLib.Variant("b", False) 258 elif property_name == "Shuffle": 259 return App().settings.get_value("shuffle") 260 elif property_name in ["Rate", "MinimumRate", "MaximumRate"]: 261 return GLib.Variant("d", 1.0) 262 elif property_name == "Identity": 263 return GLib.Variant("s", "Lollypop") 264 elif property_name == "DesktopEntry": 265 return GLib.Variant("s", "org.gnome.Lollypop") 266 elif property_name == "SupportedUriSchemes": 267 return GLib.Variant("as", ["file", "http"]) 268 elif property_name == "SupportedMimeTypes": 269 return GLib.Variant("as", ["application/ogg", 270 "audio/x-vorbis+ogg", 271 "audio/x-flac", 272 "audio/mpeg"]) 273 elif property_name == "PlaybackStatus": 274 return GLib.Variant("s", self.__get_status()) 275 elif property_name == "LoopStatus": 276 repeat = App().settings.get_enum("repeat") 277 if repeat == Repeat.ALL: 278 value = "Playlist" 279 elif repeat == Repeat.TRACK: 280 value = "Track" 281 else: 282 value = "None" 283 return GLib.Variant("s", value) 284 elif property_name == "Metadata": 285 return GLib.Variant("a{sv}", self.__metadata) 286 elif property_name == "Volume": 287 return GLib.Variant("d", App().player.volume) 288 elif property_name == "Position": 289 return GLib.Variant( 290 "x", 291 App().player.position * 1000) 292 elif property_name in ["CanGoNext", "CanGoPrevious", 293 "CanPlay", "CanPause"]: 294 return GLib.Variant("b", App().player.current_track.id is not None) 295 296 def GetAll(self, interface): 297 ret = {} 298 if interface == self.__MPRIS_IFACE: 299 for property_name in ["CanQuit", 300 "CanRaise", 301 "HasTrackList", 302 "Identity", 303 "DesktopEntry", 304 "SupportedUriSchemes", 305 "SupportedMimeTypes"]: 306 ret[property_name] = self.Get(interface, property_name) 307 elif interface == self.__MPRIS_PLAYER_IFACE: 308 for property_name in ["PlaybackStatus", 309 "LoopStatus", 310 "Rate", 311 "Shuffle", 312 "Metadata", 313 "Volume", 314 "Position", 315 "MinimumRate", 316 "MaximumRate", 317 "CanGoNext", 318 "CanGoPrevious", 319 "CanPlay", 320 "CanPause", 321 "CanSeek", 322 "CanControl"]: 323 ret[property_name] = self.Get(interface, property_name) 324 elif interface == self.__MPRIS_RATINGS_IFACE: 325 ret["HasRatingsExtension"] = GLib.Variant("b", True) 326 return ret 327 328 def Set(self, interface, property_name, new_value): 329 if property_name == "Volume": 330 App().player.set_volume(new_value) 331 elif property_name == "Shuffle": 332 App().settings.set_value("shuffle", GLib.Variant("b", new_value)) 333 elif property_name == "LoopStatus": 334 if new_value == "Playlist": 335 value = Repeat.ALL 336 elif new_value == "Track": 337 value = Repeat.TRACK 338 else: 339 value = Repeat.NONE 340 App().settings.set_enum("repeat", value) 341 342 def PropertiesChanged(self, interface_name, changed_properties, 343 invalidated_properties): 344 self.__bus.emit_signal(None, 345 self.__MPRIS_PATH, 346 "org.freedesktop.DBus.Properties", 347 "PropertiesChanged", 348 GLib.Variant.new_tuple( 349 GLib.Variant("s", interface_name), 350 GLib.Variant("a{sv}", changed_properties), 351 GLib.Variant("as", invalidated_properties))) 352 353 def Introspect(self): 354 return self.__doc__ 355 356####################### 357# PRIVATE # 358####################### 359 360 def __get_media_id(self, track_id): 361 """ 362 TrackId's must be unique even up to 363 the point that if you repeat a song 364 it must have a different TrackId. 365 """ 366 track_id = track_id + randint(10000000, 90000000) 367 return GLib.Variant("o", "/org/gnome/Lollypop/TrackId/%s" % track_id) 368 369 def __get_status(self): 370 state = App().player.get_status() 371 if state == Gst.State.PLAYING: 372 return "Playing" 373 elif state == Gst.State.PAUSED: 374 return "Paused" 375 else: 376 return "Stopped" 377 378 def __update_metadata(self): 379 self.__metadata = {} 380 if App().player.current_track.id is None or\ 381 self.__get_status() == "Stopped": 382 self.__metadata = {"mpris:trackid": GLib.Variant( 383 "o", 384 "/org/mpris/MediaPlayer2/TrackList/NoTrack")} 385 else: 386 self.__metadata["mpris:trackid"] = self.__track_id 387 track_number = App().player.current_track.number 388 if track_number is None: 389 track_number = 1 390 self.__metadata["xesam:trackNumber"] = GLib.Variant( 391 "i", 392 track_number) 393 self.__metadata["xesam:title"] = GLib.Variant( 394 "s", 395 App().player.current_track.name) 396 self.__metadata["xesam:album"] = GLib.Variant( 397 "s", 398 App().player.current_track.album.name) 399 self.__metadata["xesam:artist"] = GLib.Variant( 400 "as", 401 App().player.current_track.artists) 402 self.__metadata["xesam:albumArtist"] = GLib.Variant( 403 "as", 404 App().player.current_track.album_artists) 405 self.__metadata["mpris:length"] = GLib.Variant( 406 "x", 407 App().player.current_track.duration * 1000) 408 self.__metadata["xesam:genre"] = GLib.Variant( 409 "as", 410 App().player.current_track.genres) 411 self.__metadata["xesam:url"] = GLib.Variant( 412 "s", 413 App().player.current_track.uri) 414 self.__metadata["xesam:userRating"] = GLib.Variant( 415 "d", 416 App().player.current_track.rate / 5) 417 cover_path = App().album_art.get_cache_path( 418 App().player.current_track.album, 419 ArtSize.MPRIS, ArtSize.MPRIS) 420 if cover_path is not None: 421 self.__metadata["mpris:artUrl"] = GLib.Variant( 422 "s", 423 "file://" + cover_path) 424 425 def __on_seeked(self, player, position): 426 self.Seeked(position * 1000) 427 428 def __on_volume_changed(self, player, data=None): 429 self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, 430 {"Volume": GLib.Variant("d", 431 App().player.volume), }, 432 []) 433 434 def __on_shuffle_changed(self, settings, value): 435 properties = {"Shuffle": App().settings.get_value("shuffle")} 436 self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) 437 438 def __on_repeat_changed(self, settings, value): 439 repeat = App().settings.get_enum("repeat") 440 if repeat == Repeat.ALL: 441 value = "Playlist" 442 elif repeat == Repeat.TRACK: 443 value = "Track" 444 else: 445 value = "None" 446 properties = {"LoopStatus": GLib.Variant("s", value)} 447 self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) 448 449 def __on_rate_changed(self, player, rated_track_id, rating): 450 # We only care about the current Track's rating. 451 if rated_track_id == self.__lollypop_id: 452 self.__rating = rating 453 self.__update_metadata() 454 properties = {"Metadata": GLib.Variant("a{sv}", self.__metadata)} 455 self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) 456 457 def __on_current_changed(self, player): 458 if App().player.current_track.id is None: 459 self.__lollypop_id = 0 460 else: 461 self.__lollypop_id = App().player.current_track.id 462 # We only need to recalculate a new trackId at song changes. 463 self.__track_id = self.__get_media_id(self.__lollypop_id) 464 self.__rating = None 465 self.__update_metadata() 466 properties = {"Metadata": GLib.Variant("a{sv}", self.__metadata), 467 "CanPlay": GLib.Variant("b", True), 468 "CanPause": GLib.Variant("b", True), 469 "CanGoNext": GLib.Variant("b", True), 470 "CanGoPrevious": GLib.Variant("b", True)} 471 try: 472 self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) 473 except Exception as e: 474 Logger.error("MPRIS::__on_current_changed(): %s" % e) 475 476 def __on_status_changed(self, data=None): 477 properties = {"PlaybackStatus": GLib.Variant("s", self.__get_status())} 478 self.PropertiesChanged(self.__MPRIS_PLAYER_IFACE, properties, []) 479