]> kaliko git repositories - mpd-sima.git/commitdiff
Initial import
authorkaliko <efrim@azylum.org>
Sat, 21 Sep 2013 16:49:56 +0000 (18:49 +0200)
committerkaliko <efrim@azylum.org>
Sat, 21 Sep 2013 16:49:56 +0000 (18:49 +0200)
The global architecture with the plugin interface is there :)

launch [new file with mode: 0755]
sima/__init__.py [new file with mode: 0644]
sima/client.py [new file with mode: 0644]
sima/core.py [new file with mode: 0644]
sima/lib/__init__.py [new file with mode: 0644]
sima/lib/player.py [new file with mode: 0644]
sima/lib/plugin.py [new file with mode: 0644]
sima/lib/track.py [new file with mode: 0644]
sima/plugins/__init__.py [new file with mode: 0644]
sima/plugins/crop.py [new file with mode: 0644]

diff --git a/launch b/launch
new file mode 100755 (executable)
index 0000000..83ba3ba
--- /dev/null
+++ b/launch
@@ -0,0 +1,21 @@
+#!/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
diff --git a/sima/__init__.py b/sima/__init__.py
new file mode 100644 (file)
index 0000000..bd387be
--- /dev/null
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
+
diff --git a/sima/client.py b/sima/client.py
new file mode 100644 (file)
index 0000000..e19a5aa
--- /dev/null
@@ -0,0 +1,205 @@
+# -* 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
diff --git a/sima/core.py b/sima/core.py
new file mode 100644 (file)
index 0000000..9c96cb5
--- /dev/null
@@ -0,0 +1,51 @@
+#!/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
diff --git a/sima/lib/__init__.py b/sima/lib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sima/lib/player.py b/sima/lib/player.py
new file mode 100644 (file)
index 0000000..c5c69f8
--- /dev/null
@@ -0,0 +1,66 @@
+# -*- 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
+
diff --git a/sima/lib/plugin.py b/sima/lib/plugin.py
new file mode 100644 (file)
index 0000000..ac21b2f
--- /dev/null
@@ -0,0 +1,42 @@
+# -*- 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
diff --git a/sima/lib/track.py b/sima/lib/track.py
new file mode 100644 (file)
index 0000000..93aa350
--- /dev/null
@@ -0,0 +1,147 @@
+# -*- 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
diff --git a/sima/plugins/__init__.py b/sima/plugins/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/sima/plugins/crop.py b/sima/plugins/crop.py
new file mode 100644 (file)
index 0000000..b75f0d9
--- /dev/null
@@ -0,0 +1,26 @@
+# -*- 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