]> kaliko git repositories - mpd-sima.git/commitdiff
Add SimaDB and deal with history
authorkaliko <efrim@azylum.org>
Mon, 23 Sep 2013 18:35:20 +0000 (20:35 +0200)
committerkaliko <efrim@azylum.org>
Mon, 23 Sep 2013 18:35:20 +0000 (20:35 +0200)
README
launch
sima/client.py
sima/core.py
sima/lib/logger.py
sima/lib/plugin.py
sima/lib/simadb.py [new file with mode: 0644]
sima/plugins/addhist.py [new file with mode: 0644]
sima/plugins/crop.py
sima/plugins/lastfm.py [new file with mode: 0644]
sima/utils/config.py

diff --git a/README b/README
index ec5133d141839cbcb56ae8568a7847910a672d0d..f6eb1d0c79be8872f51b4f4cdd3a06d9e6d2f171 100644 (file)
--- a/README
+++ b/README
@@ -3,3 +3,11 @@ Design for python >= 3.3
 Requires python-musicpd:
 
                http://media.kaliko.me/src/musicpd/
+
+
+Changes with MPD_sima
+
+  * project renamed sima
+    → defaults to
+        config : ~/.config/sima/sima.cfg
+        cache  : ~/.local/share/sima/sima.db
diff --git a/launch b/launch
index f3603cb4be3e89042ab2200c88baf78dd4210a68..5c50ef12467c0a4cc4078c09a2dbbfff605db9ba 100755 (executable)
--- a/launch
+++ b/launch
@@ -1,12 +1,19 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
+"""Sima
+"""
 
 import logging
+import sys
+
+from os.path import isfile
 ##
 
 from sima import core
 from sima.plugins.crop import Crop
+from sima.plugins.addhist import History
 from sima.lib.logger import set_logger
+from sima.lib.simadb import SimaDB
 from sima.utils.config import ConfMan
 from sima.utils.startopt import StartOpt
 from sima.utils.utils import exception_log
@@ -20,10 +27,10 @@ def main():
     # StartOpt gathers options from command line call (in StartOpt().options)
     sopt = StartOpt(info)
     # set logger
-    set_logger(level='debug')
+    verbosity = sopt.options.get('verbosity', 'warning')
+    cli_loglevel = getattr(logging, verbosity.upper())
+    set_logger(level=verbosity)
     logger = logging.getLogger('sima')
-    cli_loglevel = getattr(logging,
-                           sopt.options.get('verbosity', 'warning').upper())
     logger.setLevel(cli_loglevel)
     # loads configuration
     conf_manager = ConfMan(logger, sopt.options)
@@ -32,9 +39,21 @@ def main():
                     config.get('log', 'verbosity').upper()))  # pylint: disable=E1103
 
     logger.debug('Command line say: {0}'.format(sopt.options))
+
+    # Create Database
+    if (sopt.options.get('create_db', None)
+       or not isfile(conf_manager.db_file)):
+        logger.info('Creating database in "{}"'.format(conf_manager.db_file))
+        open(conf_manager.db_file, 'a').close()
+        SimaDB(db_path=conf_manager.db_file).create_db()
+        if sopt.options.get('create_db', None):
+            logger.info('Done, bye...')
+            sys.exit(0)
+
     logger.info('Starting...')
-    sima = core.Sima()
+    sima = core.Sima(config, conf_manager.db_file)
     sima.register_plugin(Crop)
+    sima.register_plugin(History)
     try:
         sima.run()
     except KeyboardInterrupt:
index 331c02c29bdef31e686a51ecf4a7c1a277b7d5a5..641fa22166f106ff2adfa32611223e783b091c71 100644 (file)
@@ -26,6 +26,7 @@ class PlayerError(Exception):
 class PlayerCommandError(PlayerError):
     """Command error"""
 
