2 # -*- coding: utf-8 -*-
4 # Copyright (c) 2010-2013 Jack Kaliko <efrim@azylum.org>
6 # This file is part of MPD_sima
8 # MPD_sima is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # MPD_sima is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with MPD_sima. If not, see <http://www.gnu.org/licenses/>.
28 from argparse import (ArgumentParser, SUPPRESS, Action)
29 from difflib import get_close_matches
30 from locale import getpreferredencoding
31 from os import (environ, chmod, makedirs)
32 from os.path import (join, isdir, isfile, expanduser)
33 from sys import (exit, stdout, stderr)
35 from sima.lib.track import Track
36 from sima.utils import utils
37 from sima.lib import simadb
38 from musicpd import MPDClient, ConnectionError
42 simadb_cli helps you to edit entries in your own DB of similarity
46 class FooAction(Action):
47 def check(self, namespace):
48 if namespace.similarity: return True
49 if namespace.remove_art: return True
50 if namespace.remove_sim: return True
52 def __call__(self, parser, namespace, values, option_string=None):
53 opt_required = '"--remove_artist", "--remove_similarity" or "--add_similarity"'
54 if not self.check(namespace):
56 'can\'t use {0} option before or without {1}'.format(
57 option_string, opt_required))
58 setattr(namespace, self.dest, True)
61 # pop out 'sw' value before creating ArgumentParser object.
64 'sw':['-a', '--add_similarity'],
67 'help': 'Similarity to add formated as follow: ' +
68 ' "art_0,art_1:90,art_2:80..."'},
70 'sw': ['-c', '--check_names'],
71 'action': 'store_true',
73 'help': 'Turn on controls of artists names in MPD library.'},
75 'sw':['-d', '--dbfile'],
78 'action': utils.Wfile,
79 'help': 'File to read/write database from/to'},
81 'sw': ['-r', '--reciprocal'],
85 'help': 'Turn on reciprocity for similarity relation when add/remove.'},
87 'sw':['--remove_artist'],
90 'metavar': '"ARTIST TO REMOVE"',
91 'help': 'Remove an artist from DB (main artist entries).'},
93 'sw':['--remove_similarity'],
96 'metavar': '"MAIN ART,SIMI ART"',
97 'help': 'Remove an similarity relation from DB (main artist <=> similar artist).'},
99 'sw':['-v', '--view_artist'],
102 'metavar': '"ARTIST NAME"',
103 'help': 'View an artist from DB.'},
106 'action': 'store_true',
107 'help': 'View all similarity entries.'},
109 'sw': ['-S', '--host'],
113 'help': 'MPD host, as IP or FQDN (default: localhost|MPD_HOST).'},
115 'sw': ['-P', '--port'],
119 'help': 'Port MPD in listening on (default: 6600|MPD_PORT).'},
121 'sw': ['--password'],
128 'action': 'store_true',
129 'help': 'View black list.'},
131 'sw': ['--remove_bl'],
133 'help': 'Suppress a black list entry, by row id. Use --view_bl to get row id.'},
137 'metavar': 'ARTIST_NAME',
138 'help': 'Black list artist.'},
140 'sw': ['--bl_curr_art'],
141 'action': 'store_true',
142 'help': 'Black list currently playing artist.'},
144 'sw': ['--bl_curr_alb'],
145 'action': 'store_true',
146 'help': 'Black list currently playing album.'},
148 'sw': ['--bl_curr_trk'],
149 'action': 'store_true',
150 'help': 'Black list currently playing track.'},
152 'sw':['--purge_hist'],
153 'action': 'store_true',
154 'dest': 'do_purge_hist',
155 'help': 'Purge play history.'}])
158 class SimaDB_CLI(object):
159 """Command line management.
163 self.dbfile = self._get_default_dbfile()
165 self.options = dict({})
166 self.localencoding = 'UTF-8'
171 def _get_encoding(self):
172 """Get local encoding"""
173 localencoding = getpreferredencoding()
175 self.localencoding = localencoding
177 def _get_mpd_env_var(self):
179 MPD host/port environement variables are used if command line does not
180 provide host|port|passwd.
182 host, port, passwd = utils.get_mpd_environ()
183 if self.options.passwd is None and passwd:
184 self.options.passwd = passwd
185 if self.options.mpdhost is None:
187 self.options.mpdhost = host
189 self.options.mpdhost = 'localhost'
190 if self.options.mpdport is None:
192 self.options.mpdport = port
194 self.options.mpdport = 6600
197 """Upgrades DB if necessary, create one if not existing."""
198 if not isfile(self.dbfile): # No db file
200 db = simadb.SimaDB(db_path=self.dbfile)
203 def _declare_opts(self):
205 Declare options in ArgumentParser object.
207 self.parser = ArgumentParser(description=DESCRIPTION,
208 usage='%(prog)s [-h|--help] [options]',
210 epilog='Happy Listening',
213 self.parser.add_argument('--version', action='version',
214 version='%(prog)s {0}'.format(__version__))
215 # Add all options declare in OPTS
217 opt_names = opt.pop('sw')
218 self.parser.add_argument(*opt_names, **opt)
220 def _get_default_dbfile(self):
222 Use XDG directory standard if exists
223 else use "HOME/.local/share/mpd_sima/"
224 http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
226 homedir = expanduser('~')
228 if environ.get('XDG_DATA_HOME'):
229 data_dir = join(environ.get('XDG_DATA_HOME'), dirname)
231 data_dir = join(homedir, '.local', 'share', dirname)
232 if not isdir(data_dir):
234 chmod(data_dir, 0o700)
235 return join(data_dir, DB_NAME)
237 def _get_mpd_client(self):
239 # TODO: encode properly host name
240 host = self.options.mpdhost
241 port = self.options.mpdport
244 cli.connect(host=host, port=port)
245 except ConnectionError as err:
246 mess = 'ERROR: fail to connect MPD (host: %s:%s): %s' % (
248 print(mess, file=stderr)
252 def _create_db(self):
253 """Create database if necessary"""
254 if isfile(self.dbfile):
256 print('Creating database!')
257 open(self.dbfile, 'a').close()
258 simadb.SimaDB(db_path=self.dbfile).create_db()
260 def _get_art_from_db(self, art):
261 """Return (id, name, self...) from DB or None is not in DB"""
262 db = simadb.SimaDB(db_path=self.dbfile)
263 art_db = db.get_artist(art, add_not=True)
265 print('ERROR: "%s" not in data base!' % art, file=stderr)
269 def _control_similarity(self):
271 * Regex check of command line similarity
272 * Controls artist presence in MPD library
274 usage = ('USAGE: "main artist,similar artist:<match score>,other' +
275 'similar artist:<match score>,..."')
276 cli_sim = self.options.similarity
277 pattern = '^([^,]+?),([^:,]+?:\d{1,2},?)+$'
278 regexp = re.compile(pattern, re.U).match(cli_sim)
280 mess = 'ERROR: similarity badly formated: "%s"' % cli_sim
281 print(mess, file=stderr)
282 print(usage, file=stderr)
284 if self.options.check_names:
285 if not self._control_artist_names():
286 mess = 'ERROR: some artist names not found in MPD library!'
287 print(mess, file=stderr)
290 def _control_artist_names(self):
291 """Controls artist names exist in MPD library"""
292 mpd_cli = self._get_mpd_client()
293 artists_list = mpd_cli.list('artist')
294 sim_formated = self._parse_similarity()
296 if sim_formated[0] not in artists_list:
297 mess = 'WARNING: Main artist not found in MPD: %s' % sim_formated[0]
300 for sart in sim_formated[1]:
301 art = sart.get('artist')
302 if art not in artists_list:
303 mess = str('WARNING: Similar artist not found in MPD: %s' % art)
309 def _parse_similarity(self):
310 """Parse command line option similarity"""
311 cli_sim = self.options.similarity.strip(',').split(',')
314 for art in cli_sim[1:]:
315 artist = art.split(':')[0]
316 score = int(art.split(':')[1])
317 sim.append({'artist': artist, 'score': score})
320 def _print_main_art(self, art=None):
321 """Print entries, art as main artist."""
323 art = self.options.view
324 db = simadb.SimaDB(db_path=self.dbfile)
325 art_db = self._get_art_from_db(art)
326 if not art_db: return
328 [sims.append(a) for a in db._get_similar_artists_from_db(art_db[0])]
331 print('"%s" similarities:' % art)
333 mess = str(' - {score:0>2d} {artist}'.format(**art))
337 def _remove_sim(self, art1_db, art2_db):
338 """Remove single similarity between two artists."""
339 db = simadb.SimaDB(db_path=self.dbfile)
340 similarity = db._get_artist_match(art1_db[0], art2_db[0])
343 db._remove_relation_between_2_artist(art1_db[0], art2_db[0])
344 mess = 'Remove: "{0}" "{1}:{2:0>2d}"'.format(art1_db[1], art2_db[1],
349 def _revert_similarity(self, sim_formated):
350 """Revert similarity string (for reciprocal editing - add)."""
351 main_art = sim_formated[0]
352 similars = sim_formated[1]
353 for similar in similars:
354 yield (similar.get('artist'),
355 [{'artist':main_art, 'score':similar.get('score')}])
358 """Black list artist"""
359 mpd_cli = self._get_mpd_client()
362 artists_list = mpd_cli.list('artist')
363 # Unicode cli given artist name
364 cli_artist_to_bl = self.options.bl_art
365 if cli_artist_to_bl not in artists_list:
366 print('Artist not found in MPD library.')
367 match = get_close_matches(cli_artist_to_bl, artists_list, 50, 0.78)
369 print('You may be refering to %s' %
370 '/'.join([m_a for m_a in match]))
372 print('Black listing artist: %s' % cli_artist_to_bl)
373 db = simadb.SimaDB(db_path=self.dbfile)
374 db.get_bl_artist(cli_artist_to_bl)
376 def bl_current_artist(self):
377 """Black list current artist"""
378 mpd_cli = self._get_mpd_client()
381 artist = mpd_cli.currentsong().get('artist', '')
383 print('No artist found.')
385 print('Black listing artist: %s' % artist)
386 db = simadb.SimaDB(db_path=self.dbfile)
387 db.get_bl_artist(artist)
389 def bl_current_album(self):
390 """Black list current artist"""
391 mpd_cli = self._get_mpd_client()
394 track = Track(**mpd_cli.currentsong())
396 print('No album set for this track: %s' % track)
398 print('Black listing album: {0}'.format(track.album))
399 db = simadb.SimaDB(db_path=self.dbfile)
400 db.get_bl_album(track)
402 def bl_current_track(self):
403 """Black list current artist"""
404 mpd_cli = self._get_mpd_client()
407 track = Track(**mpd_cli.currentsong())
408 print('Black listing track: %s' % track)
409 db = simadb.SimaDB(db_path=self.dbfile)
410 db.get_bl_track(track)
412 def purge_history(self):
413 """Purge all entries in history"""
414 db = simadb.SimaDB(db_path=self.dbfile)
415 print('Purging history...')
416 db.purge_history(duration=int(0))
418 print('Cleaning database...')
423 """Print out entries for an artist."""
424 art = self.options.view
425 db = simadb.SimaDB(db_path=self.dbfile)
426 art_db = self._get_art_from_db(art)
427 if not art_db: return
428 if not self._print_main_art():
429 mess = str('"%s" present in DB but not as a main artist' % art)
433 [art_rev.append(a) for a in db._get_reverse_similar_artists_from_db(art_db[0])]
434 if not art_rev: return
435 mess = str('%s" appears as similar for the following artist(s): %s' %
436 (art,', '.join(art_rev)))
438 [self._print_main_art(a) for a in art_rev]
441 """Print out all entries."""
442 db = simadb.SimaDB(db_path=self.dbfile)
443 for art in db.get_artists():
444 if not art[0]: continue
445 self._print_main_art(art=art[0])
448 """Print out black list."""
449 # TODO: enhance output formating
450 db = simadb.SimaDB(db_path=self.dbfile)
451 for bl_e in db.get_black_list():
452 print('\t# '.join([str(e) for e in bl_e]))
454 def remove_similarity(self):
456 cli_sim = self.options.remove_sim
457 pattern = '^([^,]+?),([^,]+?,?)$'
458 regexp = re.compile(pattern, re.U).match(cli_sim)
460 print('ERROR: similarity badly formated: "%s"' % cli_sim, file=stderr)
461 print('USAGE: A single relation between two artists is expected here.', file=stderr)
462 print('USAGE: "main artist,similar artist"', file=stderr)
464 arts = cli_sim.split(',')
466 print('ERROR: unknown error in similarity format', file=stderr)
467 print('USAGE: "main artist,similar artist"', file=stderr)
469 art1_db = self._get_art_from_db(arts[0].strip())
470 art2_db = self._get_art_from_db(arts[1].strip())
471 if not art1_db or not art2_db: return
472 self._remove_sim(art1_db, art2_db)
473 if not self.options.reciprocal:
475 self._remove_sim(art2_db, art1_db)
477 def remove_artist(self):
478 """ Remove artist in the DB."""
480 art = self.options.remove_art
481 db = simadb.SimaDB(db_path=self.dbfile)
482 art_db = self._get_art_from_db(art)
483 if not art_db: return False
484 print('Removing "%s" from database' % art)
485 if self.options.reciprocal:
486 print('reciprocal option used, performing deep remove!')
488 db._remove_artist(art_db[0], deep=deep)
490 def remove_black_list_entry(self):
492 db = simadb.SimaDB(db_path=self.dbfile)
493 db._remove_bl(int(self.options.remove_bl))
495 def write_simi(self):
496 """Write similarity to DB.
499 sim_formated = self._parse_similarity()
500 print('About to update DB with: "%s": %s' % sim_formated)
501 db = simadb.SimaDB(db_path=self.dbfile)
502 db._update_similar_artists(*sim_formated)
503 if self.options.reciprocal:
504 print('...and with reciprocal combinations as well.')
505 for sim_formed_rec in self._revert_similarity(sim_formated):
506 db._update_similar_artists(*sim_formed_rec)
510 Parse command line and run actions.
513 self.options = self.parser.parse_args()
514 self._get_mpd_env_var()
515 if self.options.dbfile:
516 self.dbfile = self.options.dbfile
517 print('Using db file: %s' % self.dbfile)
518 if self.options.reciprocal:
519 print('Editing reciprocal similarity')
520 if self.options.bl_art:
523 if self.options.bl_curr_art:
524 self.bl_current_artist()
526 if self.options.bl_curr_alb:
527 self.bl_current_album()
529 if self.options.bl_curr_trk:
530 self.bl_current_track()
532 if self.options.view_bl:
535 if self.options.remove_bl:
536 self.remove_black_list_entry()
538 if self.options.similarity:
539 self._control_similarity()
542 if self.options.remove_art:
545 if self.options.remove_sim:
546 self.remove_similarity()
548 if self.options.view:
551 if self.options.view_all:
553 if self.options.do_purge_hist:
562 if __name__ == '__main__':
566 # vim: ai ts=4 sw=4 sts=4 expandtab