diff --git a/main.py b/main.py
index 7105fd5..5bf1421 100755
--- a/main.py
+++ b/main.py
@@ -19,6 +19,7 @@ app_id = 'fun.wkit.sonist'
home_dir = os.getenv('HOME')
+# 定义一个异步修饰器, 用于在子线程中运行一些会阻塞主线程的任务
def run_async(func):
def wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
@@ -27,7 +28,7 @@ def run_async(func):
return thread
return wrapper
-
+# 类型js的settimeout的修饰器
def set_timeout(timeout = 0.5):
def decorator(callback):
def wrapper(*args):
@@ -39,6 +40,7 @@ def set_timeout(timeout = 0.5):
return decorator
+
def get_music_dir():
with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f:
data = f.read()
@@ -52,38 +54,38 @@ def get_music_dir():
class Application(Gtk.Application):
- __gsignals__ = {
- 'playing': (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
- 'song_changed': (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
- 'state_changed': (GObject.SignalFlags.RUN_FIRST, None, (str,))
- }
-
-
def __init__(self):
Gtk.Application.__init__(self, application_id = app_id)
self.timer = None
- self.mpd = MPDClient()
-
self.music_dir = get_music_dir()
+ self.mpd = MPDClient()
self.connect('window-removed', self.on_window_removed)
- self.mpd.connect()
+
+ @run_async
+ def connect_mpd(self):
+
+ self.mpd.start()
+
def do_activate(self):
print('hello mpc')
self.set_app_menu(None)
self.set_menubar(None)
+
self.window = SonistWindow(self)
self.about = AboutWindow()
self.add_window(self.window)
self.window.show_all()
# self.about.show_all()
+ self.connect_mpd()
+
def on_window_removed(self, app, win):
if len(self.get_windows()) == 0:
diff --git a/mpd.py b/mpd.py
index 4cc467c..ad0e0ce 100644
--- a/mpd.py
+++ b/mpd.py
@@ -17,10 +17,12 @@
# You should have received a copy of the GNU Lesser General Public License
# along with python-mpd2. If not, see .
-import re, socket, sys, warnings, threading, time
+import gi
+import re, socket, sys, warnings, time
from enum import Enum
-
+gi.require_version('Gtk', '3.0')
+from gi.repository import GObject
VERSION = (3, 1, 0)
HELLO_PREFIX = "OK MPD "
@@ -30,27 +32,9 @@ SUCCESS = "OK"
NEXT = "list_OK"
-def run_async(func):
- def wrapper(*args, **kwargs):
- thread = threading.Thread(target=func, args=args, kwargs=kwargs)
- thread.daemon = True
- thread.start()
- return thread
- return wrapper
-
-def set_timeout(timeout = 0.5):
- def decorator(callback):
- def wrapper(*args):
- t = threading.Timer(timeout, callback, args=args)
- t.start()
- return t
- return wrapper
-
- return decorator
-
def escape(text):
- return text.replace("\\", "\\\\").replace('"', '\\"')
+ return text.replace("\\", "\\\\").replace('"', '\\"')
@@ -72,19 +56,11 @@ class FailureResponseCode(Enum):
EXIST = 56
-class MPDError(Exception):
+class ProtocolError(Exception):
pass
-class ConnectionError(MPDError):
- pass
-
-
-class ProtocolError(MPDError):
- pass
-
-
-class CommandError(MPDError):
+class CommandError(Exception):
def __init__(self, error):
self.errno = None
self.offset = None
@@ -99,16 +75,12 @@ class CommandError(MPDError):
self.msg = match.group("msg")
-class CommandListError(MPDError):
- pass
+class CommandListError(Exception):
+ pass
-class PendingCommandError(MPDError):
- pass
-
-
-class IteratingError(MPDError):
- pass
+class IteratingError(Exception):
+ pass
class mpd_commands(object):
@@ -171,271 +143,271 @@ class Noop(object):
mpd_commands = None
-class MPDClientBase(object):
- """Abstract MPD client.
+class MPDClientBase():
+ """Abstract MPD client.
- This class defines a general client contract, provides MPD protocol parsers
- and defines all available MPD commands and it's corresponding result
- parsing callbacks. There might be the need of overriding some callbacks on
- subclasses.
- """
-
- def __init__(self):
- self.iterate = False
- self._reset()
+ This class defines a general client contract, provides MPD protocol parsers
+ and defines all available MPD commands and it's corresponding result
+ parsing callbacks. There might be the need of overriding some callbacks on
+ subclasses.
+ """
+
+ def __init__(self):
+ self.iterate = False
+ self._reset()
- @classmethod
- def add_command(cls, name, callback):
- raise NotImplementedError(
- "Abstract ``MPDClientBase`` does not implement ``add_command``"
- )
+ @classmethod
+ def add_command(cls, name, callback):
+ raise NotImplementedError(
+ "Abstract ``MPDClientBase`` does not implement ``add_command``"
+ )
- def noidle(self):
- raise NotImplementedError(
- "Abstract ``MPDClientBase`` does not implement ``noidle``"
- )
+ def noidle(self):
+ raise NotImplementedError(
+ "Abstract ``MPDClientBase`` does not implement ``noidle``"
+ )
- def command_list_ok_begin(self):
- raise NotImplementedError(
- "Abstract ``MPDClientBase`` does not implement " "``command_list_ok_begin``"
- )
+ def command_list_ok_begin(self):
+ raise NotImplementedError(
+ "Abstract ``MPDClientBase`` does not implement " "``command_list_ok_begin``"
+ )
- def command_list_end(self):
- raise NotImplementedError(
- "Abstract ``MPDClientBase`` does not implement " "``command_list_end``"
- )
+ def command_list_end(self):
+ raise NotImplementedError(
+ "Abstract ``MPDClientBase`` does not implement " "``command_list_end``"
+ )
- def _reset(self):
- self.mpd_version = None
- self._command_list = None
+ def _reset(self):
+ self.mpd_version = None
+ self._command_list = None
- def _parse_pair(self, line, separator):
- if line is None:
- return
- pair = line.split(separator, 1)
- if len(pair) < 2:
- raise ProtocolError("Could not parse pair: '{}'".format(line))
- return pair
+ def _parse_pair(self, line, separator):
+ if line is None:
+ return
+ pair = line.split(separator, 1)
+ if len(pair) < 2:
+ raise ProtocolError("Could not parse pair: '{}'".format(line))
+ return pair
- def _parse_pairs(self, lines, separator=": "):
- for line in lines:
- yield self._parse_pair(line, separator)
+ def _parse_pairs(self, lines, separator=": "):
+ for line in lines:
+ yield self._parse_pair(line, separator)
- def _parse_objects(self, lines, delimiters=[], lookup_delimiter=False):
- obj = {}
- for key, value in self._parse_pairs(lines):
- key = key.lower()
- if lookup_delimiter and key not in delimiters:
- delimiters = delimiters + [key]
- if obj:
- if key in delimiters:
- if lookup_delimiter:
- if key in obj:
- yield obj
- obj = obj.copy()
- while delimiters[-1] != key:
- obj.pop(delimiters[-1], None)
- delimiters.pop()
- else:
- yield obj
- obj = {}
- elif key in obj:
- if not isinstance(obj[key], list):
- obj[key] = [obj[key], value]
- else:
- obj[key].append(value)
- continue
- obj[key] = value
- if obj:
- yield obj
+ def _parse_objects(self, lines, delimiters=[], lookup_delimiter=False):
+ obj = {}
+ for key, value in self._parse_pairs(lines):
+ key = key.lower()
+ if lookup_delimiter and key not in delimiters:
+ delimiters = delimiters + [key]
+ if obj:
+ if key in delimiters:
+ if lookup_delimiter:
+ if key in obj:
+ yield obj
+ obj = obj.copy()
+ while delimiters[-1] != key:
+ obj.pop(delimiters[-1], None)
+ delimiters.pop()
+ else:
+ yield obj
+ obj = {}
+ elif key in obj:
+ if not isinstance(obj[key], list):
+ obj[key] = [obj[key], value]
+ else:
+ obj[key].append(value)
+ continue
+ obj[key] = value
+ if obj:
+ yield obj
- # Use this instead of _parse_objects whenever the result is returned
- # immediately in a command implementation
- _parse_objects_direct = _parse_objects
+ # Use this instead of _parse_objects whenever the result is returned
+ # immediately in a command implementation
+ _parse_objects_direct = _parse_objects
- def _parse_raw_stickers(self, lines):
- for key, sticker in self._parse_pairs(lines):
- value = sticker.split("=", 1)
- if len(value) < 2:
- raise ProtocolError("Could not parse sticker: {}".format(repr(sticker)))
- yield tuple(value)
+ def _parse_raw_stickers(self, lines):
+ for key, sticker in self._parse_pairs(lines):
+ value = sticker.split("=", 1)
+ if len(value) < 2:
+ raise ProtocolError("Could not parse sticker: {}".format(repr(sticker)))
+ yield tuple(value)
- NOOP = mpd_commands("close", "kill")(Noop())
+ NOOP = mpd_commands("close", "kill")(Noop())
- @mpd_commands("plchangesposid", is_direct=True)
- def _parse_changes(self, lines):
- return self._parse_objects_direct(lines, ["cpos"])
+ @mpd_commands("plchangesposid", is_direct=True)
+ def _parse_changes(self, lines):
+ return self._parse_objects_direct(lines, ["cpos"])
- @mpd_commands("listall", "listallinfo", "listfiles", "lsinfo", is_direct=True)
- def _parse_database(self, lines):
- return self._parse_objects_direct(lines, ["file", "directory", "playlist"])
+ @mpd_commands("listall", "listallinfo", "listfiles", "lsinfo", is_direct=True)
+ def _parse_database(self, lines):
+ return self._parse_objects_direct(lines, ["file", "directory", "playlist"])
- @mpd_commands("idle")
- def _parse_idle(self, lines):
- return self._parse_list(lines)
+ @mpd_commands("idle")
+ def _parse_idle(self, lines):
+ return self._parse_list(lines)
- @mpd_commands("addid", "config", "replay_gain_status", "rescan", "update")
- def _parse_item(self, lines):
- pairs = list(self._parse_pairs(lines))
- if len(pairs) != 1:
- return
- return pairs[0][1]
+ @mpd_commands("addid", "config", "replay_gain_status", "rescan", "update")
+ def _parse_item(self, lines):
+ pairs = list(self._parse_pairs(lines))
+ if len(pairs) != 1:
+ return
+ return pairs[0][1]
- @mpd_commands(
- "channels", "commands", "listplaylist", "notcommands", "tagtypes", "urlhandlers"
- )
- def _parse_list(self, lines):
- seen = None
- for key, value in self._parse_pairs(lines):
- if key != seen:
- if seen is not None:
- raise ProtocolError("Expected key '{}', got '{}'".format(seen, key))
- seen = key
- yield value
+ @mpd_commands(
+ "channels", "commands", "listplaylist", "notcommands", "tagtypes", "urlhandlers"
+ )
+ def _parse_list(self, lines):
+ seen = None
+ for key, value in self._parse_pairs(lines):
+ if key != seen:
+ if seen is not None:
+ raise ProtocolError("Expected key '{}', got '{}'".format(seen, key))
+ seen = key
+ yield value
- @mpd_commands("list", is_direct=True)
- def _parse_list_groups(self, lines):
- return self._parse_objects_direct(lines, lookup_delimiter=True)
+ @mpd_commands("list", is_direct=True)
+ def _parse_list_groups(self, lines):
+ return self._parse_objects_direct(lines, lookup_delimiter=True)
- @mpd_commands("readmessages", is_direct=True)
- def _parse_messages(self, lines):
- return self._parse_objects_direct(lines, ["channel"])
+ @mpd_commands("readmessages", is_direct=True)
+ def _parse_messages(self, lines):
+ return self._parse_objects_direct(lines, ["channel"])
- @mpd_commands("listmounts", is_direct=True)
- def _parse_mounts(self, lines):
- return self._parse_objects_direct(lines, ["mount"])
+ @mpd_commands("listmounts", is_direct=True)
+ def _parse_mounts(self, lines):
+ return self._parse_objects_direct(lines, ["mount"])
- @mpd_commands("listneighbors", is_direct=True)
- def _parse_neighbors(self, lines):
- return self._parse_objects_direct(lines, ["neighbor"])
+ @mpd_commands("listneighbors", is_direct=True)
+ def _parse_neighbors(self, lines):
+ return self._parse_objects_direct(lines, ["neighbor"])
- @mpd_commands("listpartitions", is_direct=True)
- def _parse_partitions(self, lines):
- return self._parse_objects_direct(lines, ["partition"])
+ @mpd_commands("listpartitions", is_direct=True)
+ def _parse_partitions(self, lines):
+ return self._parse_objects_direct(lines, ["partition"])
- @mpd_commands(
- "add",
- "addtagid",
- "binarylimit",
- "clear",
- "clearerror",
- "cleartagid",
- "consume",
- "crossfade",
- "delete",
- "deleteid",
- "delpartition",
- "disableoutput",
- "enableoutput",
- "findadd",
- "load",
- "mixrampdb",
- "mixrampdelay",
- "mount",
- "move",
- "moveid",
- "moveoutput",
- "newpartition",
- "next",
- "outputvolume",
- "partition",
- "password",
- "pause",
- "ping",
- "play",
- "playid",
- "playlistadd",
- "playlistclear",
- "playlistdelete",
- "playlistmove",
- "previous",
- "prio",
- "prioid",
- "random",
- "rangeid",
- "rename",
- "repeat",
- "replay_gain_mode",
- "rm",
- "save",
- "searchadd",
- "searchaddpl",
- "seek",
- "seekcur",
- "seekid",
- "sendmessage",
- "setvol",
- "shuffle",
- "single",
- "sticker delete",
- "sticker set",
- "stop",
- "subscribe",
- "swap",
- "swapid",
- "toggleoutput",
- "unmount",
- "unsubscribe",
- "volume",
- )
- def _parse_nothing(self, lines):
- for line in lines:
- raise ProtocolError(
- "Got unexpected return value: '{}'".format(", ".join(lines))
- )
+ @mpd_commands(
+ "add",
+ "addtagid",
+ "binarylimit",
+ "clear",
+ "clearerror",
+ "cleartagid",
+ "consume",
+ "crossfade",
+ "delete",
+ "deleteid",
+ "delpartition",
+ "disableoutput",
+ "enableoutput",
+ "findadd",
+ "load",
+ "mixrampdb",
+ "mixrampdelay",
+ "mount",
+ "move",
+ "moveid",
+ "moveoutput",
+ "newpartition",
+ "next",
+ "outputvolume",
+ "partition",
+ "password",
+ "pause",
+ "ping",
+ "play",
+ "playid",
+ "playlistadd",
+ "playlistclear",
+ "playlistdelete",
+ "playlistmove",
+ "previous",
+ "prio",
+ "prioid",
+ "random",
+ "rangeid",
+ "rename",
+ "repeat",
+ "replay_gain_mode",
+ "rm",
+ "save",
+ "searchadd",
+ "searchaddpl",
+ "seek",
+ "seekcur",
+ "seekid",
+ "sendmessage",
+ "setvol",
+ "shuffle",
+ "single",
+ "sticker delete",
+ "sticker set",
+ "stop",
+ "subscribe",
+ "swap",
+ "swapid",
+ "toggleoutput",
+ "unmount",
+ "unsubscribe",
+ "volume",
+ )
+ def _parse_nothing(self, lines):
+ for line in lines:
+ raise ProtocolError(
+ "Got unexpected return value: '{}'".format(", ".join(lines))
+ )
- @mpd_commands("count", "currentsong", "readcomments", "stats", "status")
- def _parse_object(self, lines):
- objs = list(self._parse_objects(lines))
- if not objs:
- return {}
- return objs[0]
+ @mpd_commands("count", "currentsong", "readcomments", "stats", "status")
+ def _parse_object(self, lines):
+ objs = list(self._parse_objects(lines))
+ if not objs:
+ return {}
+ return objs[0]
- @mpd_commands("outputs", is_direct=True)
- def _parse_outputs(self, lines):
- return self._parse_objects_direct(lines, ["outputid"])
+ @mpd_commands("outputs", is_direct=True)
+ def _parse_outputs(self, lines):
+ return self._parse_objects_direct(lines, ["outputid"])
- @mpd_commands("playlist")
- def _parse_playlist(self, lines):
- for key, value in self._parse_pairs(lines, ":"):
- yield value
+ @mpd_commands("playlist")
+ def _parse_playlist(self, lines):
+ for key, value in self._parse_pairs(lines, ":"):
+ yield value
- @mpd_commands("listplaylists", is_direct=True)
- def _parse_playlists(self, lines):
- return self._parse_objects_direct(lines, ["playlist"])
+ @mpd_commands("listplaylists", is_direct=True)
+ def _parse_playlists(self, lines):
+ return self._parse_objects_direct(lines, ["playlist"])
- @mpd_commands("decoders", is_direct=True)
- def _parse_plugins(self, lines):
- return self._parse_objects_direct(lines, ["plugin"])
+ @mpd_commands("decoders", is_direct=True)
+ def _parse_plugins(self, lines):
+ return self._parse_objects_direct(lines, ["plugin"])
- @mpd_commands(
- "find",
- "listplaylistinfo",
- "playlistfind",
- "playlistid",
- "playlistinfo",
- "playlistsearch",
- "plchanges",
- "search",
- "sticker find",
- is_direct=True,
- )
- def _parse_songs(self, lines):
- return self._parse_objects_direct(lines, ["file"])
+ @mpd_commands(
+ "find",
+ "listplaylistinfo",
+ "playlistfind",
+ "playlistid",
+ "playlistinfo",
+ "playlistsearch",
+ "plchanges",
+ "search",
+ "sticker find",
+ is_direct=True,
+ )
+ def _parse_songs(self, lines):
+ return self._parse_objects_direct(lines, ["file"])
- @mpd_commands("sticker get")
- def _parse_sticker(self, lines):
- key, value = list(self._parse_raw_stickers(lines))[0]
- return value
+ @mpd_commands("sticker get")
+ def _parse_sticker(self, lines):
+ key, value = list(self._parse_raw_stickers(lines))[0]
+ return value
- @mpd_commands("sticker list")
- def _parse_stickers(self, lines):
- return dict(self._parse_raw_stickers(lines))
+ @mpd_commands("sticker list")
+ def _parse_stickers(self, lines):
+ return dict(self._parse_raw_stickers(lines))
- @mpd_commands("albumart", "readpicture", is_binary=True)
- def _parse_plain_binary(self, structure):
- return structure
+ @mpd_commands("albumart", "readpicture", is_binary=True)
+ def _parse_plain_binary(self, structure):
+ return structure
def _create_callback(self, function, wrap_result):
@@ -467,371 +439,348 @@ def _create_command(wrapper, name, return_value, wrap_result):
return mpd_command
-class _NotConnected(object):
- def __getattr__(self, attr):
- return self._dummy
-
- def _dummy(*args):
- raise ConnectionError("Not connected")
-
-
@mpd_command_provider
-class MPDClient(MPDClientBase):
-
- __events__ = {}
-
- connected = False
-
- try_conn_timer = None
- heart_beat_timer = None
-
- current_song_id = None
- current_state = 'stop'
+class MPDClient(MPDClientBase, GObject.Object):
- idletimeout = None
- _timeout = None
- _wrap_iterator_parsers = [
- MPDClientBase._parse_list,
- MPDClientBase._parse_list_groups,
- MPDClientBase._parse_playlist,
- MPDClientBase._parse_changes,
- MPDClientBase._parse_songs,
- MPDClientBase._parse_mounts,
- MPDClientBase._parse_neighbors,
- MPDClientBase._parse_partitions,
- MPDClientBase._parse_playlists,
- MPDClientBase._parse_database,
- MPDClientBase._parse_messages,
- MPDClientBase._parse_outputs,
- MPDClientBase._parse_plugins,
- ]
+ __gsignals__ = {
+ 'online': (GObject.SignalFlags.RUN_FIRST, None, ()),
+ 'offline': (GObject.SignalFlags.RUN_FIRST, None, ()),
+ 'playing': (GObject.SignalFlags.RUN_FIRST, None, (object, object)),
+ 'song_changed': (GObject.SignalFlags.RUN_FIRST, None, (object, object)),
+ 'state_changed': (GObject.SignalFlags.RUN_FIRST, None, (object,))
+ }
- def __init__(self, host = '127.0.0.1', port = 6600):
- super(MPDClient, self).__init__()
- self.host = host
- self.port = port
- def _reset(self):
- super(MPDClient, self)._reset()
- self.connected = False
- self._iterating = False
- self._sock = None
- self._rbfile = None
- self._wfile = None
+ # __events__ = {}
- def bind(self, event, callback):
- self.__events__[event] = callback
+ connected = False
- @run_async
- def emit(self, event, *args):
- # print('emit: ', event, args)
- callback = self.__events__.get(event)
- if callback is not None:
- callback(*args)
+ try_conn_timer = None
+ heart_beat_timer = None
- def _execute(self, command, args, retval):
- if self._iterating:
- raise IteratingError("Cannot execute '{}' while iterating".format(command))
- if self._command_list is not None:
- if not callable(retval):
- raise CommandListError(
- "'{}' not allowed in command list".format(command)
- )
- self._write_command(command, args)
- self._command_list.append(retval)
- else:
- self._write_command(command, args)
- if callable(retval):
- return retval()
- return retval
+ current_song_id = None
+ current_state = 'stop'
+
+ idletimeout = None
+ _timeout = None
+ _wrap_iterator_parsers = [
+ MPDClientBase._parse_list,
+ MPDClientBase._parse_list_groups,
+ MPDClientBase._parse_playlist,
+ MPDClientBase._parse_changes,
+ MPDClientBase._parse_songs,
+ MPDClientBase._parse_mounts,
+ MPDClientBase._parse_neighbors,
+ MPDClientBase._parse_partitions,
+ MPDClientBase._parse_playlists,
+ MPDClientBase._parse_database,
+ MPDClientBase._parse_messages,
+ MPDClientBase._parse_outputs,
+ MPDClientBase._parse_plugins,
+ ]
- def _write_line(self, line):
- try:
- if self._wfile == None:
- print('MPD server is not connected!!!')
- else:
- self._wfile.write("{}\n".format(line))
- self._wfile.flush()
+ def __init__(self, host = '127.0.0.1', port = 6600):
+ super(MPDClient, self).__init__()
+ super(GObject.Object, self).__init__()
+ self.host = host
+ self.port = port
- except socket.error as e:
- error_message = "Connection to server was reset"
- self._reset()
- e = ConnectionError(error_message)
- raise e.with_traceback(sys.exc_info()[2])
+ def _reset(self):
+ super(MPDClient, self)._reset()
+ self.connected = False
+ self._iterating = False
+ self._sock = None
+ self._rbfile = None
+ self._wfile = None
- def _write_command(self, command, args=[]):
- parts = [command]
- for arg in args:
- if type(arg) is tuple:
- if len(arg) == 0:
- parts.append('":"')
- elif len(arg) == 1:
- parts.append('"{}:"'.format(int(arg[0])))
- else:
- parts.append('"{}:{}"'.format(int(arg[0]), int(arg[1])))
- else:
- parts.append('"{}"'.format(escape(str(arg))))
- # Minimize logging cost if the logging is not activated.
-
- cmd = " ".join(parts)
- self._write_line(cmd)
+ def _execute(self, command, args, retval):
+ if self._iterating:
+ raise IteratingError("Cannot execute '{}' while iterating".format(command))
+ if self._command_list is not None:
+ if not callable(retval):
+ raise CommandListError(
+ "'{}' not allowed in command list".format(command)
+ )
+ self._write_command(command, args)
+ self._command_list.append(retval)
+ else:
+ self._write_command(command, args)
+ if callable(retval):
+ return retval()
+ return retval
- def _read_line(self):
- line = self._rbfile.readline().decode("utf-8")
- if not line.endswith("\n"):
- self.disconnect()
- raise ConnectionError("Connection lost while reading line")
- line = line.rstrip("\n")
- if line.startswith(ERROR_PREFIX):
- error = line[len(ERROR_PREFIX) :].strip()
- raise CommandError(error)
- if self._command_list is not None:
- if line == NEXT:
- return
- if line == SUCCESS:
- raise ProtocolError("Got unexpected '{}'".format(SUCCESS))
- elif line == SUCCESS:
- return
- return line
+ def _write_line(self, line):
+ try:
+ if self._wfile == None:
+ print('MPD server is not connected!!!')
+ else:
+ self._wfile.write("{}\n".format(line))
+ self._wfile.flush()
- def _read_lines(self):
- line = self._read_line()
- while line is not None:
- yield line
- line = self._read_line()
+ except socket.error as e:
+ error_message = "Connection to server was reset"
+ self._reset()
+ e = ConnectionError(error_message)
+ raise e.with_traceback(sys.exc_info()[2])
- def _read_chunk(self, amount):
- chunk = bytearray()
- while amount > 0:
- result = self._rbfile.read(amount)
- if len(result) == 0:
- break
- chunk.extend(result)
- amount -= len(result)
- return bytes(chunk)
+ def _write_command(self, command, args=[]):
+ parts = [command]
+ for arg in args:
+ if type(arg) is tuple:
+ if len(arg) == 0:
+ parts.append('":"')
+ elif len(arg) == 1:
+ parts.append('"{}:"'.format(int(arg[0])))
+ else:
+ parts.append('"{}:{}"'.format(int(arg[0]), int(arg[1])))
+ else:
+ parts.append('"{}"'.format(escape(str(arg))))
+ # Minimize logging cost if the logging is not activated.
+
+ cmd = " ".join(parts)
+ self._write_line(cmd)
- def _read_binary(self):
- """From the data stream, read Unicode lines until one says "binary:
- \\n"; at that point, read binary data of the given length.
-
- This behaves like _parse_objects (with empty set of delimiters; even
- returning only a single result), but rather than feeding from a lines
- iterable (which would be preprocessed too far), it reads directly off
- the stream."""
-
- obj = {}
-
- while True:
- line = self._read_line()
- if line is None:
- break
-
- key, value = self._parse_pair(line, ": ")
-
- if key == "binary":
- chunk_size = int(value)
- value = self._read_chunk(chunk_size)
-
- if len(value) != chunk_size:
- self.disconnect()
- raise ConnectionError(
- "Connection lost while reading binary data: "
- "expected %d bytes, got %d" % (chunk_size, len(value))
- )
-
- if self._rbfile.read(1) != b"\n":
- # newline after binary content
- self.disconnect()
- raise ConnectionError("Connection lost while reading line")
-
- obj[key] = value
- return obj
-
- def _execute_binary(self, command, args):
- """Execute a command repeatedly with an additional offset argument,
- keeping all the identical returned dictionary items and concatenating
- the binary chunks following the binary item into one of exactly size.
-
- This differs from _execute in that rather than passing the lines to the
- callback which'd then call on something like _parse_objects, it builds
- a parsed object on its own (as a prerequisite to the chunk driving
- process) and then joins together the chunks into a single big response."""
- if self._iterating or self._command_list is not None:
- raise IteratingError("Cannot execute '{}' with command lists".format(command))
- data = None
- args = list(args)
- assert len(args) == 1
- args.append(0)
- final_metadata = None
- while True:
- self._write_command(command, args)
- metadata = self._read_binary()
- chunk = metadata.pop('binary', None)
-
- if final_metadata is None:
- data = chunk
- final_metadata = metadata
- if not data:
- break
- try:
- size = int(final_metadata['size'])
- except KeyError:
- size = len(chunk)
- except ValueError:
- raise CommandError("Size data unsuitable for binary transfer")
- else:
- if metadata != final_metadata:
- raise CommandError("Metadata of binary data changed during transfer")
- if chunk is None:
- raise CommandError("Binary field vanished changed during transfer")
- data += chunk
- args[-1] = len(data)
- if len(data) > size:
- raise CommandListError("Binary data announced size exceeded")
- elif len(data) == size:
- break
-
- if data is not None:
- final_metadata['binary'] = data
-
- final_metadata.pop('size', None)
-
- return final_metadata
-
- def _read_command_list(self):
- try:
- for retval in self._command_list:
- yield retval()
- finally:
- self._command_list = None
- self._parse_nothing(self._read_lines())
-
- def _iterator_wrapper(self, iterator):
- try:
- for item in iterator:
- yield item
- finally:
- self._iterating = False
-
- def _wrap_iterator(self, iterator):
- if not self.iterate:
- return list(iterator)
- self._iterating = True
- return self._iterator_wrapper(iterator)
-
- def _check_is_mpd_server(self, line):
+ def _read_line(self):
+ line = self._rbfile.readline().decode("utf-8")
if not line.endswith("\n"):
- self.disconnect()
- return
-
+ self.end()
+ raise ConnectionError("Connection lost while reading line")
line = line.rstrip("\n")
- if not line.startswith(HELLO_PREFIX):
- return
-
- self.connected = True
- self.mpd_version = line[len(HELLO_PREFIX) :].strip()
+ if line.startswith(ERROR_PREFIX):
+ error = line[len(ERROR_PREFIX) :].strip()
+ raise CommandError(error)
+ if self._command_list is not None:
+ if line == NEXT:
+ return
+ if line == SUCCESS:
+ raise ProtocolError("Got unexpected '{}'".format(SUCCESS))
+ elif line == SUCCESS:
+ return
+ return line
- def _connect_unix(self):
- if not hasattr(socket, "AF_UNIX"):
- raise ConnectionError("Unix domain sockets not supported on this platform")
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- sock.settimeout(self.timeout)
- sock.connect(self.host)
- return sock
+ def _read_lines(self):
+ line = self._read_line()
+ while line is not None:
+ yield line
+ line = self._read_line()
- def _connect_tcp(self):
+ def _read_chunk(self, amount):
+ chunk = bytearray()
+ while amount > 0:
+ result = self._rbfile.read(amount)
+ if len(result) == 0:
+ break
+ chunk.extend(result)
+ amount -= len(result)
+ return bytes(chunk)
- try:
- flags = socket.AI_ADDRCONFIG
- except AttributeError:
- flags = 0
-
- for res in socket.getaddrinfo(
- self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags
- ):
- af, socktype, proto, canonname, sa = res
- sock = None
- try:
- sock = socket.socket(af, socktype, proto)
- sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
- sock.settimeout(self.timeout)
- sock.connect(sa)
- return sock
- except Exception as e:
-
- if e.strerror == 'Connection refused':
- self.emit('offline')
- else:
- self.emit('error', e)
+ def _read_binary(self):
+ """From the data stream, read Unicode lines until one says "binary:
+ \\n"; at that point, read binary data of the given length.
- if sock is not None:
- sock.close()
+ This behaves like _parse_objects (with empty set of delimiters; even
+ returning only a single result), but rather than feeding from a lines
+ iterable (which would be preprocessed too far), it reads directly off
+ the stream."""
+
+ obj = {}
+
+ while True:
+ line = self._read_line()
+ if line is None:
+ break
+
+ key, value = self._parse_pair(line, ": ")
+
+ if key == "binary":
+ chunk_size = int(value)
+ value = self._read_chunk(chunk_size)
+
+ if len(value) != chunk_size:
+ self.end()
+ raise ConnectionError(
+ "Connection lost while reading binary data: "
+ "expected %d bytes, got %d" % (chunk_size, len(value))
+ )
+
+ if self._rbfile.read(1) != b"\n":
+ # newline after binary content
+ self.end()
+ raise ConnectionError("Connection lost while reading line")
+
+ obj[key] = value
+ return obj
+
+ def _execute_binary(self, command, args):
+ """Execute a command repeatedly with an additional offset argument,
+ keeping all the identical returned dictionary items and concatenating
+ the binary chunks following the binary item into one of exactly size.
+
+ This differs from _execute in that rather than passing the lines to the
+ callback which'd then call on something like _parse_objects, it builds
+ a parsed object on its own (as a prerequisite to the chunk driving
+ process) and then joins together the chunks into a single big response."""
+ if self._iterating or self._command_list is not None:
+ raise IteratingError("Cannot execute '{}' with command lists".format(command))
+ data = None
+ args = list(args)
+ assert len(args) == 1
+ args.append(0)
+ final_metadata = None
+ while True:
+ self._write_command(command, args)
+ metadata = self._read_binary()
+ chunk = metadata.pop('binary', None)
+
+ if final_metadata is None:
+ data = chunk
+ final_metadata = metadata
+ if not data:
+ break
+ try:
+ size = int(final_metadata['size'])
+ except KeyError:
+ size = len(chunk)
+ except ValueError:
+ raise CommandError("Size data unsuitable for binary transfer")
+ else:
+ if metadata != final_metadata:
+ raise CommandError("Metadata of binary data changed during transfer")
+ if chunk is None:
+ raise CommandError("Binary field vanished changed during transfer")
+ data += chunk
+ args[-1] = len(data)
+ if len(data) > size:
+ raise CommandListError("Binary data announced size exceeded")
+ elif len(data) == size:
+ break
+
+ if data is not None:
+ final_metadata['binary'] = data
+
+ final_metadata.pop('size', None)
+
+ return final_metadata
+
+ def _read_command_list(self):
+ try:
+ for retval in self._command_list:
+ yield retval()
+ finally:
+ self._command_list = None
+ self._parse_nothing(self._read_lines())
+
+ def _iterator_wrapper(self, iterator):
+ try:
+ for item in iterator:
+ yield item
+ finally:
+ self._iterating = False
+
+ def _wrap_iterator(self, iterator):
+ if not self.iterate:
+ return list(iterator)
+ self._iterating = True
+ return self._iterator_wrapper(iterator)
+
+ def _check_is_mpd_server(self, line):
+ if not line.endswith("\n"):
+ self.end()
+ return
+
+ line = line.rstrip("\n")
+ if not line.startswith(HELLO_PREFIX):
+ return
+
+ self.connected = True
+ self.mpd_version = line[len(HELLO_PREFIX) :].strip()
+
+ def _connect_unix(self):
+ if not hasattr(socket, "AF_UNIX"):
+ raise ConnectionError("Unix domain sockets not supported on this platform")
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.settimeout(self.timeout)
+ sock.connect(self.host)
+ return sock
+
+ def _connect_tcp(self):
+
+ try:
+ flags = socket.AI_ADDRCONFIG
+ except AttributeError:
+ flags = 0
+
+ for res in socket.getaddrinfo(
+ self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags
+ ):
+ af, socktype, proto, canonname, sa = res
+ sock = None
+ try:
+ sock = socket.socket(af, socktype, proto)
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
+ sock.settimeout(self.timeout)
+ sock.connect(sa)
+ return sock
+ except Exception as e:
+
+ if e.strerror == 'Connection refused':
+ self.emit('offline', True)
+ else:
+ self.emit('error', e)
+
+ if sock is not None:
+ sock.close()
- @mpd_commands("idle")
- def _parse_idle(self, lines):
- self._sock.settimeout(self.idletimeout)
- ret = self._wrap_iterator(self._parse_list(lines))
- self._sock.settimeout(self._timeout)
- return ret
+ @mpd_commands("idle")
+ def _parse_idle(self, lines):
+ self._sock.settimeout(self.idletimeout)
+ ret = self._wrap_iterator(self._parse_list(lines))
+ self._sock.settimeout(self._timeout)
+ return ret
- @property
- def timeout(self):
- return self._timeout
-
- @timeout.setter
- def timeout(self, timeout):
- self._timeout = timeout
- if self._sock is not None:
- self._sock.settimeout(timeout)
-
- @set_timeout(2)
- def _try_connect(self):
+ @property
+ def timeout(self):
+ return self._timeout
+ @timeout.setter
+ def timeout(self, timeout):
+ self._timeout = timeout
if self._sock is not None:
- raise ConnectionError("Already connected")
+ self._sock.settimeout(timeout)
+
+
+ def _try_connect(self):
+
+ while True:
+ if self._sock is not None:
+ break
if self.host.startswith(("/", "\0")):
self._sock = self._connect_unix()
else:
self._sock = self._connect_tcp()
-
- # - Force UTF-8 encoding, since this is dependant from the LC_CTYPE
- # locale.
- # - by setting newline explicit, we force to send '\n' also on
- # windows
- if self._sock is None:
- self.connected = False
- self.connect()
- return
-
- self._rbfile = self._sock.makefile("rb", newline="\n")
- self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n")
+ if self._sock is not None:
+ self._rbfile = self._sock.makefile("rb", newline="\n")
+ self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n")
- try:
- helloline = self._rbfile.readline().decode("utf-8")
- self._check_is_mpd_server(helloline)
- if self.connected:
- self.emit('online')
- self.heart_beat_timer = self.heart_beat()
- else:
- self.emit('error', ProtocolError('Connected server is not mpd server.'))
+ try:
+ helloline = self._rbfile.readline().decode("utf-8")
+ self._check_is_mpd_server(helloline)
+ if self.connected:
+ break
- except Exception as e:
- self.connected = False
- self.emit('error', e)
- self.disconnect()
+ except Exception as e:
+ self.connected = False
+ self.end()
- @set_timeout(0.2)
- def heart_beat(self):
-
- if self.heart_beat_timer is not None:
- self.heart_beat_timer.cancel()
+ def heart_beat(self):
+ while True:
if self.connected:
try:
status = self.status()
@@ -852,72 +801,75 @@ class MPDClient(MPDClientBase):
if state == 'play':
self.emit('playing', status, song)
-
- self.heart_beat_timer = self.heart_beat()
+ time.sleep(0.2)
except Exception as err:
print(err)
- self.disconnect()
- self.connect()
+
+ else:
+ break
- def connect(self):
- self.try_conn_timer = self._try_connect()
+ self.end()
+ self.start()
+
- def destroy(self):
- if self.try_conn_timer is not None:
- self.try_conn_timer.cancel()
- if self.heart_beat_timer is not None:
- self.heart_beat_timer.cancel()
-
- self._reset()
+ def start(self):
+ self._try_connect()
+ if self.connected:
+ self.emit('online')
+ self.heart_beat()
- def disconnect(self):
- if self._rbfile is not None:
- self._rbfile.close()
- if self._wfile is not None:
- self._wfile.close()
- if self._sock is not None:
- self._sock.close()
- self._reset()
+ def destroy(self):
+ self.end()
- def fileno(self):
- if self._sock is None:
- raise ConnectionError("Not connected")
- return self._sock.fileno()
- def command_list_ok_begin(self):
- if self._command_list is not None:
- raise CommandListError("Already in command list")
- if self._iterating:
- raise IteratingError("Cannot begin command list while iterating")
- self._write_command("command_list_ok_begin")
- self._command_list = []
+ def end(self):
+ if self._rbfile is not None:
+ self._rbfile.close()
+ if self._wfile is not None:
+ self._wfile.close()
+ if self._sock is not None:
+ self._sock.close()
+ self._reset()
- def command_list_end(self):
- if self._command_list is None:
- raise CommandListError("Not in command list")
- if self._iterating:
- raise IteratingError("Already iterating over a command list")
- self._write_command("command_list_end")
- return self._wrap_iterator(self._read_command_list())
+ def fileno(self):
+ if self._sock is None:
+ raise ConnectionError("Not connected")
+ return self._sock.fileno()
- @classmethod
- def add_command(cls, name, callback):
- wrap_result = callback in cls._wrap_iterator_parsers
- if callback.mpd_commands_binary:
- method = lambda self, *args: callback(self, cls._execute_binary(self, name, args))
- else:
- method = _create_command(cls._execute, name, callback, wrap_result)
- # create new mpd commands as function:
- escaped_name = name.replace(" ", "_")
- setattr(cls, escaped_name, method)
+ def command_list_ok_begin(self):
+ if self._command_list is not None:
+ raise CommandListError("Already in command list")
+ if self._iterating:
+ raise IteratingError("Cannot begin command list while iterating")
+ self._write_command("command_list_ok_begin")
+ self._command_list = []
- @classmethod
- def remove_command(cls, name):
- if not hasattr(cls, name):
- raise ValueError("Can't remove not existent '{}' command".format(name))
- name = name.replace(" ", "_")
- delattr(cls, str(name))
+ def command_list_end(self):
+ if self._command_list is None:
+ raise CommandListError("Not in command list")
+ if self._iterating:
+ raise IteratingError("Already iterating over a command list")
+ self._write_command("command_list_end")
+ return self._wrap_iterator(self._read_command_list())
+
+ @classmethod
+ def add_command(cls, name, callback):
+ wrap_result = callback in cls._wrap_iterator_parsers
+ if callback.mpd_commands_binary:
+ method = lambda self, *args: callback(self, cls._execute_binary(self, name, args))
+ else:
+ method = _create_command(cls._execute, name, callback, wrap_result)
+ # create new mpd commands as function:
+ escaped_name = name.replace(" ", "_")
+ setattr(cls, escaped_name, method)
+
+ @classmethod
+ def remove_command(cls, name):
+ if not hasattr(cls, name):
+ raise ValueError("Can't remove not existent '{}' command".format(name))
+ name = name.replace(" ", "_")
+ delattr(cls, str(name))
# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79:
diff --git a/window.py b/window.py
index 7d64db5..f1026e9 100644
--- a/window.py
+++ b/window.py
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
-import gi, sys, os, mutagen
+import gi, sys, os, mutagen, threading
# from pprint import pprint as print
gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
+from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject
from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf
@@ -18,7 +18,11 @@ from ui.timebar import Timebar
from ui.option_menu import OptionMenu
-
+# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时)
+def idle(func):
+ def wrapper(*args):
+ GObject.idle_add(func, *args)
+ return wrapper
class SonistWindow(Gtk.Window):
@@ -34,12 +38,11 @@ class SonistWindow(Gtk.Window):
self.connect("destroy", self.quited)
- self.mpd.bind('offline', lambda : self.reset_player())
- self.mpd.bind('online', lambda : self.sync_state(None, None, True))
- self.mpd.bind('state_changed', lambda stat: self.update_play_stat(stat == 'play'))
- self.mpd.bind('song_changed', lambda stat, song: self.sync_state(stat, song, False))
- self.mpd.bind('playing', lambda stat, song: self.update_playtime(stat))
-
+ self.mpd.connect('offline', lambda o: self.reset_player())
+ self.mpd.connect('online', lambda o: self.sync_state(None, None, True))
+ self.mpd.connect('state_changed', lambda o, stat: self.update_play_stat(stat == 'play'))
+ self.mpd.connect('song_changed', lambda o, stat, song: self.sync_state(stat, song, False))
+ self.mpd.connect('playing', lambda o, stat, song: self.update_playtime(stat))
self.set_name('SonistWindow')
self.set_default_size(320, 384)
@@ -108,6 +111,9 @@ class SonistWindow(Gtk.Window):
self.add(layout)
+
+
+
def get_mpd_stat(self):
try:
@@ -201,7 +207,7 @@ class SonistWindow(Gtk.Window):
except:
pass
-
+ @idle
def update_play_stat(self, played = True):
if not self.mpd.connected:
return
@@ -214,13 +220,16 @@ class SonistWindow(Gtk.Window):
# 切换播放按钮状态
self.ctrl_box.toggle_play_btn(played)
-
+ @idle
def update_playtime(self, stat = {}):
times = stat['time'].split(':')
self.timebar.update_time(int(times[0]), int(times[1]))
-
+
+ @idle
def sync_state(self, stat = None, song = None, first = False):
self.ctrl_box.disabled = False
+ print(threading.active_count())
+ print(threading.current_thread())
self.stat = stat or self.get_mpd_stat()
@@ -262,16 +271,19 @@ class SonistWindow(Gtk.Window):
except:
pass
+ @idle
def reset_player(self):
self.ctrl_box.disabled = True
self.title_box.set_text('mpd is offline...')
self.timebar.update_time()
self.update_album('./usr/share/sonist/avatar.jpg')
+ @idle
def update_album(self, filepath):
self.set_background_image(filepath)
self.album.reset(filepath).set_radius(64)
+
def quited(self, win):
self.app.remove_window(self)