+PlayerUnHandledError = MPDError
 
 class PlayerClient(Player):
     """MPC Client
@@ -91,6 +92,13 @@ class PlayerClient(Player):
                 return Track(**ans)
         return ans
 
+    def __skipped_track(self, old_curr):
+        if (self.state == 'stop'
+            or not hasattr(old_curr, 'id')
+            or not hasattr(self.current, 'id')):
+            return False
+        return (self.current.id != old_curr.id)  # pylint: disable=no-member
+
     def find_track(self, artist, title=None):
         #return getattr(self, 'find')('artist', artist, 'title', title)
         if title:
@@ -108,10 +116,14 @@ class PlayerClient(Player):
         return self.find('artist', artist, 'album', album)
 
     def monitor(self):
+        curr = self.current
         try:
             self._client.send_idle('database', 'playlist', 'player', 'options')
             select([self._client], [], [], 60)
-            return self._client.fetch_idle()
+            ret = self._client.fetch_idle()
+            if self.__skipped_track(curr):
+                ret.append('skipped')
+            return ret
         except (MPDError, IOError) as err:
             raise PlayerError("Couldn't init idle: %s" % err)
 
index c751e5ba2b63a6bd0cafb2f7c79dd62770bc118a..3710d92f3ac03f991012bef8ec8377872b51d41e 100644 (file)
@@ -7,52 +7,108 @@ __version__ = '0.12.0.b'
 __author__ = 'kaliko jack'
 __url__ = 'git://git.kaliko.me/sima.git'
 
+import sys
+import time
+
 from logging import getLogger
 
-from .client import PlayerClient
+from .client import PlayerClient, Track
+from .client import PlayerError, PlayerUnHandledError
+from .lib.simadb import SimaDB
 
 class Sima(object):
     """Main class, plugin and player management
     """
 
-    def __init__(self):
+    def __init__(self, conf, dbfile):
+        self.config = conf
+        self.sdb = SimaDB(db_path=dbfile)
         self.log = getLogger('sima')
         self.plugins = list()
-        self.player = None
-        self.connect_player()
+        self.player = PlayerClient()  # Player client
+        try:
+            self.player.connect()
+        except (PlayerError, PlayerUnHandledError) as err:
+            self.log.error('Fails to connect player: {}'.format(err))
+            self.shutdown()
         self.current_track = None
 
     def register_plugin(self, plugin_class):
+        """Registers plubin in Sima instance..."""
         self.plugins.append(plugin_class(self))
 
     def foreach_plugin(self, method, *args, **kwds):
+        """Plugin's callbacks dispatcher"""
         for plugin in self.plugins:
             getattr(plugin, method)(*args, **kwds)
 
-    def connect_player(self):
-        """Instanciate player client and connect
+    def reconnect_player(self):
+        """Trying to reconnect cycling through longer timeout
+        cycle : 5s 10s 1m 5m 20m 1h
         """
-        self.player = PlayerClient()  # Player client
-        self.player.connect()
+        sleepfor = [5, 10, 60, 300, 1200, 3600]
+        while True:
+            tmp = sleepfor.pop(0)
+            sleepfor.append(tmp)
+            self.log.info('Trying to reconnect in {:>4d} seconds'.format(tmp))
+            time.sleep(tmp)
+            try:
+                self.player.connect()
+            except PlayerError:
+                continue
+            except PlayerUnHandledError as err:
+                #TODO: unhandled Player exceptions
+                self.log.warning('Unhandled player exception: %s' % err)
+            self.log.info('Got reconnected')
+            break
 
     def shutdown(self):
         """General shutdown method
         """
+        self.log.warning('Starting shutdown.')
         self.player.disconnect()
         self.foreach_plugin('shutdown')
 
+        self.log.info('The way is shut, it was made by those who are dead. '
+                      'And the dead keep it…')
+        self.log.info('bye...')
+        sys.exit(0)
+
     def run(self):
-        """Dispatching callbacks to plugins
         """
-        self.log.debug(self.player.status())
-        self.log.info(self.player.current)
+        """
+        self.current_track = Track()
         while 42:
-            # hanging here untill a monitored event is raised in the player
-            changed = self.player.monitor()
-            if 'playlist' in changed:
-                self.foreach_plugin('callback_playlist')
-            if 'player' in changed:
-                self.log.info(self.player.current)
+            try:
+                self.loop()
+            except PlayerUnHandledError as err:
+                #TODO: unhandled Player exceptions
+                self.log.warning('Unhandled player exception: {}'.format(err))
+                del(self.player)
+                self.player = PlayerClient()
+                time.sleep(10)
+            except PlayerError as err:
+                self.log.warning('Player error: %s' % err)
+                self.reconnect_player()
+
+    def loop(self):
+        """Dispatching callbacks to plugins
+        """
+        # hanging here untill a monitored event is raised in the player
+        if getattr(self, 'changed', False): # first loop detection
+            self.changed = self.player.monitor()
+        else:
+            self.changed = ['playlist', 'player', 'skipped']
+        self.log.debug('changed: {}'.format(', '.join(self.changed)))
+        if 'playlist' in self.changed:
+            self.foreach_plugin('callback_playlist')
+        if 'player' in self.changed:
+            self.foreach_plugin('callback_player')
+        if 'skipped' in self.changed:
+            if self.player.state == 'play':
+                self.log.info('Playing: {}'.format(self.player.current))
+                self.foreach_plugin('callback_next_song')
+                self.current_track = self.player.current
 
 
 # VIM MODLINE
