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