1# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2# Copyright (C) 2011 Rick Spencer <rick.spencer@canonical.com> 3# Copyright (C) 2011-2012 Kevin Mehall <km@kevinmehall.net> 4# Copyright (C) 2017 Jason Gray <jasonlevigray3@gmail.com> 5# This program is free software: you can redistribute it and/or modify it 6# under the terms of the GNU General Public License version 3, as published 7# by the Free Software Foundation. 8# 9# This program is distributed in the hope that it will be useful, but 10# WITHOUT ANY WARRANTY; without even the implied warranties of 11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 12# PURPOSE. See the GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License along 15# with this program. If not, see <http://www.gnu.org/licenses/>. 16# 17# See <https://specifications.freedesktop.org/mpris-spec/latest/interfaces.html> 18# for documentation. 19 20import codecs 21import logging 22import math 23 24from gi.repository import ( 25 GLib, 26 Gio, 27 Gtk 28) 29from .dbus_util.DBusServiceObject import ( 30 DBusServiceObject, 31 dbus_method, 32 dbus_signal, 33 dbus_property 34) 35 36from pithos.plugin import PithosPlugin 37 38 39class MprisPlugin(PithosPlugin): 40 preference = 'enable_mpris' 41 description = 'Control with external programs' 42 43 def on_prepare(self): 44 if self.bus is None: 45 logging.debug('Failed to connect to DBus') 46 self.prepare_complete(error='Failed to connect to DBus') 47 else: 48 try: 49 self.mpris = PithosMprisService(self.window, connection=self.bus) 50 except Exception as e: 51 logging.warning('Failed to create DBus mpris service: {}'.format(e)) 52 self.prepare_complete(error='Failed to create DBus mpris service') 53 else: 54 self.preferences_dialog = MprisPluginPrefsDialog(self.window, self.settings) 55 self.prepare_complete() 56 57 def on_enable(self): 58 '''Enables the mpris plugin.''' 59 self.mpris.connect() 60 61 def on_disable(self): 62 '''Disables the mpris plugin.''' 63 self.mpris.disconnect() 64 65 66class PithosMprisService(DBusServiceObject): 67 MEDIA_PLAYER2_IFACE = 'org.mpris.MediaPlayer2' 68 MEDIA_PLAYER2_PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player' 69 MEDIA_PLAYER2_PLAYLISTS_IFACE = 'org.mpris.MediaPlayer2.Playlists' 70 MEDIA_PLAYER2_TRACKLIST_IFACE = 'org.mpris.MediaPlayer2.TrackList' 71 72 # As per https://lists.freedesktop.org/archives/mpris/2012q4/000054.html 73 # Secondary mpris interfaces to allow for options not allowed within the 74 # confines of the mrpis spec are a completely valid use case. 75 # This interface allows clients to love, ban, set tired and unrate songs. 76 MEDIA_PLAYER2_RATINGS_IFACE = 'org.mpris.MediaPlayer2.ExtensionPithosRatings' 77 78 TRACK_OBJ_PATH = '/io/github/Pithos/TrackId/' 79 NO_TRACK_OBJ_PATH = '/org/mpris/MediaPlayer2/TrackList/NoTrack' 80 PLAYLIST_OBJ_PATH = '/io/github/Pithos/PlaylistId/' 81 82 NO_TRACK_METADATA = { 83 'mpris:trackid': GLib.Variant('o', NO_TRACK_OBJ_PATH), 84 } 85 86 def __init__(self, window, **kwargs): 87 '''Creates a PithosMprisService object.''' 88 super().__init__(object_path='/org/mpris/MediaPlayer2', **kwargs) 89 self.window = window 90 91 def _reset(self): 92 '''Resets state to default.''' 93 self._has_thumbprint_radio = False 94 self._volume = math.pow(self.window.player.props.volume, 1.0 / 3.0) 95 self._metadata = self.NO_TRACK_METADATA 96 self._metadata_list = [self.NO_TRACK_METADATA] 97 self._tracks = [self.NO_TRACK_OBJ_PATH] 98 self._playback_status = 'Stopped' 99 self._playlists = [('/', '', '')] 100 self._current_playlist = False, ('/', '', '') 101 self._orderings = ['CreationDate'] 102 self._stations_dlg_handlers = [] 103 self._window_handlers = [] 104 self._stations_dlg_handlers = [] 105 self._volumechange_handler_id = None 106 self._sort_order_handler_id = None 107 108 def connect(self): 109 '''Takes ownership of the Pithos mpris Interfaces.''' 110 self._reset() 111 112 def on_name_acquired(connection, name): 113 logging.info('Got bus name: {}'.format(name)) 114 self._update_handlers() 115 self._connect_handlers() 116 117 self.bus_id = Gio.bus_own_name_on_connection( 118 self.connection, 119 'org.mpris.MediaPlayer2.io.github.Pithos', 120 Gio.BusNameOwnerFlags.NONE, 121 on_name_acquired, 122 None, 123 ) 124 125 def disconnect(self): 126 '''Disowns the Pithos mpris Interfaces.''' 127 self._disconnect_handlers() 128 if self.bus_id: 129 Gio.bus_unown_name(self.bus_id) 130 self.bus_id = 0 131 132 def _update_handlers(self): 133 '''Updates signal handlers.''' 134 # Update some of our dynamic props if mpris 135 # was enabled after a song has already started. 136 window = self.window 137 station = self.window.current_station 138 song = self.window.current_song 139 140 if station: 141 self._current_playlist_handler( 142 window, 143 station, 144 ) 145 146 self._update_playlists_handler( 147 window, 148 window.pandora.stations, 149 ) 150 151 if song: 152 self._songs_added_handler( 153 window, 154 4, 155 ) 156 157 self._metadatachange_handler( 158 window, 159 song, 160 ) 161 162 self._playstate_handler( 163 window, 164 window.playing, 165 ) 166 167 self._sort_order_handler() 168 169 def _connect_handlers(self): 170 '''Connects signal handlers.''' 171 window = self.window 172 self._window_handlers = [ 173 window.connect( 174 'metadata-changed', 175 self._metadatachange_handler, 176 ), 177 178 window.connect( 179 'play-state-changed', 180 self._playstate_handler, 181 ), 182 183 window.connect( 184 'buffering-finished', 185 lambda window, position: self.Seeked(position // 1000), 186 ), 187 188 window.connect( 189 'station-changed', 190 self._current_playlist_handler, 191 ), 192 193 window.connect( 194 'stations-processed', 195 self._update_playlists_handler, 196 ), 197 198 window.connect( 199 'stations-dlg-ready', 200 self._stations_dlg_ready_handler, 201 ), 202 203 window.connect( 204 'songs-added', 205 self._songs_added_handler, 206 ), 207 208 window.connect( 209 'station-added', 210 self._add_playlist_handler, 211 ), 212 ] 213 214 if window.stations_dlg: 215 # If stations_dlg exsists already 216 # we missed the ready signal and 217 # we should connect our handlers. 218 self._stations_dlg_ready_handler() 219 220 self._volumechange_handler_id = window.player.connect( 221 'notify::volume', 222 self._volumechange_handler, 223 ) 224 225 self._sort_order_handler_id = window.settings.connect( 226 'changed::sort-stations', 227 self._sort_order_handler, 228 ) 229 230 def _disconnect_handlers(self): 231 '''Disconnects signal handlers.''' 232 window = self.window 233 stations_dlg = self.window.stations_dlg 234 235 if self._window_handlers: 236 for handler in self._window_handlers: 237 window.disconnect(handler) 238 239 if stations_dlg and self._stations_dlg_handlers: 240 for handler in self._stations_dlg_handlers: 241 stations_dlg.disconnect(handler) 242 243 if self._volumechange_handler_id: 244 window.player.disconnect(self._volumechange_handler_id) 245 246 if self._sort_order_handler_id: 247 window.settings.disconnect(self._sort_order_handler_id) 248 249 def _stations_dlg_ready_handler(self, *ignore): 250 '''Connects stations dialog handlers.''' 251 stations_dlg = self.window.stations_dlg 252 self._stations_dlg_handlers = [ 253 stations_dlg.connect( 254 'station-renamed', 255 self._rename_playlist_handler, 256 ), 257 258 stations_dlg.connect( 259 'station-added', 260 self._add_playlist_handler, 261 ), 262 263 stations_dlg.connect( 264 'station-removed', 265 self._remove_playlist_handler, 266 ), 267 ] 268 269 def _sort_order_handler(self, *ignore): 270 '''Changes the Playlist Orderings Property based on the station popover sort order.''' 271 if self.window.settings['sort-stations']: 272 new_orderings = ['Alphabetical'] 273 else: 274 new_orderings = ['CreationDate'] 275 if self._orderings != new_orderings: 276 self._orderings = new_orderings 277 self.PropertiesChanged( 278 self.MEDIA_PLAYER2_PLAYLISTS_IFACE, 279 {'Orderings': GLib.Variant('as', self._orderings)}, 280 [], 281 ) 282 283 def _update_playlists_handler(self, window, stations): 284 '''Updates the Playlist Interface when stations are loaded/refreshed.''' 285 # The Thumbprint Radio Station may not exist if it does it will be the 2nd station. 286 self._has_thumbprint_radio = stations[1].isThumbprint 287 self._playlists = [(self.PLAYLIST_OBJ_PATH + station.id, station.name, '') for station in stations] 288 self.PropertiesChanged( 289 self.MEDIA_PLAYER2_PLAYLISTS_IFACE, 290 {'PlaylistCount': GLib.Variant('u', len(self._playlists))}, 291 [], 292 ) 293 294 def _remove_playlist_handler(self, window, station): 295 '''Removes a deleted station from the Playlist Interface.''' 296 for index, playlist in enumerate(self._playlists[:]): 297 if playlist[0].strip(self.PLAYLIST_OBJ_PATH) == station.id: 298 del self._playlists[index] 299 self.PropertiesChanged( 300 self.MEDIA_PLAYER2_PLAYLISTS_IFACE, 301 {'PlaylistCount': GLib.Variant('u', len(self._playlists))}, 302 [], 303 ) 304 break 305 306 def _rename_playlist_handler(self, stations_dlg, data): 307 '''Renames the corresponding Playlist when a station is renamed.''' 308 station_id, new_name = data 309 for index, playlist in enumerate(self._playlists): 310 if playlist[0].strip(self.PLAYLIST_OBJ_PATH) == station_id: 311 self._playlists[index] = (self.PLAYLIST_OBJ_PATH + station_id, new_name, '') 312 self.PlaylistChanged(self._playlists[index]) 313 break 314 315 def _add_playlist_handler(self, window, station): 316 '''Adds a new station to the Playlist Interface when it is created.''' 317 new_playlist = (self.PLAYLIST_OBJ_PATH + station.id, station.name, '') 318 if new_playlist not in self._playlists: 319 if self._has_thumbprint_radio: 320 self._playlists.insert(2, new_playlist) 321 else: 322 self._playlists.insert(1, new_playlist) 323 self.PropertiesChanged( 324 self.MEDIA_PLAYER2_PLAYLISTS_IFACE, 325 {'PlaylistCount': GLib.Variant('u', len(self._playlists))}, 326 [], 327 ) 328 329 def _current_playlist_handler(self, window, station): 330 '''Sets the ActivePlaylist Property to the current station.''' 331 new_current_playlist = (self.PLAYLIST_OBJ_PATH + station.id, station.name, '') 332 if self._current_playlist != (True, new_current_playlist): 333 self._current_playlist = (True, new_current_playlist) 334 self.PropertiesChanged( 335 self.MEDIA_PLAYER2_PLAYLISTS_IFACE, 336 {'ActivePlaylist': GLib.Variant('(b(oss))', self._current_playlist)}, 337 [], 338 ) 339 340 def _playstate_handler(self, window, state): 341 '''Updates the mpris PlaybackStatus Property.''' 342 play_state = 'Playing' if state else 'Paused' 343 344 if self._playback_status != play_state: # stops unneeded updates 345 self._playback_status = play_state 346 self.PropertiesChanged( 347 self.MEDIA_PLAYER2_PLAYER_IFACE, 348 {'PlaybackStatus': GLib.Variant('s', self._playback_status)}, 349 [], 350 ) 351 352 def _volumechange_handler(self, player, spec): 353 '''Updates the mpris Volume Property.''' 354 volume = math.pow(player.props.volume, 1.0 / 3.0) 355 356 if self._volume != volume: # stops unneeded updates 357 self._volume = volume 358 self.PropertiesChanged( 359 self.MEDIA_PLAYER2_PLAYER_IFACE, 360 {'Volume': GLib.Variant('d', self._volume)}, 361 [], 362 ) 363 364 def _songs_added_handler(self, window, song_count): 365 '''Adds songs to the TrackList Interface.''' 366 songs_model = window.songs_model 367 stop = len(songs_model) 368 start = max(0, stop - (song_count + 1)) 369 songs = [songs_model[i][0] for i in range(start, stop)] 370 self._tracks = [self._track_id_from_song(song) for song in songs] 371 self._metadata_list = [self._get_metadata(window, song) for song in songs] 372 self.TrackListReplaced(self._tracks, self._tracks[0]) 373 374 def _metadatachange_handler(self, window, song): 375 '''Updates the metadata for the Player and TrackList Interfaces.''' 376 # Ignore songs that have no chance of being in our Tracks list. 377 if song.index < max(0, len(window.songs_model) - 5): 378 return 379 metadata = self._get_metadata(window, song) 380 trackId = self._track_id_from_song(song) 381 if trackId in self._tracks: 382 for index, track_id in enumerate(self._tracks): 383 if track_id == trackId and not self._metadata_equal(self._metadata_list[index], metadata): 384 self._metadata_list[index] = metadata 385 self.TrackMetadataChanged(trackId, metadata) 386 break 387 # No need to update the current metadata if the current song has been banned 388 # or set tired as it will be skipped anyway very shortly. 389 if (song is window.current_song and not (song.tired or song.rating == 'ban') and 390 not self._metadata_equal(self._metadata, metadata)): 391 self._metadata = metadata 392 self.PropertiesChanged( 393 self.MEDIA_PLAYER2_PLAYER_IFACE, 394 {'Metadata': GLib.Variant('a{sv}', self._metadata)}, 395 [], 396 ) 397 398 def _get_metadata(self, window, song): 399 '''Generates metadata for a song.''' 400 # Map pithos ratings to something MPRIS understands 401 userRating = 1.0 if song.rating == 'love' else 0.0 402 duration = song.get_duration_sec() * 1000000 403 pithos_rating = window.song_icon(song) or '' 404 trackid = self._track_id_from_song(song) 405 406 metadata = { 407 'mpris:trackid': GLib.Variant('o', trackid), 408 'xesam:title': GLib.Variant('s', song.title or 'Title Unknown'), 409 'xesam:artist': GLib.Variant('as', [song.artist] or ['Artist Unknown']), 410 'xesam:album': GLib.Variant('s', song.album or 'Album Unknown'), 411 'xesam:userRating': GLib.Variant('d', userRating), 412 'xesam:url': GLib.Variant('s', song.audioUrl), 413 'mpris:length': GLib.Variant('x', duration), 414 'pithos:rating': GLib.Variant('s', pithos_rating), 415 } 416 417 # If we don't have an artUrl the best thing we can 418 # do is not even have 'mpris:artUrl' in the metadata, 419 # and let the applet decide what to do. 420 if song.artUrl is not None: 421 metadata['mpris:artUrl'] = GLib.Variant('s', song.artUrl) 422 423 return metadata 424 425 def _metadata_equal(self, m1, m2): 426 # Test to see if 2 sets of metadata are the same 427 # to avoid unneeded updates. 428 if len(m1) != len(m2): 429 return False 430 for key in m1.keys(): 431 if not m1[key].equal(m2[key]): 432 return False 433 return True 434 435 def _song_from_track_id(self, TrackId): 436 '''Convenience method that takes a TrackId and returns the corresponding song object.''' 437 if TrackId not in self._tracks: 438 return 439 if self.window.current_song_index is None: 440 return 441 songs_model = self.window.songs_model 442 stop = len(songs_model) 443 start = max(0, stop - 5) 444 for i in range(start, stop): 445 song = songs_model[i][0] 446 if TrackId == self._track_id_from_song(song): 447 return song 448 449 def _track_id_from_song(self, song): 450 '''Convenience method that generates a TrackId based on a song.''' 451 return self.TRACK_OBJ_PATH + codecs.encode(bytes(song.trackToken, 'ascii'), 'hex').decode('ascii') 452 453 @dbus_property(MEDIA_PLAYER2_IFACE, signature='b') 454 def CanQuit(self): 455 '''b Read only Interface MediaPlayer2''' 456 return True 457 458 @dbus_property(MEDIA_PLAYER2_IFACE, signature='b') 459 def Fullscreen(self): 460 '''b Read/Write (optional) Interface MediaPlayer2''' 461 return False 462 463 @Fullscreen.setter 464 def Fullscreen(self, Fullscreen): 465 '''Not Implemented''' 466 # Spec says the Fullscreen property should be read/write so we 467 # include this dummy setter for applets that might wrongly ignore 468 # the CanSetFullscreen property and try to set the Fullscreen 469 # property anyway. 470 pass 471 472 @dbus_property(MEDIA_PLAYER2_IFACE, signature='b') 473 def CanSetFullscreen(self): 474 '''b Read only (optional) Interface MediaPlayer2''' 475 return False 476 477 @dbus_property(MEDIA_PLAYER2_IFACE, signature='b') 478 def CanRaise(self): 479 '''b Read only Interface MediaPlayer2''' 480 return True 481 482 @dbus_property(MEDIA_PLAYER2_IFACE, signature='b') 483 def HasTrackList(self): 484 '''b Read only Interface MediaPlayer2''' 485 return True 486 487 @dbus_property(MEDIA_PLAYER2_IFACE, signature='s') 488 def Identity(self): 489 '''s Read only Interface MediaPlayer2''' 490 return 'Pithos' 491 492 @dbus_property(MEDIA_PLAYER2_IFACE, signature='s') 493 def DesktopEntry(self): 494 '''s Read only (optional) Interface MediaPlayer2''' 495 return 'io.github.Pithos' 496 497 @dbus_property(MEDIA_PLAYER2_IFACE, signature='as') 498 def SupportedUriSchemes(self): 499 '''as Read only Interface MediaPlayer2''' 500 return [] 501 502 @dbus_property(MEDIA_PLAYER2_IFACE, signature='as') 503 def SupportedMimeTypes(self): 504 '''as Read only Interface MediaPlayer2''' 505 return [] 506 507 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='s') 508 def PlaybackStatus(self): 509 '''s Read only Interface MediaPlayer2.Player''' 510 return self._playback_status 511 512 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='s') 513 def LoopStatus(self): 514 '''s Read/Write only (optional) Interface MediaPlayer2.Player''' 515 return 'None' 516 517 @LoopStatus.setter 518 def LoopStatus(self, LoopStatus): 519 '''Not Implemented''' 520 # There is no way to tell clients this property can't be set. 521 pass 522 523 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 524 def Shuffle(self): 525 '''b Read/Write (optional) Interface MediaPlayer2.Player''' 526 return False 527 528 @Shuffle.setter 529 def Shuffle(self, Shuffle): 530 '''Not Implemented''' 531 # There is no way to tell clients this property can't be set. 532 pass 533 534 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='d') 535 def Rate(self): 536 '''d Read/Write Interface MediaPlayer2.Player''' 537 return 1.0 538 539 @Rate.setter 540 def Rate(self, Rate): 541 '''Not Implemented''' 542 # There is no way to tell clients this property can't be set. 543 pass 544 545 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='a{sv}') 546 def Metadata(self): 547 '''a{sv} Read only Interface MediaPlayer2.Player''' 548 return self._metadata 549 550 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='d') 551 def Volume(self): 552 '''d Read/Write Interface MediaPlayer2.Player''' 553 volume = self.window.player.get_property('volume') 554 scaled_volume = math.pow(volume, 1.0 / 3.0) 555 return scaled_volume 556 557 @Volume.setter 558 def Volume(self, new_volume): 559 scaled_vol = math.pow(new_volume, 3.0 / 1.0) 560 self.window.player.set_property('volume', scaled_vol) 561 562 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='x') 563 def Position(self): 564 '''x Read only Interface MediaPlayer2.Player''' 565 position = self.window.query_position() 566 if position is not None: 567 return position // 1000 568 else: 569 return 0 570 571 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='d') 572 def MinimumRate(self): 573 '''d Read only Interface MediaPlayer2.Player''' 574 return 1.0 575 576 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='d') 577 def MaximumRate(self): 578 '''d Read only Interface MediaPlayer2.Player''' 579 return 1.0 580 581 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 582 def CanGoNext(self): 583 '''b Read only Interface MediaPlayer2.Player''' 584 return True 585 586 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 587 def CanGoPrevious(self): 588 '''b Read only Interface MediaPlayer2.Player''' 589 return False 590 591 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 592 def CanPlay(self): 593 '''b Read only Interface MediaPlayer2.Player''' 594 return True 595 596 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 597 def CanPause(self): 598 '''b Read only Interface MediaPlayer2.Player''' 599 return True 600 601 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 602 def CanSeek(self): 603 '''b Read only Interface MediaPlayer2.Player''' 604 return False 605 606 @dbus_property(MEDIA_PLAYER2_PLAYER_IFACE, signature='b') 607 def CanControl(self): 608 '''b Read only Interface MediaPlayer2.Player''' 609 return True 610 611 @dbus_property(MEDIA_PLAYER2_PLAYLISTS_IFACE, signature='(b(oss))') 612 def ActivePlaylist(self): 613 '''(b(oss)) Read only Interface MediaPlayer2.Playlists''' 614 return self._current_playlist 615 616 @dbus_property(MEDIA_PLAYER2_PLAYLISTS_IFACE, signature='u') 617 def PlaylistCount(self): 618 '''u Read only Interface MediaPlayer2.Playlists''' 619 return len(self._playlists) 620 621 @dbus_property(MEDIA_PLAYER2_PLAYLISTS_IFACE, signature='as') 622 def Orderings(self): 623 '''as Read only Interface MediaPlayer2.Playlists''' 624 return self._orderings 625 626 @dbus_property(MEDIA_PLAYER2_TRACKLIST_IFACE, signature='ao') 627 def Tracks(self): 628 '''ao Read only Interface MediaPlayer2.TrackList''' 629 return self._tracks 630 631 @dbus_property(MEDIA_PLAYER2_TRACKLIST_IFACE, signature='b') 632 def CanEditTracks(self): 633 '''b Read only Interface MediaPlayer2.TrackList''' 634 return False 635 636 @dbus_property(MEDIA_PLAYER2_RATINGS_IFACE, signature='b') 637 def HasPithosExtension(self): 638 '''b Read only Interface MediaPlayer2.ExtensionPithosRatings''' 639 # This property exists so that applets can check it to make sure 640 # the MediaPlayer2.ExtensionPithosRatings interface actually exists. 641 # It's much more convenient for them then wrapping all their 642 # ratings code in the equivalent of a try except block. 643 # Not all versions of Pithos will have this interface. 644 # It serves a similar function as HasTrackList. 645 return True 646 647 @dbus_method(MEDIA_PLAYER2_IFACE) 648 def Raise(self): 649 '''() -> nothing Interface MediaPlayer2''' 650 self.window.bring_to_top() 651 652 @dbus_method(MEDIA_PLAYER2_IFACE) 653 def Quit(self): 654 '''() -> nothing Interface MediaPlayer2''' 655 self.window.quit() 656 657 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE) 658 def Previous(self): 659 '''Not Implemented''' 660 pass 661 662 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE) 663 def Next(self): 664 '''() -> nothing Interface MediaPlayer2.Player''' 665 if not self.window.waiting_for_playlist: 666 self.window.next_song() 667 668 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE) 669 def PlayPause(self): 670 '''() -> nothing Interface MediaPlayer2.Player''' 671 if self.window.current_song: 672 self.window.playpause() 673 674 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE) 675 def Play(self): 676 '''() -> nothing Interface MediaPlayer2.Player''' 677 if self.window.current_song: 678 self.window.play() 679 680 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE) 681 def Pause(self): 682 '''() -> nothing Interface MediaPlayer2.Player''' 683 if self.window.current_song: 684 self.window.pause() 685 686 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE) 687 def Stop(self): 688 '''Stop is only used internally, mapping to pause instead.''' 689 if self.window.current_song: 690 self.window.pause() 691 692 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE, in_signature='x') 693 def Seek(self, Offset): 694 '''Not Implemented''' 695 pass 696 697 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE, in_signature='s') 698 def OpenUri(self, Uri): 699 '''Not Implemented''' 700 pass 701 702 @dbus_method(MEDIA_PLAYER2_PLAYER_IFACE, in_signature='ox') 703 def SetPosition(self, TrackId, Position): 704 '''Not Implemented''' 705 pass 706 707 @dbus_method(MEDIA_PLAYER2_PLAYLISTS_IFACE, in_signature='uusb', out_signature='a(oss)') 708 def GetPlaylists(self, Index, MaxCount, Order, ReverseOrder): 709 '''(uusb) -> a(oss) Interface MediaPlayer2.Playlists''' 710 playlists = self._playlists[:] 711 always_first = [playlists.pop(0)] # the QuickMix 712 if self._has_thumbprint_radio: 713 always_first.append(playlists.pop(0)) # Thumbprint Radio if it exists 714 715 if Order not in ('CreationDate', 'Alphabetical') or Order == 'Alphabetical': 716 playlists = sorted(playlists, key=lambda playlists: playlists[1]) 717 if ReverseOrder: 718 playlists.reverse() 719 playlists = always_first + playlists[Index:MaxCount - len(always_first)] 720 return playlists 721 722 @dbus_method(MEDIA_PLAYER2_PLAYLISTS_IFACE, in_signature='o') 723 def ActivatePlaylist(self, PlaylistId): 724 '''(o) -> nothing Interface MediaPlayer2.Playlists''' 725 stations = self.window.pandora.stations 726 station_id = PlaylistId.strip(self.PLAYLIST_OBJ_PATH) 727 for station in stations: 728 if station.id == station_id: 729 self.window.station_changed(station) 730 break 731 732 @dbus_method(MEDIA_PLAYER2_TRACKLIST_IFACE, in_signature='ao', out_signature='aa{sv}') 733 def GetTracksMetadata(self, TrackIds): 734 '''(ao) -> aa{sv} Interface MediaPlayer2.TrackList''' 735 return [self._metadata_list[self._tracks.index(TrackId)] for TrackId in TrackIds if TrackId in self._tracks] 736 737 @dbus_method(MEDIA_PLAYER2_TRACKLIST_IFACE, in_signature='sob') 738 def AddTrack(self, Uri, AfterTrack, SetAsCurrent): 739 '''Not Implemented''' 740 pass 741 742 @dbus_method(MEDIA_PLAYER2_TRACKLIST_IFACE, in_signature='o') 743 def RemoveTrack(self, TrackId): 744 '''Not Implemented''' 745 pass 746 747 @dbus_method(MEDIA_PLAYER2_TRACKLIST_IFACE, in_signature='o') 748 def GoTo(self, TrackId): 749 '''(o) -> nothing Interface MediaPlayer2.TrackList''' 750 song = self._song_from_track_id(TrackId) 751 if song and song.index > self.window.current_song_index and not (song.tired or song.rating == 'ban'): 752 self.window.start_song(song.index) 753 754 @dbus_method(MEDIA_PLAYER2_RATINGS_IFACE, in_signature='o') 755 def LoveSong(self, TrackId): 756 '''(o) -> nothing Interface MediaPlayer2.ExtensionPithosRatings''' 757 song = self._song_from_track_id(TrackId) 758 if song: 759 self.window.love_song(song=song) 760 761 @dbus_method(MEDIA_PLAYER2_RATINGS_IFACE, in_signature='o') 762 def BanSong(self, TrackId): 763 '''(o) -> nothing Interface MediaPlayer2.ExtensionPithosRatings''' 764 song = self._song_from_track_id(TrackId) 765 if song: 766 self.window.ban_song(song=song) 767 768 @dbus_method(MEDIA_PLAYER2_RATINGS_IFACE, in_signature='o') 769 def TiredSong(self, TrackId): 770 '''(o) -> nothing Interface MediaPlayer2.ExtensionPithosRatings''' 771 song = self._song_from_track_id(TrackId) 772 if song: 773 self.window.tired_song(song=song) 774 775 @dbus_method(MEDIA_PLAYER2_RATINGS_IFACE, in_signature='o') 776 def UnRateSong(self, TrackId): 777 '''(o) -> nothing Interface MediaPlayer2.ExtensionPithosRatings''' 778 song = self._song_from_track_id(TrackId) 779 if song: 780 self.window.unrate_song(song=song) 781 782 @dbus_signal(MEDIA_PLAYER2_PLAYER_IFACE, signature='x') 783 def Seeked(self, Position): 784 '''x Interface MediaPlayer2.Player''' 785 # Unsupported, but some applets depend on this. 786 pass 787 788 @dbus_signal(MEDIA_PLAYER2_PLAYLISTS_IFACE, signature='(oss)') 789 def PlaylistChanged(self, Playlist): 790 '''(oss) Interface MediaPlayer2.Playlists''' 791 pass 792 793 @dbus_signal(MEDIA_PLAYER2_TRACKLIST_IFACE, signature='aoo') 794 def TrackListReplaced(self, Tracks, CurrentTrack): 795 '''aoo Interface MediaPlayer2.TrackList''' 796 pass 797 798 @dbus_signal(MEDIA_PLAYER2_TRACKLIST_IFACE, signature='a{sv}o') 799 def TrackAdded(self, Metadata, AfterTrack): 800 '''a{sv}o Interface MediaPlayer2.TrackList''' 801 pass 802 803 @dbus_signal(MEDIA_PLAYER2_TRACKLIST_IFACE, signature='o') 804 def TrackRemoved(self, TrackId): 805 '''o Interface MediaPlayer2.TrackList''' 806 pass 807 808 @dbus_signal(MEDIA_PLAYER2_TRACKLIST_IFACE, signature='oa{sv}') 809 def TrackMetadataChanged(self, TrackId, Metadata): 810 '''oa{sv} Interface MediaPlayer2.TrackList''' 811 pass 812 813 def PropertiesChanged(self, interface, changed, invalidated): 814 '''Emits mpris Property changes.''' 815 try: 816 self.connection.emit_signal(None, '/org/mpris/MediaPlayer2', 817 'org.freedesktop.DBus.Properties', 818 'PropertiesChanged', 819 GLib.Variant.new_tuple( 820 GLib.Variant('s', interface), 821 GLib.Variant('a{sv}', changed), 822 GLib.Variant('as', invalidated) 823 )) 824 except GLib.Error as e: 825 logging.warning(e) 826 827 828class MprisPluginPrefsDialog(Gtk.Dialog): 829 __gtype_name__ = 'MprisPluginPrefsDialog' 830 831 def __init__(self, window, settings): 832 super().__init__(use_header_bar=1) 833 self.set_title(_('Hide on Close')) 834 self.set_default_size(300, -1) 835 self.set_resizable(False) 836 self.connect('delete-event', self.on_close) 837 838 self.pithos = window 839 self.settings = settings 840 self.delete_handler = None 841 842 box = Gtk.Box() 843 label = Gtk.Label() 844 label.set_markup('<b>{}</b>\n{}'.format(_('Hide Pithos on Close'), _('Instead of Quitting'))) 845 label.set_halign(Gtk.Align.START) 846 box.pack_start(label, True, True, 4) 847 848 self.switch = Gtk.Switch() 849 self.switch.connect('notify::active', self.on_activated) 850 self.switch.set_active(self.settings['data'] == 'True') 851 self.settings.connect('changed::enabled', self._on_plugin_enabled) 852 self.switch.set_halign(Gtk.Align.END) 853 self.switch.set_valign(Gtk.Align.CENTER) 854 box.pack_end(self.switch, False, False, 2) 855 856 content_area = self.get_content_area() 857 content_area.add(box) 858 content_area.show_all() 859 860 def on_close(self, window, event): 861 window.hide() 862 return True 863 864 def on_activated(self, *ignore): 865 if self.switch.get_active(): 866 self.settings['data'] = 'True' 867 self._enable_hide_on_delete() 868 else: 869 self.settings['data'] = 'False' 870 self._disable_hide_on_delete() 871 872 def _on_plugin_enabled(self, *ignore): 873 if self.settings['enabled']: 874 self.switch.set_active(self.settings['data'] == 'True') 875 if self.switch.get_active(): 876 self._enable_hide_on_delete() 877 else: 878 self._disable_hide_on_delete() 879 else: 880 self._disable_hide_on_delete() 881 882 def _disable_hide_on_delete(self): 883 if self.delete_handler: 884 self.pithos.disconnect(self.delete_handler) 885 self.delete_handler = self.pithos.connect('delete-event', self.pithos.on_destroy) 886 887 def _enable_hide_on_delete(self): 888 if self.delete_handler: 889 self.pithos.disconnect(self.delete_handler) 890 self.delete_handler = self.pithos.connect('delete-event', self.on_close) 891