]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Enable file logging (cli and conf file
[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             'user_db': "false",
49             'history_duration': "8",
50             'queue_length': "1",
51             'consume': "0",},
52         'daemon':{
53             'daemon': "false",
54             'pidfile': "",},
55         'log': {
56             'verbosity': "info",
57             'logfile': "",
58             },
59         'echonest': {
60             },
61         'lastfm': {
62             'dynamic': "10",
63             'similarity': "15",
64             'queue_mode': "track", #TODO control values
65             'single_album': "false",
66             'track_to_add': "1",
67             'album_to_add': "1",
68             'depth': "1",
69             },
70         'randomfallback': {
71             'flavour': "sensible", # in pure, sensible, genre
72             'track_to_add': "1",
73             }
74         }
75 #
76
77
78 class ConfMan(object):  # CONFIG MANAGER CLASS
79     """
80     Configuration manager.
81     Default configuration is stored in DEFAULT_CONF dictionnary.
82     First init_config() run to get config from file.
83     Then control_conf() is run and retrieve configuration from defaults if not
84     set in conf files.
85     These settings are then updated with command line options with
86     supersedes_config_with_cmd_line_options().
87
88     Order of priority for the origin of an option is then (lowest to highest):
89         * DEFAULT_CONF
90         * Env. Var for MPD host, port and password
91         * configuration file (overrides previous)
92         * command line options (overrides previous)
93     """
94
95     def __init__(self, logger, options=None):
96         # options settings priority:
97         # defauts < conf. file < command line
98         self.conf_file = options.get('conf_file')
99         self.config = None
100         self.defaults = dict(DEFAULT_CONF)
101         self.startopt = options
102         ## Sima sqlite DB
103         self.db_file = None
104
105         self.log = logger
106         ## INIT CALLS
107         self.use_envar()
108         self.init_config()
109         self.control_conf()
110         self.supersedes_config_with_cmd_line_options()
111         self.config['sima']['db_file'] = self.db_file
112
113     def get_pw(self):
114         try:
115             self.config.getboolean('MPD', 'password')
116             self.log.debug('No password set, proceeding without ' +
117                            'authentication...')
118             return None
119         except ValueError:
120             # ValueError if password not a boolean, hence an actual password.
121             pwd = self.config.get('MPD', 'password')
122             if not pwd:
123                 self.log.debug('Password set as an empty string.')
124                 return None
125             return pwd
126
127     def control_mod(self):
128         """
129         Controls conf file permissions.
130         """
131         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
132         self.log.debug('file permission is: %o' % mode)
133         if mode & S_IRWXO or mode & S_IRWXG:
134             self.log.warning('File is readable by "other" and/or' +
135                              ' "group" (actual permission %o octal).' %
136                              mode)
137             self.log.warning('Consider setting permissions' +
138                              ' to 600 octal.')
139
140     def supersedes_config_with_cmd_line_options(self):
141         """Updates defaults settings with command line options"""
142         for sec in self.config.sections():
143             for opt in self.config.options(sec):
144                 if opt in list(self.startopt.keys()):
145                     self.config.set(sec, opt, str(self.startopt.get(opt)))
146
147     def use_envar(self):
148         """Use MPD en.var. to set defaults"""
149         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
150         if mpd_host:
151             self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
152             self.defaults['MPD']['host'] = mpd_host
153         if passwd:
154             self.log.info('Env. variable MPD_HOST contains password.')
155             self.defaults['MPD']['password'] = passwd
156         if mpd_port:
157             self.log.info('Env. variable MPD_PORT set to "%s".'
158                                   % mpd_port)
159             self.defaults['MPD']['port'] = mpd_port
160
161     def control_conf(self):
162         """Get through options/values and set defaults if not in conf file."""
163         # Control presence of obsolete settings
164         for option in ['history', 'history_length', 'top_tracks']:
165             if self.config.has_option('sima', option):
166                 self.log.warning('Obsolete setting found in conf file: "%s"'
167                         % option)
168         # Setting default if not specified
169         for section in DEFAULT_CONF.keys():
170             if section not in self.config.sections():
171                 self.log.debug('[%s] NOT in conf file' % section)
172                 self.config.add_section(section)
173                 for option in self.defaults[section]:
174                     self.config.set(section,
175                             option,
176                             self.defaults[section][option])
177                     self.log.debug(
178                             'Setting option with default value: %s = %s' %
179                             (option, self.defaults[section][option]))
180             elif section in self.config.sections():
181                 self.log.debug('[%s] present in conf file' % section)
182                 for option in self.defaults[section]:
183                     if self.config.has_option(section, option):
184                         #self.log.debug(u'option "%s" set to "%s" in conf. file' %
185                         #              (option, self.config.get(section, option)))
186                         pass
187                     else:
188                         self.log.debug(
189                                 'Option "%s" missing in section "%s"' %
190                                 (option, section))
191                         self.log.debug('=> setting default "%s" (may not suit you…)' %
192                                        self.defaults[section][option])
193                         self.config.set(section, option,
194                                         self.defaults[section][option])
195
196     def init_config(self):
197         """
198         Use XDG directory standard if exists
199         else use "HOME/(.config|.local/share)/sima/"
200         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
201         """
202
203         homedir = environ.get('HOME')
204
205         if environ.get('XDG_DATA_HOME'):
206             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
207         elif self.startopt.get('var_dir'):
208             # If var folder is provided via CLI set data_dir accordingly
209             data_dir = join(self.startopt.get('var_dir'))
210         elif (homedir and isdir(homedir) and homedir not in ['/']):
211             data_dir = join(homedir, '.local', 'share', DIRNAME)
212         else:
213             self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
214             self.log.error('Please use "--var_dir" to set a proper location')
215             sys.exit(1)
216
217         if not isdir(data_dir):
218             makedirs(data_dir)
219             chmod(data_dir, 0o700)
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             # Create conf_dir if necessary
229             if not isdir(conf_dir):
230                 makedirs(conf_dir)
231                 chmod(conf_dir, 0o700)
232             self.conf_file = join(conf_dir, CONF_FILE)
233         else:
234             self.log.error('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
235             self.log.error('Please use "--config" to locate the conf file')
236             sys.exit(1)
237
238         self.db_file = join(data_dir, 'sima.db')
239
240         config = configparser.SafeConfigParser()
241         # If no conf file present, uses defaults
242         if not isfile(self.conf_file):
243             self.config = config
244             return
245
246         self.log.info('Loading configuration from:  %s' % self.conf_file)
247         self.control_mod()
248
249         try:
250             config.read(self.conf_file)
251         except Error as err:
252             self.log.error(err)
253             sys.exit(1)
254
255         self.config = config
256
257 # VIM MODLINE
258 # vim: ai ts=4 sw=4 sts=4 expandtab