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)