]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Fixed mopidy compatibility implementation (Closes #21)
[mpd-sima.git] / sima / utils / config.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009, 2010, 2011, 2013, 2014, 2015 Jack Kaliko <kaliko@azylum.org>
3 #
4 #  This file is part of sima
5 #
6 #  sima is free software: you can redistribute it and/or modify
7 #  it under the terms of the GNU General Public License as published by
8 #  the Free Software Foundation, either version 3 of the License, or
9 #  (at your option) any later version.
10 #
11 #  sima is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public License
17 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 # pylint: disable=bad-continuation
20
21 """
22 Deal with configuration and data files.
23 Parse configuration file and set defaults for missing options.
24 """
25
26 # IMPORTS
27 import configparser
28 import logging
29 import sys
30
31 from configparser import Error
32 from os import (access, makedirs, environ, stat, chmod, W_OK)
33 from os.path import (join, isdir, isfile, dirname, exists)
34 from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
35
36 from . import utils
37
38 # DEFAULTS
39 DIRNAME = 'mpd_sima'
40 CONF_FILE = 'mpd_sima.cfg'
41
42 DEFAULT_CONF = {
43         'MPD': {
44             'host': "localhost",
45             #'password': "",
46             'port': 6600,
47             },
48         'sima': {
49             'internal': "Crop, Lastfm, Random",
50             'contrib': "",
51             'user_db': "false",
52             'history_duration': 8,
53             'queue_length': 2,
54             'var_dir': 'empty',
55             'musicbrainzid': "true",
56             'repeat_disable_queue': "true",
57             'single_disable_queue': "true",
58             'mopidy_compat': "false",
59             },
60         'daemon':{
61             'daemon': False,
62             'pidfile': "",
63             },
64         'log': {
65             'verbosity': "info",
66             'logfile': "",
67             },
68         'crop': {
69             'consume': 10,
70             'priority': 0,
71             },
72         'lastfm': {
73             'queue_mode': "track", #TODO control values
74             'max_art': 10,
75             'single_album': "false",
76             'track_to_add': 1,
77             'album_to_add': 1,
78             'depth': 1,
79             'cache': True,
80             'priority': 100,
81             },
82         'random': {
83             'flavour': "sensible", # in pure, sensible
84             'track_to_add': 1,
85             'priority': 50,
86             },
87         }
88 #
89
90
91 class ConfMan(object):  # CONFIG MANAGER CLASS
92     """
93     Configuration manager.
94     Default configuration is stored in DEFAULT_CONF dictionnary.
95     First init_config() run to get config from file.
96     Then control_conf() is run and retrieve configuration from defaults if not
97     set in conf files.
98     These settings are then updated with command line options with
99     supersedes_config_with_cmd_line_options().
100
101     Order of priority for the origin of an option is then (lowest to highest):
102         * DEFAULT_CONF
103         * Env. Var for MPD host, port and password
104         * configuration file (overrides previous)
105         * command line options (overrides previous)
106     """
107
108     def __init__(self, options=None):
109         self.log = logging.getLogger('sima')
110         # options settings priority:
111         # defauts < env. var. < conf. file < command line
112         self.conf_file = options.get('conf_file')
113         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
114         self.config.read_dict(DEFAULT_CONF)
115         # update DEFAULT_CONF with env. var.
116         self.use_envar()
117         self.startopt = options
118
119         ## INIT CALLS
120         self.init_config()
121         self.supersedes_config_with_cmd_line_options()
122         # Controls files access
123         self.control_facc()
124         # set dbfile
125         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
126
127         # Create directories
128         data_dir = self.config['sima']['var_dir']
129         if not isdir(data_dir):
130             self.log.trace('Creating "{}"'.format(data_dir))
131             makedirs(data_dir)
132             chmod(data_dir, 0o700)
133
134     def control_facc(self):
135         """Controls file access.
136         This is relevant only for file provided through the configuration file
137         since files provided on the command line are already checked with
138         argparse.
139         """
140         ok = True
141         for op, ftochk in [('logfile', self.config.get('log', 'logfile')),
142                            ('pidfile', self.config.get('daemon', 'pidfile')),]:
143             if not ftochk:
144                 continue
145             if isdir(ftochk):
146                 self.log.critical('Need a file not a directory: "%s"', ftochk)
147                 ok = False
148             if not exists(ftochk):
149                 # Is parent directory writable then
150                 filedir = dirname(ftochk)
151                 if not access(filedir, W_OK):
152                     self.log.critical('no write access to "%s" (%s)', filedir, op)
153                     ok = False
154             else:
155                 if not access(ftochk, W_OK):
156                     self.log.critical('no write access to "%s" (%s)', ftochk, op)
157                     ok = False
158         if not ok:
159             if exists(self.conf_file):
160                 self.log.warning('Try to check the configuration file: %s', self.conf_file)
161             sys.exit(2)
162
163     def control_mod(self):
164         """
165         Controls conf file permissions.
166         """
167         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
168         self.log.debug('file permission is: %o', mode)
169         if mode & S_IRWXO or mode & S_IRWXG:
170             self.log.warning('File is readable by "other" and/or' +
171                              ' "group" (actual permission %o octal).' %
172                              mode)
173             self.log.warning('Consider setting permissions' +
174                              ' to 600 octal.')
175
176     def supersedes_config_with_cmd_line_options(self):
177         """Updates defaults settings with command line options"""
178         for sec in self.config.sections():
179             for opt in self.config.options(sec):
180                 if opt in list(self.startopt.keys()):
181                     self.config.set(sec, opt, str(self.startopt.get(opt)))
182         # honor MPD_HOST format as in mpc(1)  for command line option --host
183         if self.startopt.get('host'):
184             if '@' in self.startopt.get('host'):
185                 print(self.startopt.get('host').split('@'))
186                 passwd, host = self.startopt.get('host').split('@')
187                 self.config.set('MPD', 'password', passwd)
188                 self.config.set('MPD', 'host', host)
189
190     def use_envar(self):
191         """Use MPD en.var. to set defaults"""
192         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
193         if mpd_host:
194             self.log.info('Env. variable MPD_HOST set to "%s"', mpd_host)
195             self.config['MPD'].update(host=mpd_host)
196         if passwd:
197             self.log.info('Env. variable MPD_HOST contains password.')
198             self.config['MPD'].update(password=passwd)
199         if mpd_port:
200             self.log.info('Env. variable MPD_PORT set to "%s".', mpd_port)
201             self.config['MPD'].update(port=mpd_port)
202
203     def init_config(self):
204         """
205         Use XDG directory standard if exists
206         else use "HOME/(.config|.local/share)/sima/"
207         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
208         """
209
210         homedir = environ.get('HOME')
211
212         if environ.get('XDG_DATA_HOME'):
213             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
214         elif homedir and isdir(homedir) and homedir not in ['/']:
215             data_dir = join(homedir, '.local', 'share', DIRNAME)
216         else:
217             self.log.critical('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
218             self.log.critical('Please use "--var-dir" to set a proper location')
219             sys.exit(1)
220
221         if self.startopt.get('conf_file'):
222             # No need to handle conf file location
223             pass
224         elif environ.get('XDG_CONFIG_HOME'):
225             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
226         elif homedir and isdir(homedir) and homedir not in ['/']:
227             conf_dir = join(homedir, '.config', DIRNAME)
228             self.conf_file = join(conf_dir, CONF_FILE)
229         else:
230             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
231             self.log.critical('Please use "--config" to locate the conf file')
232             sys.exit(1)
233
234         ## Sima sqlite DB
235         self.config['sima']['var_dir'] = join(data_dir)
236
237         # If no conf file present, uses defaults
238         if not isfile(self.conf_file):
239             return
240
241         self.log.info('Loading configuration from:  %s', self.conf_file)
242         self.control_mod()
243
244         try:
245             self.config.read(self.conf_file)
246         except Error as err:
247             self.log.error(err)
248             sys.exit(1)
249
250 # VIM MODLINE
251 # vim: ai ts=4 sw=4 sts=4 expandtab