]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Fix default conf file name, revert to previous name
[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 #
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, R_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, RandomFallBack",
50             'contrib': "",
51             'user_db': "false",
52             'history_duration': 8,
53             'queue_length': 1,
54             'var_dir': 'empty',
55             'musicbrainzid': "true",
56             },
57         'daemon':{
58             'daemon': False,
59             'pidfile': "",
60             },
61         'log': {
62             'verbosity': "info",
63             'logfile': "",
64             },
65         'crop': {
66             'consume': 10,
67             },
68         'echonest': {
69             'queue_mode': "track", #TODO control values
70             'max_art': 15,
71             'single_album': "false",
72             'track_to_add': 1,
73             'album_to_add': 1,
74             'depth': 1,
75             },
76         'lastfm': {
77             'queue_mode': "track", #TODO control values
78             'max_art': 10,
79             'single_album': "false",
80             'track_to_add': 1,
81             'album_to_add': 1,
82             'depth': 1,
83             'cache': True,
84             },
85         'randomfallback': {
86             'flavour': "sensible", # in pure, sensible
87             'track_to_add': 1,
88             }
89         }
90 #
91
92
93 class ConfMan(object):  # CONFIG MANAGER CLASS
94     """
95     Configuration manager.
96     Default configuration is stored in DEFAULT_CONF dictionnary.
97     First init_config() run to get config from file.
98     Then control_conf() is run and retrieve configuration from defaults if not
99     set in conf files.
100     These settings are then updated with command line options with
101     supersedes_config_with_cmd_line_options().
102
103     Order of priority for the origin of an option is then (lowest to highest):
104         * DEFAULT_CONF
105         * Env. Var for MPD host, port and password
106         * configuration file (overrides previous)
107         * command line options (overrides previous)
108     """
109
110     def __init__(self, options=None):
111         self.log = logging.getLogger('sima')
112         # options settings priority:
113         # defauts < env. var. < conf. file < command line
114         self.conf_file = options.get('conf_file')
115         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
116         self.config.read_dict(DEFAULT_CONF)
117         # update DEFAULT_CONF with env. var.
118         self.use_envar()
119         self.startopt = options
120
121         ## INIT CALLS
122         self.init_config()
123         self.supersedes_config_with_cmd_line_options()
124         # Controls files access
125         self.control_facc()
126         # generate dbfile
127         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
128
129     def control_facc(self):
130         """TODO: redundant with startopt cli args controls
131         """
132         ok = True
133         for op, ftochk in [('log', self.config['log']['logfile']),
134                            ('pidfile', self.config['daemon']['pidfile']),]:
135             if not ftochk:
136                 continue
137             if not exists(ftochk):
138                 # Is parent directory writable then
139                 filedir = dirname(ftochk)
140                 if not access(filedir, W_OK):
141                     self.log.critical('no write access to "{0}" ({1})'.format(filedir, op))
142                     ok = False
143             else:
144                 if not access(ftochk, W_OK):
145                     self.log.critical('no write access to "{0}" ({1}))'.format(ftochk, op))
146                     ok = False
147         if not ok:
148             sys.exit(2)
149
150     def control_mod(self):
151         """
152         Controls conf file permissions.
153         """
154         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
155         self.log.debug('file permission is: %o' % mode)
156         if mode & S_IRWXO or mode & S_IRWXG:
157             self.log.warning('File is readable by "other" and/or' +
158                              ' "group" (actual permission %o octal).' %
159                              mode)
160             self.log.warning('Consider setting permissions' +
161                              ' to 600 octal.')
162
163     def supersedes_config_with_cmd_line_options(self):
164         """Updates defaults settings with command line options"""
165         for sec in self.config.sections():
166             for opt in self.config.options(sec):
167                 if opt in list(self.startopt.keys()):
168                     self.config.set(sec, opt, str(self.startopt.get(opt)))
169
170     def use_envar(self):
171         """Use MPD en.var. to set defaults"""
172         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
173         if mpd_host:
174             self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
175             self.config['MPD'].update(host=mpd_host)
176         if passwd:
177             self.log.info('Env. variable MPD_HOST contains password.')
178             self.config['MPD'].update(password=passwd)
179         if mpd_port:
180             self.log.info('Env. variable MPD_PORT set to "%s".' % mpd_port)
181             self.config['MPD'].update(port=mpd_port)
182
183     def init_config(self):
184         """
185         Use XDG directory standard if exists
186         else use "HOME/(.config|.local/share)/sima/"
187         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
188         """
189
190         homedir = environ.get('HOME')
191
192         if environ.get('XDG_DATA_HOME'):
193             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
194         elif homedir and isdir(homedir) and homedir not in ['/']:
195             data_dir = join(homedir, '.local', 'share', DIRNAME)
196         else:
197             self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
198             self.log.error('Please use "--var_dir" to set a proper location')
199             sys.exit(1)
200
201         if not isdir(data_dir):
202             makedirs(data_dir)
203             chmod(data_dir, 0o700)
204
205         if self.startopt.get('conf_file'):
206             # No need to handle conf file location
207             pass
208         elif environ.get('XDG_CONFIG_HOME'):
209             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
210         elif homedir and isdir(homedir) and homedir not in ['/']:
211             conf_dir = join(homedir, '.config', DIRNAME)
212             # Create conf_dir if necessary
213             if not isdir(conf_dir):
214                 makedirs(conf_dir)
215                 chmod(conf_dir, 0o700)
216             self.conf_file = join(conf_dir, CONF_FILE)
217         else:
218             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
219             self.log.critical('Please use "--config" to locate the conf file')
220             sys.exit(1)
221
222         ## Sima sqlite DB
223         self.config['sima']['var_dir'] = join(data_dir)
224
225         # If no conf file present, uses defaults
226         if not isfile(self.conf_file):
227             return
228
229         self.log.info('Loading configuration from:  %s' % self.conf_file)
230         self.control_mod()
231
232         try:
233             self.config.read(self.conf_file)
234         except Error as err:
235             self.log.error(err)
236             sys.exit(1)
237
238 # VIM MODLINE
239 # vim: ai ts=4 sw=4 sts=4 expandtab