Big restructuring

This commit is contained in:
coderkun 2013-02-25 17:32:33 +01:00
commit 9bec6c7675
2 changed files with 1272 additions and 658 deletions

654
mcg.py
View file

@ -8,10 +8,46 @@
import mpd import mpd
import os import os
import threading
import queue
from hashlib import md5 from hashlib import md5
from threading import Thread import urllib.request
import configparser
class MCGClient:
class MCGBase():
def __init__(self):
self._callbacks = {}
def connect_signal(self, signal, callback):
"""Connects a callback function to a signal (event).
"""
self._callbacks[signal] = callback
def disconnect_signal(self, signal):
if self._has_callback(signal):
del self._callbacks[signal]
def _has_callback(self, signal):
"""Checks if there is a registered callback function for a
signal.
"""
return signal in self._callbacks
def _callback(self, signal, *data):
if signal in self._callbacks:
callback = self._callbacks[signal]
callback(*data)
class MCGClient(MCGBase, mpd.MPDClient):
"""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.
@ -20,30 +56,37 @@ class MCGClient:
""" """
# Signal: connect/disconnect event # Signal: connect/disconnect event
SIGNAL_CONNECT = 'connect' SIGNAL_CONNECT = 'connect'
# Signal: general idle event
SIGNAL_IDLE = 'idle'
# Signal: status event # Signal: status event
SIGNAL_STATUS = 'status' SIGNAL_STATUS = 'status'
# Signal: update event # Signal: load albums
SIGNAL_UPDATE = 'update' SIGNAL_LOAD_ALBUMS = 'load-albums'
# Signal: load playlist
SIGNAL_LOAD_PLAYLIST = 'load-playlist'
def __init__(self): def __init__(self):
"""Sets class variables and instantiates the MPDClient. """Sets class variables and instantiates the MPDClient.
""" """
MCGBase.__init__(self)
mpd.MPDClient.__init__(self)
self._connected = False self._connected = False
self._albums = {} self._client_lock = threading.Lock()
self._callbacks = {} self._client_stop = threading.Event()
self._actions = [] self._actions = queue.Queue()
self._worker = None self._worker = None
self._client = mpd.MPDClient() self._albums = {}
self._go = True self._host = None
self._image_dir = ""
def connect(self, host="localhost", port="6600", password=None): # Connection commands
def connect(self, host="localhost", port="6600", password=None, image_dir=""):
"""Connects to MPD with the given host, port and password or """Connects to MPD with the given host, port and password or
with standard values. with standard values.
""" """
self._host = host
self._image_dir = image_dir
self._add_action(self._connect, host, port, password) self._add_action(self._connect, host, port, password)
@ -56,28 +99,15 @@ class MCGClient:
def disconnect(self): def disconnect(self):
"""Disconnects from the connected MPD. """Disconnects from the connected MPD.
""" """
self._client_stop.set()
self._add_action(self._disconnect) self._add_action(self._disconnect)
def close(self): def join(self):
"""Closes the connection and stops properly the worker thread. self._actions.join()
This method is to stop the whole appliction.
"""
if not self.is_connected():
return
try:
self._go = False
self._client.noidle()
self._client.disconnect()
except TypeError as e:
pass
def update(self): # Status commands
"""Updates the album list.
"""
self._add_action(self._update)
def get_status(self): def get_status(self):
"""Determines the current status. """Determines the current status.
@ -85,50 +115,50 @@ class MCGClient:
self._add_action(self._get_status) self._add_action(self._get_status)
def play_album(self, album): # Playback option commands
"""Plays the given album.
"""
self._add_action(self._play, album)
# Playback control commands
def playpause(self): def playpause(self):
"""Plays or pauses the current state. """Plays or pauses the current state.
""" """
self._add_action(self._playpause) self._add_action(self._playpause)
def next_song(self): def play_album(self, album):
"""Plays the next album in the current order """Plays the given album.
""" """
self._add_action(self._next_song) self._add_action(self._play_album, album)
def connect_signal(self, signal, callback): def stop(self):
"""Connects a callback function to a signal (event). self._add_action(self._stop)
"""
self._callbacks[signal] = callback
def _has_callback(self, signal): # Playlist commands
"""Checks if there is a registered callback function for a
signal. def load_playlist(self):
""" self._add_action(self._load_playlist)
return signal in self._callbacks
def _callback(self, signal, *args): # Database commands
"""Calls the callback function for a signal.
"""
if self._has_callback(signal):
callback = self._callbacks[signal]
callback(*args)
def load_albums(self):
self._add_action(self._load_albums)
def update(self):
self._add_action(self._update)
# Private methods
def _add_action(self, method, *args): def _add_action(self, method, *args):
"""Adds an action to the action list. """Adds an action to the action list.
""" """
action = [method, args] action = [method, args]
self._actions.append(action) self._actions.put(action)
self._start_worker() self._start_worker()
@ -137,173 +167,251 @@ class MCGClient:
performed. performed.
""" """
if self._worker is None or not self._worker.is_alive(): if self._worker is None or not self._worker.is_alive():
self._worker = Thread(target=self._work, name='worker', args=()) self._worker = threading.Thread(target=self._run, name='mcg-worker', args=())
self._worker.setDaemon(True)
self._worker.start() self._worker.start()
else: else:
try: try:
self._client.noidle() self.noidle()
except TypeError as e: except mpd.ConnectionError as e:
pass self._callback(MCGClient.SIGNAL_CONNECT, False, e)
def _work(self): def _work(self, action):
"""Performs the next action or waits for an idle event. method = action[0]
""" params = action[1]
while True: method(*params)
if self._actions:
action = self._actions.pop(0)
method = action[0]
params = action[1]
method(*params)
else:
if not self.is_connected():
break
modules = self._client.idle()
if not self._go:
break
self._idle(modules)
def _call(self, command, *args):
return getattr(super(), command)(*args)
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):
"""Action: Performs the real connect to MPD.
"""
try: try:
self._client.connect(host, port) self._call('connect', host, port)
if password: if password:
self._client.password(password) self._call('password', password)
# TODO Verbindung testen self._set_connction_status(True, None)
self._connected = True except mpd.ConnectionError as e:
self._callback(self.SIGNAL_CONNECT, self._connected, None) self._set_connction_status(False, e)
except IOError as e: except OSError as e:
self._connected = False self._set_connction_status(False, e)
self._callback(self.SIGNAL_CONNECT, self._connected, e)
def _disconnect(self): def _disconnect(self):
"""Action: Performs the real disconnect from MPD.
"""
if not self.is_connected():
return
try: try:
#self._client.close() self._call('noidle')
self._client.disconnect() self._call('disconnect')
except: self._set_connction_status(False, None)
self._client = mpd.MPDClient() except mpd.ConnectionError as e:
self._connected = False self._set_connction_status(False, e)
self._callback(self.SIGNAL_CONNECT, self._connected, None)
def _update(self): # Status commands
"""Action: Performs the real update.
"""
for song in self._client.listallinfo():
try:
hash = MCGAlbum.hash(song['artist'], song['album'])
if hash in self._albums.keys():
album = self._albums[hash]
else:
album = MCGAlbum(song['artist'], song['album'], song['date'], os.path.dirname(song['file']))
self._albums[album.get_hash()] = album
track = MCGTrack(song['title'], song['track'], song['time'], song['file'])
album.add_track(track)
except KeyError:
pass
self._callback(self.SIGNAL_UPDATE, self._albums)
def _get_status(self): def _get_status(self):
"""Action: Performs the real status determination """Action: Performs the real status determination
""" """
if not self._has_callback(self.SIGNAL_STATUS): try:
return self._call('noidle')
status = self._client.status() status = self._call('status')
state = status['state'] state = status['state']
song = self._client.currentsong() self._call('noidle')
album = None song = self._call('currentsong')
pos = None album = None
if song: pos = None
album = self._albums[MCGAlbum(song['artist'], song['album'], song['date'], os.path.dirname(song['file'])).get_hash()] if song:
pos = int(song['pos']) hash = MCGAlbum.hash(song['artist'], song['album'])
self._callback(self.SIGNAL_STATUS, state, album, pos) if hash in self._albums:
album = self._albums[hash]
pos = int(song['pos'])
self._callback(MCGClient.SIGNAL_STATUS, state, album, pos, None)
except mpd.ConnectionError as e:
self._set_connction_status(False, e)
def _play(self, album): # Playback option commants
"""Action: Performs the real play command.
"""
self._client.clear()
track_ids = []
for track in self._albums[album].get_tracks():
track_id = self._client.addid(track.get_file())
track_ids.append(track_id)
self._client.moveid(track_id, len(track_ids)-1)
self._client.playid(track_ids[0])
# Playback control commands
def _playpause(self): def _playpause(self):
"""Action: Performs the real play/pause command. """Action: Performs the real play/pause command.
""" """
status = self._client.status() status = self._call('status')
state = status['state'] state = status['state']
if state == 'play': if state == 'play':
self._client.pause() self._call('pause')
else: else:
self._client.play() self._call('play')
def _next_song(self): def _play_album(self, album):
"""Action: Performs the real next command. """Action: Performs the real play command.
""" """
self._client.next() if not album in self._albums:
# TODO print
print("album not found")
def _idle(self, modules):
"""Reacts to idle events from MPD.
"""
if not modules:
return return
if 'player' in modules: try:
self._get_status() self._call('clear')
if 'database' in modules: track_ids = []
# TODO update DB for track in self._albums[album].get_tracks():
pass track_id = self._call('addid', track.get_file())
if 'update' in modules: track_ids.append(track_id)
# TODO update self._call('moveid', track_id, len(track_ids)-1)
pass self._call('playid', track_ids[0])
if 'mixer' in modules: except mpd.ConnectionError as e:
# TODO mixer self._set_connction_status(False, e)
pass
def _idle_playlist(self): def _stop(self):
""" Reacts on the playlist idle event. try:
self._call('stop')
except mpd.ConnectionError as e:
self._set_connction_status(False, e)
# Playlist commands
def _load_playlist(self):
try:
playlist = []
for song in self._call('playlistinfo'):
try:
hash = MCGAlbum.hash(song['artist'], song['album'])
if len(playlist) == 0 or playlist[len(playlist)-1].get_hash() != hash:
date = ""
if 'date' in song:
date = song['date']
path = ""
if 'file' in song:
path = os.path.dirname(song['file'])
album = MCGAlbum(song['artist'], song['album'], date, path, self._host, self._image_dir)
playlist.append(album)
else:
album = playlist[len(playlist)-1]
track = MCGTrack(song['title'], song['track'], song['time'], song['file'])
album.add_track(track)
except KeyError:
pass
self._callback(MCGClient.SIGNAL_LOAD_PLAYLIST, playlist, None)
except mpd.ConnectionError as e:
self._set_connction_status(False, e)
# Database commands
def _load_albums(self):
"""Action: Performs the real update.
""" """
pass try:
for song in self._call('listallinfo'):
try:
hash = MCGAlbum.hash(song['artist'], song['album'])
if hash in self._albums.keys():
album = self._albums[hash]
else:
date = ""
if 'date' in song:
date = song['date']
path = ""
if 'file' in song:
path = os.path.dirname(song['file'])
album = MCGAlbum(song['artist'], song['album'], date, path, self._host, self._image_dir)
self._albums[album.get_hash()] = album
track = MCGTrack(song['title'], song['track'], song['time'], song['file'])
album.add_track(track)
except KeyError:
pass
self._callback(MCGClient.SIGNAL_LOAD_ALBUMS, self._albums, None)
except mpd.ConnectionError as e:
self._set_connction_status(False, e)
def _update(self):
try:
self._call('update')
except mpd.ConnectionError as e:
self._set_connction_status(False, e)
def _set_connction_status(self, status, error):
self._connected = status
self._callback(MCGClient.SIGNAL_CONNECT, status, error)
if not status:
self._client_stop.set()
def _idle(self):
"""Reacts to idle events from MPD.
"""
try:
modules = self._call('idle')
if not modules:
return
if 'player' in modules:
self.get_status()
if 'mixer' in modules:
# TODO mixer
print("not implemented: idle mixer")
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()
except ConnectionResetError as e:
self._set_connction_status(False, e)
except mpd.ConnectionError as e:
self._set_connction_status(False, e)
class MCGAlbum: class MCGAlbum:
_file_names = ['folder', 'cover']
_file_exts = ['jpg', 'jpeg', 'png']
SORT_BY_ARTIST = 'artist' SORT_BY_ARTIST = 'artist'
SORT_BY_TITLE = 'title' SORT_BY_TITLE = 'title'
SORT_BY_YEAR = 'year' SORT_BY_YEAR = 'year'
_file_names = ['folder', 'cover']
_file_exts = ['jpg', 'jpeg', 'png']
def __init__(self, artist, title, date, path): def __init__(self, artist, title, date, path, host, image_dir):
self._artist = artist self._artist = artist
if type(self._artist) is list: if type(self._artist) is list:
self._artist = self._artist[0] self._artist = self._artist[0]
self._title = title self._title = title
self._date = date self._date = date
self._path = path self._path = path
self._host = host
self._image_dir = image_dir
self._tracks = [] self._tracks = []
self._cover = None self._cover = None
self._cover_searched = False
self._set_hash() self._set_hash()
self._find_cover()
def get_artist(self): def get_artist(self):
@ -332,35 +440,17 @@ class MCGAlbum:
def get_cover(self): def get_cover(self):
if self._cover is None and not self._cover_searched:
self._find_cover()
return self._cover return self._cover
def _find_cover(self):
names = list(self._file_names)
names.append(self._title)
names.append(' - '.join((self._artist, self._title)))
for name in names:
for ext in self._file_exts:
filename = os.path.join('/home/oliver/Musik/', self._path, '.'.join([name, ext]))
if os.path.isfile(filename):
self._cover = filename
break
if self._cover is not None:
break
def hash(artist, title): def hash(artist, title):
if type(artist) is list: if type(artist) is list:
artist = artist[0] artist = artist[0]
return md5(artist.encode('utf-8')+title.encode('utf-8')).hexdigest() return md5(artist.encode('utf-8')+title.encode('utf-8')).hexdigest()
def _set_hash(self):
self._hash = MCGAlbum.hash(self._artist, self._title)
def get_hash(self): def get_hash(self):
return self._hash return self._hash
@ -393,6 +483,47 @@ class MCGAlbum:
return 1 return 1
def _set_hash(self):
self._hash = MCGAlbum.hash(self._artist, self._title)
def _find_cover(self):
names = list(self._file_names)
names.append(self._title)
names.append(' - '.join([self._artist, self._title]))
if self._host == "localhost" or self._host == "127.0.0.1":
self._cover = self._find_cover_local(names)
else:
self._cover = self._find_cover_web(names)
self._cover_searched = True
def _find_cover_web(self, names):
for name in names:
for ext in self._file_exts:
url = '/'.join([
'http:/',
self._host,
urllib.request.quote(self._path),
urllib.request.quote('.'.join([name, ext]))
])
request = urllib.request.Request(url)
try:
response = urllib.request.urlopen(request)
return url
except urllib.error.URLError as e:
pass
def _find_cover_local(self, names):
for name in names:
for ext in self._file_exts:
filename = os.path.join(self._image_dir, self._path, '.'.join([name, ext]))
if os.path.isfile(filename):
return filename
class MCGTrack: class MCGTrack:
@ -418,3 +549,164 @@ class MCGTrack:
def get_file(self): def get_file(self):
return self._file return self._file
class MCGConfig(configparser.ConfigParser):
CONFIG_DIR = '~/.config/mcg/'
def __init__(self, filename):
configparser.ConfigParser.__init__(self)
self._filename = os.path.expanduser(os.path.join(MCGConfig.CONFIG_DIR, filename))
self._create_dir()
def load(self):
if os.path.isfile(self._filename):
self.read(self._filename)
def save(self):
with open(self._filename, 'w') as configfile:
self.write(configfile)
def _create_dir(self):
dirname = os.path.dirname(self._filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
class MCGProfileConfig(MCGConfig):
CONFIG_FILE = 'profiles.conf'
def __init__(self):
MCGConfig.__init__(self, MCGProfileConfig.CONFIG_FILE)
self._profiles = []
def add_profile(self, profile):
self._profiles.append(profile)
def get_profiles(self):
return self._profiles
def load(self):
super().load()
count = 0
if self.has_section('profiles'):
if self.has_option('profiles', 'count'):
count = self.getint('profiles', 'count')
for index in range(count):
section = 'profile'+str(index+1)
if self.has_section(section):
profile = MCGProfile()
for attribute in profile.get_attributes():
if self.has_option(section, attribute):
profile.set(attribute, self.get(section, attribute))
self._profiles.append(profile)
def save(self):
if not self.has_section('profiles'):
self.add_section('profiles')
self.set('profiles', 'count', str(len(self._profiles)))
for index in range(len(self._profiles)):
profile = self._profiles[index]
section = 'profile'+str(index+1)
if not self.has_section(section):
self.add_section(section)
for attribute in profile.get_attributes():
self.set(section, attribute, str(profile.get(attribute)))
super().save()
class MCGConfigurable:
def __init__(self):
self._attributes = []
def get(self, attribute):
return getattr(self, attribute)
def set(self, attribute, value):
setattr(self, attribute, value)
if attribute not in self._attributes:
self._attributes.append(attribute)
def get_attributes(self):
return self._attributes
class MCGProfile(MCGConfigurable):
def __init__(self):
MCGConfigurable.__init__(self)
self.set('host', "localhost")
self.set('port', 6600)
self.set('password', "")
self.set('image_dir', "")
self.set('tags', "")
def get_tags(self):
return self.get('tags').split(',')
def set_tags(self, tags):
self.set('tags', ','.join(tags))
class MCGCache():
DIRNAME = '~/.cache/mcg/'
SIZE_FILENAME = 'size'
def __init__(self, host, size):
self._dirname = os.path.expanduser(os.path.join(MCGCache.DIRNAME, host))
if not os.path.exists(self._dirname):
os.makedirs(self._dirname)
self._size = size
self._read_size()
def create_filename(self, album):
return os.path.join(self._dirname, '-'.join([album.get_hash()]))
def _read_size(self):
size = 100
filename = os.path.join(self._dirname, MCGCache.SIZE_FILENAME)
if os.path.exists(filename):
with open(filename, 'r') as f:
size = int(f.readline())
if size != self._size:
self._clear()
with open(filename, 'w') as f:
f.write(str(self._size))
def _clear(self):
for filename in os.listdir(self._dirname):
path = os.path.join(self._dirname, filename)
try:
if os.path.isfile(path):
os.unlink(path)
except Exception as e:
print("clear:", e)

1282
mcgGtk.py

File diff suppressed because it is too large Load diff