--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+def main():
+ from sima import core
+ from sima.plugins.crop import Crop
+ m = core.Sima()
+ m.register_plugin(Crop)
+ try:
+ m.run()
+ except KeyboardInterrupt:
+ m.shutdown()
+
+
+# Script starts here
+if __name__ == '__main__':
+ main()
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
+
--- /dev/null
+# -* coding: utf-8 -*-
+"""MPD client for Sima
+
+This client is built above python-musicpd a fork of python-mpd
+"""
+
+# standart library import
+from select import select
+
+# third parties componants
+try:
+ from musicpd import (MPDClient, MPDError, CommandError)
+except ImportError as err:
+ from sys import exit as sexit
+ print('ERROR: missing python-musicpd?\n{0}'.format(err))
+ sexit(1)
+
+# local import
+from .lib.player import Player
+from .lib.track import Track
+
+
+class PlayerError(Exception):
+ """Fatal error in poller."""
+
+class PlayerCommandError(PlayerError):
+ """Command error"""
+
+
+class PlayerClient(Player):
+ """MPC Client
+ From python-musicpd:
+ _fetch_nothing …
+ _fetch_item single str
+ _fetch_object single dict
+ _fetch_list list of str
+ _fetch_playlist list of str
+ _fetch_changes list of dict
+ _fetch_database list of dict
+ _fetch_songs list of dict, especially tracks
+ _fetch_plugins,
+ TODO: handle exception in command not going through _client_wrapper() (ie.
+ find_aa, remove…)
+ """
+ def __init__(self, host="localhost", port="6600", password=None):
+ self._host = host
+ self._port = port
+ self._password = password
+ self._client = MPDClient()
+ self._client.iterate = True
+
+ def __getattr__(self, attr):
+ command = attr
+ wrapper = self._execute
+ return lambda *args: wrapper(command, args)
+
+ def _execute(self, command, args):
+ self._write_command(command, args)
+ return self._client_wrapper()
+
+ def _write_command(self, command, args=[]):
+ self._comm = command
+ self._args = list()
+ for arg in args:
+ self._args.append(arg)
+
+ def _client_wrapper(self):
+ func = self._client.__getattr__(self._comm)
+ try:
+ ans = func(*self._args)
+ # WARNING: MPDError is an ancestor class of # CommandError
+ except CommandError as err:
+ raise PlayerCommandError('MPD command error: %s' % err)
+ except (MPDError, IOError) as err:
+ raise PlayerError(err)
+ return self._track_format(ans)
+
+ def _track_format(self, ans):
+ # TODO: ain't working for "sticker find" and "sticker list"
+ tracks_listing = ["playlistfind", "playlistid", "playlistinfo",
+ "playlistsearch", "plchanges", "listplaylistinfo", "find",
+ "search", "sticker find",]
+ track_obj = ['currentsong']
+ unicode_obj = ["idle", "listplaylist", "list", "sticker list",
+ "commands", "notcommands", "tagtypes", "urlhandlers",]
+ if self._comm in tracks_listing + track_obj:
+ # pylint: disable=w0142
+ if isinstance(ans, list):
+ return [Track(**track) for track in ans]
+ elif isinstance(ans, dict):
+ return Track(**ans)
+ return ans
+
+ def find_track(self, artist, title=None):
+ #return getattr(self, 'find')('artist', artist, 'title', title)
+ if title:
+ return self.find('artist', artist, 'title', title)
+ return self.find('artist', artist)
+
+ def find_album(self, artist, album):
+ """
+ Special wrapper around album search:
+ Album lookup is made through AlbumArtist/Album instead of Artist/Album
+ """
+ alb_art_search = self.find('albumartist', artist, 'album', album)
+ if alb_art_search:
+ return alb_art_search
+ return self.find('artist', artist, 'album', album)
+
+ def monitor(self):
+ try:
+ self._client.send_idle('database', 'playlist', 'player', 'options')
+ select([self._client], [], [], 60)
+ return self._client.fetch_idle()
+ except (MPDError, IOError) as err:
+ raise PlayerError("Couldn't init idle: %s" % err)
+
+ def idle(self):
+ try:
+ self._client.send_idle('database', 'playlist', 'player', 'options')
+ select([self._client], [], [], 60)
+ return self._client.fetch_idle()
+ except (MPDError, IOError) as err:
+ raise PlayerError("Couldn't init idle: %s" % err)
+
+ def remove(self, position=0):
+ self._client.delete(position)
+
+ @property
+ def state(self):
+ return str(self._client.status().get('state'))
+
+ @property
+ def current(self):
+ return self.currentsong()
+
+ def playlist(self):
+ """
+ Override deprecated MPD playlist command
+ """
+ return self.playlistinfo()
+
+ def connect(self):
+ self.disconnect()
+ try:
+ self._client.connect(self._host, self._port)
+
+ # Catch socket errors
+ except IOError as err:
+ raise PlayerError('Could not connect to "%s:%s": %s' %
+ (self._host, self._port, err.strerror))
+
+ # Catch all other possible errors
+ # ConnectionError and ProtocolError are always fatal. Others may not
+ # be, but we don't know how to handle them here, so treat them as if
+ # they are instead of ignoring them.
+ except MPDError as err:
+ raise PlayerError('Could not connect to "%s:%s": %s' %
+ (self._host, self._port, err))
+
+ if self._password:
+ try:
+ self._client.password(self._password)
+
+ # Catch errors with the password command (e.g., wrong password)
+ except CommandError as err:
+ raise PlayerError("Could not connect to '%s': "
+ "password command failed: %s" %
+ (self._host, err))
+
+ # Catch all other possible errors
+ except (MPDError, IOError) as err:
+ raise PlayerError("Could not connect to '%s': "
+ "error with password command: %s" %
+ (self._host, err))
+ # Controls we have sufficient rights for MPD_sima
+ needed_cmds = ['status', 'stats', 'add', 'find', \
+ 'search', 'currentsong', 'ping']
+
+ available_cmd = self._client.commands()
+ for nddcmd in needed_cmds:
+ if nddcmd not in available_cmd:
+ self.disconnect()
+ raise PlayerError('Could connect to "%s", '
+ 'but command "%s" not available' %
+ (self._host, nddcmd))
+
+ def disconnect(self):
+ # Try to tell MPD we're closing the connection first
+ try:
+ self._client.close()
+ # If that fails, don't worry, just ignore it and disconnect
+ except (MPDError, IOError):
+ pass
+
+ try:
+ self._client.disconnect()
+ # Disconnecting failed, so use a new client object instead
+ # This should never happen. If it does, something is seriously broken,
+ # and the client object shouldn't be trusted to be re-used.
+ except (MPDError, IOError):
+ self._client = MPDClient()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from .client import PlayerClient
+
+class Sima(object):
+ """Main class, plugin and player management
+ """
+
+ def __init__(self):
+ self.plugins = list()
+ self.player = None
+ self.connect_player()
+
+ def register_plugin(self, plugin_class):
+ self.plugins.append(plugin_class(self))
+
+ def foreach_plugin(self, method, *args, **kwds):
+ for plugin in self.plugins:
+ getattr(plugin, method)(*args, **kwds)
+
+ def connect_player(self):
+ """Instanciate player client and connect it
+ """
+ self.player = PlayerClient() # Player client
+ self.player.connect()
+
+ def shutdown(self):
+ """General shutdown method
+ """
+ self.player.disconnect()
+ self.foreach_plugin('shutdown')
+
+ def run(self):
+ """Dispatching callbacks to plugins
+ """
+ print(self.player.status())
+ while 42:
+ # hanging here untill a monitored event is raised in the player
+ changed = self.player.monitor()
+ print(changed)
+ print(self.player.current)
+ if 'playlist' in changed:
+ self.foreach_plugin('callback_playlist')
+ if 'player' in changed:
+ pass
+
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# TODO:
+# Add decorator to filter through history?
+
+from sima.lib.track import Track
+
+
+class Player(object):
+
+ """Player interface to inherit from.
+
+ When querying palyer music library for tracks, Player instance *must* return
+ Track objects (usually a list of them)
+ """
+
+ def __init__(self):
+ self.state = {}
+ self.current = {}
+
+ def monitor(self):
+ """Monitor player for change
+ Returns :
+ * database player media library has changed
+ * playlist playlist modified
+ * options player options changed: repeat mode, etc…
+ * player player state changed: paused, stopped, skip track…
+ """
+ raise NotImplementedError
+
+ def remove(self, position=0):
+ """Removes the oldest element of the playlist (index 0)
+ """
+ raise NotImplementedError
+
+ def find_track(self, artist, title=None):
+ """
+ Find tracks for a specific artist or filtering with a track title
+ >>> player.find_track('The Beatles')
+ >>> player.find_track('Nirvana', title='Smells Like Teen Spirit')
+
+ Returns a list of Track objects
+ """
+ raise NotImplementedError
+
+ def find_album(self, artist, album):
+ """
+ Find tracks by track's album name
+ >>> player.find_track('Nirvana', 'Nevermind')
+
+ Returns a list of Track objects
+ """
+
+ def disconnect(self):
+ """Closing client connection with the Player
+ """
+ raise NotImplementedError
+
+ def connect(self):
+ """Connect client to the Player
+ """
+ raise NotImplementedError
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
+
--- /dev/null
+# -*- coding: utf-8 -*-
+
+class Plugin():
+ def __init__(self, daemon):
+ self.__daemon = daemon
+ #self.history = daemon.player.history
+
+ @property
+ def name(self):
+ return self.__class__.__name__.lower()
+
+ def callback_playlist(self):
+ """
+ Called on playlist changes
+
+ Not returning data
+ """
+ pass
+
+ def callback_next_song(self):
+ """Not returning data,
+ Could be use to scrobble
+ """
+ pass
+
+ def callback_need_song(self):
+ """Returns a list of Track objects to add
+ """
+ pass
+
+ def callback_player_stop(self):
+ """Not returning data,
+ Could be use to ensure player never stops
+ """
+ pass
+
+ def shutdown(self):
+ pass
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009, 2010, 2011, 2013 Jack Kaliko <efrim@azylum.org>
+# Copyright (c) 2009 J. Alexander Treuman (Tag collapse method)
+# Copyright (c) 2008 Rick van Hattem
+#
+# This file is part of MPD_sima
+#
+# MPD_sima is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# MPD_sima 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with MPD_sima. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import time
+
+
+class Track(object):
+ """
+ Track object.
+ Instanciate with mpd replies.
+ """
+
+ def __init__(self, file=None, time=0, pos=0, **kwargs):
+ self.title = self.artist = self.album = self.albumartist = ''
+ self._pos = pos
+ self.empty = False
+ self._file = file
+ if not kwargs:
+ self.empty = True
+ self.time = time
+ self.__dict__.update(**kwargs)
+ self.tags_to_collapse = list(['artist', 'album', 'title', 'date',
+ 'genre', 'albumartist'])
+ # have tags been collapsed?
+ self.collapse_tags_bool = False
+ self.collapsed_tags = list()
+ # Needed for multiple tags which returns a list instead of a string
+ self.collapse_tags()
+
+ def collapse_tags(self):
+ """
+ Necessary to deal with tags defined multiple times.
+ These entries are set as lists instead of strings.
+ """
+ for tag, value in self.__dict__.items():
+ if tag not in self.tags_to_collapse:
+ continue
+ if isinstance(value, list):
+ self.collapse_tags_bool = True
+ self.collapsed_tags.append(tag)
+ self.__dict__.update({tag: ', '.join(set(value))})
+
+ def get_filename(self):
+ """return filename"""
+ if not self.file:
+ return None
+ return self.file
+
+ def __repr__(self):
+ return '%s(artist="%s", album="%s", title="%s", filename="%s")' % (
+ self.__class__.__name__,
+ self.artist,
+ self.album,
+ self.title,
+ self.file,
+ )
+
+ def __str__(self):
+ return '{artist} - {album} - {title} ({duration})'.format(
+ duration=self.duration,
+ **self.__dict__
+ )
+
+ def __int__(self):
+ return self.time
+
+ def __add__(self, other):
+ return Track(time=self.time + other.time)
+
+ def __sub__(self, other):
+ return Track(time=self.time - other.time)
+
+ def __hash__(self):
+ if self.file:
+ return hash(self.file)
+ else:
+ return id(self)
+
+ def __eq__(self, other):
+ return hash(self) == hash(other)
+
+ def __ne__(self, other):
+ return hash(self) != hash(other)
+
+ def __bool__(self):
+ return not self.empty
+
+ @property
+ def pos(self):
+ """return position of track in the playlist"""
+ return int(self._pos)
+
+ @property
+ def file(self):
+ """file is an immutable attribute that's used for the hash method"""
+ return self._file
+
+ def get_time(self):
+ """get time property"""
+ return self._time
+
+ def set_time(self, value):
+ """set time property"""
+ self._time = int(value)
+
+ time = property(get_time, set_time, doc='song duration in seconds')
+
+ @property
+ def duration(self):
+ """Compute fancy duration"""
+ temps = time.gmtime(int(self.time))
+ if temps.tm_hour:
+ fmt = '%H:%M:%S'
+ else:
+ fmt = '%M:%S'
+ return time.strftime(fmt, temps)
+
+
+def main():
+ pass
+
+# Script starts here
+if __name__ == '__main__':
+ main()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+"""Crops playlist
+"""
+
+# standart library import
+#from select import select
+
+# third parties componants
+
+# local import
+from ..lib.plugin import Plugin
+
+class Crop(Plugin):
+ """
+ Crop playlist on next track
+ """
+
+ def callback_playlist(self):
+ player = self._Plugin__daemon.player
+ target_lengh = 10
+ while player.currentsong().pos > target_lengh:
+ player.remove()
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab