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