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