优化线程通讯, mpd改为基于Gtk的信号槽机制通讯

master
yutent 2023-08-24 14:33:20 +08:00
parent 1b4834d022
commit 271a8838fb
3 changed files with 643 additions and 677 deletions

24
main.py
View File

@ -19,6 +19,7 @@ app_id = 'fun.wkit.sonist'
home_dir = os.getenv('HOME') home_dir = os.getenv('HOME')
# 定义一个异步修饰器, 用于在子线程中运行一些会阻塞主线程的任务
def run_async(func): def run_async(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs) thread = threading.Thread(target=func, args=args, kwargs=kwargs)
@ -27,7 +28,7 @@ def run_async(func):
return thread return thread
return wrapper return wrapper
# 类型js的settimeout的修饰器
def set_timeout(timeout = 0.5): def set_timeout(timeout = 0.5):
def decorator(callback): def decorator(callback):
def wrapper(*args): def wrapper(*args):
@ -39,6 +40,7 @@ def set_timeout(timeout = 0.5):
return decorator return decorator
def get_music_dir(): def get_music_dir():
with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f: with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f:
data = f.read() data = f.read()
@ -52,38 +54,38 @@ def get_music_dir():
class Application(Gtk.Application): 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): def __init__(self):
Gtk.Application.__init__(self, application_id = app_id) Gtk.Application.__init__(self, application_id = app_id)
self.timer = None self.timer = None
self.mpd = MPDClient()
self.music_dir = get_music_dir() self.music_dir = get_music_dir()
self.mpd = MPDClient()
self.connect('window-removed', self.on_window_removed) self.connect('window-removed', self.on_window_removed)
self.mpd.connect()
@run_async
def connect_mpd(self):
self.mpd.start()
def do_activate(self): def do_activate(self):
print('hello mpc') print('hello mpc')
self.set_app_menu(None) self.set_app_menu(None)
self.set_menubar(None) self.set_menubar(None)
self.window = SonistWindow(self) self.window = SonistWindow(self)
self.about = AboutWindow() self.about = AboutWindow()
self.add_window(self.window) self.add_window(self.window)
self.window.show_all() self.window.show_all()
# self.about.show_all() # self.about.show_all()
self.connect_mpd()
def on_window_removed(self, app, win): def on_window_removed(self, app, win):
if len(self.get_windows()) == 0: if len(self.get_windows()) == 0:

144
mpd.py
View File

