]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Fixed crash when setting XDG_CONFIG_HOME (closes #50)
[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         # Controls files access
146         self.control_facc()
147         # set dbfile
148         self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
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.
162         """
163         ok = True
164         for op, ftochk in [('logfile', self.config.get('log', 'logfile')),
165                            ('pidfile', self.config.get('daemon', 'pidfile')),]:
166             if not ftochk:
167                 continue
168             if isdir(ftochk):
169                 self.log.critical('Need a file not a directory: "%s"', ftochk)
170                 ok = False
171             if not exists(ftochk):
172                 # Is parent directory writable then
173                 filedir = dirname(ftochk)
174                 if not access(filedir, W_OK):
175                     self.log.critical('no write access to "%s" (%s)', filedir, op)
176                     ok = False
177             else:
178                 if not access(ftochk, W_OK):
179                     self.log.critical('no write access to "%s" (%s)', ftochk, op)
180                     ok = False
181         if not ok:
182             if exists(self.conf_file):
183                 self.log.warning('Try to check the configuration file: %s', self.conf_file)
184             sys.exit(2)
185
186     def control_mod(self):
187         """
188         Controls conf file permissions.
189         """
190         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
191         self.log.debug('file permission is: %o', mode)
192         if mode & S_IRWXO or mode & S_IRWXG:
193             self.log.warning('File is readable by "other" and/or' +
194                              ' "group" (actual permission %o octal).' %
195                              mode)
196             self.log.warning('Consider setting permissions' +
197                              ' to 600 octal.')
198
199     def supersedes_config_with_cmd_line_options(self):
200         """Updates defaults settings with command line options"""
201         for sec in self.config.sections():
202             for opt in self.config.options(sec):
203                 if opt in list(self.startopt.keys()):
204                     self.config.set(sec, opt, str(self.startopt.get(opt)))
205         # honor MPD_HOST format as in mpc(1)  for command line option --host
206         if self.startopt.get('host'):
207             if '@' in self.startopt.get('host'):
208                 passwd, host = self.startopt.get('host').split('@')
209                 self.config.set('MPD', 'password', passwd)
210                 self.config.set('MPD', 'host', host)
211
212     def use_envar(self):
213         """Use MPD en.var. to set defaults"""
214         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
215         if mpd_host:
216             self.log.info('Env. variable MPD_HOST set to "%s"', mpd_host)
217             self.config['MPD'].update(host=mpd_host)
218         if passwd:
219             self.log.info('Env. variable MPD_HOST contains password.')
220             self.config['MPD'].update(password=passwd)
221         if mpd_port:
222             self.log.info('Env. variable MPD_PORT set to "%s".', mpd_port)
223             self.config['MPD'].update(port=mpd_port)
224
225     def init_config(self):
226         """
227         Use XDG directory standard if exists
228         else use "HOME/(.config|.local/share)/sima/"
229         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
230         """
231
232         homedir = environ.get('HOME')
233
234         if environ.get('XDG_DATA_HOME'):
235             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
236         elif homedir and isdir(homedir) and homedir not in ['/']:
237             data_dir = join(homedir, '.local', 'share', DIRNAME)
238         else:
239             self.log.critical('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
240             self.log.critical('Please use "--var-dir" to set a proper location')
241             sys.exit(1)
242
243         if self.startopt.get('conf_file'):
244             # No need to handle conf file location
245             pass
246         elif environ.get('XDG_CONFIG_HOME'):
247             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
248             self.conf_file = join(conf_dir, CONF_FILE)
249         elif homedir and isdir(homedir) and homedir not in ['/']:
250             conf_dir = join(homedir, '.config', DIRNAME)
251             self.conf_file = join(conf_dir, CONF_FILE)
252         else:
253             self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
254             self.log.critical('Please use "--config" to locate the conf file')
255             sys.exit(1)
256
257         # Sima sqlite DB
258         self.config['sima']['var_dir'] = join(data_dir)
259
260         # If no conf file present, uses defaults
261         if not isfile(self.conf_file):
262             return
263
264         self.log.info('Loading configuration from:  %s', self.conf_file)
265         self.control_mod()
266
267         try:
268             self.config.read(self.conf_file)
269         except Error as err:
270             self.log.error(err)
271             sys.exit(1)
272
273 # VIM MODLINE
274 # vim: ai ts=4 sw=4 sts=4 expandtab