优化mpd交互
parent
b27c816047
commit
1b4834d022
63
main.py
63
main.py
|
@ -13,7 +13,7 @@ from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject
|
||||||
from window import SonistWindow
|
from window import SonistWindow
|
||||||
from about_app import AboutWindow
|
from about_app import AboutWindow
|
||||||
|
|
||||||
from mpd.base import MPDClient
|
from mpd import MPDClient
|
||||||
|
|
||||||
app_id = 'fun.wkit.sonist'
|
app_id = 'fun.wkit.sonist'
|
||||||
home_dir = os.getenv('HOME')
|
home_dir = os.getenv('HOME')
|
||||||
|
@ -28,6 +28,17 @@ def run_async(func):
|
||||||
return wrapper
|
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():
|
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()
|
||||||
|
@ -51,8 +62,7 @@ class Application(Gtk.Application):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Gtk.Application.__init__(self, application_id = app_id)
|
Gtk.Application.__init__(self, application_id = app_id)
|
||||||
|
|
||||||
self.mpd_state = None
|
self.timer = None
|
||||||
self.mpd_curr_song = None
|
|
||||||
|
|
||||||
self.mpd = MPDClient()
|
self.mpd = MPDClient()
|
||||||
|
|
||||||
|
@ -60,44 +70,7 @@ class Application(Gtk.Application):
|
||||||
|
|
||||||
self.connect('window-removed', self.on_window_removed)
|
self.connect('window-removed', self.on_window_removed)
|
||||||
|
|
||||||
|
self.mpd.connect()
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
|
|
||||||
def do_activate(self):
|
def do_activate(self):
|
||||||
|
@ -105,24 +78,22 @@ class Application(Gtk.Application):
|
||||||
self.set_app_menu(None)
|
self.set_app_menu(None)
|
||||||
self.set_menubar(None)
|
self.set_menubar(None)
|
||||||
|
|
||||||
self.mpd_is_online = self.mpd_connect()
|
|
||||||
|
|
||||||
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.ping()
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
if self.timer is not None:
|
||||||
|
self.timer.cancel()
|
||||||
|
self.mpd.destroy()
|
||||||
print('朕要休息了~~~')
|
print('朕要休息了~~~')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
""" class ApplicationService(dbus.service.Object):
|
""" class ApplicationService(dbus.service.Object):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
|
@ -17,15 +17,11 @@
|
||||||
# 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 logging
|
import re, socket, sys, warnings, threading, time
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
VERSION = (3, 1, 0)
|
VERSION = (3, 1, 0)
|
||||||
HELLO_PREFIX = "OK MPD "
|
HELLO_PREFIX = "OK MPD "
|
||||||
ERROR_PREFIX = "ACK "
|
ERROR_PREFIX = "ACK "
|
||||||
|
@ -34,22 +30,29 @@ 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('"', '\\"')
|
||||||
|
|
||||||
|
|
||||||
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
|
# MPD Protocol errors as found in CommandError exceptions
|
||||||
# https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx
|
# https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx
|
||||||
|
@ -177,26 +180,10 @@ class MPDClientBase(object):
|
||||||
subclasses.
|
subclasses.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, use_unicode=None):
|
def __init__(self):
|
||||||
self.iterate = False
|
self.iterate = False
|
||||||
if use_unicode is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"use_unicode parameter to ``MPDClientBase`` constructor is "
|
|
||||||
"deprecated",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
self._reset()
|
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
|
@classmethod
|
||||||
def add_command(cls, name, callback):
|
def add_command(cls, name, callback):
|
||||||
|
@ -490,6 +477,17 @@ class _NotConnected(object):
|
||||||
|
|
||||||
@mpd_command_provider
|
@mpd_command_provider
|
||||||
class MPDClient(MPDClientBase):
|
class MPDClient(MPDClientBase):
|
||||||
|
|
||||||
|
__events__ = {}
|
||||||
|
|
||||||
|
connected = False
|
||||||
|
|
||||||
|
try_conn_timer = None
|
||||||
|
heart_beat_timer = None
|
||||||
|
|
||||||
|
current_song_id = None
|
||||||
|
current_state = 'stop'
|
||||||
|
|
||||||
idletimeout = None
|
idletimeout = None
|
||||||
_timeout = None
|
_timeout = None
|
||||||
_wrap_iterator_parsers = [
|
_wrap_iterator_parsers = [
|
||||||
|
@ -508,22 +506,28 @@ class MPDClient(MPDClientBase):
|
||||||
MPDClientBase._parse_plugins,
|
MPDClientBase._parse_plugins,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, use_unicode=None):
|
def __init__(self, host = '127.0.0.1', port = 6600):
|
||||||
if use_unicode is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"use_unicode parameter to ``MPDClient`` constructor is "
|
|
||||||
"deprecated",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
super(MPDClient, self).__init__()
|
super(MPDClient, self).__init__()
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
super(MPDClient, self)._reset()
|
super(MPDClient, self)._reset()
|
||||||
|
self.connected = False
|
||||||
self._iterating = False
|
self._iterating = False
|
||||||
self._sock = None
|
self._sock = None
|
||||||
self._rbfile = _NotConnected()
|
self._rbfile = None
|
||||||
self._wfile = _NotConnected()
|
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:
|
||||||
|
@ -543,11 +547,14 @@ class MPDClient(MPDClientBase):
|
||||||
|
|
||||||
def _write_line(self, line):
|
def _write_line(self, line):
|
||||||
try:
|
try:
|
||||||
|
if self._wfile == None:
|
||||||
|
print('MPD server is not connected!!!')
|
||||||
|
else:
|
||||||
self._wfile.write("{}\n".format(line))
|
self._wfile.write("{}\n".format(line))
|
||||||
self._wfile.flush()
|
self._wfile.flush()
|
||||||
|
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
error_message = "Connection to server was reset"
|
error_message = "Connection to server was reset"
|
||||||
logger.info(error_message)
|
|
||||||
self._reset()
|
self._reset()
|
||||||
e = ConnectionError(error_message)
|
e = ConnectionError(error_message)
|
||||||
raise e.with_traceback(sys.exc_info()[2])
|
raise e.with_traceback(sys.exc_info()[2])
|
||||||
|
@ -565,11 +572,7 @@ class MPDClient(MPDClientBase):
|
||||||
else:
|
else:
|
||||||
parts.append('"{}"'.format(escape(str(arg))))
|
parts.append('"{}"'.format(escape(str(arg))))
|
||||||
# Minimize logging cost if the logging is not activated.
|
# 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)
|
cmd = " ".join(parts)
|
||||||
self._write_line(cmd)
|
self._write_line(cmd)
|
||||||
|
|
||||||
|
@ -716,31 +719,35 @@ class MPDClient(MPDClientBase):
|
||||||
self._iterating = True
|
self._iterating = True
|
||||||
return self._iterator_wrapper(iterator)
|
return self._iterator_wrapper(iterator)
|
||||||
|
|
||||||
def _hello(self, line):
|
def _check_is_mpd_server(self, line):
|
||||||
if not line.endswith("\n"):
|
if not line.endswith("\n"):
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
raise ConnectionError("Connection lost while reading MPD hello")
|
return
|
||||||
|
|
||||||
line = line.rstrip("\n")
|
line = line.rstrip("\n")
|
||||||
if not line.startswith(HELLO_PREFIX):
|
if not line.startswith(HELLO_PREFIX):
|
||||||
raise ProtocolError("Got invalid MPD hello: '{}'".format(line))
|
return
|
||||||
|
|
||||||
|
self.connected = True
|
||||||
self.mpd_version = line[len(HELLO_PREFIX) :].strip()
|
self.mpd_version = line[len(HELLO_PREFIX) :].strip()
|
||||||
|
|
||||||
def _connect_unix(self, path):
|
def _connect_unix(self):
|
||||||
if not hasattr(socket, "AF_UNIX"):
|
if not hasattr(socket, "AF_UNIX"):
|
||||||
raise ConnectionError("Unix domain sockets not supported on this platform")
|
raise ConnectionError("Unix domain sockets not supported on this platform")
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock.settimeout(self.timeout)
|
sock.settimeout(self.timeout)
|
||||||
sock.connect(path)
|
sock.connect(self.host)
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
def _connect_tcp(self, host, port):
|
def _connect_tcp(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
flags = socket.AI_ADDRCONFIG
|
flags = socket.AI_ADDRCONFIG
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
flags = 0
|
flags = 0
|
||||||
err = None
|
|
||||||
for res in socket.getaddrinfo(
|
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
|
af, socktype, proto, canonname, sa = res
|
||||||
sock = None
|
sock = None
|
||||||
|
@ -751,14 +758,16 @@ class MPDClient(MPDClientBase):
|
||||||
sock.settimeout(self.timeout)
|
sock.settimeout(self.timeout)
|
||||||
sock.connect(sa)
|
sock.connect(sa)
|
||||||
return sock
|
return sock
|
||||||
except socket.error as e:
|
except Exception as e:
|
||||||
err = e
|
|
||||||
|
if e.strerror == 'Connection refused':
|
||||||
|
self.emit('offline')
|
||||||
|
else:
|
||||||
|
self.emit('error', e)
|
||||||
|
|
||||||
if sock is not None:
|
if sock is not None:
|
||||||
sock.close()
|
sock.close()
|
||||||
if err is not None:
|
|
||||||
raise err
|
|
||||||
else:
|
|
||||||
raise ConnectionError("getaddrinfo returns an empty list")
|
|
||||||
|
|
||||||
@mpd_commands("idle")
|
@mpd_commands("idle")
|
||||||
def _parse_idle(self, lines):
|
def _parse_idle(self, lines):
|
||||||
|
@ -777,47 +786,95 @@ class MPDClient(MPDClientBase):
|
||||||
if self._sock is not None:
|
if self._sock is not None:
|
||||||
self._sock.settimeout(timeout)
|
self._sock.settimeout(timeout)
|
||||||
|
|
||||||
def connect(self, host, port=None, timeout=None):
|
@set_timeout(2)
|
||||||
logger.info("Calling MPD connect(%r, %r, timeout=%r)", host, port, timeout)
|
def _try_connect(self):
|
||||||
|
|
||||||
if self._sock is not None:
|
if self._sock is not None:
|
||||||
raise ConnectionError("Already connected")
|
raise ConnectionError("Already connected")
|
||||||
if timeout is not None:
|
|
||||||
warnings.warn(
|
if self.host.startswith(("/", "\0")):
|
||||||
"The timeout parameter in connect() is deprecated! "
|
self._sock = self._connect_unix()
|
||||||
"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)
|
|
||||||
else:
|
else:
|
||||||
if port is None:
|
self._sock = self._connect_tcp()
|
||||||
raise ValueError(
|
|
||||||
"port argument must be specified when connecting via tcp"
|
|
||||||
)
|
|
||||||
self._sock = self._connect_tcp(host, port)
|
|
||||||
|
|
||||||
# - Force UTF-8 encoding, since this is dependant from the LC_CTYPE
|
# - Force UTF-8 encoding, since this is dependant from the LC_CTYPE
|
||||||
# locale.
|
# locale.
|
||||||
# - by setting newline explicit, we force to send '\n' also on
|
# - by setting newline explicit, we force to send '\n' also on
|
||||||
# windows
|
# 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")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
helloline = self._rbfile.readline().decode("utf-8")
|
helloline = self._rbfile.readline().decode("utf-8")
|
||||||
self._hello(helloline)
|
self._check_is_mpd_server(helloline)
|
||||||
except Exception:
|
if self.connected:
|
||||||
|
self.emit('online')
|
||||||
|
self.heart_beat_timer = self.heart_beat()
|
||||||
|
else:
|
||||||
|
self.emit('error', ProtocolError('Connected server is not mpd server.'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.connected = False
|
||||||
|
self.emit('error', e)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
raise
|
|
||||||
|
|
||||||
|
@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:
|
||||||
|
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):
|
def disconnect(self):
|
||||||
logger.info("Calling MPD disconnect()")
|
if self._rbfile is not None:
|
||||||
if self._rbfile is not None and not isinstance(self._rbfile, _NotConnected):
|
|
||||||
self._rbfile.close()
|
self._rbfile.close()
|
||||||
if self._wfile is not None and not isinstance(self._wfile, _NotConnected):
|
if self._wfile is not None:
|
||||||
self._wfile.close()
|
self._wfile.close()
|
||||||
if self._sock is not None:
|
if self._sock is not None:
|
||||||
self._sock.close()
|
self._sock.close()
|
|
@ -1,29 +0,0 @@
|
||||||
# python-mpd2: Python MPD client library
|
|
||||||
#
|
|
||||||
# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
|
|
||||||
# Copyright (C) 2012 J. Thalheim <jthalheim@gmail.com>
|
|
||||||
# Copyright (C) 2016 Robert Niederreiter <rnix@squarewave.at>
|
|
||||||
#
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
|
@ -16,7 +16,7 @@ class CtrlBox(Gtk.Box):
|
||||||
def __init__(self, spacing = 6):
|
def __init__(self, spacing = 6):
|
||||||
Gtk.Box.__init__(self, spacing = spacing)
|
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.modes = ['./usr/share/sonist/all.png','./usr/share/sonist/rand.png','./usr/share/sonist/single.png']
|
||||||
self.curr_mode = 0
|
self.curr_mode = 0
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import gi
|
import gi, threading
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gtk, GObject
|
from gi.repository import Gtk, GObject
|
||||||
|
|
||||||
|
@ -25,6 +25,9 @@ class Timebar(Gtk.Fixed):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Gtk.Fixed.__init__(self)
|
Gtk.Fixed.__init__(self)
|
||||||
|
|
||||||
|
self.timer = None
|
||||||
|
self.pending = False
|
||||||
|
|
||||||
self._duration = 0
|
self._duration = 0
|
||||||
self._time = 0
|
self._time = 0
|
||||||
|
|
||||||
|
@ -37,7 +40,8 @@ class Timebar(Gtk.Fixed):
|
||||||
self.duration.set_xalign(1)
|
self.duration.set_xalign(1)
|
||||||
self.duration.set_name('text')
|
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.curr, 3, 0)
|
||||||
self.put(self.duration, 233, 0)
|
self.put(self.duration, 233, 0)
|
||||||
|
@ -47,14 +51,32 @@ class Timebar(Gtk.Fixed):
|
||||||
def update_time(self, curr = 0, duration = 0):
|
def update_time(self, curr = 0, duration = 0):
|
||||||
self._duration = duration
|
self._duration = duration
|
||||||
self._time = curr
|
self._time = curr
|
||||||
|
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
|
progress = curr * 100 / duration
|
||||||
self.curr.set_text(time_to_str(curr))
|
self.curr.set_text(time_to_str(curr))
|
||||||
self.duration.set_text(time_to_str(duration))
|
self.duration.set_text(time_to_str(duration))
|
||||||
|
# 优化拖拽进度时的更新
|
||||||
|
if not self.pending:
|
||||||
self.slider.set_value(progress)
|
self.slider.set_value(progress)
|
||||||
|
|
||||||
|
|
||||||
def on_timeupdate(self, slider, a, b):
|
|
||||||
# print(slider.get_value(), a, b)
|
def on_timeupdate(self):
|
||||||
p = slider.get_value()
|
self.pending = False
|
||||||
|
p = self.slider.get_value()
|
||||||
time = p * self._duration / 100
|
time = p * self._duration / 100
|
||||||
self.emit('seeked', time)
|
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()
|
95
window.py
95
window.py
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import gi, sys, os, mutagen
|
import gi, sys, os, mutagen
|
||||||
from pprint import pprint as print
|
# from pprint import pprint as print
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
@ -22,16 +22,23 @@ from ui.option_menu import OptionMenu
|
||||||
|
|
||||||
|
|
||||||
class SonistWindow(Gtk.Window):
|
class SonistWindow(Gtk.Window):
|
||||||
|
app = None
|
||||||
|
mpd = None
|
||||||
|
stat = {}
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
Gtk.Window.__init__(self)
|
Gtk.Window.__init__(self)
|
||||||
|
|
||||||
self.app = app
|
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.mpd.bind('offline', lambda : self.reset_player())
|
||||||
self.app.connect('song_changed', lambda a, id: self.sync_state(False))
|
self.mpd.bind('online', lambda : self.sync_state(None, None, True))
|
||||||
self.app.connect('playing', lambda a, id: self.update_playtime())
|
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')
|
self.set_name('SonistWindow')
|
||||||
|
@ -88,7 +95,7 @@ class SonistWindow(Gtk.Window):
|
||||||
|
|
||||||
# 播放进度
|
# 播放进度
|
||||||
self.timebar = Timebar()
|
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)
|
layout.put(self.timebar, 24, 270)
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,22 +103,17 @@ class SonistWindow(Gtk.Window):
|
||||||
self.ctrl_box = CtrlBox()
|
self.ctrl_box = CtrlBox()
|
||||||
self.ctrl_box.connect('clicked', self.ctrl_clicked)
|
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)
|
layout.put(self.ctrl_box, 48, 312)
|
||||||
|
|
||||||
self.add(layout)
|
self.add(layout)
|
||||||
|
|
||||||
self.sync_state(True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_mpd_stat(self):
|
def get_mpd_stat(self):
|
||||||
try:
|
try:
|
||||||
self.stat = self.app.mpd.status()
|
self.stat = self.mpd.status()
|
||||||
except:
|
except:
|
||||||
self.app.ping()
|
self.stat = {}
|
||||||
self.stat = self.app.mpd.status()
|
|
||||||
|
|
||||||
return self.stat
|
return self.stat
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,19 +152,19 @@ class SonistWindow(Gtk.Window):
|
||||||
case 'mode_btn':
|
case 'mode_btn':
|
||||||
# repeat all
|
# repeat all
|
||||||
if self.ctrl_box.curr_mode == 0:
|
if self.ctrl_box.curr_mode == 0:
|
||||||
self.app.mpd.repeat(1)
|
self.mpd.repeat(1)
|
||||||
self.app.mpd.random(0)
|
self.mpd.random(0)
|
||||||
self.app.mpd.single(0)
|
self.mpd.single(0)
|
||||||
# random
|
# random
|
||||||
elif self.ctrl_box.curr_mode == 1:
|
elif self.ctrl_box.curr_mode == 1:
|
||||||
self.app.mpd.repeat(0)
|
self.mpd.repeat(0)
|
||||||
self.app.mpd.random(1)
|
self.mpd.random(1)
|
||||||
self.app.mpd.single(0)
|
self.mpd.single(0)
|
||||||
# single
|
# single
|
||||||
else:
|
else:
|
||||||
self.app.mpd.repeat(0)
|
self.mpd.repeat(0)
|
||||||
self.app.mpd.random(0)
|
self.mpd.random(0)
|
||||||
self.app.mpd.single(1)
|
self.mpd.single(1)
|
||||||
|
|
||||||
case 'prev_btn':
|
case 'prev_btn':
|
||||||
self.prev_song()
|
self.prev_song()
|
||||||
|
@ -178,39 +180,30 @@ class SonistWindow(Gtk.Window):
|
||||||
def toggle_play(self):
|
def toggle_play(self):
|
||||||
try:
|
try:
|
||||||
if self.stat.get('state') == 'stop':
|
if self.stat.get('state') == 'stop':
|
||||||
self.app.mpd.play()
|
self.mpd.play()
|
||||||
else:
|
else:
|
||||||
self.app.mpd.pause()
|
self.mpd.pause()
|
||||||
except:
|
except:
|
||||||
self.app.ping()
|
pass
|
||||||
self.toggle_play()
|
|
||||||
return
|
|
||||||
|
|
||||||
# self.sync_state()
|
|
||||||
self.update_play_stat(self.stat.get('state') == 'play')
|
self.update_play_stat(self.stat.get('state') == 'play')
|
||||||
|
|
||||||
|
|
||||||
def prev_song(self):
|
def prev_song(self):
|
||||||
try:
|
try:
|
||||||
self.app.mpd.previous()
|
self.mpd.previous()
|
||||||
except:
|
except:
|
||||||
self.app.ping()
|
pass
|
||||||
self.prev_song()
|
|
||||||
return
|
|
||||||
# self.sync_state()
|
|
||||||
|
|
||||||
def next_song(self):
|
def next_song(self):
|
||||||
try:
|
try:
|
||||||
self.app.mpd.next()
|
self.mpd.next()
|
||||||
except:
|
except:
|
||||||
self.app.ping()
|
pass
|
||||||
self.next_song()
|
|
||||||
return
|
|
||||||
# self.sync_state()
|
|
||||||
|
|
||||||
|
|
||||||
def update_play_stat(self, played = True):
|
def update_play_stat(self, played = True):
|
||||||
if not self.app.mpd_is_online:
|
if not self.mpd.connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
if played:
|
if played:
|
||||||
|
@ -222,19 +215,16 @@ class SonistWindow(Gtk.Window):
|
||||||
self.ctrl_box.toggle_play_btn(played)
|
self.ctrl_box.toggle_play_btn(played)
|
||||||
|
|
||||||
|
|
||||||
def update_playtime(self):
|
def update_playtime(self, stat = {}):
|
||||||
stat = self.get_mpd_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]))
|
||||||
|
|
||||||
def sync_state(self, first = False):
|
def sync_state(self, stat = None, song = None, first = False):
|
||||||
if not self.app.mpd_is_online:
|
self.ctrl_box.disabled = False
|
||||||
return
|
|
||||||
|
|
||||||
self.stat = self.get_mpd_stat()
|
self.stat = stat or self.get_mpd_stat()
|
||||||
|
|
||||||
played = self.stat.get('state')
|
played = self.stat.get('state')
|
||||||
song = self.app.mpd.currentsong()
|
|
||||||
|
|
||||||
if first:
|
if first:
|
||||||
self.update_play_stat(played == 'play')
|
self.update_play_stat(played == 'play')
|
||||||
|
@ -246,14 +236,14 @@ class SonistWindow(Gtk.Window):
|
||||||
|
|
||||||
|
|
||||||
if played != 'stop':
|
if played != 'stop':
|
||||||
|
song = song or self.mpd.currentsong()
|
||||||
# 更新歌曲信息
|
# 更新歌曲信息
|
||||||
self.title_box.set_text("%s - %s" % (song.get('artist'), song.get('title')))
|
self.title_box.set_text(f"{song.get('artist')} - {song.get('title')}")
|
||||||
self.update_playtime()
|
self.update_playtime(self.stat)
|
||||||
|
|
||||||
filepath = f"./album/{song['title']}.png"
|
filepath = f"./album/{song['title']}.png"
|
||||||
songpath = f"{self.app.music_dir}/{song['file']}"
|
songpath = f"{self.app.music_dir}/{song['file']}"
|
||||||
|
|
||||||
|
|
||||||
if os.path.isfile(filepath):
|
if os.path.isfile(filepath):
|
||||||
self.update_album(filepath)
|
self.update_album(filepath)
|
||||||
else:
|
else:
|
||||||
|
@ -272,11 +262,16 @@ class SonistWindow(Gtk.Window):
|
||||||
except:
|
except:
|
||||||
pass
|
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):
|
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 quit(self, win):
|
def quited(self, win):
|
||||||
self.app.remove_window(self)
|
self.app.remove_window(self)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue