diff --git a/control/meta_mpd.py b/control/meta_mpd.py index 7a5379e8..081c1a1e 100755 --- a/control/meta_mpd.py +++ b/control/meta_mpd.py @@ -175,7 +175,6 @@ tag_mapping = { # Default url handlers if MPD doesn't support 'urlhandlers' command urlhandlers = ['http://'] -downloaded_covers = ['~/.covers/%s-%s.jpg'] def send(json_msg): @@ -201,18 +200,10 @@ class MPDWrapper(object): self._watch_id = None self._idling = False - self._status = { - 'state': None, - 'volume': None, - 'random': None, - 'repeat': None, - } - self._metadata = {} - self._temp_song_url = None - self._temp_cover = None + self._status = {} + self._currentsong = {} self._position = 0 self._time = 0 - self._currentsong = None def run(self): """ @@ -318,11 +309,6 @@ class MPDWrapper(object): self.run() def disconnect(self): - self._temp_song_url = None - if self._temp_cover: - self._temp_cover.close() - self._temp_cover = None - self.client.disconnect() def init_state(self): @@ -429,7 +415,7 @@ class MPDWrapper(object): self.single(0) else: send({"jsonrpc": "2.0", "error": {"code": -32601, - "message": "Method not found"}, "id": id}) + "message": "Method not found"}, "id": id}) success = False else: send({"jsonrpc": "2.0", "error": {"code": -32601, @@ -476,8 +462,7 @@ class MPDWrapper(object): if subsystem in ("player", "mixer", "options", "playlist"): if not updated: logger.info(f'Subsystem: {subsystem}') - self._update_properties( - force=subsystem == 'player') + self._update_properties(force=True) updated = True self.idle_enter() return True @@ -554,8 +539,8 @@ class MPDWrapper(object): except KeyError: logger.warning(f'tag "{key}" not supported') except (ValueError, TypeError): - logger.warning("Can't cast value %r to %s" % - (value, tag_mapping[key][1])) + logger.warning( + f"Can't cast value {value} to {tag_mapping[key][1]}") logger.debug(f'snapcast meta: {snapmeta}') @@ -576,16 +561,47 @@ class MPDWrapper(object): send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta}) self.update_albumart(snapmeta) + def __diff_map(self, old_map, new_map): + diff = {} + for key, value in new_map.items(): + if not key in old_map: + diff[key] = [None, value] + elif value != old_map[key]: + diff[key] = [old_map[key], value] + for key, value in old_map.items(): + if not key in new_map: + diff[key] = [value, None] + return diff + def _update_properties(self, force=False): - old_status = self._status old_position = self._position old_time = self._time - currentsong = self.client.currentsong() - if self._currentsong != currentsong: - self._currentsong = currentsong - force = True + + new_song = self.client.currentsong() + if not new_song: + logger.debug("_update_properties: failed to get current song") + return + new_status = self.client.status() + if not new_status: + logger.debug("_update_properties: failed to get new status") + return + + changed_status = self.__diff_map(self._status, new_status) + if len(changed_status) > 0: + self._status = new_status + + changed_song = self.__diff_map(self._currentsong, new_song) + if len(changed_song) > 0: + self._currentsong = new_song + + if len(changed_song) == 0 and len(changed_status) == 0: + logger.debug('nothing to do') + return + logger.info(f'new status: {new_status}') + logger.info(f'changed_status: {changed_status}') + logger.info(f'changed_song: {changed_song}') self._time = new_time = int(time.time()) snapstatus = {} @@ -597,10 +613,10 @@ class MPDWrapper(object): logger.debug( f'key: {key}, value: {value}, mapped key: {mapped_key}, mapped value: {mapped_val}') except KeyError: - logger.warning(f'tag "{key}" not supported') + logger.debug(f'tag "{key}" not supported') except (ValueError, TypeError): - logger.warning("Can't cast value %r to %s" % - (value, status_mapping[key][1])) + logger.warning( + f"Can't cast value {value} to {status_mapping[key][1]}") snapstatus['canGoNext'] = True snapstatus['canGoPrevious'] = True @@ -610,15 +626,6 @@ class MPDWrapper(object): snapstatus['canControl'] = True send({"jsonrpc": "2.0", "method": "Player.Properties", "params": snapstatus}) - if not new_status: - logger.debug("_update_properties: failed to get new status") - return - - self._status = new_status - logger.debug("_update_properties: current song = %r" % - self._currentsong) - logger.debug("_update_properties: current status = %r" % self._status) - if 'elapsed' in new_status: new_position = float(new_status['elapsed']) elif 'time' in new_status: @@ -630,13 +637,7 @@ class MPDWrapper(object): # "player" subsystem - if old_status['state'] != new_status['state']: - logger.info('state changed') - - if not force: - old_id = old_status.get('songid', None) - new_id = new_status.get('songid', None) - force = (old_id != new_id) + force = len(changed_song) > 0 if not force: if new_status['state'] == 'play': @@ -652,26 +653,8 @@ class MPDWrapper(object): else: # Update current song metadata - old_meta = self._metadata.copy() self.update_metadata() - # "mixer" subsystem - if old_status.get('volume') != new_status.get('volume'): - logger.info('volume changed') - - # "options" subsystem - # also triggered if consume, crossfade or ReplayGain are updated - - if old_status['random'] != new_status['random']: - logger.info('random changed') - - if (old_status['repeat'] != new_status['repeat'] - or old_status.get('single', 0) != new_status.get('single', 0)): - logger.info('repeat changed') - - if ("nextsongid" in old_status) != ("nextsongid" in new_status): - logger.info('nextsongid changed') - # Compatibility functions # Fedora 17 still has python-mpd 0.2, which lacks fileno(). diff --git a/control/snapcast_mpris.py b/control/snapcast_mpris.py index f8f2272f..9c01b26c 100755 --- a/control/snapcast_mpris.py +++ b/control/snapcast_mpris.py @@ -25,12 +25,9 @@ import websocket import logging import threading import json - -from configparser import ConfigParser +import webbrowser import os import sys -import re -import shlex import getopt import time import socket @@ -106,31 +103,6 @@ defaults = { notification = None -# MPRIS allowed metadata tags -allowed_tags = { - 'mpris:trackid': dbus.ObjectPath, - 'mpris:length': dbus.Int64, - 'mpris:artUrl': str, - 'xesam:album': str, - 'xesam:albumArtist': list, - 'xesam:artist': list, - 'xesam:asText': str, - 'xesam:audioBPM': int, - 'xesam:comment': list, - 'xesam:composer': list, - 'xesam:contentCreated': str, - 'xesam:discNumber': int, - 'xesam:firstUsed': str, - 'xesam:genre': list, - 'xesam:lastUsed': str, - 'xesam:lyricist': str, - 'xesam:title': str, - 'xesam:trackNumber': int, - 'xesam:url': str, - 'xesam:useCount': int, - 'xesam:userRating': float, -} - # python dbus bindings don't include annotations and properties MPRIS2_INTROSPECTION = """ @@ -259,28 +231,42 @@ class SnapcastRpcListener: tag_mapping = { - 'trackId': 'mpris:trackid', - 'artUrl': 'mpris:artUrl', - 'duration': 'mpris:length', - 'album': 'xesam:album', - 'albumArtist': 'xesam:albumArtist', - 'artist': 'xesam:artist', - 'asText': 'xesam:asText', - 'audioBPM': 'xesam:audioBPM', - 'autoRating': 'xesam:autoRating', - 'comment': 'xesam:comment', - 'composer': 'xesam:composer', - 'contentCreated': 'xesam:contentCreated', - 'discNumber': 'xesam:discNumber', - 'firstUsed': 'xesam:firstUsed', - 'genre': 'xesam:genre', - 'lastUsed': 'xesam:lastUsed', - 'lyricist': 'xesam:lyricist', - 'title': 'xesam:title', - 'trackNumber': 'xesam:trackNumber', - 'url': 'xesam:url', - 'useCount': 'xesam:useCount', - 'userRating': 'xesam:userRating', + 'trackId': ['mpris:trackid', lambda val: dbus.ObjectPath(f'/org/mpris/MediaPlayer2/Track/{val}')], + 'artUrl': ['mpris:artUrl', str], + 'duration': ['mpris:length', lambda val: int(val * 1000000)], + 'album': ['xesam:album', str], + 'albumArtist': ['xesam:albumArtist', list], + 'artist': ['xesam:artist', list], + 'asText': ['xesam:asText', str], + 'audioBPM': ['xesam:audioBPM', int], + 'autoRating': ['xesam:autoRating', float], + 'comment': ['xesam:comment', list], + 'composer': ['xesam:composer', list], + 'contentCreated': ['xesam:contentCreated', str], + 'discNumber': ['xesam:discNumber', int], + 'firstUsed': ['xesam:firstUsed', str], + 'genre': ['xesam:genre', list], + 'lastUsed': ['xesam:lastUsed', str], + 'lyricist': ['xesam:lyricist', str], + 'title': ['xesam:title', str], + 'trackNumber': ['xesam:trackNumber', int], + 'url': ['xesam:url', str], + 'useCount': ['xesam:useCount', int], + 'userRating': ['xesam:userRating', float] +} + + +property_mapping = { + 'playbackStatus': 'PlaybackStatus', + 'loopStatus': 'LoopStatus', + 'shuffle': 'Shuffle', + 'volume': 'Volume', + 'canGoNext': 'CanGoNext', + 'canGoPrevious': 'CanGoPrevious', + 'canPlay': 'CanPlay', + 'canPause': 'CanPause', + 'canSeek': 'CanSeek', + 'canControl': 'CanControl' } @@ -334,8 +320,7 @@ class MPDWrapper(object): logger.info("Ending SnapcastRpcWebsocketWrapper loop") def on_ws_message(self, ws, message): - logger.debug("Snapcast RPC websocket message received") - logger.debug(message) + logger.debug(f'Snapcast RPC websocket message received: {message}') jmsg = json.loads(message) if jmsg["method"] == "Stream.OnMetadata": logger.info(f'Stream meta changed for "{jmsg["params"]["id"]}"') @@ -348,14 +333,14 @@ class MPDWrapper(object): for key, value in meta.items(): if key in tag_mapping: - self._metadata[tag_mapping[key]] = value - - if 'mpris:length' in self._metadata: - self._metadata['mpris:length'] = int(1000 * 1000 * - self._metadata['mpris:length']) - if 'mpris:trackid' in self._metadata: - self._metadata[ - 'mpris:trackid'] = f'/org/mpris/MediaPlayer2/Track/{self._metadata["mpris:trackid"]}' + try: + self._metadata[tag_mapping[key][0] + ] = tag_mapping[key][1](value) + except KeyError: + logger.warning(f'tag "{key}" not supported') + except (ValueError, TypeError): + logger.warning( + f"Can't cast value {value} to {tag_mapping[key][1]}") if not 'mpris:artUrl' in self._metadata: self._metadata['mpris:artUrl'] = f'http://{self._params["host"]}:{self._params["port"]}/launcher-icon.png' @@ -375,21 +360,10 @@ class MPDWrapper(object): props['received'] = time.time() changed_properties = self.update_properties(props) logger.info(f'Changed properties: "{changed_properties}"') - if 'playbackStatus' in changed_properties: - self._dbus_service.update_property( - 'org.mpris.MediaPlayer2.Player', 'PlaybackStatus') - if 'loopStatus' in changed_properties: - self._dbus_service.update_property( - 'org.mpris.MediaPlayer2.Player', 'LoopStatus') - if 'shuffle' in changed_properties: - self._dbus_service.update_property( - 'org.mpris.MediaPlayer2.Player', 'Shuffle') - if 'volume' in changed_properties: - self._dbus_service.update_property( - 'org.mpris.MediaPlayer2.Player', 'Volume') - if 'canGoNext' in changed_properties: - self._dbus_service.update_property( - 'org.mpris.MediaPlayer2.Player', 'CanGoNext') + for key, value in changed_properties.items(): + if key in property_mapping: + self._dbus_service.update_property( + 'org.mpris.MediaPlayer2.Player', property_mapping[key]) def on_ws_error(self, ws, error): logger.error("Snapcast RPC websocket error") @@ -407,7 +381,7 @@ class MPDWrapper(object): # '/org/mpris/MediaPlayer2') self._dbus_service.acquire_name() - self.send({"id": 1, "jsonrpc": "2.0", "method": "Server.GetStatus"}) + self.send_request("Server.GetStatus") def on_ws_close(self, ws): logger.info("Snapcast RPC websocket closed") @@ -415,8 +389,14 @@ class MPDWrapper(object): self._dbus_service.release_name() # self._dbus_service.remove_from_connection() - def send(self, json_msg): - self.websocket.send(json.dumps(json_msg)) + def send_request(self, method, params=None): + j = {"id": self._req_id, "jsonrpc": "2.0", "method": str(method)} + if not params is None: + j["params"] = params + logger.info(f'send_request: {j}') + url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc' + self._req_id += 1 + self.websocket.send(json.dumps(j)) def stop(self): self.websocket.keep_running = False @@ -490,25 +470,15 @@ class MPDWrapper(object): # return self._currentsong.copy() def control(self, command, params={}): - j = {"id": self._req_id, "jsonrpc": "2.0", "method": "Stream.Control", - "params": {"id": "Pipe", "command": command, "params": params}} - logger.info(f'Control: {command}, json: {j}') - url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc' - logger.info(f'url: {url}') - self._req_id += 1 - self.send(j) + self.send_request("Stream.Control", { + "id": "Pipe", "command": command, "params": params}) def set_property(self, property, value): properties = {} properties[property] = value logger.info(f'set_properties {properties}') - j = {"id": self._req_id, "jsonrpc": "2.0", "method": "Stream.SetProperties", - "params": {"id": "Pipe", "properties": properties}} - logger.info(f'Set properties: {properties}, json: {j}') - url = f'http://{self._params["host"]}:{self._params["port"]}/jsonrpc' - logger.info(f'url: {url}') - self._req_id += 1 - self.send(j) + self.send_request("Stream.SetProperties", { + "id": "Pipe", "properties": properties}) @property def metadata(self): @@ -526,7 +496,7 @@ class MPDWrapper(object): elif value != self._properties[key]: changed_properties[key] = [self._properties[key], value] for key, value in self._properties.items(): - if not key in self._properties: + if not key in new_properties: changed_properties[key] = [value, None] self._properties = new_properties return changed_properties @@ -823,7 +793,7 @@ class MPRISInterface(dbus.service.Object): __root_interface = "org.mpris.MediaPlayer2" __root_props = { "CanQuit": (False, None), - "CanRaise": (False, None), + "CanRaise": (True, None), "DesktopEntry": ("mpdris2", None), "HasTrackList": (False, None), "Identity": (identity, None), @@ -880,6 +850,36 @@ class MPRISInterface(dbus.service.Object): logger.debug(f'get_position: {position}') return dbus.Int64(position) + def __get_can_go_next(): + can_go_next = snapcast_wrapper.property('canGoNext', False) + logger.debug(f'get_can_go_next "{can_go_next}"') + return can_go_next + + def __get_can_go_previous(): + can_go_previous = snapcast_wrapper.property('canGoPrevious', False) + logger.debug(f'get_can_go_previous "{can_go_previous}"') + return can_go_previous + + def __get_can_play(): + can_play = snapcast_wrapper.property('canPlay', False) + logger.debug(f'get_can_play "{can_play}"') + return can_play + + def __get_can_pause(): + can_pause = snapcast_wrapper.property('canPause', False) + logger.debug(f'get_can_pause "{can_pause}"') + return can_pause + + def __get_can_seek(): + can_seek = snapcast_wrapper.property('canSeek', False) + logger.debug(f'get_can_seek "{can_seek}"') + return can_seek + + def __get_can_control(): + can_control = snapcast_wrapper.property('canControl', False) + logger.debug(f'get_can_control "{can_control}"') + return can_control + __player_interface = "org.mpris.MediaPlayer2.Player" __player_props = { "PlaybackStatus": (__get_playback_status, None), @@ -897,6 +897,12 @@ class MPRISInterface(dbus.service.Object): "CanPause": (True, None), "CanSeek": (True, None), "CanControl": (True, None), + "CanGoNext": (__get_can_go_next, None), + "CanGoPrevious": (__get_can_go_previous, None), + "CanPlay": (__get_can_play, None), + "CanPause": (__get_can_pause, None), + "CanSeek": (__get_can_seek, None), + "CanControl": (__get_can_control, None), } __prop_mapping = { @@ -953,6 +959,7 @@ class MPRISInterface(dbus.service.Object): @ dbus.service.method(__root_interface, in_signature='', out_signature='') def Raise(self): logger.info('Raise') + webbrowser.open(url=f'http://{params["host"]}:{params["port"]}', new=1) return @ dbus.service.method(__root_interface, in_signature='', out_signature='')