Clean ups

This commit is contained in:
badaix 2021-06-02 08:49:59 +02:00
parent 7c11cb7559
commit 4ffecbb9c1
2 changed files with 148 additions and 158 deletions

View file

@ -175,7 +175,6 @@ tag_mapping = {
# Default url handlers if MPD doesn't support 'urlhandlers' command # Default url handlers if MPD doesn't support 'urlhandlers' command
urlhandlers = ['http://'] urlhandlers = ['http://']
downloaded_covers = ['~/.covers/%s-%s.jpg']
def send(json_msg): def send(json_msg):
@ -201,18 +200,10 @@ class MPDWrapper(object):
self._watch_id = None self._watch_id = None
self._idling = False self._idling = False
self._status = { self._status = {}
'state': None, self._currentsong = {}
'volume': None,
'random': None,
'repeat': None,
}
self._metadata = {}
self._temp_song_url = None
self._temp_cover = None
self._position = 0 self._position = 0
self._time = 0 self._time = 0
self._currentsong = None
def run(self): def run(self):
""" """
@ -318,11 +309,6 @@ class MPDWrapper(object):
self.run() self.run()
def disconnect(self): def disconnect(self):
self._temp_song_url = None
if self._temp_cover:
self._temp_cover.close()
self._temp_cover = None
self.client.disconnect() self.client.disconnect()
def init_state(self): def init_state(self):
@ -476,8 +462,7 @@ class MPDWrapper(object):
if subsystem in ("player", "mixer", "options", "playlist"): if subsystem in ("player", "mixer", "options", "playlist"):
if not updated: if not updated:
logger.info(f'Subsystem: {subsystem}') logger.info(f'Subsystem: {subsystem}')
self._update_properties( self._update_properties(force=True)
force=subsystem == 'player')
updated = True updated = True
self.idle_enter() self.idle_enter()
return True return True
@ -554,8 +539,8 @@ class MPDWrapper(object):
except KeyError: except KeyError:
logger.warning(f'tag "{key}" not supported') logger.warning(f'tag "{key}" not supported')
except (ValueError, TypeError): except (ValueError, TypeError):
logger.warning("Can't cast value %r to %s" % logger.warning(
(value, tag_mapping[key][1])) f"Can't cast value {value} to {tag_mapping[key][1]}")
logger.debug(f'snapcast meta: {snapmeta}') logger.debug(f'snapcast meta: {snapmeta}')
@ -576,16 +561,47 @@ class MPDWrapper(object):
send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta}) send({"jsonrpc": "2.0", "method": "Player.Metadata", "params": snapmeta})
self.update_albumart(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): def _update_properties(self, force=False):
old_status = self._status
old_position = self._position old_position = self._position
old_time = self._time old_time = self._time
currentsong = self.client.currentsong()
if self._currentsong != currentsong: new_song = self.client.currentsong()
self._currentsong = currentsong if not new_song:
force = True logger.debug("_update_properties: failed to get current song")
return
new_status = self.client.status() 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'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()) self._time = new_time = int(time.time())
snapstatus = {} snapstatus = {}
@ -597,10 +613,10 @@ class MPDWrapper(object):
logger.debug( logger.debug(
f'key: {key}, value: {value}, mapped key: {mapped_key}, mapped value: {mapped_val}') f'key: {key}, value: {value}, mapped key: {mapped_key}, mapped value: {mapped_val}')
except KeyError: except KeyError:
logger.warning(f'tag "{key}" not supported') logger.debug(f'tag "{key}" not supported')
except (ValueError, TypeError): except (ValueError, TypeError):
logger.warning("Can't cast value %r to %s" % logger.warning(
(value, status_mapping[key][1])) f"Can't cast value {value} to {status_mapping[key][1]}")
snapstatus['canGoNext'] = True snapstatus['canGoNext'] = True
snapstatus['canGoPrevious'] = True snapstatus['canGoPrevious'] = True
@ -610,15 +626,6 @@ class MPDWrapper(object):
snapstatus['canControl'] = True snapstatus['canControl'] = True
send({"jsonrpc": "2.0", "method": "Player.Properties", "params": snapstatus}) 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: if 'elapsed' in new_status:
new_position = float(new_status['elapsed']) new_position = float(new_status['elapsed'])
elif 'time' in new_status: elif 'time' in new_status:
@ -630,13 +637,7 @@ class MPDWrapper(object):
# "player" subsystem # "player" subsystem
if old_status['state'] != new_status['state']: force = len(changed_song) > 0
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)
if not force: if not force:
if new_status['state'] == 'play': if new_status['state'] == 'play':
@ -652,26 +653,8 @@ class MPDWrapper(object):
else: else:
# Update current song metadata # Update current song metadata
old_meta = self._metadata.copy()
self.update_metadata() 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 # Compatibility functions
# Fedora 17 still has python-mpd 0.2, which lacks fileno(). # Fedora 17 still has python-mpd 0.2, which lacks fileno().

