]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Controls MPD protocol version (need 0.21 at least for filters)
[mpd-sima.git] / sima / utils / config.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2015, 2019-2020 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:  # 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 "%s"', 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                 passwd, host = self.startopt.get('host').split('@')
189                 self.config.set('MPD', 'password', passwd)
190                 self.config.set('MPD', 'host', host)
191
192     def use_envar(self):
193         """Use MPD en.var. to set defaults"""
194         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
195         if mpd_host:
196             self.log.info('Env. variable MPD_HOST set to "%s"', mpd_host)
197             self.config['MPD'].update(host=mpd_host)
198         if passwd:
199             self.log.info('Env. variable MPD_HOST contains password.')
200             self.config['MPD'].update(password=passwd)
201         if mpd_port:
202             self.log.info('Env. variable MPD_PORT set to "%s".', mpd_port)
203             self.config['MPD'].update(port=mpd_port)
204
205     def init_config(self):
206         """
207         Use XDG directory standard if exists
208         else use "HOME/(.config|.local/share)/sima/"
209         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
210         """
211
212         homedir = environ.get('HOME')
213
214         if environ.get('XDG_DATA_HOME'):
215             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
216         elif homedir and isdir(homedir) and homedir not in ['/']:
217             data_dir = join(homedir, '.local', 'share', DIRNAME)
218         else:
219             self.log.critical('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
220             self.log.critical('Please use "--var-dir" to set a proper location')
221             sys.exit(1)
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             self.conf_file = join(conf_dir, CONF_FILE)
231         else:
232             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
233             self.log.critical('Please use "--config" to locate the conf file')
234             sys.exit(1)
235
236         ## Sima sqlite DB
237         self.config['sima']['var_dir'] = join(data_dir)
238
239         # If no conf file present, uses defaults
240         if not isfile(self.conf_file):
241             return
242
243         self.log.info('Loading configuration from:  %s', self.conf_file)
244         self.control_mod()
245
246         try:
247             self.config.read(self.conf_file)
248         except Error as err:
249             self.log.error(err)
250             sys.exit(1)
251
252 # VIM MODLINE
253 # vim: ai ts=4 sw=4 sts=4 expandtab