]> kaliko git repositories - mpd-sima.git/blob - simadb_cli
Sphinx documentation and API cleanup
[mpd-sima.git] / simadb_cli
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3
4 # Copyright (c) 2010-2015 Jack Kaliko <kaliko@azylum.org>
5 #
6 #  This file is part of MPD_sima
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 #
22
23 __version__ = '0.4.0'
24
25 # IMPORT#
26 import re
27
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)
34
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
39
40
41 DESCRIPTION = """
42 simadb_cli helps you to edit entries in your own DB of similarity
43 between artists."""
44 DB_NAME = 'sima.db'
45
46 # Options list
47 # pop out 'sw' value before creating ArgumentParser object.
48 OPTS = list([
49     {
50         'sw':['-d', '--dbfile'],
51         'type': str,
52         'dest':'dbfile',
53         'action': utils.Wfile,
54         'help': 'File to read/write database from/to'},
55     {
56         'sw': ['-S', '--host'],
57         'type': str,
58         'dest': 'mpdhost',
59         'default': None,
60         'help': 'MPD host, as IP or FQDN (default: localhost|MPD_HOST).'},
61     {
62         'sw': ['-P', '--port'],
63         'type': int,
64         'dest': 'mpdport',
65         'default': None,
66         'help': 'Port MPD in listening on (default: 6600|MPD_PORT).'},
67     {
68         'sw': ['--password'],
69         'type': str,
70         'dest': 'passwd',
71         'default': None,
72         'help': SUPPRESS},
73     {
74         'sw': ['--view_bl'],
75         'action': 'store_true',
76         'dest': 'view_bl',
77         'help': 'View black list.'},
78     {
79         'sw': ['--remove_bl'],
80         'type': int,
81         'help': 'Suppress a black list entry, by row id. Use --view_bl to get row id.'},
82     {
83         'sw': ['--bl_art'],
84         'type': str,
85         'metavar': 'ARTIST_NAME',
86         'help': 'Black list artist.'},
87     {
88         'sw': ['--bl_curr_art'],
89         'action': 'store_true',
90         'help': 'Black list currently playing artist.'},
91     {
92         'sw': ['--bl_curr_alb'],
93         'action': 'store_true',
94         'help': 'Black list currently playing album.'},
95     {
96         'sw': ['--bl_curr_trk'],
97         'action': 'store_true',
98         'help': 'Black list currently playing track.'},
99     {
100         'sw':['--purge_hist'],
101         'action': 'store_true',
102         'dest': 'do_purge_hist',
103         'help': 'Purge play history.'}])
104
105
106 class SimaDB_CLI(object):
107     """Command line management.
108     """
109
110     def __init__(self):
111         self.dbfile = self._get_default_dbfile()
112         self.parser = None
113         self.options = dict({})
114         self.localencoding = 'UTF-8'
115         self._get_encoding()
116         self.main()
117
118     def _get_encoding(self):
119         """Get local encoding"""
120         localencoding = getpreferredencoding()
121         if localencoding:
122             self.localencoding = localencoding
123
124     def _get_mpd_env_var(self):
125         """
126         MPD host/port environement variables are used if command line does not
127         provide host|port|passwd.
128         """
129         host, port, passwd = utils.get_mpd_environ()
130         if self.options.passwd is None and passwd:
131             self.options.passwd = passwd
132         if self.options.mpdhost is None:
133             if host:
134                 self.options.mpdhost = host
135             else:
136                 self.options.mpdhost = 'localhost'
137         if self.options.mpdport is None:
138             if port:
139                 self.options.mpdport = port
140             else:
141                 self.options.mpdport = 6600
142
143     def _upgrade(self):
144         """Upgrades DB if necessary, create one if not existing."""
145         if not isfile(self.dbfile): # No db file
146             return
147         db = simadb.SimaDB(db_path=self.dbfile)
148         db.upgrade()
149
150     def _declare_opts(self):
151         """
152         Declare options in ArgumentParser object.
153         """
154         self.parser = ArgumentParser(description=DESCRIPTION,
155                                    usage='%(prog)s [-h|--help] [options]',
156                                    prog='simadb_cli',
157                                    epilog='Happy Listening',
158                                    )
159
160         self.parser.add_argument('--version', action='version',
161                 version='%(prog)s {0}'.format(__version__))
162         # Add all options declare in OPTS
163         for opt in OPTS:
164             opt_names = opt.pop('sw')
165             self.parser.add_argument(*opt_names, **opt)
166
167     def _get_default_dbfile(self):
168         """
169         Use XDG directory standard if exists
170         else use "HOME/.local/share/mpd_sima/"
171         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
172         """
173         homedir = expanduser('~')
174         dirname = 'mpd_sima'
175         if environ.get('XDG_DATA_HOME'):
176             data_dir = join(environ.get('XDG_DATA_HOME'), dirname)
177         else:
178             data_dir = join(homedir, '.local', 'share', dirname)
179         if not isdir(data_dir):
180             makedirs(data_dir)
181             chmod(data_dir, 0o700)
182         return join(data_dir, DB_NAME)
183
184     def _get_mpd_client(self):
185         """"""
186         # TODO: encode properly host name
187         host = self.options.mpdhost
188         port = self.options.mpdport
189         cli = MPDClient()
190         try:
191             cli.connect(host=host, port=port)
192         except ConnectionError as err:
193             mess = 'ERROR: fail to connect MPD (host: %s:%s): %s' % (
194                     host, port, err)
195             print(mess, file=stderr)
196             exit(1)
197         return cli
198
199     def _create_db(self):
200         """Create database if necessary"""
201         if isfile(self.dbfile):
202             return
203         print('Creating database!')
204         open(self.dbfile, 'a').close()
205         simadb.SimaDB(db_path=self.dbfile).create_db()
206
207     def _get_art_from_db(self, art):
208         """Return (id, name, self...) from DB or None is not in DB"""
209         db = simadb.SimaDB(db_path=self.dbfile)
210         art_db = db.get_artist(art, add_not=True)
211         if not art_db:
212             print('ERROR: "%s" not in data base!' % art, file=stderr)
213             return None
214         return art_db
215
216     def _control_similarity(self):
217         """
218          * Regex check of command line similarity
219          * Controls artist presence in MPD library
220         """
221         usage = ('USAGE: "main artist,similar artist:<match score>,other' +
222                 'similar artist:<match score>,..."')
223         cli_sim = self.options.similarity
224         pattern = '^([^,]+?),([^:,]+?:\d{1,2},?)+$'
225         regexp = re.compile(pattern, re.U).match(cli_sim)
226         if not regexp:
227             mess = 'ERROR: similarity badly formated: "%s"' % cli_sim
228             print(mess, file=stderr)
229             print(usage, file=stderr)
230             exit(1)
231         if self.options.check_names:
232             if not self._control_artist_names():
233                 mess = 'ERROR: some artist names not found in MPD library!'
234                 print(mess, file=stderr)
235                 exit(1)
236
237     def _control_artist_names(self):
238         """Controls artist names exist in MPD library"""
239         mpd_cli = self._get_mpd_client()
240         artists_list = mpd_cli.list('artist')
241         sim_formated = self._parse_similarity()
242         control = True
243         if sim_formated[0] not in artists_list:
244             mess = 'WARNING: Main artist not found in MPD: %s' % sim_formated[0]
245             print(mess)
246             control = False
247         for sart in sim_formated[1]:
248             art = sart.get('artist')
249             if art not in artists_list:
250                 mess = str('WARNING: Similar artist not found in MPD: %s' % art)
251                 print(mess)
252                 control = False
253         mpd_cli.disconnect()
254         return control
255
256     def bl_artist(self):
257         """Black list artist"""
258         mpd_cli = self._get_mpd_client()
259         if not mpd_cli:
260             return False
261         artists_list = mpd_cli.list('artist')
262         # Unicode cli given artist name
263         cli_artist_to_bl = self.options.bl_art
264         if cli_artist_to_bl not in artists_list:
265             print('Artist not found in MPD library.')
266             match = get_close_matches(cli_artist_to_bl, artists_list, 50, 0.78)
267             if match:
268                 print('You may be refering to %s' %
269                         '/'.join([m_a for m_a in match]))
270             return False
271         print('Black listing artist: %s' % cli_artist_to_bl)
272         db = simadb.SimaDB(db_path=self.dbfile)
273         db.get_bl_artist(cli_artist_to_bl)
274
275     def bl_current_artist(self):
276         """Black list current artist"""
277         mpd_cli = self._get_mpd_client()
278         if not mpd_cli:
279             return False
280         artist = mpd_cli.currentsong().get('artist', '')
281         if not artist:
282             print('No artist found.')
283             return False
284         print('Black listing artist: %s' % artist)
285         db = simadb.SimaDB(db_path=self.dbfile)
286         db.get_bl_artist(artist)
287
288     def bl_current_album(self):
289         """Black list current artist"""
290         mpd_cli = self._get_mpd_client()
291         if not mpd_cli:
292             return False
293         track = Track(**mpd_cli.currentsong())
294         if not track.album:
295             print('No album set for this track: %s' % track)
296             return False
297         print('Black listing album: {0}'.format(track.album))
298         db = simadb.SimaDB(db_path=self.dbfile)
299         db.get_bl_album(track)
300
301     def bl_current_track(self):
302         """Black list current artist"""
303         mpd_cli = self._get_mpd_client()
304         if not mpd_cli:
305             return False
306         track = Track(**mpd_cli.currentsong())
307         print('Black listing track: %s' % track)
308         db = simadb.SimaDB(db_path=self.dbfile)
309         db.get_bl_track(track)
310
311     def purge_history(self):
312         """Purge all entries in history"""
313         db = simadb.SimaDB(db_path=self.dbfile)
314         print('Purging history...')
315         db.purge_history(duration=int(0))
316         print('done.')
317         print('Cleaning database...')
318         db.clean_database()
319         print('done.')
320
321     def view_bl(self):
322         """Print out black list."""
323         # TODO: enhance output formating
324         db = simadb.SimaDB(db_path=self.dbfile)
325         for bl_e in db.get_black_list():
326             print('\t# '.join([str(e) for e in bl_e]))
327
328     def remove_black_list_entry(self):
329         """"""
330         db = simadb.SimaDB(db_path=self.dbfile)
331         db._remove_bl(int(self.options.remove_bl))
332
333     def main(self):
334         """
335         Parse command line and run actions.
336         """
337         self._declare_opts()
338         self.options = self.parser.parse_args()
339         self._get_mpd_env_var()
340         if self.options.dbfile:
341             self.dbfile = self.options.dbfile
342             print('Using db file: %s' % self.dbfile)
343         if self.options.bl_art:
344             self.bl_artist()
345             return
346         if self.options.bl_curr_art:
347             self.bl_current_artist()
348             return
349         if self.options.bl_curr_alb:
350             self.bl_current_album()
351             return
352         if self.options.bl_curr_trk:
353             self.bl_current_track()
354             return
355         if self.options.view_bl:
356             self.view_bl()
357             return
358         if self.options.remove_bl:
359             self.remove_black_list_entry()
360             return
361         if self.options.do_purge_hist:
362             self.purge_history()
363         exit(0)
364
365
366 def main():
367     SimaDB_CLI()
368
369 # Script starts here
370 if __name__ == '__main__':
371     main()
372
373 # VIM MODLINE
374 # vim: ai ts=4 sw=4 sts=4 expandtab