Trying to get DBus PropertiesChanged propagation working

This commit is contained in:
Sumner Evans
2019-10-12 01:11:58 -06:00
parent 0f24824673
commit 2030e8e38f
2 changed files with 321 additions and 155 deletions

View File

@@ -1,3 +1,4 @@
import functools
import os import os
import math import math
import random import random
@@ -13,12 +14,28 @@ from .ui.main import MainWindow
from .ui.configure_servers import ConfigureServersDialog from .ui.configure_servers import ConfigureServersDialog
from .ui.settings import SettingsDialog from .ui.settings import SettingsDialog
from .dbus_manager import DBusManager
from .state_manager import ApplicationState, RepeatType from .state_manager import ApplicationState, RepeatType
from .cache_manager import CacheManager from .cache_manager import CacheManager
from .server.api_objects import Child from .server.api_objects import Child
from .ui.common.players import PlayerEvent, MPVPlayer, ChromecastPlayer from .ui.common.players import PlayerEvent, MPVPlayer, ChromecastPlayer
def dbus_propagate(param_self=None):
"""
Wraps a function which causes changes to DBus properties.
"""
def decorator(function):
@functools.wraps(function)
def wrapper(*args):
function(*args)
(param_self or args[0]).dbus_manager.property_diff()
return wrapper
return decorator
class LibremsonicApp(Gtk.Application): class LibremsonicApp(Gtk.Application):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__( super().__init__(
@@ -205,6 +222,8 @@ class LibremsonicApp(Gtk.Application):
) )
self.player = self.mpv_player self.player = self.mpv_player
self.player.volume = self.state.volume
if self.state.current_device != 'this device': if self.state.current_device != 'this device':
# TODO figure out how to activate the chromecast if possible # TODO figure out how to activate the chromecast if possible
# without blocking the main thread. Also, need to make it obvious # without blocking the main thread. Also, need to make it obvious
@@ -220,42 +239,14 @@ class LibremsonicApp(Gtk.Application):
# ########## DBUS MANAGMENT ########## # # ########## DBUS MANAGMENT ########## #
def do_dbus_register(self, connection, path): def do_dbus_register(self, connection, path):
Gio.bus_own_name_on_connection( self.dbus_manager = DBusManager(
connection, connection,
'org.mpris.MediaPlayer2.libremsonic', self.on_dbus_method_call,
Gio.BusNameOwnerFlags.NONE, self.on_dbus_set_property,
self.dbus_name_acquired, lambda: (self.state, self.player),
self.dbus_name_lost,
) )
return True return True
def dbus_name_acquired(self, connection, name):
specs = [
'org.mpris.MediaPlayer2.xml',
'org.mpris.MediaPlayer2.Player.xml',
'org.mpris.MediaPlayer2.Playlists.xml',
'org.mpris.MediaPlayer2.TrackList.xml',
]
for spec in specs:
spec_path = os.path.join(
os.path.dirname(__file__),
f'ui/mpris_specs/{spec}',
)
with open(spec_path) as f:
node_info = Gio.DBusNodeInfo.new_for_xml(f.read())
connection.register_object(
'/org/mpris/MediaPlayer2',
node_info.interfaces[0],
self.on_dbus_method_call,
self.on_dbus_get_property,
self.on_dbus_set_property,
)
# TODO: I have no idea what to do here.
def dbus_name_lost(self, *args):
pass
def on_dbus_method_call( def on_dbus_method_call(
self, self,
connection, connection,
@@ -303,129 +294,6 @@ class LibremsonicApp(Gtk.Application):
print('Unknown method:', method) print('Unknown method:', method)
invocation.return_value(method(*params) if callable(method) else None) invocation.return_value(method(*params) if callable(method) else None)
def on_dbus_get_property(
self,
connection,
sender,
path,
interface,
property_name,
):
second_microsecond_conversion = 1000000
has_current_song = self.state.current_song is not None
has_next_song = False
if self.state.repeat_type in (RepeatType.REPEAT_QUEUE,
RepeatType.REPEAT_SONG):
has_next_song = True
elif has_current_song and self.state.current_song.id in self.state.play_queue:
current = self.state.play_queue.index(self.state.current_song.id)
has_next_song = current < len(self.state.play_queue) - 1
response_map = {
'org.mpris.MediaPlayer2': {
'CanQuit': True,
'CanRaise': True,
'HasTrackList': True,
'Identity': 'Libremsonic',
# TODO should implement in #29
'DesktopEntry': 'foo',
'SupportedUriSchemes': [],
'SupportedMimeTypes': [],
},
'org.mpris.MediaPlayer2.Player': {
'PlaybackStatus': {
(False, False): 'Stopped',
(False, True): 'Stopped',
(True, False): 'Paused',
(True, True): 'Playing',
}[self.player.song_loaded, self.state.playing],
'LoopStatus':
self.state.repeat_type.as_mpris_loop_status(),
'Rate':
1.0,
'Shuffle':
self.state.shuffle_on,
'Metadata': {
'mpris:trackid':
self.state.current_song.id,
'mpris:length':
GLib.Variant(
'i',
self.state.current_song.duration
* second_microsecond_conversion,
),
# TODO this won't work. Need to get the cached version or
# give a URL which downloads from the server.
'mpris:artUrl':
self.state.current_song.coverArt,
'xesam:album':
self.state.current_song.album,
'xesam:albumArtist': [self.state.current_song.artist],
'xesam:artist': [self.state.current_song.artist],
'xesam:title':
self.state.current_song.title,
} if self.state.current_song else {},
'Volume':
self.state.volume,
'Position':
GLib.Variant(
'x',
int(
self.state.song_progress
* second_microsecond_conversion),
),
'MinimumRate':
1.0,
'MaximumRate':
1.0,
'CanGoNext':
has_current_song and has_next_song,
'CanGoPrevious':
has_current_song,
'CanPlay':
True,
'CanPause':
True,
'CanSeek':
True,
'CanControl':
True,
},
'org.mpris.MediaPlayer2.TrackList': {
'Tracks': self.state.play_queue,
'CanEditTracks': self.state.play_queue,
},
}
response = response_map.get(interface, {}).get(property_name)
if response is None:
print('get FAILED', interface, property_name)
# TODO finish implementing all of this
if callable(response):
response = response()
if type(response) == dict:
return GLib.Variant(
'a{sv}',
{
k:
v if isinstance(v, GLib.Variant) else GLib.Variant('s', v)
for k, v in response.items()
},
)
elif type(response) == list:
return GLib.Variant('as', response)
elif type(response) == str:
return GLib.Variant('s', response)
elif type(response) == int:
return GLib.Variant('i', response)
elif type(response) == float:
return GLib.Variant('d', response)
elif type(response) == bool:
return GLib.Variant('b', response)
else:
return response
def on_dbus_set_property( def on_dbus_set_property(
self, self,
connection, connection,
@@ -462,6 +330,7 @@ class LibremsonicApp(Gtk.Application):
setter(value) setter(value)
# ########## ACTION HANDLERS ########## # # ########## ACTION HANDLERS ########## #
@dbus_propagate()
def on_refresh_window(self, _, state_updates, force=False): def on_refresh_window(self, _, state_updates, force=False):
for k, v in state_updates.items(): for k, v in state_updates.items():
setattr(self.state, k, v) setattr(self.state, k, v)
@@ -491,6 +360,7 @@ class LibremsonicApp(Gtk.Application):
self.reset_cache_manager() self.reset_cache_manager()
dialog.destroy() dialog.destroy()
@dbus_propagate()
def on_play_pause(self, *args): def on_play_pause(self, *args):
if self.state.current_song is None: if self.state.current_song is None:
return return
@@ -505,6 +375,7 @@ class LibremsonicApp(Gtk.Application):
self.state.playing = not self.state.playing self.state.playing = not self.state.playing
self.update_window() self.update_window()
@dbus_propagate()
def on_next_track(self, *args): def on_next_track(self, *args):
current_idx = self.state.play_queue.index(self.state.current_song.id) current_idx = self.state.play_queue.index(self.state.current_song.id)
@@ -517,6 +388,7 @@ class LibremsonicApp(Gtk.Application):
self.play_song(self.state.play_queue[current_idx + 1], reset=True) self.play_song(self.state.play_queue[current_idx + 1], reset=True)
@dbus_propagate()
def on_prev_track(self, *args): def on_prev_track(self, *args):
# TODO there is a bug where you can't go back multiple songs fast # TODO there is a bug where you can't go back multiple songs fast
current_idx = self.state.play_queue.index(self.state.current_song.id) current_idx = self.state.play_queue.index(self.state.current_song.id)
@@ -535,12 +407,14 @@ class LibremsonicApp(Gtk.Application):
self.play_song(self.state.play_queue[song_to_play], reset=True) self.play_song(self.state.play_queue[song_to_play], reset=True)
@dbus_propagate()
def on_repeat_press(self, action, params): def on_repeat_press(self, action, params):
# Cycle through the repeat types. # Cycle through the repeat types.
new_repeat_type = RepeatType((self.state.repeat_type.value + 1) % 3) new_repeat_type = RepeatType((self.state.repeat_type.value + 1) % 3)
self.state.repeat_type = new_repeat_type self.state.repeat_type = new_repeat_type
self.update_window() self.update_window()
@dbus_propagate()
def on_shuffle_press(self, action, params): def on_shuffle_press(self, action, params):
if self.state.shuffle_on: if self.state.shuffle_on:
# Revert to the old play queue. # Revert to the old play queue.
@@ -560,6 +434,7 @@ class LibremsonicApp(Gtk.Application):
def on_play_queue_click(self, action, song_id): def on_play_queue_click(self, action, song_id):
self.play_song(song_id.get_string(), reset=True) self.play_song(song_id.get_string(), reset=True)
@dbus_propagate()
def on_play_next(self, action, song_ids): def on_play_next(self, action, song_ids):
if self.state.current_song is None: if self.state.current_song is None:
insert_at = 0 insert_at = 0
@@ -573,6 +448,7 @@ class LibremsonicApp(Gtk.Application):
self.state.old_play_queue.extend(song_ids) self.state.old_play_queue.extend(song_ids)
self.update_window() self.update_window()
@dbus_propagate()
def on_add_to_queue(self, action, song_ids): def on_add_to_queue(self, action, song_ids):
self.state.play_queue.extend(song_ids) self.state.play_queue.extend(song_ids)
self.state.old_play_queue.extend(song_ids) self.state.old_play_queue.extend(song_ids)
@@ -639,6 +515,7 @@ class LibremsonicApp(Gtk.Application):
play_queue=song_queue, play_queue=song_queue,
) )
@dbus_propagate()
def on_song_scrub(self, _, scrub_value): def on_song_scrub(self, _, scrub_value):
if not hasattr(self.state, 'current_song'): if not hasattr(self.state, 'current_song'):
return return
@@ -675,11 +552,13 @@ class LibremsonicApp(Gtk.Application):
if was_playing: if was_playing:
self.on_play_pause() self.on_play_pause()
@dbus_propagate()
def on_mute_toggle(self, action, _): def on_mute_toggle(self, action, _):
self.state.is_muted = not self.state.is_muted self.state.is_muted = not self.state.is_muted
self.player.is_muted = self.state.is_muted self.player.is_muted = self.state.is_muted
self.update_window() self.update_window()
@dbus_propagate()
def on_volume_change(self, _, value): def on_volume_change(self, _, value):
self.state.volume = value self.state.volume = value
self.player.volume = self.state.volume self.player.volume = self.state.volume
@@ -705,6 +584,7 @@ class LibremsonicApp(Gtk.Application):
self.state.save() self.state.save()
self.save_play_queue() self.save_play_queue()
self.dbus_manager.shutdown()
CacheManager.shutdown() CacheManager.shutdown()
# ########## PROPERTIES ########## # # ########## PROPERTIES ########## #
@@ -793,6 +673,7 @@ class LibremsonicApp(Gtk.Application):
): ):
# Do this the old fashioned way so that we can have access to ``reset`` # Do this the old fashioned way so that we can have access to ``reset``
# in the callback. # in the callback.
@dbus_propagate(self)
def do_play_song(song: Child): def do_play_song(song: Child):
uri, stream = CacheManager.get_song_filename_or_stream( uri, stream = CacheManager.get_song_filename_or_stream(
song, song,

285
libremsonic/dbus_manager.py Normal file
View File

@@ -0,0 +1,285 @@
import os
import re
from collections import defaultdict
from deepdiff import DeepDiff
from gi.repository import Gio, GLib
from .state_manager import RepeatType
class DBusManager:
second_microsecond_conversion = 1000000
current_state = {}
def __init__(
self,
connection,
do_on_method_call,
on_set_property,
get_state_and_player,
):
self.get_state_and_player = get_state_and_player
self.do_on_method_call = do_on_method_call
self.on_set_property = on_set_property
self.connection = connection
def dbus_name_acquired(connection, name):
specs = [
'org.mpris.MediaPlayer2.xml',
'org.mpris.MediaPlayer2.Player.xml',
'org.mpris.MediaPlayer2.Playlists.xml',
'org.mpris.MediaPlayer2.TrackList.xml',
]
for spec in specs:
spec_path = os.path.join(
os.path.dirname(__file__),
f'ui/mpris_specs/{spec}',
)
with open(spec_path) as f:
node_info = Gio.DBusNodeInfo.new_for_xml(f.read())
connection.register_object(
'/org/mpris/MediaPlayer2',
node_info.interfaces[0],
self.on_method_call,
self.on_get_property,
self.on_set_property,
)
# TODO: I have no idea what to do here.
def dbus_name_lost(*args):
pass
self.bus_number = Gio.bus_own_name_on_connection(
connection,
'org.mpris.MediaPlayer2.libremsonic',
Gio.BusNameOwnerFlags.NONE,
dbus_name_acquired,
dbus_name_lost,
)
def shutdown(self):
Gio.bus_unown_name(self.bus_number)
def on_get_property(
self,
connection,
sender,
path,
interface,
property_name,
):
return self.to_variant(
self.property_dict().get(
interface,
{},
).get(property_name))
def on_method_call(
self,
connection,
sender,
path,
interface,
method,
params,
invocation,
):
if interface == 'org.freedesktop.DBus.Properties':
if method == 'Get':
invocation.return_value(
self.on_get_property(
connection, sender, path, interface, *params))
elif method == 'Set':
self.on_set_property(
connection, sender, path, interface, *params)
elif method == 'GetAll':
all_properties = {
k: self.to_variant(v)
for k, v in self.property_dict()[interface].items()
}
invocation.return_value(
GLib.Variant('(a{sv})', (all_properties, )))
return
self.do_on_method_call(
connection,
sender,
path,
interface,
method,
params,
invocation,
)
def to_variant(self, value):
if callable(value):
return self.to_variant(value())
if isinstance(value, GLib.Variant):
return value
if type(value) == tuple:
return GLib.Variant(*value)
if type(value) == dict:
return GLib.Variant(
'a{sv}',
{k: self.to_variant(v)
for k, v in value.items()},
)
variant_type = {
list: 'as',
str: 's',
int: 'i',
float: 'd',
bool: 'b',
}.get(type(value))
if not variant_type:
return value
return GLib.Variant(variant_type, value)
def property_dict(self):
state, player = self.get_state_and_player()
has_current_song = state.current_song is not None
has_next_song = False
if state.repeat_type in (RepeatType.REPEAT_QUEUE,
RepeatType.REPEAT_SONG):
has_next_song = True
elif has_current_song and state.current_song.id in state.play_queue:
current = state.play_queue.index(state.current_song.id)
has_next_song = current < len(state.play_queue) - 1
return {
'org.mpris.MediaPlayer2': {
'CanQuit': True,
'CanRaise': True,
'HasTrackList': True,
'Identity': 'Libremsonic',
# TODO should implement in #29
'DesktopEntry': 'foo',
'SupportedUriSchemes': [],
'SupportedMimeTypes': [],
},
'org.mpris.MediaPlayer2.Player': {
'PlaybackStatus': {
(False, False): 'Stopped',
(False, True): 'Stopped',
(True, False): 'Paused',
(True, True): 'Playing',
}[player.song_loaded, state.playing],
'LoopStatus':
state.repeat_type.as_mpris_loop_status(),
'Rate':
1.0,
'Shuffle':
state.shuffle_on,
'Metadata': {
'mpris:trackid':
state.current_song.id,
'mpris:length': (
'i',
(state.current_song.duration or 0)
* self.second_microsecond_conversion,
),
# TODO this won't work. Need to get the cached version or
# give a URL which downloads from the server.
'mpris:artUrl':
state.current_song.coverArt,
'xesam:album':
state.current_song.album,
'xesam:albumArtist': [state.current_song.artist],
'xesam:artist': [state.current_song.artist],
'xesam:title':
state.current_song.title,
} if state.current_song else {},
'Volume':
0.0 if state.is_muted else state.volume,
'Position': (
'x',
int(
(state.song_progress or 0)
* self.second_microsecond_conversion),
),
'MinimumRate':
1.0,
'MaximumRate':
1.0,
'CanGoNext':
has_current_song and has_next_song,
'CanGoPrevious':
has_current_song,
'CanPlay':
True,
'CanPause':
True,
'CanSeek':
True,
'CanControl':
True,
},
'org.mpris.MediaPlayer2.TrackList': {
'Tracks': state.play_queue,
'CanEditTracks': True,
},
}
diff_parse_re = re.compile(r"root\['(.*?)'\]\['(.*?)'\](?:\[.*\])?")
def property_diff(self):
new_property_dict = self.property_dict()
diff = DeepDiff(self.current_state, new_property_dict)
if 'dictionary_item_added' in diff.keys():
for interface, property_dict in new_property_dict.items():
self.connection.emit_signal(
None,
'/org/mpris/MediaPlayer2',
'org.freedesktop.DBus.Properties',
'PropertiesChanged',
GLib.Variant(
'(sa{sv}as)', (
interface,
self.to_variant(property_dict),
self.to_variant([]),
)),
)
self.current_state = new_property_dict
return
changes = defaultdict(dict)
for path, change in diff.get('values_changed', {}).items():
interface, property_name = self.diff_parse_re.match(path).groups()
changes[interface][property_name] = change['new_value']
for interface, changed_props in changes.items():
# If the metadata has changed, just make the entire Metadata object
# part of the update.
if 'Metadata' in changed_props.keys():
changed_props['Metadata'] = new_property_dict[interface][
'Metadata']
if 'Position' in changed_props.keys():
del changed_props['Position']
print(interface, changed_props)
print(self.to_variant(changed_props))
self.connection.emit_signal(
None,
'/org/mpris/MediaPlayer2',
'org.freedesktop.DBus.Properties',
'PropertiesChanged',
GLib.Variant(
"(sa{sv}as)", (
interface,
self.to_variant(changed_props),
self.to_variant([]),
)),
)
self.current_state = new_property_dict