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