diff --git a/main.py b/main.py
index f3dcf42..7105fd5 100755
--- a/main.py
+++ b/main.py
@@ -13,7 +13,7 @@ from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject
from window import SonistWindow
from about_app import AboutWindow
-from mpd.base import MPDClient
+from mpd import MPDClient
app_id = 'fun.wkit.sonist'
home_dir = os.getenv('HOME')
@@ -28,6 +28,17 @@ def run_async(func):
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 get_music_dir():
with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f:
data = f.read()
@@ -51,8 +62,7 @@ class Application(Gtk.Application):
def __init__(self):
Gtk.Application.__init__(self, application_id = app_id)
- self.mpd_state = None
- self.mpd_curr_song = None
+ self.timer = None
self.mpd = MPDClient()
@@ -60,44 +70,7 @@ class Application(Gtk.Application):
self.connect('window-removed', self.on_window_removed)
-
-
-
- @run_async
- def ping(self):
- if self.mpd_is_online:
- while True:
- try:
- self.mpd.ping()
- stat = self.mpd.status()
- song = self.mpd.currentsong() or {}
- state = stat.get('state')
-
- if self.mpd_curr_song != song.get('id'):
- self.mpd_curr_song = song.get('id')
- self.emit('song_changed', self.mpd_curr_song)
-
- if state == 'play':
- self.emit('playing', False)
-
- if self.mpd_state != state:
- self.emit('state_changed', state)
-
- self.mpd_state = state
- time.sleep(0.5)
-
- except Exception as e:
- self.mpd.kill()
- time.sleep(2)
- self.mpd_is_online = self.mpd_connect()
-
-
- def mpd_connect(self):
- try:
- self.mpd.connect("127.0.0.1", 6600)
- return True
- except:
- return False
+ self.mpd.connect()
def do_activate(self):
@@ -105,24 +78,22 @@ class Application(Gtk.Application):
self.set_app_menu(None)
self.set_menubar(None)
- self.mpd_is_online = self.mpd_connect()
-
self.window = SonistWindow(self)
self.about = AboutWindow()
self.add_window(self.window)
self.window.show_all()
# self.about.show_all()
- self.ping()
-
def on_window_removed(self, app, win):
if len(self.get_windows()) == 0:
+ if self.timer is not None:
+ self.timer.cancel()
+ self.mpd.destroy()
print('朕要休息了~~~')
-
""" class ApplicationService(dbus.service.Object):
def __init__(self, app):
self.app = app
diff --git a/mpd/base.py b/mpd.py
similarity index 82%
rename from mpd/base.py
rename to mpd.py
index 56dfc8f..4cc467c 100644
--- a/mpd/base.py
+++ b/mpd.py
@@ -17,15 +17,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with python-mpd2. If not, see .
-import logging
-import re
-import socket
-import sys
-import warnings
-
+import re, socket, sys, warnings, threading, time
from enum import Enum
+
VERSION = (3, 1, 0)
HELLO_PREFIX = "OK MPD "
ERROR_PREFIX = "ACK "
@@ -34,22 +30,29 @@ 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('"', '\\"')
-try:
- from logging import NullHandler
-except ImportError: # NullHandler was introduced in python2.7
-
- class NullHandler(logging.Handler):
- def emit(self, record):
- pass
-
-
-logger = logging.getLogger(__name__)
-logger.addHandler(NullHandler())
-
# MPD Protocol errors as found in CommandError exceptions
# https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx
@@ -177,26 +180,10 @@ class MPDClientBase(object):
subclasses.
"""
- def __init__(self, use_unicode=None):
+ def __init__(self):
self.iterate = False
- if use_unicode is not None:
- warnings.warn(
- "use_unicode parameter to ``MPDClientBase`` constructor is "
- "deprecated",
- DeprecationWarning,
- stacklevel=2,
- )
self._reset()
- @property
- def use_unicode(self):
- warnings.warn(
- "``use_unicode`` is deprecated: python-mpd 2.x always uses "
- "Unicode",
- DeprecationWarning,
- stacklevel=2,
- )
- return True
@classmethod
def add_command(cls, name, callback):
@@ -490,6 +477,17 @@ class _NotConnected(object):
@mpd_command_provider
class MPDClient(MPDClientBase):
+
+ __events__ = {}
+
+ connected = False
+
+ try_conn_timer = None
+ heart_beat_timer = None
+
+ current_song_id = None
+ current_state = 'stop'
+
idletimeout = None
_timeout = None
_wrap_iterator_parsers = [
@@ -508,22 +506,28 @@ class MPDClient(MPDClientBase):
MPDClientBase._parse_plugins,
]
- def __init__(self, use_unicode=None):
- if use_unicode is not None:
- warnings.warn(
- "use_unicode parameter to ``MPDClient`` constructor is "
- "deprecated",
- DeprecationWarning,
- stacklevel=2,
- )
- super(MPDClient, self).__init__()
+ 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._iterating = False
- self._sock = None
- self._rbfile = _NotConnected()
- self._wfile = _NotConnected()
+ super(MPDClient, self)._reset()
+ self.connected = False
+ self._iterating = False
+ self._sock = None
+ self._rbfile = 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):
if self._iterating:
@@ -542,15 +546,18 @@ class MPDClient(MPDClientBase):
return retval
def _write_line(self, line):
- try:
- self._wfile.write("{}\n".format(line))
- self._wfile.flush()
- except socket.error as e:
- error_message = "Connection to server was reset"
- logger.info(error_message)
- self._reset()
- e = ConnectionError(error_message)
- raise e.with_traceback(sys.exc_info()[2])
+ try:
+ if self._wfile == None:
+ print('MPD server is not connected!!!')
+ else:
+ self._wfile.write("{}\n".format(line))
+ self._wfile.flush()
+
+ 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 _write_command(self, command, args=[]):
parts = [command]
@@ -565,11 +572,7 @@ class MPDClient(MPDClientBase):
else:
parts.append('"{}"'.format(escape(str(arg))))
# Minimize logging cost if the logging is not activated.
- if logger.isEnabledFor(logging.DEBUG):
- if command == "password":
- logger.debug("Calling MPD password(******)")
- else:
- logger.debug("Calling MPD %s%r", command, args)
+
cmd = " ".join(parts)
self._write_line(cmd)
@@ -716,31 +719,35 @@ class MPDClient(MPDClientBase):
self._iterating = True
return self._iterator_wrapper(iterator)
- def _hello(self, line):
- if not line.endswith("\n"):
- self.disconnect()
- raise ConnectionError("Connection lost while reading MPD hello")
- line = line.rstrip("\n")
- if not line.startswith(HELLO_PREFIX):
- raise ProtocolError("Got invalid MPD hello: '{}'".format(line))
- self.mpd_version = line[len(HELLO_PREFIX) :].strip()
+ def _check_is_mpd_server(self, line):
+ if not line.endswith("\n"):
+ self.disconnect()
+ 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, path):
+ 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(path)
+ sock.connect(self.host)
return sock
- def _connect_tcp(self, host, port):
+ def _connect_tcp(self):
+
try:
flags = socket.AI_ADDRCONFIG
except AttributeError:
flags = 0
- err = None
+
for res in socket.getaddrinfo(
- host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags
+ self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, flags
):
af, socktype, proto, canonname, sa = res
sock = None
@@ -751,14 +758,16 @@ class MPDClient(MPDClientBase):
sock.settimeout(self.timeout)
sock.connect(sa)
return sock
- except socket.error as e:
- err = e
+ except Exception as e:
+
+ if e.strerror == 'Connection refused':
+ self.emit('offline')
+ else:
+ self.emit('error', e)
+
if sock is not None:
sock.close()
- if err is not None:
- raise err
- else:
- raise ConnectionError("getaddrinfo returns an empty list")
+
@mpd_commands("idle")
def _parse_idle(self, lines):
@@ -777,51 +786,99 @@ class MPDClient(MPDClientBase):
if self._sock is not None:
self._sock.settimeout(timeout)
- def connect(self, host, port=None, timeout=None):
- logger.info("Calling MPD connect(%r, %r, timeout=%r)", host, port, timeout)
- if self._sock is not None:
- raise ConnectionError("Already connected")
- if timeout is not None:
- warnings.warn(
- "The timeout parameter in connect() is deprecated! "
- "Use MPDClient.timeout = yourtimeout instead.",
- DeprecationWarning,
- )
- self.timeout = timeout
- if host.startswith("@"):
- host = "\0" + host[1:]
- if host.startswith(("/", "\0")):
- self._sock = self._connect_unix(host)
+ @set_timeout(2)
+ def _try_connect(self):
+
+ if self._sock is not None:
+ raise ConnectionError("Already connected")
+
+ 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")
+
+ 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:
- if port is None:
- raise ValueError(
- "port argument must be specified when connecting via tcp"
- )
- self._sock = self._connect_tcp(host, port)
+ self.emit('error', ProtocolError('Connected server is not mpd server.'))
- # - 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
- self._rbfile = self._sock.makefile("rb", newline="\n")
- self._wfile = self._sock.makefile("w", encoding="utf-8", newline="\n")
+ except Exception as e:
+ self.connected = False
+ self.emit('error', e)
+ self.disconnect()
+
+ @set_timeout(0.2)
+ def heart_beat(self):
+
+ if self.heart_beat_timer is not None:
+ self.heart_beat_timer.cancel()
+
+ if self.connected:
try:
- helloline = self._rbfile.readline().decode("utf-8")
- self._hello(helloline)
- except Exception:
- self.disconnect()
- raise
+ status = self.status()
+ song = self.currentsong()
+ state = status.get('state')
+
+ if state != self.current_state:
+ self.emit('state_changed', state)
+ self.current_state = state
+
+
+ if song.get('id') != self.current_song_id:
+ if self.current_song_id is not None:
+ self.emit('song_changed', status, song)
+ self.current_song_id = song.get('id')
+
+
+ if state == 'play':
+ self.emit('playing', status, song)
+
+
+ self.heart_beat_timer = self.heart_beat()
+
+ except Exception as err:
+ print(err)
+ self.disconnect()
+ self.connect()
+
+ def connect(self):
+ self.try_conn_timer = self._try_connect()
+
+ 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 disconnect(self):
- logger.info("Calling MPD disconnect()")
- if self._rbfile is not None and not isinstance(self._rbfile, _NotConnected):
- self._rbfile.close()
- if self._wfile is not None and not isinstance(self._wfile, _NotConnected):
- self._wfile.close()
- if self._sock is not None:
- self._sock.close()
- self._reset()
+ 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 fileno(self):
if self._sock is None:
diff --git a/mpd/__init__.py b/mpd/__init__.py
deleted file mode 100644
index 4332922..0000000
--- a/mpd/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# python-mpd2: Python MPD client library
-#
-# Copyright (C) 2008-2010 J. Alexander Treuman
-# Copyright (C) 2012 J. Thalheim
-# Copyright (C) 2016 Robert Niederreiter
-#
-# python-mpd2 is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# python-mpd2 is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with python-mpd2. If not, see .
-
-from mpd.base import CommandError
-from mpd.base import CommandListError
-from mpd.base import ConnectionError
-from mpd.base import FailureResponseCode
-from mpd.base import IteratingError
-from mpd.base import MPDClient
-from mpd.base import MPDError
-from mpd.base import PendingCommandError
-from mpd.base import ProtocolError
-from mpd.base import VERSION
diff --git a/ui/ctrl_box.py b/ui/ctrl_box.py
index 3ac17b1..12be310 100644
--- a/ui/ctrl_box.py
+++ b/ui/ctrl_box.py
@@ -16,7 +16,7 @@ class CtrlBox(Gtk.Box):
def __init__(self, spacing = 6):
Gtk.Box.__init__(self, spacing = spacing)
- self.disabled = False
+ self.disabled = True
self.modes = ['./usr/share/sonist/all.png','./usr/share/sonist/rand.png','./usr/share/sonist/single.png']
self.curr_mode = 0
diff --git a/ui/timebar.py b/ui/timebar.py
index ed91599..a1f1070 100644
--- a/ui/timebar.py
+++ b/ui/timebar.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-import gi
+import gi, threading
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
@@ -25,6 +25,9 @@ class Timebar(Gtk.Fixed):
def __init__(self):
Gtk.Fixed.__init__(self)
+ self.timer = None
+ self.pending = False
+
self._duration = 0
self._time = 0
@@ -37,7 +40,8 @@ class Timebar(Gtk.Fixed):
self.duration.set_xalign(1)
self.duration.set_name('text')
- self.slider.connect('change-value', self.on_timeupdate)
+ self.slider.connect('change-value', self.debounce)
+ self.slider.set_sensitive(False)
self.put(self.curr, 3, 0)
self.put(self.duration, 233, 0)
@@ -47,14 +51,32 @@ class Timebar(Gtk.Fixed):
def update_time(self, curr = 0, duration = 0):
self._duration = duration
self._time = curr
- progress = curr * 100 / duration
- self.curr.set_text(time_to_str(curr))
- self.duration.set_text(time_to_str(duration))
- self.slider.set_value(progress)
+ self.slider.set_sensitive(duration > 0)
+ if duration == 0:
+ self.curr.set_text('')
+ self.duration.set_text('')
+ self.slider.set_value(0)
+ else:
+ progress = curr * 100 / duration
+ self.curr.set_text(time_to_str(curr))
+ self.duration.set_text(time_to_str(duration))
+ # 优化拖拽进度时的更新
+ if not self.pending:
+ self.slider.set_value(progress)
- def on_timeupdate(self, slider, a, b):
- # print(slider.get_value(), a, b)
- p = slider.get_value()
+
+ def on_timeupdate(self):
+ self.pending = False
+ p = self.slider.get_value()
time = p * self._duration / 100
- self.emit('seeked', time)
\ No newline at end of file
+ self.emit('seeked', time)
+
+
+ def debounce(self, slider, a, b):
+ if self.timer is not None:
+ self.timer.cancel()
+
+ self.pending = True
+ self.timer = threading.Timer(0.2, self.on_timeupdate)
+ self.timer.start()
\ No newline at end of file
diff --git a/window.py b/window.py
index 95e6ec3..7d64db5 100644
--- a/window.py
+++ b/window.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import gi, sys, os, mutagen
-from pprint import pprint as print
+# from pprint import pprint as print
gi.require_version('Gtk', '3.0')
@@ -22,16 +22,23 @@ from ui.option_menu import OptionMenu
class SonistWindow(Gtk.Window):
+ app = None
+ mpd = None
+ stat = {}
+
def __init__(self, app):
Gtk.Window.__init__(self)
self.app = app
+ self.mpd = app.mpd
- self.connect("destroy", self.quit)
+ self.connect("destroy", self.quited)
- self.app.connect('state_changed', lambda a, x: self.update_play_stat(x == 'play'))
- self.app.connect('song_changed', lambda a, id: self.sync_state(False))
- self.app.connect('playing', lambda a, id: self.update_playtime())
+ 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.set_name('SonistWindow')
@@ -88,7 +95,7 @@ class SonistWindow(Gtk.Window):
# 播放进度
self.timebar = Timebar()
- self.timebar.connect('seeked', lambda a,v: self.app.mpd.seekcur(v))
+ self.timebar.connect('seeked', lambda a,v: self.mpd.seekcur(v))
layout.put(self.timebar, 24, 270)
@@ -96,22 +103,17 @@ class SonistWindow(Gtk.Window):
self.ctrl_box = CtrlBox()
self.ctrl_box.connect('clicked', self.ctrl_clicked)
- self.ctrl_box.disabled = not self.app.mpd_is_online
layout.put(self.ctrl_box, 48, 312)
self.add(layout)
- self.sync_state(True)
-
def get_mpd_stat(self):
try:
- self.stat = self.app.mpd.status()
+ self.stat = self.mpd.status()
except:
- self.app.ping()
- self.stat = self.app.mpd.status()
-
+ self.stat = {}
return self.stat
@@ -150,19 +152,19 @@ class SonistWindow(Gtk.Window):
case 'mode_btn':
# repeat all
if self.ctrl_box.curr_mode == 0:
- self.app.mpd.repeat(1)
- self.app.mpd.random(0)
- self.app.mpd.single(0)
+ self.mpd.repeat(1)
+ self.mpd.random(0)
+ self.mpd.single(0)
# random
elif self.ctrl_box.curr_mode == 1:
- self.app.mpd.repeat(0)
- self.app.mpd.random(1)
- self.app.mpd.single(0)
+ self.mpd.repeat(0)
+ self.mpd.random(1)
+ self.mpd.single(0)
# single
else:
- self.app.mpd.repeat(0)
- self.app.mpd.random(0)
- self.app.mpd.single(1)
+ self.mpd.repeat(0)
+ self.mpd.random(0)
+ self.mpd.single(1)
case 'prev_btn':
self.prev_song()
@@ -178,39 +180,30 @@ class SonistWindow(Gtk.Window):
def toggle_play(self):
try:
if self.stat.get('state') == 'stop':
- self.app.mpd.play()
+ self.mpd.play()
else:
- self.app.mpd.pause()
+ self.mpd.pause()
except:
- self.app.ping()
- self.toggle_play()
- return
+ pass
- # self.sync_state()
self.update_play_stat(self.stat.get('state') == 'play')
def prev_song(self):
try:
- self.app.mpd.previous()
+ self.mpd.previous()
except:
- self.app.ping()
- self.prev_song()
- return
- # self.sync_state()
+ pass
def next_song(self):
try:
- self.app.mpd.next()
+ self.mpd.next()
except:
- self.app.ping()
- self.next_song()
- return
- # self.sync_state()
+ pass
def update_play_stat(self, played = True):
- if not self.app.mpd_is_online:
+ if not self.mpd.connected:
return
if played:
@@ -222,19 +215,16 @@ class SonistWindow(Gtk.Window):
self.ctrl_box.toggle_play_btn(played)
- def update_playtime(self):
- stat = self.get_mpd_stat()
+ def update_playtime(self, stat = {}):
times = stat['time'].split(':')
self.timebar.update_time(int(times[0]), int(times[1]))
- def sync_state(self, first = False):
- if not self.app.mpd_is_online:
- return
+ def sync_state(self, stat = None, song = None, first = False):
+ self.ctrl_box.disabled = False
- self.stat = self.get_mpd_stat()
+ self.stat = stat or self.get_mpd_stat()
played = self.stat.get('state')
- song = self.app.mpd.currentsong()
if first:
self.update_play_stat(played == 'play')
@@ -244,16 +234,16 @@ class SonistWindow(Gtk.Window):
elif self.stat.get('random') == '1':
self.ctrl_box.toggle_mode_btn(mode = 'random')
-
+
if played != 'stop':
+ song = song or self.mpd.currentsong()
# 更新歌曲信息
- self.title_box.set_text("%s - %s" % (song.get('artist'), song.get('title')))
- self.update_playtime()
+ self.title_box.set_text(f"{song.get('artist')} - {song.get('title')}")
+ self.update_playtime(self.stat)
filepath = f"./album/{song['title']}.png"
songpath = f"{self.app.music_dir}/{song['file']}"
-
if os.path.isfile(filepath):
self.update_album(filepath)
else:
@@ -271,12 +261,17 @@ class SonistWindow(Gtk.Window):
self.update_album(album)
except:
pass
-
+
+ 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')
def update_album(self, filepath):
self.set_background_image(filepath)
self.album.reset(filepath).set_radius(64)
- def quit(self, win):
+ def quited(self, win):
self.app.remove_window(self)