View file

@ -25,12 +25,9 @@ import websocket
import logging import logging
import threading import threading
import json import json
import webbrowser
from configparser import ConfigParser
import os import os
import sys import sys
import re
import shlex
import getopt import getopt
import time import time
import socket import socket
@ -106,31 +103,6 @@ defaults = {
notification = None 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 # python dbus bindings don't include annotations and properties
MPRIS2_INTROSPECTION = """<node name="/org/mpris/MediaPlayer2"> MPRIS2_INTROSPECTION = """<node name="/org/mpris/MediaPlayer2">
@ -259,28 +231,42 @@ class SnapcastRpcListener:
tag_mapping = { tag_mapping = {
'trackId': 'mpris:trackid', 'trackId': ['mpris:trackid', lambda val: dbus.ObjectPath(f'/org/mpris/MediaPlayer2/Track/{val}')],
'artUrl': 'mpris:artUrl', 'artUrl': ['mpris:artUrl', str],
'duration': 'mpris:length', 'duration': ['mpris:length', lambda val: int(val * 1000000)],
'album': 'xesam:album', 'album': ['xesam:album', str],
'albumArtist': 'xesam:albumArtist', 'albumArtist': ['xesam:albumArtist', list],
'artist': 'xesam:artist', 'artist': ['xesam:artist', list],
'asText': 'xesam:asText', 'asText': ['xesam:asText', str],
'audioBPM': 'xesam:audioBPM', 'audioBPM': ['xesam:audioBPM', int],
'autoRating': 'xesam:autoRating', 'autoRating': ['xesam:autoRating', float],
'comment': 'xesam:comment', 'comment': ['xesam:comment', list],
'composer': 'xesam:composer', 'composer': ['xesam:composer', list],
'contentCreated': 'xesam:contentCreated', 'contentCreated': ['xesam:contentCreated', str],
'discNumber': 'xesam:discNumber', 'discNumber': ['xesam:discNumber', int],
'firstUsed': 'xesam:firstUsed', 'firstUsed': ['xesam:firstUsed', str],
'genre': 'xesam:genre', 'genre': ['xesam:genre', list],
'lastUsed': 'xesam:lastUsed', 'lastUsed': ['xesam:lastUsed', str],
'lyricist': 'xesam:lyricist', 'lyricist': ['xesam:lyricist', str],
'title': 'xesam:title', 'title': ['xesam:title', str],
'trackNumber': 'xesam:trackNumber', 'trackNumber': ['xesam:trackNumber', int],
'url': 'xesam:url', 'url': ['xesam:url', str],
'useCount': 'xesam:useCount', 'useCount': ['xesam:useCount', int],
'userRating': 'xesam:userRating', '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") logger.info("Ending SnapcastRpcWebsocketWrapper loop")
def on_ws_message(self, ws, message): def on_ws_message(self, ws, message):
logger.debug("Snapcast RPC websocket message received") logger.debug(f'Snapcast RPC websocket message received: {message}')
logger.debug(message)
jmsg = json.loads(message) jmsg = json.loads(message)
if jmsg["method"] == "Stream.OnMetadata": if jmsg["method"] == "Stream.OnMetadata":
logger.info(f'Stream meta changed for "{jmsg["params"]["id"]}"') logger.info(f'Stream meta changed for "{jmsg["params"]["id"]}"')
@ -348,14 +333,14 @@ class MPDWrapper(object):
for key, value in meta.items(): for key, value in meta.items():
if key in tag_mapping: if key in tag_mapping:
self._metadata[tag_mapping[key]] = value try:
self._metadata[tag_mapping[key][0]
if 'mpris:length' in self._metadata: ] = tag_mapping[key][1](value)
self._metadata['mpris:length'] = int(1000 * 1000 * except KeyError:
self._metadata['mpris:length']) logger.warning(f'tag "{key}" not supported')
if 'mpris:trackid' in self._metadata: except (ValueError, TypeError):
self._metadata[ logger.warning(
'mpris:trackid'] = f'/org/mpris/MediaPlayer2/Track/{self._metadata["mpris:trackid"]}' f"Can't cast value {value} to {tag_mapping[key][1]}")
if not 'mpris:artUrl' in self._metadata: if not 'mpris:artUrl' in self._metadata:
self._metadata['mpris:artUrl'] = f'http://{self._params["host"]}:{self._params["port"]}/launcher-icon.png' 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() props['received'] = time.time()
changed_properties = self.update_properties(props) changed_properties = self.update_properties(props)
logger.info(f'Changed properties: "{changed_properties}"') logger.info(f'Changed properties: "{changed_properties}"')
if 'playbackStatus' in changed_properties: for key, value in changed_properties.items():
if key in property_mapping:
self._dbus_service.update_property( self._dbus_service.update_property(
'org.mpris.MediaPlayer2.Player', 'PlaybackStatus') 'org.mpris.MediaPlayer2.Player', property_mapping[key])
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')
def on_ws_error(self, ws, error): def on_ws_error(self, ws, error):
logger.error("Snapcast RPC websocket error") logger.error("Snapcast RPC websocket error")
@ -407,7 +381,7 @@ class MPDWrapper(object):
# '/org/mpris/MediaPlayer2') # '/org/mpris/MediaPlayer2')
self._dbus_service.acquire_name() 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): def on_ws_close(self, ws):
logger.info("Snapcast RPC websocket closed") logger.info("Snapcast RPC websocket closed")
@ -415,8 +389,14 @@ class MPDWrapper(object):
self._dbus_service.release_name() self._dbus_service.release_name()
# self._dbus_service.remove_from_connection() # self._dbus_service.remove_from_connection()
def send(self, json_msg): def send_request(self, method, params=None):
self.websocket.send(json.dumps(json_msg)) 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): def stop(self):
self.websocket.keep_running = False self.websocket.keep_running = False
@ -490,25 +470,15 @@ class MPDWrapper(object):
# return self._currentsong.copy() # return self._currentsong.copy()
def control(self, command, params={}): def control(self, command, params={}):
j = {"id": self._req_id, "jsonrpc": "2.0", "method": "Stream.Control", self.send_request("Stream.Control", {
"params": {"id": "Pipe", "command": command, "params": 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)
def set_property(self, property, value): def set_property(self, property, value):
properties = {} properties = {}
properties[property] = value properties[property] = value
logger.info(f'set_properties {properties}') logger.info(f'set_properties {properties}')
j = {"id": self._req_id, "jsonrpc": "2.0", "method": "Stream.SetProperties", self.send_request("Stream.SetProperties", {
"params": {"id": "Pipe", "properties": properties}} "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)
@property @property
def metadata(self): def metadata(self):
@ -526,7 +496,7 @@ class MPDWrapper(object):
elif value != self._properties[key]: elif value != self._properties[key]:
changed_properties[key] = [self._properties[key], value] changed_properties[key] = [self._properties[key], value]
for key, value in self._properties.items(): for key, value in self._properties.items():
if not key in self._properties: if not key in new_properties:
changed_properties[key] = [value, None] changed_properties[key] = [value, None]
self._properties = new_properties self._properties = new_properties
return changed_properties return changed_properties
@ -823,7 +793,7 @@ class MPRISInterface(dbus.service.Object):
__root_interface = "org.mpris.MediaPlayer2" __root_interface = "org.mpris.MediaPlayer2"
__root_props = { __root_props = {
"CanQuit": (False, None), "CanQuit": (False, None),
"CanRaise": (False, None), "CanRaise": (True, None),
"DesktopEntry": ("mpdris2", None), "DesktopEntry": ("mpdris2", None),
"HasTrackList": (False, None), "HasTrackList": (False, None),
"Identity": (identity, None), "Identity": (identity, None),
@ -880,6 +850,36 @@ class MPRISInterface(dbus.service.Object):
logger.debug(f'get_position: {position}') logger.debug(f'get_position: {position}')
return dbus.Int64(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_interface = "org.mpris.MediaPlayer2.Player"
__player_props = { __player_props = {
"PlaybackStatus": (__get_playback_status, None), "PlaybackStatus": (__get_playback_status, None),
@ -897,6 +897,12 @@ class MPRISInterface(dbus.service.Object):
"CanPause": (True, None), "CanPause": (True, None),
"CanSeek": (True, None), "CanSeek": (True, None),
"CanControl": (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 = { __prop_mapping = {
@ -953,6 +959,7 @@ class MPRISInterface(dbus.service.Object):
@ dbus.service.method(__root_interface, in_signature='', out_signature='') @ dbus.service.method(__root_interface, in_signature='', out_signature='')
def Raise(self): def Raise(self):
logger.info('Raise') logger.info('Raise')
webbrowser.open(url=f'http://{params["host"]}:{params["port"]}', new=1)
return return
@ dbus.service.method(__root_interface, in_signature='', out_signature='') @ dbus.service.method(__root_interface, in_signature='', out_signature='')