rewrite client, implement protocol without lib, replace tabs with spaces

This commit is contained in:
coderkun 2014-12-01 20:55:28 +01:00
parent 2e95986047
commit 965a536779
3 changed files with 2024 additions and 1915 deletions

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks.""" """MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks."""
@ -11,7 +11,9 @@ __status__ = "Development"
import math import math
import logging
import os import os
import sys
import threading import threading
import urllib import urllib
@ -105,7 +107,7 @@ class Window(Gtk.ApplicationWindow):
Gtk.Window.__init__(self, title=title, application=app) Gtk.Window.__init__(self, title=title, application=app)
self._settings = settings self._settings = settings
self._panels = [] self._panels = []
self._mcg = mcg.MCGClient() self._mcg = mcg.Client()
self._size = self._settings.get_value(Application.SETTING_WINDOW_SIZE) self._size = self._settings.get_value(Application.SETTING_WINDOW_SIZE)
self._maximized = self._settings.get_boolean(Application.SETTING_WINDOW_MAXIMIZED) self._maximized = self._settings.get_boolean(Application.SETTING_WINDOW_MAXIMIZED)
self._fullscreened = False self._fullscreened = False
@ -176,11 +178,11 @@ class Window(Gtk.ApplicationWindow):
self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_ITEM_SIZE_CHANGED, self.on_library_panel_item_size_changed) self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_ITEM_SIZE_CHANGED, self.on_library_panel_item_size_changed)
self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_ORDER_CHANGED, self.on_library_panel_sort_order_changed) self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_ORDER_CHANGED, self.on_library_panel_sort_order_changed)
self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_TYPE_CHANGED, self.on_library_panel_sort_type_changed) self._panels[Window._PANEL_INDEX_LIBRARY].connect_signal(LibraryPanel.SIGNAL_SORT_TYPE_CHANGED, self.on_library_panel_sort_type_changed)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_CONNECT, self.on_mcg_connect) self._mcg.connect_signal(mcg.Client.SIGNAL_CONNECTION, self.on_mcg_connect)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_STATUS, self.on_mcg_status) self._mcg.connect_signal(mcg.Client.SIGNAL_STATUS, self.on_mcg_status)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist) self._mcg.connect_signal(mcg.Client.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums) self._mcg.connect_signal(mcg.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
self._mcg.connect_signal(mcg.MCGClient.SIGNAL_ERROR, self.on_mcg_error) self._mcg.connect_signal(mcg.Client.SIGNAL_ERROR, self.on_mcg_error)
self._settings.connect('changed::'+Application.SETTING_PANEL, self.on_settings_panel_changed) self._settings.connect('changed::'+Application.SETTING_PANEL, self.on_settings_panel_changed)
self._settings.connect('changed::'+Application.SETTING_ITEM_SIZE, self.on_settings_item_size_changed) self._settings.connect('changed::'+Application.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
self._settings.connect('changed::'+Application.SETTING_SORT_ORDER, self.on_settings_sort_order_changed) self._settings.connect('changed::'+Application.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
@ -223,6 +225,7 @@ class Window(Gtk.ApplicationWindow):
def on_header_bar_playpause(self): def on_header_bar_playpause(self):
self._mcg.playpause() self._mcg.playpause()
self._mcg.get_status()
def on_header_bar_set_volume(self, volume): def on_header_bar_set_volume(self, volume):
@ -275,15 +278,15 @@ class Window(Gtk.ApplicationWindow):
# MCG callbacks # MCG callbacks
def on_mcg_connect(self, connected, error): def on_mcg_connect(self, connected):
if connected: if connected:
GObject.idle_add(self._connect_connected) GObject.idle_add(self._connect_connected)
self._mcg.load_playlist() self._mcg.load_playlist()
self._mcg.load_albums() self._mcg.load_albums()
self._mcg.get_status() self._mcg.get_status()
else: else:
if error: # if error:
GObject.idle_add(self._show_error, str(error)) # GObject.idle_add(self._show_error, str(error))
GObject.idle_add(self._connect_disconnected) GObject.idle_add(self._connect_disconnected)
@ -307,11 +310,11 @@ class Window(Gtk.ApplicationWindow):
self._show_error(error) self._show_error(error)
def on_mcg_load_playlist(self, playlist, error): def on_mcg_load_playlist(self, playlist):
self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), playlist) self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), playlist)
def on_mcg_load_albums(self, albums, error): def on_mcg_load_albums(self, albums):
self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), albums) self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._panels[self._PANEL_INDEX_CONNECTION].get_host(), albums)
@ -398,7 +401,7 @@ class Window(Gtk.ApplicationWindow):
class HeaderBar(mcg.MCGBase, Gtk.HeaderBar): class HeaderBar(mcg.Base, Gtk.HeaderBar):
SIGNAL_STACK_SWITCHED = 'stack-switched' SIGNAL_STACK_SWITCHED = 'stack-switched'
SIGNAL_CONNECT = 'connect' SIGNAL_CONNECT = 'connect'
SIGNAL_PLAYPAUSE = 'playpause' SIGNAL_PLAYPAUSE = 'playpause'
@ -406,7 +409,7 @@ class HeaderBar(mcg.MCGBase, Gtk.HeaderBar):
def __init__(self, stack): def __init__(self, stack):
mcg.MCGBase.__init__(self) mcg.Base.__init__(self)
Gtk.HeaderBar.__init__(self) Gtk.HeaderBar.__init__(self)
self._stack = stack self._stack = stack
self._buttons = {} self._buttons = {}
@ -430,7 +433,7 @@ class HeaderBar(mcg.MCGBase, Gtk.HeaderBar):
# Buttons left: Separator # Buttons left: Separator
self._left_toolbar.add(Gtk.SeparatorToolItem()) self._left_toolbar.add(Gtk.SeparatorToolItem())
# Buttons left: Playback # Buttons left: Playback
self._buttons[HeaderBar.SIGNAL_PLAYPAUSE] = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_MEDIA_PAUSE) self._buttons[HeaderBar.SIGNAL_PLAYPAUSE] = Gtk.ToggleToolButton.new_from_stock(Gtk.STOCK_MEDIA_PLAY)
self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_sensitive(False) self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_sensitive(False)
self._left_toolbar.add(self._buttons[HeaderBar.SIGNAL_PLAYPAUSE]) self._left_toolbar.add(self._buttons[HeaderBar.SIGNAL_PLAYPAUSE])
# Buttons right # Buttons right
@ -490,13 +493,13 @@ class HeaderBar(mcg.MCGBase, Gtk.HeaderBar):
def set_play(self): def set_play(self):
self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PLAY) #self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PLAY)
with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]): with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]):
self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(True) self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(True)
def set_pause(self): def set_pause(self):
self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PAUSE) #self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_stock_id(Gtk.STOCK_MEDIA_PAUSE)
with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]): with self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].handler_block(self._button_handlers[HeaderBar.SIGNAL_PLAYPAUSE]):
self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(False) self._buttons[HeaderBar.SIGNAL_PLAYPAUSE].set_active(False)
@ -544,11 +547,11 @@ class InfoBar(Gtk.InfoBar):
class Panel(mcg.MCGBase): class Panel(mcg.Base):
def __init__(self): def __init__(self):
mcg.MCGBase.__init__(self) mcg.Base.__init__(self)
def get_name(self): def get_name(self):
@ -1467,12 +1470,12 @@ class LibraryPanel(Panel, Gtk.VBox):
class StackSwitcher(mcg.MCGBase, Gtk.StackSwitcher): class StackSwitcher(mcg.Base, Gtk.StackSwitcher):
SIGNAL_STACK_SWITCHED = 'stack-switched' SIGNAL_STACK_SWITCHED = 'stack-switched'
def __init__(self): def __init__(self):
mcg.MCGBase.__init__(self) mcg.Base.__init__(self)
Gtk.StackSwitcher.__init__(self) Gtk.StackSwitcher.__init__(self)
self._temp_button = None self._temp_button = None
@ -1490,7 +1493,3 @@ class StackSwitcher(mcg.MCGBase, Gtk.StackSwitcher):
else: else:
self._temp_button = None self._temp_button = None
self._callback(StackSwitcher.SIGNAL_STACK_SWITCHED, self) self._callback(StackSwitcher.SIGNAL_STACK_SWITCHED, self)

665
mcg.py
View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks.""" """MPDCoverGrid is a client for the Music Player Daemon, focused on albums instead of single tracks."""
@ -6,45 +6,60 @@
__author__ = "coderkun" __author__ = "coderkun"
__email__ = "<olli@coderkun.de>" __email__ = "<olli@coderkun.de>"
__license__ = "GPL" __license__ = "GPL"
__version__ = "0.2" __version__ = "0.3"
__status__ = "Development" __status__ = "Development"
import configparser import configparser
import glob import glob
import logging
import os import os
import queue import queue
import socket
import sys
import threading import threading
import urllib.request import urllib.request
from hashlib import md5 from hashlib import md5
import mpd
class MPDException(Exception):
pass
class ConnectionException(MPDException):
pass
class ProtocolException(MPDException):
pass
class CommandException(MPDException):
pass
class MCGBase(): class Base():
def __init__(self): def __init__(self):
self._callbacks = {} self._callbacks = {}
def connect_signal(self, signal, callback): def connect_signal(self, signal, callback):
"""Connect a callback function to a signal (event). """Connect a callback function to a signal (event)."""
"""
self._callbacks[signal] = callback self._callbacks[signal] = callback
def disconnect_signal(self, signal): def disconnect_signal(self, signal):
"""Disconnect a callback function from a signal (event). """Disconnect a callback function from a signal (event)."""
"""
if self._has_callback(signal): if self._has_callback(signal):
del self._callbacks[signal] del self._callbacks[signal]
def _has_callback(self, signal): def _has_callback(self, signal):
"""Check if there is a registered callback function for a """Check if there is a registered callback function for a signal."""
signal.
"""
return signal in self._callbacks return signal in self._callbacks
@ -56,16 +71,21 @@ class MCGBase():
class MCGClient(MCGBase, mpd.MPDClient): class Client(Base):
"""Client library for handling the connection to the Music Player Daemon. """Client library for handling the connection to the Music Player Daemon.
This class implements an album-based MPD client. This class implements an album-based MPD client. It offers a non-blocking
It offers a non-blocking threaded worker model for use in graphical threaded worker model for use in graphical environments.
environments and is based on python-mpd2.
""" """
# Signal: connect/disconnect event # Protocol: greeting mark
SIGNAL_CONNECT = 'connect' PROTOCOL_GREETING = 'OK MPD '
# Signal: status event # Protocol: completion mark
PROTOCOL_COMPLETION = 'OK'
# Protocol: error mark
PROTOCOL_ERROR = 'ACK '
# Signal: connection status
SIGNAL_CONNECTION = 'connection'
# Signal: status
SIGNAL_STATUS = 'status' SIGNAL_STATUS = 'status'
# Signal: load albums # Signal: load albums
SIGNAL_LOAD_ALBUMS = 'load-albums' SIGNAL_LOAD_ALBUMS = 'load-albums'
@ -76,41 +96,50 @@ class MCGClient(MCGBase, mpd.MPDClient):
def __init__(self): def __init__(self):
"""Set class variables and instantiates the MPDClient.""" """Set class variables and instantiates the Client."""
MCGBase.__init__(self) Base.__init__(self)
mpd.MPDClient.__init__(self) self._logger = logging.getLogger(__name__)
self._connected = False self._sock = None
self._state = None self._sock_read = None
self._client_lock = threading.Lock() self._sock_write = None
self._client_stop = threading.Event() self._stop = threading.Event()
self._actions = queue.Queue() self._actions = queue.Queue()
self._worker = None self._worker = None
self._idling = False
self._host = None
self._albums = {} self._albums = {}
self._playlist = [] self._playlist = []
self._host = None
self._image_dir = "" self._image_dir = ""
self._state = None
# Connection commands def get_logger(self):
return self._logger
def connect(self, host="localhost", port="6600", password=None, image_dir=""):
"""Connect to MPD with the given host, port and password or # Client commands
with standard values.
def connect(self, host, port, password=None, image_dir=""):
"""Connect to MPD with the given host, port and password or with
standard values.
""" """
self._logger.info("connect")
self._host = host self._host = host
self._image_dir = image_dir self._image_dir = image_dir
self._add_action(self._connect, host, port, password) self._add_action(self._connect, host, port, password)
self._stop.clear()
self._start_worker()
def is_connected(self): def is_connected(self):
"""Return the connection status. """Return the connection status."""
""" return self._worker is not None
return self._connected
def disconnect(self): def disconnect(self):
"""Disconnect from the connected MPD.""" """Disconnect from the connected MPD."""
self._client_stop.set() self._logger.info("disconnect")
self._stop.set()
self._add_action(self._disconnect) self._add_action(self._disconnect)
@ -118,193 +147,196 @@ class MCGClient(MCGBase, mpd.MPDClient):
self._actions.join() self._actions.join()
# Status commands
def get_status(self): def get_status(self):
"""Determine the current status.""" """Determine the current status."""
self._logger.info("get status")
self._add_action(self._get_status) self._add_action(self._get_status)
# Playback option commands def load_albums(self):
self._logger.info("load albums")
def set_volume(self, volume): self._add_action(self._load_albums)
self._add_action(self._set_volume, volume)
# Playback control commands def update(self):
self._logger.info("update")
self._add_action(self._update)
def playpause(self):
"""Play or pauses the current state."""
self._add_action(self._playpause)
def play_album(self, album):
"""Play the given album.
"""
self._add_action(self._play_album, album)
def seek(self, pos, time):
"""Seeks to a song at a position
"""
self._add_action(self._seek, pos, time)
def stop(self):
self._add_action(self._stop)
# Playlist commands
def load_playlist(self): def load_playlist(self):
self._logger.info("load playlist")
self._add_action(self._load_playlist) self._add_action(self._load_playlist)
def clear_playlist(self): def clear_playlist(self):
"""Clear the current playlist""" """Clear the current playlist"""
self._logger.info("clear playlist")
self._add_action(self._clear_playlist) self._add_action(self._clear_playlist)
# Database commands def playpause(self):
"""Play or pauses the current state."""
def load_albums(self): self._logger.info("playpause")
self._add_action(self._load_albums) self._add_action(self._playpause)
def update(self): def play_album(self, album):
self._add_action(self._update) """Play the given album."""
self._logger.info("play album")
self._add_action(self._play_album, album)
def seek(self, pos, time):
"""Seeks to a song at a position"""
self._logger.info("seek")
self._add_action(self._seek, pos, time)
def stop(self):
self._logger.info("stop")
self._add_action(self._stop)
def set_volume(self, volume):
self._logger.info("set volume")
self._add_action(self._set_volume, volume)
# Private methods # Private methods
def _add_action(self, method, *args):
"""Add an action to the action list.
"""
action = [method, args]
self._actions.put(action)
self._start_worker()
def _start_worker(self):
"""Start the worker thread which waits for action to be
performed."""
if self._worker is None or not self._worker.is_alive():
self._worker = threading.Thread(target=self._run, name='mcg-worker', args=())
self._worker.setDaemon(True)
self._worker.start()
else:
try:
self._call('noidle')
except BrokenPipeError:
pass
except ConnectionResetError as e:
self._set_connection_status(False, e)
except mpd.ConnectionError as e:
self._set_connection_status(False, e)
def _work(self, action):
method = action[0]
params = action[1]
method(*params)
def _call(self, command, *args):
try:
return getattr(super(), command)(*args)
except mpd.CommandError as e:
self._callback(MCGClient.SIGNAL_ERROR, e)
except mpd.ConnectionError as e:
self._set_connection_status(False, e)
except ConnectionResetError as e:
self._set_connection_status(False, e)
except BrokenPipeError:
pass
def _run(self):
while not self._client_stop.is_set() or not self._actions.empty():
if self._actions.empty():
self._actions.put([self._idle, ()])
action = self._actions.get()
self._client_lock.acquire()
self._work(action)
self._client_lock.release()
self._actions.task_done()
# Connection commands
def _connect(self, host, port, password): def _connect(self, host, port, password):
self._logger.info("connecting to host %r, port %r", host, port)
if self._sock is not None:
return
try: try:
self._call('connect', host, port) self._sock = self._connect_socket(host, port)
self._sock_read = self._sock.makefile("r", encoding="utf-8")
self._sock_write = self._sock.makefile("w", encoding="utf-8")
self._greet()
self._logger.info("connected")
if password: if password:
try: self._logger.info("setting password")
self._call('password', password) self._call("password", password)
except mpd.CommandError as e:
self._disconnect()
raise e
self._set_connection_status(True) self._set_connection_status(True)
except OSError as e: except OSError as e:
self._set_connection_status(False, e) raise ConnectionException("connection failed: {}".format(e))
def _connect_socket(self, host, port):
sock = None
error = None
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP):
af, socktype, proto, canonname, sa = res
try:
sock = socket.socket(af, socktype, proto)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.connect(sa)
return sock
except Exception as e:
error = e
if sock is not None:
sock.close()
break
if error is not None:
raise ConnectionException("connection failed: {}".format(error))
else:
raise ConnectionException("no suitable socket")
def _greet(self):
greeting = self._sock_read.readline()
self._logger.debug("greeting: %s", greeting.strip())
if not greeting.endswith("\n"):
self._disconnect_socket()
raise ConnectionException("incomplete line")
if not greeting.startswith(Client.PROTOCOL_GREETING):
self._disconnect_socket()
raise ProtocolException("invalid greeting: {}".format(greeting))
self._protocol_version = greeting[len(Client.PROTOCOL_GREETING):].strip()
self._logger.debug("protocol version: %s", self._protocol_version)
def _disconnect(self): def _disconnect(self):
self._call('noidle') self._logger.info("disconnecting")
self._call('disconnect') self._disconnect_socket()
def _disconnect_socket(self):
if self._sock_read is not None:
self._sock_read.close()
if self._sock_write is not None:
self._sock_write.close()
if self._sock is not None:
self._sock.close()
self._logger.info("disconnected")
self._set_connection_status(False) self._set_connection_status(False)
# Status commands def _idle(self):
"""React to idle events from MPD."""
self._logger.info("idle")
self._idling = True
subsystems = self._parse_dict(self._call("idle"))
self._idling = False
self._logger.info("idle subsystems: %r", subsystems)
if subsystems:
if subsystems['changed'] == 'player':
self.get_status()
if subsystems['changed'] == 'mixer':
self.get_status()
if subsystems['changed'] == 'playlist':
self.load_playlist()
if subsystems['changed'] == 'database':
self.load_albums()
self.load_playlist()
self.get_status()
if subsystems['changed'] == 'update':
self.load_albums()
self.load_playlist()
self.get_status()
def _noidle(self):
if self._idling:
self._logger.debug("noidle")
self._write("noidle")
def _get_status(self): def _get_status(self):
"""Action: Perform the real status determination.""" """Action: Perform the real status determination."""
# current status self._logger.info("getting status")
status = self._call('status') status = self._parse_dict(self._call("status"))
if 'state' not in status: self._logger.debug("status: %r", status)
return
# State
state = None
if 'state' in status:
state = status['state'] state = status['state']
self._state = state
# Time
time = 0 time = 0
if 'time' in status: if 'time' in status:
time = int(status['time'].split(':')[0]) time = int(status['time'].split(':')[0])
# Volume
volume = 0 volume = 0
if 'volume' in status: if 'volume' in status:
volume = int(status['volume']) volume = int(status['volume'])
# Error
error = None error = None
if 'error' in status: if 'error' in status:
error = status['error'] error = status['error']
# Album
# current song
song = self._call('currentsong')
album = None album = None
pos = None pos = 0
song = self._parse_dict(self._call("currentsong"))
if song: if song:
# Track
if 'artist' not in song:
return
if 'title' not in song:
return
if 'track' not in song:
song['track'] = None
if 'time' not in song:
song['time'] = 0
if 'date' not in song:
song['date'] = None
if 'file' not in song:
return
track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
# Album # Album
if 'album' not in song: if 'album' not in song:
song['album'] = 'Various' song['album'] = MCGAlbum.DEFAULT_ALBUM
hash = MCGAlbum.hash(song['album']) hash = MCGAlbum.hash(song['album'])
if hash not in self._albums: if hash in self._albums.keys():
return
album = self._albums[hash] album = self._albums[hash]
# Position # Position
pos = 0
if 'pos' in song: if 'pos' in song:
pos = int(song['pos']) pos = int(song['pos'])
for palbum in self._playlist: for palbum in self._playlist:
@ -312,37 +344,104 @@ class MCGClient(MCGBase, mpd.MPDClient):
album = palbum album = palbum
break break
pos = pos - len(palbum.get_tracks()) pos = pos - len(palbum.get_tracks())
self._callback(Client.SIGNAL_STATUS, state, album, pos, time, volume, error)
self._state = state
self._callback(MCGClient.SIGNAL_STATUS, state, album, pos, time, volume, error)
# Playback option commants def _load_albums(self):
"""Action: Perform the real update."""
def _set_volume(self, volume): self._albums = {}
self._call('setvol', volume) for song in self._parse_list(self._call('listallinfo'), ['file', 'directory']):
self._logger.debug("song: %r", song)
if 'file' in song:
# Track
track = None
if 'artist' in song and 'title' in song and 'file' in song:
if 'track' not in song:
song['track'] = None
if 'time' not in song:
song['time'] = 0
if 'date' not in song:
song['date'] = None
track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
self._logger.debug("track: %r", track)
# Album
if 'album' not in song:
song['album'] = MCGAlbum.DEFAULT_ALBUM
hash = MCGAlbum.hash(song['album'])
if hash in self._albums.keys():
album = self._albums[hash]
else:
album = MCGAlbum(song['album'], self._host, self._image_dir)
self._albums[album.get_hash()] = album
self._logger.debug("album: %r", album)
# Add track to album
if track:
album.add_track(track)
self._callback(Client.SIGNAL_LOAD_ALBUMS, self._albums)
# Playback control commands def _update(self):
self._call('update')
def _load_playlist(self):
self._playlist = []
for song in self._parse_list(self._call('playlistinfo'), ['file', 'playlist']):
self._logger.debug("song: %r", song)
# Track
track = None
if 'artist' in song and 'title' in song and 'file' in song:
if 'track' not in song:
song['track'] = None
if 'time' not in song:
song['time'] = 0
if 'date' not in song:
song['date'] = None
track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
self._logger.debug("track: %r", track)
# Album
if 'album' not in song:
song['album'] = MCGAlbum.DEFAULT_ALBUM
hash = MCGAlbum.hash(song['album'])
if len(self._playlist) == 0 or self._playlist[len(self._playlist)-1].get_hash() != hash:
album = MCGAlbum(song['album'], self._host, self._image_dir)
self._playlist.append(album)
else:
album = self._playlist[len(self._playlist)-1]
self._logger.debug("album: %r", album)
if track:
album.add_track(track)
self._callback(Client.SIGNAL_LOAD_PLAYLIST, self._playlist)
def _clear_playlist(self):
"""Action: Perform the real clearing of the current playlist."""
self._call('clear')
def _playpause(self): def _playpause(self):
"""Action: Perform the real play/pause command.""" """Action: Perform the real play/pause command."""
status = self._call('status') #status = self._parse_dict(self._call('status'))
state = status['state'] #if 'state' in status:
if state == 'play': if self._state == 'play':
self._call('pause') self._call('pause')
else: else:
self._call('play') self._call('play')
def _play_album(self, album): def _play_album(self, album):
if album not in self._albums: if album in self._albums:
return
track_ids = [] track_ids = []
for track in self._albums[album].get_tracks(): for track in self._albums[album].get_tracks():
track_id = self._call('addid', track.get_file()) self._logger.info("addid: %r", track.get_file())
track_id = None
track_id_response = self._parse_dict(self._call('addid', track.get_file()))
if 'id' in track_id_response:
track_id = track_id_response['id']
self._logger.debug("track id: %r", track_id)
if track_id is not None:
track_ids.append(track_id) track_ids.append(track_id)
if self._state != 'play': if self._state != 'play' and track_ids:
self._call('playid', track_ids[0]) self._call('playid', track_ids[0])
@ -354,118 +453,127 @@ class MCGClient(MCGBase, mpd.MPDClient):
self._call('stop') self._call('stop')
# Playlist commands def _set_volume(self, volume):
self._call('setvol', volume)
def _load_playlist(self):
self._playlist = [] def _start_worker(self):
for song in self._call('playlistinfo'): """Start the worker thread which waits for action to be performed."""
self._logger.debug("start worker")
self._worker = threading.Thread(target=self._run, name='mcg-worker', args=())
self._worker.setDaemon(True)
self._worker.start()
self._logger.debug("worker started")
def _run(self):
while not self._stop.is_set() or not self._actions.empty():
if self._sock is not None and self._actions.empty():
self._add_action(self._idle)
action = self._actions.get()
self._logger.debug("next action: %r", action)
self._work(action)
self._actions.task_done()
self._logger.debug("action done")
self._logger.debug("worker finished")
def _add_action(self, method, *args):
"""Add an action to the action list."""
self._logger.debug("add action %r (%r)", method.__name__, args)
action = (method, args)
self._actions.put(action)
self._noidle()
def _work(self, action):
(method, args) = action
self._logger.debug("work: %r", method.__name__)
try: try:
# Track method(*args)
if 'artist' not in song: except ConnectionException as e:
continue self._logger.exception(e)
if 'title' not in song: self._callback(Client.SIGNAL_ERROR, e)
continue self._disconnect_socket()
if 'track' not in song: except Exception as e:
song['track'] = None self._logger.exception(e)
if 'time' not in song: self._callback(Client.SIGNAL_ERROR, e)
song['time'] = 0
if 'date' not in song:
song['date'] = None
if 'file' not in song:
continue
track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
# Album
if 'album' not in song: def _call(self, command, *args):
song['album'] = 'Various' try:
hash = MCGAlbum.hash(song['album']) self._write(command, args)
if len(self._playlist) == 0 or self._playlist[len(self._playlist)-1].get_hash() != hash: return self._read()
album = MCGAlbum(song['album'], self._host, self._image_dir) except MPDException as e:
self._playlist.append(album) self._callback(Client.SIGNAL_ERROR, e)
def _write(self, command, args=None):
if args is not None and len(args) > 0:
line = '{} "{}"\n'.format(command, '" "'.join(str(x) for x in args))
else: else:
album = self._playlist[len(self._playlist)-1] line = '{}\n'.format(command)
album.add_track(track) self._logger.debug("write: %r", line)
except KeyError: self._sock_write.write(line)
pass self._sock_write.flush()
self._callback(MCGClient.SIGNAL_LOAD_PLAYLIST, self._playlist, None)
def _clear_playlist(self): def _read(self):
"""Action: Perform the real clearing of the current playlist.""" self._logger.debug("reading response")
self._call('clear') response = []
line = self._sock_read.readline()
if not line.endswith("\n"):
self._disconnect_socket()
raise ConnectionException("incomplete line")
while not line.startswith(Client.PROTOCOL_COMPLETION) and not line.startswith(Client.PROTOCOL_ERROR):
response.append(line.strip())
line = self._sock_read.readline()
if not line.endswith("\n"):
self._disconnect_socket()
raise ConnectionException("incomplete line")
if line.startswith(Client.PROTOCOL_COMPLETION):
self._logger.debug("response complete")
if line.startswith(Client.PROTOCOL_ERROR):
error = line[len(Client.PROTOCOL_ERROR):].strip()
self._logger.debug("command failed: %r", error)
raise CommandException(error)
self._logger.debug("response: %r", response)
return response
# Database commands def _parse_dict(self, response):
dict = {}
def _load_albums(self): for line in response:
"""Action: Perform the real update.""" key, value = self._split_line(line)
self._albums = {} dict[key] = value
for song in self._call('listallinfo'): return dict
if 'directory' in song:
continue
# Track
if 'artist' not in song:
continue
if 'title' not in song:
continue
if 'track' not in song:
song['track'] = None
if 'time' not in song:
song['time'] = 0
if 'date' not in song:
song['date'] = None
if 'file' not in song:
continue
track = MCGTrack(song['artist'], song['title'], song['track'], song['time'], song['date'], song['file'])
# Album
if 'album' not in song:
song['album'] = 'Various'
hash = MCGAlbum.hash(song['album'])
if hash in self._albums.keys():
album = self._albums[hash]
else:
album = MCGAlbum(song['album'], self._host, self._image_dir)
self._albums[album.get_hash()] = album
album.add_track(track)
self._callback(MCGClient.SIGNAL_LOAD_ALBUMS, self._albums, None)
def _update(self): def _parse_list(self, response, delimiters):
self._call('update') entry = {}
for line in response:
def _set_connection_status(self, status, error=None): key, value = self._split_line(line)
self._connected = status if entry and key in delimiters:
self._callback(MCGClient.SIGNAL_CONNECT, status, error) yield entry
if not status: entry = {}
self._client_stop.set() entry[key] = value
if entry:
yield entry
def _idle(self): def _split_line(self, line):
"""React to idle events from MPD.""" parts = line.split(': ')
modules = self._call('idle') return parts[0].lower(), ': '.join(parts[1:])
if not modules:
return
if 'player' in modules: def _set_connection_status(self, status):
self.get_status() self._callback(Client.SIGNAL_CONNECTION, status)
if 'mixer' in modules:
self.get_status()
if 'playlist' in modules:
self.load_playlist()
if 'database' in modules:
self.load_albums()
self.load_playlist()
self.get_status()
if 'update' in modules:
self.load_albums()
self.load_playlist()
self.get_status()
class MCGAlbum: class MCGAlbum:
DEFAULT_ALBUM = 'Various'
SORT_BY_ARTIST = 'artist' SORT_BY_ARTIST = 'artist'
SORT_BY_TITLE = 'title' SORT_BY_TITLE = 'title'
SORT_BY_YEAR = 'year' SORT_BY_YEAR = 'year'
@ -596,7 +704,7 @@ class MCGAlbum:
names.append(self._title) names.append(self._title)
names.append(' - '.join([self._artists[0], self._title])) names.append(' - '.join([self._artists[0], self._title]))
if self._host == "localhost" or self._host == "127.0.0.1": if self._host == "localhost" or self._host == "127.0.0.1" or self._host == "::1":
self._cover = self._find_cover_local(names) self._cover = self._find_cover_local(names)
else: else:
self._cover = self._find_cover_web(names) self._cover = self._find_cover_web(names)
@ -610,6 +718,7 @@ class MCGAlbum:
url = '/'.join([ url = '/'.join([
'http:/', 'http:/',
self._host, self._host,
urllib.request.quote(self._image_dir.strip("/")),
urllib.request.quote(path), urllib.request.quote(path),
urllib.request.quote('.'.join([name, ext])) urllib.request.quote('.'.join([name, ext]))
]) ])
@ -878,5 +987,3 @@ class MCGCache():
os.unlink(path) os.unlink(path)
except Exception as e: except Exception as e:
print("clear:", e) print("clear:", e)

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""MPDCoverGrid (GTK version) is a client for the Music Player Daemon, focused on albums instead of single tracks.""" """MPDCoverGrid (GTK version) is a client for the Music Player Daemon, focused on albums instead of single tracks."""
@ -30,9 +30,12 @@ if not os.environ.get('GSETTINGS_SCHEMA_DIR'):
if __name__ == "__main__": def start():
# Start application
app = gtk.Application() app = gtk.Application()
exit_status = app.run(sys.argv) exit_status = app.run(sys.argv)
sys.exit(exit_status) sys.exit(exit_status)
if __name__ == "__main__":
# Start application
start()