From: kaliko Date: Tue, 10 Jun 2014 13:58:01 +0000 (+0200) Subject: Add simadb_cli X-Git-Tag: mpd-sima/0.12.0pr4~2 X-Git-Url: https://git.kaliko.me/?a=commitdiff_plain;h=563504b3c2f42a4f9617e381d95176f167afce55;p=mpd-sima.git Add simadb_cli --- diff --git a/setup.py b/setup.py index cce58f8..93da156 100755 --- 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 index 0000000..52f45bc --- /dev/null +++ b/simadb_cli @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2010-2013 Jack Kaliko +# +# 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 . +# +# + +__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:,other' + + 'similar artist:,..."') + 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