index 752118847658688be6e8cacaf2ef59fae11e4221..8c6cd2ff0b38ba346d982ed49b2d9093342e21a8 100644 (file)
@@ -27,7 +27,8 @@ import logging
 import sys
 
 LOG_FORMATS = {
-        logging.DEBUG: '{asctime} {filename}:{lineno}({funcName}) {levelname}: {message}',
+        logging.DEBUG: '{asctime} {filename}:{lineno}({funcName}) '
+                                 '{levelname}: {message}',
         logging.INFO:  '{asctime} {levelname}: {message}'
         }
 DATE_FMT = "%Y-%m-%d %H:%M:%S"
index ac21b2f6117b2752ba88a855bafd8bb0ede842a4..f846e2e9d16565244f7178d32cede6edaa3da49a 100644 (file)
@@ -2,6 +2,7 @@
 
 class Plugin():
     def __init__(self, daemon):
+        self.log = daemon.log
         self.__daemon = daemon
         #self.history = daemon.player.history
 
@@ -9,6 +10,12 @@ class Plugin():
     def name(self):
         return self.__class__.__name__.lower()
 
+    def callback_player(self):
+        """
+        Called on player changes
+        """
+        pass
+
     def callback_playlist(self):
         """
         Called on playlist changes
@@ -28,12 +35,6 @@ class Plugin():
         """
         pass
 
-    def callback_player_stop(self):
-        """Not returning data,
-        Could be use to ensure player never stops
-        """
-        pass
-
     def shutdown(self):
         pass
 
