]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Optional persistent cache for last.fm
[mpd-sima.git] / sima / utils / config.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009, 2010, 2011, 2013, 2014 Jack Kaliko <kaliko@azylum.org>
3 #
4 #  This file is part of sima
5 #
6 #  sima is free software: you can redistribute it and/or modify
7 #  it under the terms of the GNU General Public License as published by
8 #  the Free Software Foundation, either version 3 of the License, or
9 #  (at your option) any later version.
10 #
11 #  sima is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public License
17 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
18 #
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 sys
29
30 from configparser import Error
31 from os import (makedirs, environ, stat, chmod)
32 from os.path import (join, isdir, isfile)
33 from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
34
35 from . import utils
36
37 # DEFAULTS
38 DIRNAME = 'mpd_sima'
39 CONF_FILE = 'sima.cfg'
40
41 DEFAULT_CONF = {
42         'MPD': {
43             'host': "localhost",
44             #'password': "",
45             'port': 6600,
46             },
47         'sima': {
48             'internal': "Crop, Lastfm, RandomFallBack",
49             'contrib': "",
50             'user_db': "false",
51             'history_duration': 8,
52             'queue_length': 1,
53             'var_dir': 'empty',
54             },
55         'daemon':{
56             'daemon': False,
57             'pidfile': "",
58             },
59         'log': {
60             'verbosity': "info",
61             'logfile': "",
62             },
63         'crop': {
64             'consume': 10,
65             },
66         'echonest': {
67             'queue_mode': "track", #TODO control values
68             'max_art': 15,
69             'single_album': "false",
70             'track_to_add': 1,
71             'album_to_add': 1,
72             'depth': 1,
73             },
74         'lastfm': {
75             'queue_mode': "track", #TODO control values
76             'max_art': 10,
77             'single_album': "false",
78             'track_to_add': 1,
79             'album_to_add': 1,
80             'depth': 1,
81             'cache': True,
82             },
83         'randomfallback': {
84             'flavour': "sensible", # in pure, sensible
85             'track_to_add': 1,
86             }
87         }
88 #
89
90
91 class ConfMan(object):  # CONFIG MANAGER CLASS
92     """
93     Configuration manager.
94     Default configuration is stored in DEFAULT_CONF dictionnary.
95     First init_config() run to get config from file.
96     Then control_conf() is run and retrieve configuration from defaults if not
97     set in conf files.
98     These settings are then updated with command line options with
99     supersedes_config_with_cmd_line_options().
100
101     Order of priority for the origin of an option is then (lowest to highest):
102         * DEFAULT_CONF
103         * Env. Var for MPD host, port and password
104         * configuration file (overrides previous)
105         * command line options (overrides previous)
106     """
107
108     def __init__(self, logger, options=None):
109         self.log = logger
110         # options settings priority:
111         # defauts < env. var. < conf. file < command line
112         self.conf_file = options.get('conf_file')
113         self.config = configparser.ConfigParser(inline_comment_prefixes='#')
114         self.config.read_dict(DEFAULT_CONF)
115         # update DEFAULT_CONF with env. var.
116         self.use_envar()
117         self.startopt = options
118
119         ## INIT CALLS
120         self.init_config()
121         self.supersedes_config_with_cmd_line_options()
122
123     def get_pw(self):
124         try:
125             self.config.getboolean('MPD', 'password')
126             self.log.debug('No password set, proceeding without ' +
127                            'authentication...')
128             return None
129         except ValueError:
130             # ValueError if password not a boolean, hence an actual password.
131             pwd = self.config.get('MPD', 'password')
132             if not pwd:
133                 self.log.debug('Password set as an empty string.')
134                 return None
135             return pwd
136
137     def control_mod(self):
138         """
139         Controls conf file permissions.
140         """
141         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
142         self.log.debug('file permission is: %o' % mode)
143         if mode & S_IRWXO or mode & S_IRWXG:
144             self.log.warning('File is readable by "other" and/or' +
145                              ' "group" (actual permission %o octal).' %
146                              mode)
147             self.log.warning('Consider setting permissions' +
148                              ' to 600 octal.')
149
150     def supersedes_config_with_cmd_line_options(self):
151         """Updates defaults settings with command line options"""
152         for sec in self.config.sections():
153             for opt in self.config.options(sec):
154                 if opt in list(self.startopt.keys()):
155                     self.config.set(sec, opt, str(self.startopt.get(opt)))
156
157     def use_envar(self):
158         """Use MPD en.var. to set defaults"""
159         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
160         if mpd_host:
161             self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
162             self.config['MPD'].update(host=mpd_host)
163         if passwd:
164             self.log.info('Env. variable MPD_HOST contains password.')
165             self.config['MPD'].update(password=passwd)
166         if mpd_port:
167             self.log.info('Env. variable MPD_PORT set to "%s".' % mpd_port)
168             self.config['MPD'].update(port=mpd_port)
169
170     def init_config(self):
171         """
172         Use XDG directory standard if exists
173         else use "HOME/(.config|.local/share)/sima/"
174         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
175         """
176
177         homedir = environ.get('HOME')
178
179         if environ.get('XDG_DATA_HOME'):
180             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
181         elif homedir and isdir(homedir) and homedir not in ['/']:
182             data_dir = join(homedir, '.local', 'share', DIRNAME)
183         else:
184             self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
185             self.log.error('Please use "--var_dir" to set a proper location')
186             sys.exit(1)
187
188         if not isdir(data_dir):
189             makedirs(data_dir)
190             chmod(data_dir, 0o700)
191
192         if self.startopt.get('conf_file'):
193             # No need to handle conf file location
194             pass
195         elif environ.get('XDG_CONFIG_HOME'):
196             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
197         elif homedir and isdir(homedir) and homedir not in ['/']:
198             conf_dir = join(homedir, '.config', DIRNAME)
199             # Create conf_dir if necessary
200             if not isdir(conf_dir):
201                 makedirs(conf_dir)
202                 chmod(conf_dir, 0o700)
203             self.conf_file = join(conf_dir, CONF_FILE)
204         else:
205             self.log.error('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
206             self.log.error('Please use "--config" to locate the conf file')
207             sys.exit(1)
208
209         ## Sima sqlite DB
210         self.config['sima']['var_dir'] = join(data_dir)
211         self.config['sima']['db_file'] = join(data_dir, 'sima.db')
212
213         # If no conf file present, uses defaults
214         if not isfile(self.conf_file):
215             return
216
217         self.log.info('Loading configuration from:  %s' % self.conf_file)
218         self.control_mod()
219
220         try:
221             self.config.read(self.conf_file)
222         except Error as err:
223             self.log.error(err)
224             sys.exit(1)
225
226 # VIM MODLINE
227 # vim: ai ts=4 sw=4 sts=4 expandtab