优化mpd交互

master
yutent 2023-08-23 20:36:52 +08:00
parent b27c816047
commit 1b4834d022
6 changed files with 275 additions and 259 deletions

63
main.py
View File

@ -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

View File

@ -17,15 +17,11 @@
# 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/>.
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:

View File

@ -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

View File

@ -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

View File

@ -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)
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()

View File

@ -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)