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