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