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