diff --git a/sima/lib/simadb.py b/sima/lib/simadb.py
new file mode 100644 (file)
index 0000000..4db498d
--- /dev/null
@@ -0,0 +1,730 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009-2013 Jack Kaliko <jack@azylum.org>
+# Copyright (c) 2009, Eric Casteleijn <thisfred@gmail.com>
+# Copyright (c) 2008 Rick van Hattem
+#
+#  This file is part of sima
+#
+#  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.
+#
+#  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 sima.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+#    DOC:
+#    MuscicBrainz ID: <http://musicbrainz.org/doc/MusicBrainzIdentifier>
+#    Artists: <http://musicbrainz.org/doc/Artist_Name>
+#             <http://musicbrainz.org/doc/Same_Artist_With_Different_Names>
+
+__DB_VERSION__ = 2
+__HIST_DURATION__ = int(7 * 24)  # in hours
+
+import sqlite3
+
+from datetime import (datetime, timedelta)
+from os.path import dirname, isdir
+from os import (access, W_OK, F_OK)
+from shutil import copyfile
+
+
+class SimaDBError(Exception):
+    """
+    Exceptions.
+    """
+    pass
+
+
+class SimaDBAccessError(SimaDBError):
+    """Error on accessing DB file"""
+    pass
+
+
+class SimaDBNoFile(SimaDBError):
+    """No DB file present"""
+    pass
+
+
+class SimaDBUpgradeError(SimaDBError):
+    """Error on upgrade"""
+    pass
+
+
+class SimaDB(object):
+    "SQLite management"
+
+    def __init__(self, db_path=None):
+        self._db_path = db_path
+        self.db_path_mod_control()
+
+    def db_path_mod_control(self):
+        db_path = self._db_path
+        # Controls directory access
+        if not isdir(dirname(db_path)):
+            raise SimaDBAccessError('Not a regular directory: "%s"' %
+                    dirname(db_path))
+        if not access(dirname(db_path), W_OK):
+            raise SimaDBAccessError('No write access to "%s"' % dirname(db_path))
+        # Is a file but no write access
+        if access(db_path, F_OK) and not access(db_path, W_OK | F_OK):
+            raise SimaDBAccessError('No write access to "%s"' % db_path)
+        # No file
+        if not access(db_path, F_OK):
+            raise SimaDBNoFile('No DB file in "%s"' % db_path)
+
+    def close_database_connection(self, connection):
+        """Close the database connection."""
+        connection.close()
+
+    def get_database_connection(self):
+        """get database reference"""
+        connection = sqlite3.connect(
+            self._db_path, timeout=5.0, isolation_level="immediate")
+        #connection.text_factory = str
+        return connection
+
+    def upgrade(self):
+        """upgrade DB from previous versions"""
+        connection = self.get_database_connection()
+        try:
+            connection.execute('SELECT version FROM db_info')
+        except Exception as err:
+            if err.__str__() == "no such table: db_info":
+                # db version < 2 (MPD_sima 0.6)
+                copyfile(self._db_path, self._db_path + '.0.6')
+                connection.execute('DROP TABLE tracks')
+                connection.commit()
+                self.create_db()
+            else:
+                raise SimaDBUpgradeError('Could not upgrade database: "%s"' %
+                        err)
+        self.close_database_connection(connection)
+
+    def get_artist(self, artist_name, mbid=None,
+            with_connection=None, add_not=False):
+        """get artist information from the database.
+        if not in database insert new entry."""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        rows = connection.execute(
+            "SELECT * FROM artists WHERE name = ?", (artist_name,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if add_not:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return False
+        connection.execute(
+            "INSERT INTO artists (name, mbid) VALUES (?, ?)",
+            (artist_name, mbid))
+        connection.commit()
+        rows = connection.execute(
+            "SELECT * FROM artists WHERE name = ?", (artist_name,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if not with_connection:
+            self.close_database_connection(connection)
+
+    def get_track(self, track, with_connection=None, add_not=False):
+        """
+        Get a track from Tracks table, add if not existing,
+        Attention: use Track() object!!
+        if not in database insert new entry."""
+        art = track.artist
+        nam = track.title
+        fil = track.get_filename()
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        art_id = self.get_artist(art, with_connection=connection)[0]
+        alb_id = self.get_album(track, with_connection=connection)[0]
+        rows = connection.execute(
+            "SELECT * FROM tracks WHERE name = ? AND"
+            " artist = ? AND file = ?", (nam, art_id, fil))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if add_not:
+            return False
+        connection.execute(
+            "INSERT INTO tracks (artist, album, name, file) VALUES (?, ?, ?, ?)",
+            (art_id, alb_id, nam, fil))
+        connection.commit()
+        rows = connection.execute(
+            "SELECT * FROM tracks WHERE name = ? AND"
+            " artist = ? AND album = ? AND file = ?",
+            (nam, art_id, alb_id, fil,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def get_album(self, track, mbid=None,
+            with_connection=None, add_not=False):
+        """
+        get album information from the database.
+        if not in database insert new entry.
+        Attention: use Track() object!!
+        Use AlbumArtist tag is provided, fallback to Album tag
+        """
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        if track.albumartist:
+            artist = track.albumartist
+        else:
+            artist = track.artist
+        art_id = self.get_artist(artist, with_connection=connection)[0]
+        album = track.album
+        rows = connection.execute(
+            "SELECT * FROM albums WHERE name = ? AND artist = ?",
+            (album, art_id))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if add_not:
+            return False
+        connection.execute(
+            "INSERT INTO albums (name, artist, mbid) VALUES (?, ?, ?)",
+            (album, art_id, mbid))
+        connection.commit()
+        rows = connection.execute(
+            "SELECT * FROM albums WHERE name = ? AND artist = ?",
+            (album, art_id))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if not with_connection:
+            self.close_database_connection(connection)
+
+    def get_artists(self, with_connection=None):
+        """Returns all artists in DB"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        rows = connection.execute("SELECT name FROM artists ORDER BY name")
+        results = [row for row in rows]
+        if not with_connection:
+            self.close_database_connection(connection)
+        for artist in results:
+            yield artist
+
+    def get_bl_artist(self, artist_name,
+            with_connection=None, add_not=None):
+        """get blacklisted artist information from the database."""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        art = self.get_artist(artist_name,
+                with_connection=connection, add_not=add_not)
+        if not art:
+            return False
+        art_id = art[0]
+        rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
+                (art_id,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if add_not:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return False
+        connection.execute("INSERT INTO black_list (artist) VALUES (?)",
+                (art_id,))
+        connection.execute("UPDATE black_list SET updated = DATETIME('now')"
+                " WHERE artist = ?", (art_id,))
+        connection.commit()
+        rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
+                (art_id,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if not with_connection:
+            self.close_database_connection(connection)
+        return False
+
+    def get_bl_album(self, track,
+            with_connection=None, add_not=None):
+        """get blacklisted track information from the database."""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        album = self.get_album(track,
+                with_connection=connection, add_not=add_not)
+        if not album:
+            return False
+        alb_id = album[0]
+        rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
+                (alb_id,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if add_not:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return False
+        connection.execute("INSERT INTO black_list (album) VALUES (?)",
+                (alb_id,))
+        connection.execute("UPDATE black_list SET updated = DATETIME('now')"
+                " WHERE album = ?", (alb_id,))
+        connection.commit()
+        rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
+                (alb_id,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if not with_connection:
+            self.close_database_connection(connection)
+        return False
+
+    def get_bl_track(self, track, with_connection=None, add_not=None):
+        """get blacklisted track information from the database."""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        track = self.get_track(track,
+                with_connection=connection, add_not=add_not)
+        if not track:
+            return False
+        track_id = track[0]
+        rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
+                (track_id,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if add_not:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return False
+        connection.execute("INSERT INTO black_list (track) VALUES (?)",
+                (track_id,))
+        connection.execute("UPDATE black_list SET updated = DATETIME('now')"
+                " WHERE track = ?", (track_id,))
+        connection.commit()
+        rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
+                (track_id,))
+        for row in rows:
+            if not with_connection:
+                self.close_database_connection(connection)
+            return row
+        if not with_connection:
+            self.close_database_connection(connection)
+        return False
+
+    def get_history(self, artist=None, artists=None, duration=__HIST_DURATION__):
+        """Retrieve complete play history, most recent tracks first
+        artist  : filter history for specific artist
+        artists : filter history for specific artists list
+        """
+        date = datetime.utcnow() - timedelta(hours=duration)
+        connection = self.get_database_connection()
+        if artist:
+            rows = connection.execute(
+                "SELECT arts.name, albs.name, trs.name, trs.file, hist.last_play"
+                " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
+                " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
+                " AND hist.last_play > ? AND arts.name = ?"
+                " ORDER BY hist.last_play DESC", (date.isoformat(' '), artist,))
+        else:
+            rows = connection.execute(
+                "SELECT arts.name, albs.name, trs.name, trs.file"
+                " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
+                " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
+                " AND hist.last_play > ? ORDER BY hist.last_play DESC", (date.isoformat(' '),))
+        for row in rows:
+            if artists and row[0] not in artists:
+                continue
+            yield row
+        self.close_database_connection(connection)
+
+    def get_black_list(self):
+        """Retrieve complete black list."""
+        connection = self.get_database_connection()
+        rows = connection.execute('SELECT black_list.rowid, artists.name'
+                ' FROM artists INNER JOIN black_list'
+                ' ON artists.id = black_list.artist')
+        yield ('Row ID', 'Actual black listed element', 'Extra information',)
+        yield ('',)
+        yield ('Row ID', 'Artist',)
+        for row in rows:
+            yield row
+        rows = connection.execute('SELECT black_list.rowid, albums.name, artists.name'
+                ' FROM artists, albums INNER JOIN black_list'
+                ' ON albums.id = black_list.album'
+                ' WHERE artists.id = albums.artist')
+        yield ('',)
+        yield ('Row ID', 'Album', 'Artist name')
+        for row in rows:
+            yield row
+        rows = connection.execute('SELECT black_list.rowid, tracks.name, artists.name'
+                ' FROM artists, tracks INNER JOIN black_list'
+                ' ON tracks.id = black_list.track'
+                ' WHERE tracks.artist = artists.id')
+        yield ('',)
+        yield ('Row ID', 'Title', 'Artist name')
+        for row in rows:
+            yield row
+        self.close_database_connection(connection)
+
+    def _set_mbid(self, artist_id=None, mbid=None, with_connection=None):
+        """get artist information from the database.
+        if not in database insert new entry."""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        connection.execute("UPDATE artists SET mbid = ? WHERE id = ?",
+            (mbid, artist_id))
+        connection.commit()
+        if not with_connection:
+            self.close_database_connection(connection)
+
+    def _get_similar_artists_from_db(self, artist_id):
+        connection = self.get_database_connection()
+        results = [row for row in connection.execute(
+            "SELECT match, name FROM usr_artist_2_artist INNER JOIN"
+            " artists ON usr_artist_2_artist.artist2 = artists.id WHERE"
+            " usr_artist_2_artist.artist1 = ? ORDER BY match DESC;",
+            (artist_id,))]
+        self.close_database_connection(connection)
+        for score, artist in results:
+            yield {'score': score, 'artist': artist}
+
+    def _get_reverse_similar_artists_from_db(self, artist_id):
+        connection = self.get_database_connection()
+        results = [row for row in connection.execute(
+            "SELECT name FROM usr_artist_2_artist INNER JOIN"
+            " artists ON usr_artist_2_artist.artist1 = artists.id WHERE"
+            " usr_artist_2_artist.artist2 = ?;",
+            (artist_id,))]
+        self.close_database_connection(connection)
+        for artist in results:
+            yield artist[0]
+
+    def get_similar_artists(self, artist_name):
+        """get similar artists from the database sorted by descending
+        match score"""
+        artist_id = self.get_artist(artist_name)[0]
+        for result in self._get_similar_artists_from_db(artist_id):
+            yield result
+
+    def _get_artist_match(self, artist1, artist2, with_connection=None):
+        """get artist match score from database"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        rows = connection.execute(
+            "SELECT match FROM usr_artist_2_artist WHERE artist1 = ?"
+            " AND artist2 = ?",
+            (artist1, artist2))
+        result = 0
+        for row in rows:
+            result = row[0]
+            break
+        if not with_connection:
+            self.close_database_connection(connection)
+        return result
+
+    def _remove_relation_between_2_artist(self, artist1, artist2):
+        """Remove a similarity relation"""
+        connection = self.get_database_connection()
+        connection.execute(
+            'DELETE FROM usr_artist_2_artist'
+            ' WHERE artist1 = ? AND artist2 = ?;',
+            (artist1, artist2))
+        self.clean_database(with_connection=connection)
+        self._update_artist(artist_id=artist1, with_connection=connection)
+        connection.commit()
+        self.close_database_connection(connection)
+
+    def _remove_artist(self, artist_id, deep=False, with_connection=None):
+        """Remove all artist1 reference"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        if deep:
+            connection.execute(
+                'DELETE FROM usr_artist_2_artist'
+                ' WHERE artist1 = ? OR artist2 = ?;',
+                (artist_id, artist_id))
+            connection.execute(
+                'DELETE FROM artists WHERE id = ?;',
+                (artist_id,))
+        else:
+            connection.execute(
+                'DELETE FROM usr_artist_2_artist WHERE artist1 = ?;',
+                (artist_id,))
+        self.clean_database(with_connection=connection)
+        self._update_artist(artist_id=artist_id, with_connection=connection)
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def _remove_bl(self, rowid):
+        """Remove bl row id"""
+        connection = self.get_database_connection()
+        connection.execute('DELETE FROM black_list'
+                ' WHERE black_list.rowid = ?', (rowid,))
+        connection.commit()
+        self.close_database_connection(connection)
+
+    def _insert_artist_match(
+        self, artist1, artist2, match, with_connection=None):
+        """write match score to the database.
+        Does not update time stamp in table artist/*_updated"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        connection.execute(
+            "INSERT INTO usr_artist_2_artist (artist1, artist2, match) VALUES"
+            " (?, ?, ?)",
+            (artist1, artist2, match))
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def add_history(self, track):
+        """Add to history"""
+        connection = self.get_database_connection()
+        track_id = self.get_track(track, with_connection=connection)[0]
+        rows = connection.execute("SELECT * FROM history WHERE track = ? ", (track_id,))
+        if not rows.fetchone():
+            connection.execute("INSERT INTO history (track) VALUES (?)", (track_id,))
+        connection.execute("UPDATE history SET last_play = DATETIME('now') "
+                " WHERE track = ?", (track_id,))
+        connection.commit()
+        self.close_database_connection(connection)
+
+    def _update_artist(self, artist_id, with_connection=None):
+        """write artist information to the database"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        connection.execute(
+            "UPDATE artists SET usr_updated = DATETIME('now') WHERE id = ?",
+            (artist_id,))
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def _update_artist_match(
+        self, artist1, artist2, match, with_connection=None):
+        """write match score to the database"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        connection.execute(
+            "UPDATE usr_artist_2_artist SET match = ? WHERE artist1 = ? AND"
+            " artist2 = ?",
+            (match, artist1, artist2))
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def _update_similar_artists(self, artist, similar_artists):
+        """write user similar artist information to the database
+        """
+        # DOC:   similar_artists = list([{'score': match, 'artist': name}])
+        #
+        connection = self.get_database_connection()
+        artist_id = self.get_artist(artist, with_connection=connection)[0]
+        for artist in similar_artists:
+            id2 = self.get_artist(
+                artist['artist'], with_connection=connection)[0]
+            if self._get_artist_match(
+                artist_id, id2, with_connection=connection):
+                self._update_artist_match(
+                    artist_id, id2, artist['score'],
+                    with_connection=connection)
+                continue
+            self._insert_artist_match(
+                artist_id, id2, artist['score'],
+                with_connection=connection)
+        self._update_artist(artist_id, with_connection=connection)
+        connection.commit()
+        self.close_database_connection(connection)
+
+    def _clean_artists_table(self, with_connection=None):
+        """Clean orphan artists"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        artists_ids = set([row[0] for row in connection.execute(
+            "SELECT id FROM artists")])
+        artist_2_artist_ids = set([row[0] for row in connection.execute(
+            "SELECT artist1 FROM usr_artist_2_artist")] +
+            [row[0] for row in connection.execute(
+            "SELECT artist2 FROM usr_artist_2_artist")] +
+            [row[0] for row in connection.execute(
+            "SELECT artist FROM black_list")] +
+            [row[0] for row in connection.execute(
+            "SELECT artist FROM albums")] +
+            [row[0] for row in connection.execute(
+            "SELECT artist FROM tracks")])
+        orphans = [ (orphan,) for orphan in artists_ids - artist_2_artist_ids ]
+        connection.executemany('DELETE FROM artists WHERE id = (?);', orphans)
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def _clean_albums_table(self, with_connection=None):
+        """Clean orphan albums"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        orphan_black_ids = set([row[0] for row in connection.execute(
+            """SELECT albums.id FROM albums
+            LEFT JOIN black_list ON albums.id = black_list.album
+            WHERE ( black_list.album IS NULL )""")])
+        orphan_tracks_ids = set([row[0] for row in connection.execute(
+            """SELECT albums.id FROM albums
+            LEFT JOIN tracks ON albums.id = tracks.album
+            WHERE tracks.album IS NULL""")])
+        orphans = [ (orphan,) for orphan in orphan_black_ids & orphan_tracks_ids ]
+        connection.executemany('DELETE FROM albums WHERE id = (?);', orphans)
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def _clean_tracks_table(self, with_connection=None):
+        """Clean orphan tracks"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        hist_orphan_ids = set([row[0] for row in connection.execute(
+            """SELECT tracks.id FROM tracks
+            LEFT JOIN history ON tracks.id = history.track
+            WHERE history.track IS NULL""")])
+        black_list_orphan_ids = set([row[0] for row in connection.execute(
+            """SELECT tracks.id FROM tracks
+            LEFT JOIN black_list ON tracks.id = black_list.track
+            WHERE black_list.track IS NULL""")])
+        orphans = [ (orphan,) for orphan in hist_orphan_ids & black_list_orphan_ids ]
+        connection.executemany('DELETE FROM tracks WHERE id = (?);', orphans)
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def clean_database(self, with_connection=None):
+        """Wrapper around _clean_* methods"""
+        if with_connection:
+            connection = with_connection
+        else:
+            connection = self.get_database_connection()
+        self._clean_tracks_table(with_connection=connection)
+        self._clean_albums_table(with_connection=connection)
+        self._clean_artists_table(with_connection=connection)
+        connection.execute("VACUUM")
+        if not with_connection:
+            connection.commit()
+            self.close_database_connection(connection)
+
+    def purge_history(self, duration=__HIST_DURATION__):
+        """Remove old entries in history"""
+        connection = self.get_database_connection()
+        connection.execute("DELETE FROM history WHERE last_play"
+                " < datetime('now', '-%i hours')" % duration)
+        connection.commit()
+        self.close_database_connection(connection)
+
+    def _set_dbversion(self):
+        connection = self.get_database_connection()
+        connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)',
+                (__DB_VERSION__, 'Sima DB'))
+        connection.commit()
+        self.close_database_connection(connection)
+
+    def create_db(self):
+        """ Set up a database for the artist similarity scores
+        """
+        connection = self.get_database_connection()
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS db_info'
+            ' (version INTEGER, name CHAR(36))')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS artists (id INTEGER PRIMARY KEY, name'
+            ' VARCHAR(100), mbid CHAR(36), lfm_updated DATE, usr_updated DATE)')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS usr_artist_2_artist (artist1 INTEGER,'
+            ' artist2 INTEGER, match INTEGER)')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS lfm_artist_2_artist (artist1 INTEGER,'
+            ' artist2 INTEGER, match INTEGER)')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS albums (id INTEGER PRIMARY KEY,'
+            ' artist INTEGER, name VARCHAR(100), mbid CHAR(36))')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS tracks (id INTEGER PRIMARY KEY,'
+            ' name VARCHAR(100), artist INTEGER, album INTEGER,'
+            ' file VARCHAR(500), mbid CHAR(36))')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS black_list (artist INTEGER,'
+            ' album INTEGER, track INTEGER, updated DATE)')
+        connection.execute(
+            'CREATE TABLE IF NOT EXISTS history (last_play DATE,'
+            ' track integer)')
+        connection.execute(
+            "CREATE INDEX IF NOT EXISTS a2aa1x ON usr_artist_2_artist (artist1)")
+        connection.execute(
+            "CREATE INDEX IF NOT EXISTS a2aa2x ON usr_artist_2_artist (artist2)")
+        connection.execute(
+            "CREATE INDEX IF NOT EXISTS lfma2aa1x ON lfm_artist_2_artist (artist1)")
+        connection.execute(
+            "CREATE INDEX IF NOT EXISTS lfma2aa2x ON lfm_artist_2_artist (artist2)")
+        connection.commit()
+        self.close_database_connection(connection)
+        self._set_dbversion()
+
+
+def main():
+    db = SimaDB(db_path='/tmp/sima.db')
+    db.purge_history(int(4))
+    db.clean_database()
+
+
+# Script starts here
+if __name__ == '__main__':
+    main()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/plugins/addhist.py b/sima/plugins/addhist.py
new file mode 100644 (file)
index 0000000..8b8da7a
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+"""Add playing tracks to history
+"""
+
+# standart library import
+#from select import select
+
+# third parties componants
+
+# local import
+from ..lib.plugin import Plugin
+
+class History(Plugin):
+    """
+    History
+    """
+    def __init__(self, daemon):
+        Plugin.__init__(self, daemon)
+        self.sdb = daemon.sdb
+        self.player = daemon.player
+
+    def shutdown(self):
+        self.log.info('Cleaning database')
+        self.sdb.purge_history()
+        self.sdb.clean_database()
+
+    def callback_next_song(self):
+        current = self.player.current
+        self.log.debug('add history: "{}"'.format(current))
+        self.sdb.add_history(current)
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
index b75f0d9a68e2dfadedc4d069f3bc0b4be68f268a..8467ba45c750cba363cbdc6e89545b1a42e4921f 100644 (file)
@@ -19,6 +19,7 @@ class Crop(Plugin):
         player = self._Plugin__daemon.player
         target_lengh = 10
         while player.currentsong().pos > target_lengh:
+            self.log.debug('cropping playlist')
             player.remove()
 
 
diff --git a/sima/plugins/lastfm.py b/sima/plugins/lastfm.py
new file mode 100644 (file)
index 0000000..43a1ae5
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+# standart library import
+#from select import select
+
+# third parties componants
+
+# local import
+from ..lib.plugin import Plugin
+
+class Last(Plugin):
+    """last.fm similar artists
+    """
+    pass
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
index 84367d66b233ca3b003bdc78cb219e538eb06154..1dd9567835d6411affbcf963edd6a7b31b4b4107 100644 (file)
@@ -36,8 +36,8 @@ from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
 from . import utils
 
 # DEFAULTS
-DIRNAME = 'mpd_sima'
-CONF_FILE = 'mpd_sima.cfg'
+DIRNAME = 'sima'
+CONF_FILE = 'sima.cfg'
 
 DEFAULT_CONF = {
         'MPD': {
@@ -64,7 +64,7 @@ DEFAULT_CONF = {
 #
 
 
-class ConfMan(object):#CONFIG MANAGER CLASS
+class ConfMan(object):  # CONFIG MANAGER CLASS
     """
     Configuration manager.
     Default configuration is stored in DEFAULT_CONF dictionnary.
@@ -89,7 +89,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS
         self.defaults = dict(DEFAULT_CONF)
         self.startopt = options
         ## Sima sqlite DB
-        self.userdb_file = None
+        self.db_file = None
 
         self.log = logger
         ## INIT CALLS
@@ -117,7 +117,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS
         Controls conf file permissions.
         """
         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
-        self.log.debug('file permision is: %o' % mode)
+        self.log.debug('file permission is: %o' % mode)
         if mode & S_IRWXO or mode & S_IRWXG:
             self.log.warning('File is readable by "other" and/or' +
                              ' "group" (actual permission %o octal).' %
@@ -184,7 +184,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS
     def init_config(self):
         """
         Use XDG directory standard if exists
-        else use "HOME/(.config|.local/share)/mpd_sima/"
+        else use "HOME/(.config|.local/share)/sima/"
         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
         """
 
@@ -223,7 +223,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS
             self.log.error('Please use "--config" to locate the conf file')
             sys.exit(1)
 
-        self.userdb_file = join(data_dir, 'sima.db')
+        self.db_file = join(data_dir, 'sima.db')
 
         config = configparser.SafeConfigParser()