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