]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
a46c0c66493e3519ab1af41cefe7d88e8087ddbf
[mpd-sima.git] / sima / utils / config.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009-2015, 2019-2021 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         'tags': {
91             'comment': "",
92             'date': "",
93             'genre': "",
94             'label': "",
95             'originaldate': "",
96             'filter': "",
97             'queue_mode': "track",
98             'single_album': False,
99             'track_to_add': 1,
100             'album_to_add': 1,
101             'priority': 80,
102             },
103         'genre': {
104             'queue_mode': "track",
105             'single_album': False,
106             'track_to_add': 1,
107             'album_to_add': 1,
108             'priority': 80,
109             },
110         }
111 #
112
113
114 class ConfMan:  # CONFIG MANAGER CLASS
115     """
116     Configuration manager.
117     Default configuration is stored in DEFAULT_CONF dictionnary.
118     First init_config() run to get config from file.
119     Then control_conf() is run and retrieve configuration from defaults if not
120     set in conf files.
121     These settings are then updated with command line options with
122     supersedes_config_with_cmd_line_options().
123
124     Order of priority for the origin of an option is then (lowest to highest):
125         * DEFAULT_CONF
126         * Env. Var for MPD host, port and password
127         * configuration file (overrides previous)
128         * command line options (overrides previous)
129     """
130
131     def __init__(self, options=None):
132         self.log = logging.getLogger('sima')
133         # options settings priority:
134         # defauts < env. var. < conf. file < command line
135         self.conf_file = options.get('conf_file')
136         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
137         self.config.read_dict(DEFAULT_CONF)
138         # update DEFAULT_CONF with env. var.
139         self.use_envar()
140         self.startopt = options
141
142         # INIT CALLS
143         self.init_config()
144         self.supersedes_config_with_cmd_line_options()
145         # set dbfile
146         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
147         # Controls files access
148         self.control_facc()
149
150         # Create directories
151         data_dir = self.config['sima']['var_dir']
152         if not isdir(data_dir):
153             self.log.trace('Creating "%s"', data_dir)
154             makedirs(data_dir)
155             chmod(data_dir, 0o700)
156
157     def control_facc(self):
158         """Controls file access.
159         This is relevant only for file provided through the configuration file
160         since files provided on the command line are already checked with
161         argparse. Also add config['sima']['db_file'] contructed here in init
162         """
163         ok = True
164         for op, ftochk in [('logfile', self.config.get('log', 'logfile')),
165                            ('pidfile', self.config.get('daemon', 'pidfile')),
166                            ('db file', self.config.get('sima', 'db_file'))]:
167             if not ftochk:
168                 continue
169             if isdir(ftochk):
170                 self.log.critical('Need a file not a directory: "%s"', ftochk)
171                 ok = False
172             if not exists(ftochk):
173                 # Is parent directory writable then
174                 filedir = dirname(ftochk)
175                 if not access(filedir, W_OK):
176                     self.log.critical('no write access to "%s" (%s)', filedir, op)
177                     ok = False
178             else:
179                 if not access(ftochk, W_OK):
180                     self.log.critical('no write access to "%s" (%s)', ftochk, op)
181                     ok = False
182         if not ok:
183             sys.exit(2)
184
185     def control_mod(self):
186         """
187         Controls conf file permissions.
188         """
189         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
190         self.log.debug('file permission is: %o', mode)
191         if mode & S_IRWXO or mode & S_IRWXG:
192             self.log.warning('File is readable by "other" and/or' +
193                              ' "group" (actual permission %o octal).' %
194                              mode)
195             self.log.warning('Consider setting permissions' +
196                              ' to 600 octal.')
197
198     def supersedes_config_with_cmd_line_options(self):
199         """Updates defaults settings with command line options"""
200         for sec in self.config.sections():
201             for opt in self.config.options(sec):
202                 if opt in list(self.startopt.keys()):
203                     self.config.set(sec, opt, str(self.startopt.get(opt)))
204         # honor MPD_HOST format as in mpc(1)  for command line option --host
205         if self.startopt.get('host'):
206             if '@' in self.startopt.get('host'):
207                 host, passwd = utils.parse_mpd_host(self.startopt.get('host'))
208                 if passwd:
209                     self.config.set('MPD', 'password', passwd)
210                 if host:
211                     self.config.set('MPD', 'host', host)
212
213     def use_envar(self):
214         """Use MPD en.var. to set defaults"""
215         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
216         if mpd_host:
217             self.log.info('Env. variable MPD_HOST set to "%s"', mpd_host)
218             self.config['MPD'].update(host=mpd_host)
219         if passwd:
220             self.log.info('Env. variable MPD_HOST contains password.')
221             self.config['MPD'].update(password=passwd)
222         if mpd_port:
223             self.log.info('Env. variable MPD_PORT set to "%s".', mpd_port)
224             self.config['MPD'].update(port=mpd_port)
225
226     def init_config(self):
227         """
228         Use XDG directory standard if exists
229         else use "HOME/(.config|.local/share)/sima/"
230         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
231         """
232
233         homedir = environ.get('HOME')
234
235         if environ.get('XDG_DATA_HOME'):
236             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
237         elif homedir and isdir(homedir) and homedir not in ['/']:
238             data_dir = join(homedir, '.local', 'share', DIRNAME)
239         else:
240             self.log.critical('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
241             self.log.critical('Please use "--var-dir" to set a proper location')
242             sys.exit(1)
243
244         if self.startopt.get('conf_file'):
245             # No need to handle conf file location
246             pass
247         elif environ.get('XDG_CONFIG_HOME'):
248             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
249             self.conf_file = join(conf_dir, CONF_FILE)
250         elif homedir and isdir(homedir) and homedir not in ['/']:
251             conf_dir = join(homedir, '.config', DIRNAME)
252             self.conf_file = join(conf_dir, CONF_FILE)
253         else:
254             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
255             self.log.critical('Please use "--config" to locate the conf file')
256             sys.exit(1)
257
258         # Sima sqlite DB
259         self.config['sima']['var_dir'] = join(data_dir)
260
261         # If no conf file present, uses defaults
262         if not isfile(self.conf_file):
263             return
264
265         self.log.info('Loading configuration from:  %s', self.conf_file)
266         self.control_mod()
267
268         try:
269             self.config.read(self.conf_file)
270         except Error as err:
271             self.log.error(err)
272             sys.exit(1)
273
274 # VIM MODLINE
275 # vim: ai ts=4 sw=4 sts=4 expandtab