]> kaliko git repositories - mpd-sima.git/commitdiff
Add simadb_cli
authorkaliko <kaliko@azylum.org>
Tue, 10 Jun 2014 13:58:01 +0000 (15:58 +0200)
committerkaliko <kaliko@azylum.org>
Tue, 10 Jun 2014 13:58:01 +0000 (15:58 +0200)
setup.py
simadb_cli [new file with mode: 0755]

index cce58f8eecf1716b6a5a9c600351a039a9817c2e..93da156532bf64b61db17b1c763b80b61d77d074 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,7 @@ setup(name='sima',
       packages=find_packages(),
       include_package_data=True,
       data_files=data_files,
-      #scripts=['mpd-sima'],
+      scripts=['simadb_cli'],
       entry_points={
           'console_scripts': ['mpd-sima = sima.launch:main',]
           },
diff --git a/simadb_cli b/simadb_cli
new file mode 100755 (executable)
index 0000000..52f45bc
--- /dev/null
@@ -0,0 +1,566 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2010-2013 Jack Kaliko <efrim@azylum.org>
+#
+#  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/>.
+#
+#
+
+__version__ = '0.4.0'
+
+# IMPORT#
+import re
+
+from argparse import (ArgumentParser, SUPPRESS, Action)
+from difflib import get_close_matches
+from locale import getpreferredencoding
+from os import (environ, chmod, makedirs)
+from os.path import (join, isdir, isfile, expanduser)
+from sys import (exit, stdout, stderr)
+
+from sima.lib.track import Track
+from sima.utils import utils
+from sima.lib import simadb
+from musicpd import MPDClient, ConnectionError
+
+
+DESCRIPTION = """
+simadb_cli helps you to edit entries in your own DB of similarity
+between artists."""
+DB_NAME = 'sima.db'
+
+class FooAction(Action):
+    def check(self, namespace):
+        if namespace.similarity: return True
+        if namespace.remove_art: return True
+        if namespace.remove_sim: return True
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        opt_required = '"--remove_artist", "--remove_similarity" or "--add_similarity"'
+        if not self.check(namespace):
+            parser.error(
+                    'can\'t use {0} option before or without {1}'.format(
+                        option_string, opt_required))
+        setattr(namespace, self.dest, True)
+
+# Options list
+# pop out 'sw' value before creating ArgumentParser object.
+OPTS = list([
+    {
+        'sw':['-a', '--add_similarity'],
+        'type': str,
+        'dest':'similarity',
+        'help': 'Similarity to add formated as follow: ' +
+        ' "art_0,art_1:90,art_2:80..."'},
+    {
+        'sw': ['-c', '--check_names'],
+        'action': 'store_true',
+        'default': False,
+        'help': 'Turn on controls of artists names in MPD library.'},
+    {
+        'sw':['-d', '--dbfile'],
+        'type': str,
+        'dest':'dbfile',
+        'action': utils.Wfile,
+        'help': 'File to read/write database from/to'},
+    {
+        'sw': ['-r', '--reciprocal'],
+        'default': False,
+        'nargs': 0, 
+        'action': FooAction,
+        'help': 'Turn on reciprocity for similarity relation when add/remove.'},
+    {
+        'sw':['--remove_artist'],
+        'type': str,
+        'dest': 'remove_art',
+        'metavar': '"ARTIST TO REMOVE"',
+        'help': 'Remove an artist from DB (main artist entries).'},
+    {
+        'sw':['--remove_similarity'],
+        'type': str,
+        'dest': 'remove_sim',
+        'metavar': '"MAIN ART,SIMI ART"',
+        'help': 'Remove an similarity relation from DB (main artist <=> similar artist).'},
+    {
+        'sw':['-v', '--view_artist'],
+        'type': str,
+        'dest':'view',
+        'metavar': '"ARTIST NAME"',
+        'help': 'View an artist from DB.'},
+    {
+        'sw':['--view_all'],
+        'action': 'store_true',
+        'help': 'View all similarity entries.'},
+    {
+        'sw': ['-S', '--host'],
+        'type': str,
+        'dest': 'mpdhost',
+        'default': None,
+        'help': 'MPD host, as IP or FQDN (default: localhost|MPD_HOST).'},
+    {
+        'sw': ['-P', '--port'],
+        'type': int,
+        'dest': 'mpdport',
+        'default': None,
+        'help': 'Port MPD in listening on (default: 6600|MPD_PORT).'},
+    {
+        'sw': ['--password'],
+        'type': str,
+        'dest': 'passwd',
+        'default': None,
+        'help': SUPPRESS},
+    {
+        'sw': ['--view_bl'],
+        'action': 'store_true',
+        'help': 'View black list.'},
+    {
+        'sw': ['--remove_bl'],
+        'type': int,
+        'help': 'Suppress a black list entry, by row id. Use --view_bl to get row id.'},
+    {
+        'sw': ['--bl_art'],
+        'type': str,
+        'metavar': 'ARTIST_NAME',
+        'help': 'Black list artist.'},
+    {
+        'sw': ['--bl_curr_art'],
+        'action': 'store_true',
+        'help': 'Black list currently playing artist.'},
+    {
+        'sw': ['--bl_curr_alb'],
+        'action': 'store_true',
+        'help': 'Black list currently playing album.'},
+    {
+        'sw': ['--bl_curr_trk'],
+        'action': 'store_true',
+        'help': 'Black list currently playing track.'},
+    {
+        'sw':['--purge_hist'],
+        'action': 'store_true',
+        'dest': 'do_purge_hist',
+        'help': 'Purge play history.'}])
+
+
+class SimaDB_CLI(object):
+    """Command line management.
+    """
+
+    def __init__(self):
+        self.dbfile = self._get_default_dbfile()
+        self.parser = None
+        self.options = dict({})
+        self.localencoding = 'UTF-8'
+        self._get_encoding()
+        self._upgrade()
+        self.main()
+
+    def _get_encoding(self):
+        """Get local encoding"""
+        localencoding = getpreferredencoding()
+        if localencoding:
+            self.localencoding = localencoding
+
+    def _get_mpd_env_var(self):
+        """
+        MPD host/port environement variables are used if command line does not
+        provide host|port|passwd.
+        """
+        host, port, passwd = utils.get_mpd_environ()
+        if self.options.passwd is None and passwd:
+            self.options.passwd = passwd
+        if self.options.mpdhost is None:
+            if host:
+                self.options.mpdhost = host
+            else:
+                self.options.mpdhost = 'localhost'
+        if self.options.mpdport is None:
+            if port:
+                self.options.mpdport = port
+            else:
+                self.options.mpdport = 6600
+
+    def _upgrade(self):
+        """Upgrades DB if necessary, create one if not existing."""
+        if not isfile(self.dbfile): # No db file
+            return
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.upgrade()
+
+    def _declare_opts(self):
+        """
+        Declare options in ArgumentParser object.
+        """
+        self.parser = ArgumentParser(description=DESCRIPTION,
+                                   usage='%(prog)s [-h|--help] [options]',
+                                   prog='simadb_cli',
+                                   epilog='Happy Listening',
+                                   )
+
+        self.parser.add_argument('--version', action='version',
+                version='%(prog)s {0}'.format(__version__))
+        # Add all options declare in OPTS
+        for opt in OPTS:
+            opt_names = opt.pop('sw')
+            self.parser.add_argument(*opt_names, **opt)
+
+    def _get_default_dbfile(self):
+        """
+        Use XDG directory standard if exists
+        else use "HOME/.local/share/mpd_sima/"
+        http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
+        """
+        homedir = expanduser('~')
+        dirname = 'mpd_sima'
+        if environ.get('XDG_DATA_HOME'):
+            data_dir = join(environ.get('XDG_DATA_HOME'), dirname)
+        else:
+            data_dir = join(homedir, '.local', 'share', dirname)
+        if not isdir(data_dir):
+            makedirs(data_dir)
+            chmod(data_dir, 0o700)
+        return join(data_dir, DB_NAME)
+
+    def _get_mpd_client(self):
+        """"""
+        # TODO: encode properly host name
+        host = self.options.mpdhost
+        port = self.options.mpdport
+        cli = MPDClient()
+        try:
+            cli.connect(host=host, port=port)
+        except ConnectionError as err:
+            mess = 'ERROR: fail to connect MPD (host: %s:%s): %s' % (
+                    host, port, err)
+            print(mess, file=stderr)
+            exit(1)
+        return cli
+
+    def _create_db(self):
+        """Create database if necessary"""
+        if isfile(self.dbfile):
+            return
+        print('Creating database!')
+        open(self.dbfile, 'a').close()
+        simadb.SimaDB(db_path=self.dbfile).create_db()
+
+    def _get_art_from_db(self, art):
+        """Return (id, name, self...) from DB or None is not in DB"""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = db.get_artist(art, add_not=True)
+        if not art_db:
+            print('ERROR: "%s" not in data base!' % art, file=stderr)
+            return None
+        return art_db
+
+    def _control_similarity(self):
+        """
+         * Regex check of command line similarity
+         * Controls artist presence in MPD library
+        """
+        usage = ('USAGE: "main artist,similar artist:<match score>,other' +
+                'similar artist:<match score>,..."')
+        cli_sim = self.options.similarity
+        pattern = '^([^,]+?),([^:,]+?:\d{1,2},?)+$'
+        regexp = re.compile(pattern, re.U).match(cli_sim)
+        if not regexp:
+            mess = 'ERROR: similarity badly formated: "%s"' % cli_sim
+            print(mess, file=stderr)
+            print(usage, file=stderr)
+            exit(1)
+        if self.options.check_names:
+            if not self._control_artist_names():
+                mess = 'ERROR: some artist names not found in MPD library!'
+                print(mess, file=stderr)
+                exit(1)
+
+    def _control_artist_names(self):
+        """Controls artist names exist in MPD library"""
+        mpd_cli = self._get_mpd_client()
+        artists_list = mpd_cli.list('artist')
+        sim_formated = self._parse_similarity()
+        control = True
+        if sim_formated[0] not in artists_list:
+            mess = 'WARNING: Main artist not found in MPD: %s' % sim_formated[0]
+            print(mess)
+            control = False
+        for sart in sim_formated[1]:
+            art = sart.get('artist')
+            if art not in artists_list:
+                mess = str('WARNING: Similar artist not found in MPD: %s' % art)
+                print(mess)
+                control = False
+        mpd_cli.disconnect()
+        return control
+
+    def _parse_similarity(self):
+        """Parse command line option similarity"""
+        cli_sim = self.options.similarity.strip(',').split(',')
+        sim = list([])
+        main = cli_sim[0]
+        for art in cli_sim[1:]:
+            artist = art.split(':')[0]
+            score = int(art.split(':')[1])
+            sim.append({'artist': artist, 'score': score})
+        return (main, sim)
+
+    def _print_main_art(self, art=None):
+        """Print entries, art as main artist."""
+        if not art:
+            art = self.options.view
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = self._get_art_from_db(art)
+        if not art_db: return
+        sims = list([])
+        [sims.append(a) for a in db._get_similar_artists_from_db(art_db[0])]
+        if len(sims) == 0:
+            return False
+        print('"%s" similarities:' % art)
+        for art in sims:
+            mess = str('  - {score:0>2d} {artist}'.format(**art))
+            print(mess)
+        return True
+
+    def _remove_sim(self, art1_db, art2_db):
+        """Remove single similarity between two artists."""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        similarity = db._get_artist_match(art1_db[0], art2_db[0])
+        if similarity == 0:
+            return False
+        db._remove_relation_between_2_artist(art1_db[0], art2_db[0])
+        mess = 'Remove: "{0}" "{1}:{2:0>2d}"'.format(art1_db[1], art2_db[1],
+                                                     similarity)
+        print(mess)
+        return True
+
+    def _revert_similarity(self, sim_formated):
+        """Revert similarity string (for reciprocal editing - add)."""
+        main_art = sim_formated[0]
+        similars = sim_formated[1]
+        for similar in similars:
+            yield (similar.get('artist'),
+                [{'artist':main_art, 'score':similar.get('score')}])
+
+    def bl_artist(self):
+        """Black list artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        artists_list = mpd_cli.list('artist')
+        # Unicode cli given artist name
+        cli_artist_to_bl = self.options.bl_art
+        if cli_artist_to_bl not in artists_list:
+            print('Artist not found in MPD library.')
+            match = get_close_matches(cli_artist_to_bl, artists_list, 50, 0.78)
+            if match:
+                print('You may be refering to %s' %
+                        '/'.join([m_a for m_a in match]))
+            return False
+        print('Black listing artist: %s' % cli_artist_to_bl)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_artist(cli_artist_to_bl)
+
+    def bl_current_artist(self):
+        """Black list current artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        artist = mpd_cli.currentsong().get('artist', '')
+        if not artist:
+            print('No artist found.')
+            return False
+        print('Black listing artist: %s' % artist)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_artist(artist)
+
+    def bl_current_album(self):
+        """Black list current artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        track = Track(**mpd_cli.currentsong())
+        if not track.album:
+            print('No album set for this track: %s' % track)
+            return False
+        print('Black listing album: {0}'.format(track.album))
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_album(track)
+
+    def bl_current_track(self):
+        """Black list current artist"""
+        mpd_cli = self._get_mpd_client()
+        if not mpd_cli:
+            return False
+        track = Track(**mpd_cli.currentsong())
+        print('Black listing track: %s' % track)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db.get_bl_track(track)
+
+    def purge_history(self):
+        """Purge all entries in history"""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        print('Purging history...')
+        db.purge_history(duration=int(0))
+        print('done.')
+        print('Cleaning database...')
+        db.clean_database()
+        print('done.')
+
+    def view(self):
+        """Print out entries for an artist."""
+        art = self.options.view
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = self._get_art_from_db(art)
+        if not art_db: return
+        if not self._print_main_art():
+            mess = str('"%s" present in DB but not as a main artist' % art)
+            print(mess)
+        else: print('')
+        art_rev = list([])
+        [art_rev.append(a) for a in db._get_reverse_similar_artists_from_db(art_db[0])]
+        if not art_rev: return
+        mess = str('%s" appears as similar for the following artist(s): %s' %
+                (art,', '.join(art_rev)))
+        print(mess)
+        [self._print_main_art(a) for a in art_rev]
+
+    def view_all(self):
+        """Print out all entries."""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        for art in db.get_artists():
+            if not art[0]: continue
+            self._print_main_art(art=art[0])
+
+    def view_bl(self):
+        """Print out black list."""
+        # TODO: enhance output formating
+        db = simadb.SimaDB(db_path=self.dbfile)
+        for bl_e in db.get_black_list():
+            print('\t# '.join([str(e) for e in bl_e]))
+
+    def remove_similarity(self):
+        """Remove entry"""
+        cli_sim = self.options.remove_sim
+        pattern = '^([^,]+?),([^,]+?,?)$'
+        regexp = re.compile(pattern, re.U).match(cli_sim)
+        if not regexp:
+            print('ERROR: similarity badly formated: "%s"' % cli_sim, file=stderr)
+            print('USAGE: A single relation between two artists is expected here.', file=stderr)
+            print('USAGE: "main artist,similar artist"', file=stderr)
+            exit(1)
+        arts = cli_sim.split(',')
+        if len(arts) != 2:
+            print('ERROR: unknown error in similarity format', file=stderr)
+            print('USAGE: "main artist,similar artist"', file=stderr)
+            exit(1)
+        art1_db = self._get_art_from_db(arts[0].strip())
+        art2_db = self._get_art_from_db(arts[1].strip())
+        if not art1_db or not art2_db: return
+        self._remove_sim(art1_db, art2_db)
+        if not self.options.reciprocal:
+            return
+        self._remove_sim(art2_db, art1_db)
+
+    def remove_artist(self):
+        """ Remove artist in the DB."""
+        deep = False
+        art = self.options.remove_art
+        db = simadb.SimaDB(db_path=self.dbfile)
+        art_db = self._get_art_from_db(art)
+        if not art_db: return False
+        print('Removing "%s" from database' % art)
+        if self.options.reciprocal:
+            print('reciprocal option used, performing deep remove!')
+            deep = True
+        db._remove_artist(art_db[0], deep=deep)
+
+    def remove_black_list_entry(self):
+        """"""
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db._remove_bl(int(self.options.remove_bl))
+
+    def write_simi(self):
+        """Write similarity to DB.
+        """
+        self._create_db()
+        sim_formated = self._parse_similarity()
+        print('About to update DB with: "%s": %s' % sim_formated)
+        db = simadb.SimaDB(db_path=self.dbfile)
+        db._update_similar_artists(*sim_formated)
+        if self.options.reciprocal:
+            print('...and with reciprocal combinations as well.')
+            for sim_formed_rec in self._revert_similarity(sim_formated):
+                db._update_similar_artists(*sim_formed_rec)
+
+    def main(self):
+        """
+        Parse command line and run actions.
+        """
+        self._declare_opts()
+        self.options = self.parser.parse_args()
+        self._get_mpd_env_var()
+        if self.options.dbfile:
+            self.dbfile = self.options.dbfile
+            print('Using db file: %s' % self.dbfile)
+        if self.options.reciprocal:
+            print('Editing reciprocal similarity')
+        if self.options.bl_art:
+            self.bl_artist()
+            return
+        if self.options.bl_curr_art:
+            self.bl_current_artist()
+            return
+        if self.options.bl_curr_alb:
+            self.bl_current_album()
+            return
+        if self.options.bl_curr_trk:
+            self.bl_current_track()
+            return
+        if self.options.view_bl:
+            self.view_bl()
+            return
+        if self.options.remove_bl:
+            self.remove_black_list_entry()
+            return
+        if self.options.similarity:
+            self._control_similarity()
+            self.write_simi()
+            return
+        if self.options.remove_art:
+            self.remove_artist()
+            return
+        if self.options.remove_sim:
+            self.remove_similarity()
+            return
+        if self.options.view:
+            self.view()
+            return
+        if self.options.view_all:
+            self.view_all()
+        if self.options.do_purge_hist:
+            self.purge_history()
+        exit(0)
+
+
+def main():
+    SimaDB_CLI()
+
+# Script starts here
+if __name__ == '__main__':
+    main()
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab