1# Copyright 2006 Federico Pelloni <federico.pelloni@gmail.com>
2#           2013 Christoph Reiter
3#           2017 Nick Boultbee
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10from gi.repository import GLib
11from gi.repository import Gio
12
13from quodlibet.util import dbusutils
14from quodlibet.query import Query
15from quodlibet.qltk.songlist import SongList
16from quodlibet.formats import decode_value
17
18
19class DBusHandler:
20    """
21    <!DOCTYPE node PUBLIC
22      "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
23      "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
24    <node name="/net/sacredchao/QuodLibet">
25      <interface name="org.freedesktop.DBus.Introspectable">
26        <method name="Introspect">
27          <arg direction="out" type="s" />
28        </method>
29      </interface>
30      <interface name="net.sacredchao.QuodLibet">
31        <signal name="SongStarted">
32          <arg type="v" name="song" />
33        </signal>
34        <signal name="SongEnded">
35          <arg type="v" name="song" />
36          <arg type="v" name="skipped" />
37        </signal>
38        <signal name="Paused"></signal>
39        <signal name="Unpaused"></signal>
40        <method name="GetPosition">
41          <arg direction="out" type="u" />
42        </method>
43        <method name="IsPlaying">
44          <arg direction="out" type="b" />
45        </method>
46        <method name="CurrentSong">
47          <arg direction="out" type="a{ss}" />
48        </method>
49        <method name="Next"></method>
50        <method name="Previous"></method>
51        <method name="Pause"></method>
52        <method name="Play"></method>
53        <method name="PlayPause">
54          <arg direction="out" type="b" />
55        </method>
56        <method name="Query">
57          <arg direction="in"  type="s" name="text" />
58          <arg direction="out" type="aa{ss}" />
59        </method>
60      </interface>
61    </node>
62    """
63
64    BUS_NAME = 'net.sacredchao.QuodLibet'
65    PATH = '/net/sacredchao/QuodLibet'
66
67    def __init__(self, player, library):
68        try:
69            self._registered_ids = []
70            self._method_outargs = {}
71            self.library = library
72            self.conn = Gio.bus_get_sync(Gio.BusType.SESSION, None)
73            Gio.bus_own_name_on_connection(self.conn, self.BUS_NAME,
74                                           Gio.BusNameOwnerFlags.NONE,
75                                           None, self.__on_name_lost)
76            self.__register_ifaces()
77        except GLib.Error:
78            pass
79        else:
80            player.connect('song-started', self.__song_started)
81            player.connect('song-ended', self.__song_ended)
82            player.connect('paused', lambda player: self.Paused())
83            player.connect('unpaused', lambda player: self.Unpaused())
84            self._player = player
85
86    def __on_name_lost(self, connection, name):
87        for _id in self._registered_ids:
88            connection.unregister_object(_id)
89
90    def __register_ifaces(self):
91        info = Gio.DBusNodeInfo.new_for_xml(self.__doc__)
92        for interface in info.interfaces:
93            for method in interface.methods:
94                self._method_outargs[method.name] = '({})'.format(
95                    ''.join([arg.signature for arg in method.out_args]))
96
97            _id = self.conn.register_object(
98                object_path=self.PATH,
99                interface_info=interface,
100                method_call_closure=self.__on_method_call)
101            self._registered_ids.append(_id)
102
103    def __on_method_call(self, connection, sender, object_path, interface_name,
104                         method_name, parameters, invocation):
105        args = list(parameters.unpack())
106        result = getattr(self, method_name)(*args)
107        if not isinstance(result, tuple):
108            result = (result,)
109
110        out_args = self._method_outargs[method_name]
111        if out_args != '()':
112            variant = GLib.Variant(out_args, result)
113            invocation.return_value(variant)
114        else:
115            invocation.return_value(None)
116
117    @staticmethod
118    def __dict(song):
119        dict = {}
120        for key, value in (song or {}).items():
121            value = decode_value(key, value)
122            dict[key] = dbusutils.dbus_unicode_validate(value)
123        if song:
124            dict["~uri"] = song("~uri")
125        return dict
126
127    def __song_started(self, player, song):
128        if song is not None:
129            song = self.__dict(song)
130            self.SongStarted(song)
131
132    def __song_ended(self, player, song, skipped):
133        if song is not None:
134            song = self.__dict(song)
135            self.SongEnded(song, skipped)
136
137    def Introspect(self):
138        return self.__doc__
139
140    def SongStarted(self, song):
141        self.conn.emit_signal(None, self.PATH, 'net.sacredchao.QuodLibet',
142                              'SongStarted', GLib.Variant('(a{ss})', (song,)))
143
144    def SongEnded(self, song, skipped):
145        self.conn.emit_signal(None, self.PATH, 'net.sacredchao.QuodLibet',
146                              'SongEnded', GLib.Variant('(a{ss}b)',
147                                                        (song, skipped)))
148
149    def Paused(self):
150        self.conn.emit_signal(None, self.PATH, 'net.sacredchao.QuodLibet',
151                              'Paused', None)
152
153    def Unpaused(self):
154        self.conn.emit_signal(None, self.PATH, 'net.sacredchao.QuodLibet',
155                              'Unpaused', None)
156
157    def GetPosition(self):
158        return self._player.get_position()
159
160    def IsPlaying(self):
161        return not self._player.paused
162
163    def CurrentSong(self):
164        return self.__dict(self._player.song)
165
166    def Next(self):
167        self._player.next()
168
169    def Previous(self):
170        self._player.previous()
171
172    def Pause(self):
173        self._player.paused = True
174
175    def Play(self):
176        self._player.play()
177
178    def PlayPause(self):
179        self._player.playpause()
180        return self._player.paused
181
182    def Query(self, text):
183        if text is not None:
184            query = Query(text, star=SongList.star)
185            if query.is_parsable:
186                return [self.__dict(s) for s in self.library.values()
187                        if query.search(s)]
188        return None
189