@ -17,10 +17,12 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with python-mpd2. If not, see <http://www.gnu.org/licenses/>. # along with python-mpd2. If not, see <http://www.gnu.org/licenses/>.
import re, socket, sys, warnings, threading, time import gi
import re, socket, sys, warnings, time
from enum import Enum from enum import Enum
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
VERSION = (3, 1, 0) VERSION = (3, 1, 0)
HELLO_PREFIX = "OK MPD " HELLO_PREFIX = "OK MPD "
@ -30,24 +32,6 @@ SUCCESS = "OK"
NEXT = "list_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): def escape(text):
return text.replace("\\", "\\\\").replace('"', '\\"') return text.replace("\\", "\\\\").replace('"', '\\"')
@ -72,19 +56,11 @@ class FailureResponseCode(Enum):
EXIST = 56 EXIST = 56
class MPDError(Exception): class ProtocolError(Exception):
pass pass
class ConnectionError(MPDError): class CommandError(Exception):
pass
class ProtocolError(MPDError):
pass
class CommandError(MPDError):
def __init__(self, error): def __init__(self, error):
self.errno = None self.errno = None
self.offset = None self.offset = None
@ -99,15 +75,11 @@ class CommandError(MPDError):
self.msg = match.group("msg") self.msg = match.group("msg")
class CommandListError(MPDError): class CommandListError(Exception):
pass pass
class PendingCommandError(MPDError): class IteratingError(Exception):
pass
class IteratingError(MPDError):
pass pass
@ -171,7 +143,7 @@ class Noop(object):
mpd_commands = None mpd_commands = None
class MPDClientBase(object): class MPDClientBase():
"""Abstract MPD client. """Abstract MPD client.
This class defines a general client contract, provides MPD protocol parsers This class defines a general client contract, provides MPD protocol parsers
@ -467,18 +439,19 @@ def _create_command(wrapper, name, return_value, wrap_result):
return mpd_command return mpd_command
class _NotConnected(object):
def __getattr__(self, attr):
return self._dummy
def _dummy(*args):
raise ConnectionError("Not connected")
@mpd_command_provider @mpd_command_provider
class MPDClient(MPDClientBase): class MPDClient(MPDClientBase, GObject.Object):
__events__ = {} __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,))
}
# __events__ = {}
connected = False connected = False
@ -508,6 +481,7 @@ class MPDClient(MPDClientBase):
def __init__(self, host = '127.0.0.1', port = 6600): def __init__(self, host = '127.0.0.1', port = 6600):
super(MPDClient, self).__init__() super(MPDClient, self).__init__()
super(GObject.Object, self).__init__()
self.host = host self.host = host
self.port = port self.port = port
@ -519,16 +493,6 @@ class MPDClient(MPDClientBase):
self._rbfile = None self._rbfile = None
self._wfile = None self._wfile = None
def bind(self, event, callback):
self.__events__[event] = callback
@run_async
def emit(self, event, *args):
# print('emit: ', event, args)
callback = self.__events__.get(event)
if callback is not None:
callback(*args)
def _execute(self, command, args, retval): def _execute(self, command, args, retval):
if self._iterating: if self._iterating:
raise IteratingError("Cannot execute '{}' while iterating".format(command)) raise IteratingError("Cannot execute '{}' while iterating".format(command))
@ -579,7 +543,7 @@ class MPDClient(MPDClientBase):
def _read_line(self): def _read_line(self):
line = self._rbfile.readline().decode("utf-8") line = self._rbfile.readline().decode("utf-8")
if not line.endswith("\n"): if not line.endswith("\n"):
self.disconnect() self.end()
raise ConnectionError("Connection lost while reading line") raise ConnectionError("Connection lost while reading line")
line = line.rstrip("\n") line = line.rstrip("\n")
if line.startswith(ERROR_PREFIX): if line.startswith(ERROR_PREFIX):
@ -633,7 +597,7 @@ class MPDClient(MPDClientBase):
value = self._read_chunk(chunk_size) value = self._read_chunk(chunk_size)
if len(value) != chunk_size: if len(value) != chunk_size:
self.disconnect() self.end()
raise ConnectionError( raise ConnectionError(
"Connection lost while reading binary data: " "Connection lost while reading binary data: "
"expected %d bytes, got %d" % (chunk_size, len(value)) "expected %d bytes, got %d" % (chunk_size, len(value))
@ -641,7 +605,7 @@ class MPDClient(MPDClientBase):
if self._rbfile.read(1) != b"\n": if self._rbfile.read(1) != b"\n":
# newline after binary content # newline after binary content
self.disconnect() self.end()
raise ConnectionError("Connection lost while reading line") raise ConnectionError("Connection lost while reading line")
obj[key] = value obj[key] = value
@ -721,7 +685,7 @@ class MPDClient(MPDClientBase):
def _check_is_mpd_server(self, line): def _check_is_mpd_server(self, line):
if not line.endswith("\n"): if not line.endswith("\n"):
self.disconnect() self.end()
return return
line = line.rstrip("\n") line = line.rstrip("\n")
@ -761,7 +725,7 @@ class MPDClient(MPDClientBase):
except Exception as e: except Exception as e:
if e.strerror == 'Connection refused': if e.strerror == 'Connection refused':
self.emit('offline') self.emit('offline', True)
else: else:
self.emit('error', e) self.emit('error', e)
@ -786,11 +750,12 @@ class MPDClient(MPDClientBase):
if self._sock is not None: if self._sock is not None:
self._sock.settimeout(timeout) self._sock.settimeout(timeout)
@set_timeout(2)
def _try_connect(self): def _try_connect(self):
while True:
if self._sock is not None: if self._sock is not None:
raise ConnectionError("Already connected") break
if self.host.startswith(("/", "\0")): if self.host.startswith(("/", "\0")):
self._sock = self._connect_unix() self._sock = self._connect_unix()
@ -798,16 +763,7 @@ class MPDClient(MPDClientBase):
self._sock = self._connect_tcp() self._sock = self._connect_tcp()
# - Force UTF-8 encoding, since this is dependant from the LC_CTYPE if self._sock is not None:
# 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._rbfile = self._sock.makefile("rb", newline="\n")
self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n") self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n")
@ -815,23 +771,16 @@ class MPDClient(MPDClientBase):
helloline = self._rbfile.readline().decode("utf-8") helloline = self._rbfile.readline().decode("utf-8")
self._check_is_mpd_server(helloline) self._check_is_mpd_server(helloline)
if self.connected: if self.connected:
self.emit('online') break
self.heart_beat_timer = self.heart_beat()
else:
self.emit('error', ProtocolError('Connected server is not mpd server.'))
except Exception as e: except Exception as e:
self.connected = False self.connected = False
self.emit('error', e) self.end()
self.disconnect()
@set_timeout(0.2)
def heart_beat(self): def heart_beat(self):
if self.heart_beat_timer is not None: while True:
self.heart_beat_timer.cancel()
if self.connected: if self.connected:
try: try:
status = self.status() status = self.status()
@ -852,26 +801,29 @@ class MPDClient(MPDClientBase):
if state == 'play': if state == 'play':
self.emit('playing', status, song) self.emit('playing', status, song)
time.sleep(0.2)
self.heart_beat_timer = self.heart_beat()
except Exception as err: except Exception as err:
print(err) print(err)
self.disconnect()
self.connect()
def connect(self): else:
self.try_conn_timer = self._try_connect() break
self.end()
self.start()
def start(self):
self._try_connect()
if self.connected:
self.emit('online')
self.heart_beat()
def destroy(self): def destroy(self):
if self.try_conn_timer is not None: self.end()
self.try_conn_timer.cancel()
if self.heart_beat_timer is not None:
self.heart_beat_timer.cancel()
self._reset()
def disconnect(self): def end(self):
if self._rbfile is not None: if self._rbfile is not None:
self._rbfile.close() self._rbfile.close()
if self._wfile is not None: if self._wfile is not None:

View File

@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import gi, sys, os, mutagen import gi, sys, os, mutagen, threading
# from pprint import pprint as print # from pprint import pprint as print
gi.require_version('Gtk', '3.0') 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 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 from ui.option_menu import OptionMenu
# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时)
def idle(func):
def wrapper(*args):
GObject.idle_add(func, *args)
return wrapper
class SonistWindow(Gtk.Window): class SonistWindow(Gtk.Window):
@ -34,12 +38,11 @@ class SonistWindow(Gtk.Window):
self.connect("destroy", self.quited) self.connect("destroy", self.quited)
self.mpd.bind('offline', lambda : self.reset_player()) self.mpd.connect('offline', lambda o: self.reset_player())
self.mpd.bind('online', lambda : self.sync_state(None, None, True)) self.mpd.connect('online', lambda o: self.sync_state(None, None, True))
self.mpd.bind('state_changed', lambda stat: self.update_play_stat(stat == 'play')) self.mpd.connect('state_changed', lambda o, stat: self.update_play_stat(stat == 'play'))
self.mpd.bind('song_changed', lambda stat, song: self.sync_state(stat, song, False)) self.mpd.connect('song_changed', lambda o, stat, song: self.sync_state(stat, song, False))
self.mpd.bind('playing', lambda stat, song: self.update_playtime(stat)) self.mpd.connect('playing', lambda o, stat, song: self.update_playtime(stat))
self.set_name('SonistWindow') self.set_name('SonistWindow')
self.set_default_size(320, 384) self.set_default_size(320, 384)
@ -109,6 +112,9 @@ class SonistWindow(Gtk.Window):
self.add(layout) self.add(layout)
def get_mpd_stat(self): def get_mpd_stat(self):
try: try:
self.stat = self.mpd.status() self.stat = self.mpd.status()
@ -201,7 +207,7 @@ class SonistWindow(Gtk.Window):
except: except:
pass pass
@idle
def update_play_stat(self, played = True): def update_play_stat(self, played = True):
if not self.mpd.connected: if not self.mpd.connected:
return return
@ -214,13 +220,16 @@ class SonistWindow(Gtk.Window):
# 切换播放按钮状态 # 切换播放按钮状态
self.ctrl_box.toggle_play_btn(played) self.ctrl_box.toggle_play_btn(played)
@idle
def update_playtime(self, stat = {}): def update_playtime(self, stat = {}):
times = stat['time'].split(':') times = stat['time'].split(':')
self.timebar.update_time(int(times[0]), int(times[1])) self.timebar.update_time(int(times[0]), int(times[1]))
@idle
def sync_state(self, stat = None, song = None, first = False): def sync_state(self, stat = None, song = None, first = False):
self.ctrl_box.disabled = False self.ctrl_box.disabled = False
print(threading.active_count())
print(threading.current_thread())
self.stat = stat or self.get_mpd_stat() self.stat = stat or self.get_mpd_stat()
@ -262,16 +271,19 @@ class SonistWindow(Gtk.Window):
except: except:
pass pass
@idle
def reset_player(self): def reset_player(self):
self.ctrl_box.disabled = True self.ctrl_box.disabled = True
self.title_box.set_text('mpd is offline...') self.title_box.set_text('mpd is offline...')
self.timebar.update_time() self.timebar.update_time()
self.update_album('./usr/share/sonist/avatar.jpg') self.update_album('./usr/share/sonist/avatar.jpg')
@idle
def update_album(self, filepath): def update_album(self, filepath):
self.set_background_image(filepath) self.set_background_image(filepath)
self.album.reset(filepath).set_radius(64) self.album.reset(filepath).set_radius(64)
def quited(self, win): def quited(self, win):
self.app.remove_window(self) self.app.remove_window(self)