--- /dev/null
+Design for python >= 3.3
+
+Requires python-musicpd:
+
+ http://media.kaliko.me/src/musicpd/
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+import logging
+##
+
+from sima import core
+from sima.plugins.crop import Crop
+from sima.lib.logger import set_logger
+from sima.utils.config import ConfMan
+from sima.utils.startopt import StartOpt
+from sima.utils.utils import exception_log
+##
+
+
def main():
- from logging import getLogger
- ##
- from sima import core
- from sima.plugins.crop import Crop
- from sima.lib.logger import set_logger
- ##
- set_logger(log_level='debug')
- logger = getLogger('sima')
- m = core.Sima()
- m.register_plugin(Crop)
+ """Entry point, deal w/ CLI and starts application
+ """
+ info = dict({'version': core.__version__,})
+ # StartOpt gathers options from command line call (in StartOpt().options)
+ sopt = StartOpt(info)
+ # set logger
+ set_logger(level='debug')
+ logger = logging.getLogger('sima')
+ cli_loglevel = getattr(logging,
+ sopt.options.get('verbosity', 'warning').upper())
+ logger.setLevel(cli_loglevel)
+ # loads configuration
+ conf_manager = ConfMan(logger, sopt.options)
+ config = conf_manager.config
+ logger.setLevel(getattr(logging,
+ config.get('log', 'verbosity').upper())) # pylint: disable=E1103
+
+ logger.debug('Command line say: {0}'.format(sopt.options))
+ logger.info('Starting...')
+ sima = core.Sima()
+ sima.register_plugin(Crop)
try:
- m.run()
+ sima.run()
except KeyboardInterrupt:
logger.info('Caught KeyboardInterrupt, stopping')
- m.shutdown()
+ sima.shutdown()
# Script starts here
if __name__ == '__main__':
- main()
+ # pylint: disable=broad-except
+ try:
+ main()
+ except Exception:
+ exception_log()
# VIM MODLINE
"""Dispatching callbacks to plugins
"""
self.log.debug(self.player.status())
+ self.log.info(self.player.current)
while 42:
# hanging here untill a monitored event is raised in the player
changed = self.player.monitor()
- self.log.debug(self.player.current)
if 'playlist' in changed:
self.foreach_plugin('callback_playlist')
if 'player' in changed:
- pass
-
+ self.log.info(self.player.current)
# VIM MODLINE
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009, 2010, 2011, 2013 Jack Kaliko <kaliko@azylum.org>
+#
+# This file is part of sima
+#
+# sima is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# sima is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with sima. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+"""
+Deal with configuration and data files.
+Parse configuration file and set defaults for missing options.
+"""
+
+# IMPORTS
+import configparser
+import sys
+
+from configparser import Error
+from os import (makedirs, environ, stat, chmod)
+from os.path import (join, isdir, isfile)
+from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
+
+from . import utils
+
+# DEFAULTS
+DIRNAME = 'mpd_sima'
+CONF_FILE = 'mpd_sima.cfg'
+
+DEFAULT_CONF = {
+ 'MPD': {
+ 'host': "localhost",
+ 'password': "false",
+ 'port': "6600"},
+ 'sima': {
+ 'similarity': "15",
+ 'dynamic': "10",
+ 'queue_mode': "track", #TODO control values
+ 'user_db': "false",
+ 'history_duration': "8",
+ 'queue_length': "1",
+ 'track_to_add': "1",
+ 'album_to_add': "1",
+ 'consume': "0",
+ 'single_album': "false",
+ 'check_new_version':"false",},
+ 'daemon':{
+ 'daemon': "false",
+ 'pidfile': "",},
+ 'log': {
+ 'verbosity': "info"}}
+#
+
+
+class ConfMan(object):#CONFIG MANAGER CLASS
+ """
+ Configuration manager.
+ Default configuration is stored in DEFAULT_CONF dictionnary.
+ First init_config() run to get config from file.
+ Then control_conf() is run and retrieve configuration from defaults if not
+ set in conf files.
+ These settings are then updated with command line options with
+ supersedes_config_with_cmd_line_options().
+
+ Order of priority for the origin of an option is then (lowest to highest):
+ * DEFAULT_CONF
+ * Env. Var for MPD host, port and password
+ * configuration file (overrides previous)
+ * command line options (overrides previous)
+ """
+
+ def __init__(self, logger, options=None):
+ # options settings priority:
+ # defauts < conf. file < command line
+ self.conf_file = options.get('conf_file')
+ self.config = None
+ self.defaults = dict(DEFAULT_CONF)
+ self.startopt = options
+ ## Sima sqlite DB
+ self.userdb_file = None
+
+ self.log = logger
+ ## INIT CALLS
+ self.use_envar()
+ self.init_config()
+ self.control_conf()
+ self.supersedes_config_with_cmd_line_options()
+
+ def get_pw(self):
+ try:
+ self.config.getboolean('MPD', 'password')
+ self.log.debug('No password set, proceeding without ' +
+ 'authentication...')
+ return None
+ except ValueError:
+ # ValueError if password not a boolean, hence an actual password.
+ pw = self.config.get('MPD', 'password')
+ if not pw:
+ self.log.debug('Password set as an empty string.')
+ return None
+ return pw
+
+ def control_mod(self):
+ """
+ Controls conf file permissions.
+ """
+ mode = S_IMODE(stat(self.conf_file)[ST_MODE])
+ self.log.debug('file permision is: %o' % mode)
+ if mode & S_IRWXO or mode & S_IRWXG:
+ self.log.warning('File is readable by "other" and/or' +
+ ' "group" (actual permission %o octal).' %
+ mode)
+ self.log.warning('Consider setting permissions' +
+ ' to 600 octal.')
+
+ def supersedes_config_with_cmd_line_options(self):
+ """Updates defaults settings with command line options"""
+ for sec in self.config.sections():
+ for opt in self.config.options(sec):
+ if opt in list(self.startopt.keys()):
+ self.config.set(sec, opt, str(self.startopt.get(opt)))
+
+ def use_envar(self):
+ """Use MPD en.var. to set defaults"""
+ mpd_host, mpd_port, passwd = utils.get_mpd_environ()
+ if mpd_host:
+ self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
+ self.defaults['MPD']['host'] = mpd_host
+ if passwd:
+ self.log.info('Env. variable MPD_HOST contains password.')
+ self.defaults['MPD']['password'] = passwd
+ if mpd_port:
+ self.log.info('Env. variable MPD_PORT set to "%s".'
+ % mpd_port)
+ self.defaults['MPD']['port'] = mpd_port
+
+ def control_conf(self):
+ """Get through options/values and set defaults if not in conf file."""
+ # Control presence of obsolete settings
+ for option in ['history', 'history_length', 'top_tracks']:
+ if self.config.has_option('sima', option):
+ self.log.warning('Obsolete setting found in conf file: "%s"'
+ % option)
+ # Setting default if not specified
+ for section in DEFAULT_CONF.keys():
+ if section not in self.config.sections():
+ self.log.debug('[%s] NOT in conf file' % section)
+ self.config.add_section(section)
+ for option in self.defaults[section]:
+ self.config.set(section,
+ option,
+ self.defaults[section][option])
+ self.log.debug(
+ 'Setting option with default value: %s = %s' %
+ (option, self.defaults[section][option]))
+ elif section in self.config.sections():
+ self.log.debug('[%s] present in conf file' % section)
+ for option in self.defaults[section]:
+ if self.config.has_option(section, option):
+ #self.log.debug(u'option "%s" set to "%s" in conf. file' %
+ # (option, self.config.get(section, option)))
+ pass
+ else:
+ self.log.debug(
+ 'Option "%s" missing in section "%s"' %
+ (option, section))
+ self.log.debug('=> setting default "%s" (may not suit you…)' %
+ self.defaults[section][option])
+ self.config.set(section, option,
+ self.defaults[section][option])
+
+ def init_config(self):
+ """
+ Use XDG directory standard if exists
+ else use "HOME/(.config|.local/share)/mpd_sima/"
+ http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
+ """
+
+ homedir = environ.get('HOME')
+
+ if environ.get('XDG_DATA_HOME'):
+ data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
+ elif self.startopt.get('var_dir'):
+ # If var folder is provided via CLI set data_dir accordingly
+ data_dir = join(self.startopt.get('var_dir'))
+ elif (homedir and isdir(homedir) and homedir not in ['/']):
+ data_dir = join(homedir, '.local', 'share', DIRNAME)
+ else:
+ self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
+ self.log.error('Please use "--var_dir" to set a proper location')
+ sys.exit(1)
+
+ if not isdir(data_dir):
+ makedirs(data_dir)
+ chmod(data_dir, 0o700)
+
+ if self.startopt.get('conf_file'):
+ # No need to handle conf file location
+ pass
+ elif environ.get('XDG_CONFIG_HOME'):
+ conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
+ elif (homedir and isdir(homedir) and homedir not in ['/']):
+ conf_dir = join(homedir, '.config', DIRNAME)
+ # Create conf_dir if necessary
+ if not isdir(conf_dir):
+ makedirs(conf_dir)
+ chmod(conf_dir, 0o700)
+ self.conf_file = join(conf_dir, CONF_FILE)
+ else:
+ self.log.error('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
+ self.log.error('Please use "--config" to locate the conf file')
+ sys.exit(1)
+
+ self.userdb_file = join(data_dir, 'sima.db')
+
+ config = configparser.SafeConfigParser()
+
+ # If no conf file present, uses defaults
+ if not isfile(self.conf_file):
+ self.config = config
+ return
+
+ self.log.info('Loading configuration from: %s' % self.conf_file)
+ self.control_mod()
+
+ try:
+ config.read(self.conf_file)
+ except Error as err:
+ self.log.error(err)
+ sys.exit(1)
+
+ self.config = config
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2009, 2010, 2011, 2012, 2013 Jack Kaliko <kaliko@azylum.org>
+#
+# This file is part of sima
+#
+# sima is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# sima is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with sima. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+from argparse import (ArgumentParser, SUPPRESS)
+
+
+from .utils import Obsolete, Wfile, Rfile, Wdir
+
+USAGE = """USAGE: %prog [--help] [options]"""
+DESCRIPTION = """
+sima automagicaly queue new tracks in MPD playlist.
+All command line options will override their equivalent in configuration
+file.
+"""
+
+
+def clean_dict(to_clean):
+ """Remove items which values are set to None/False"""
+ for k in list(to_clean.keys()):
+ if not to_clean.get(k):
+ to_clean.pop(k)
+
+
+# OPTIONS LIST
+# pop out 'sw' value before creating Parser object.
+# PAY ATTENTION:
+# If an option has to override its dual in conf file, the destination
+# identifier "dest" is to be named after that option in the conf file.
+# The supersedes_config_with_cmd_line_options method in ConfMan() (config.py)
+# is looking for command line option names identical to config file option
+# name it is meant to override.
+OPTS = [
+ {
+ 'sw':['-l', '--log'],
+ 'type': str,
+ 'dest': 'logfile',
+ 'action': Wfile,
+ 'help': 'file to log message to, default is stdout/stderr'},
+ {
+ 'sw':['-v', '--log-level'],
+ 'type': str,
+ 'dest': 'verbosity',
+ 'choices': ['debug', 'info', 'warning', 'error'],
+ 'help': 'file to log message to, default is stdout/stderr'},
+ {
+ 'sw': ['-p', '--pid'],
+ 'dest': 'pidfile',
+ 'action': Wfile,
+ 'help': 'file to save PID to, default is not to store pid'},
+ {
+ 'sw': ['-d', '--daemon'],
+ 'dest': 'daemon',
+ 'action': 'store_true',
+ 'help': 'Daemonize process.'},
+ {
+ 'sw': ['-S', '--host'],
+ 'dest': 'host',
+ 'help': 'Host MPD in running on (IP or FQDN)'},
+ {
+ 'sw': ['-P', '--port'],
+ 'type': int,
+ 'dest': 'port',
+ 'help': 'Port MPD in listening on'},
+ {
+ 'sw':['-c', '--config'],
+ 'dest': 'conf_file',
+ 'action': Rfile,
+ 'help': 'Configuration file to load'},
+ {
+ 'sw':['--var_dir'],
+ 'dest': 'var_dir',
+ 'action': Wdir,
+ 'help': 'Directory to store var content (ie. database)'},
+ {
+ 'sw': ['--create-db'],
+ 'action': 'store_true',
+ 'dest': 'create_db',
+ 'help': '''Create database and exit, use destination
+ specified in --var_dir or standard location.'''},
+ {
+ 'sw':['--queue-mode', '-q'],
+ 'dest': 'queue_mode',
+ 'choices': ['track', 'top', 'album'],
+ #'help': 'Queue mode in [track, top, album]',
+ 'help': SUPPRESS, },
+ {
+ 'sw':['--purge_history'],
+ 'action': 'store_true',
+ 'dest': 'do_purge_history',
+ 'help': SUPPRESS},
+]
+
+
+class StartOpt(object):
+ """Command line management.
+ """
+
+ def __init__(self, script_info,):
+ self.info = dict(script_info)
+ self.options = dict()
+ self.main()
+
+ def declare_opts(self):
+ """
+ Declare options in ArgumentParser object.
+ """
+ self.parser = ArgumentParser(description=DESCRIPTION,
+ usage='%(prog)s [options]',
+ prog='mpd_sima',
+ epilog='Happy Listening',
+ )
+ self.parser.add_argument('--version', action='version',
+ version='%(prog)s {version}'.format(**self.info))
+ # Add all options declare in OPTS
+ for opt in OPTS:
+ opt_names = opt.pop('sw')
+ self.parser.add_argument(*opt_names, **opt)
+
+ def main(self):
+ """
+ Look for env. var and parse command line.
+ """
+ self.declare_opts()
+ options = vars(self.parser.parse_args())
+ # Set log file to os.devnull in daemon mode to avoid logging to
+ # std(out|err).
+ # TODO: Probably useless. To be checked
+ #if options.__dict__.get('daemon', False) and \
+ # not options.__dict__.get('logfile', False):
+ # options.__dict__['logfile'] = devnull
+ self.options.update(options)
+ clean_dict(self.options)
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2010, 2011, 2013 Jack Kaliko <kaliko@azylum.org>
+#
+# This file is part of sima
+#
+# sima is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# sima is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with sima. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+"""generic tools and utilitaries for sima
+"""
+
+import traceback
+import sys
+
+from argparse import (ArgumentError, Action)
+from os import (environ, access, getcwd, W_OK, R_OK)
+from os.path import (dirname, isabs, join, normpath, exists, isdir, isfile)
+
+def get_mpd_environ():
+ """
+ Retrieve MPD env. var.
+ """
+ passwd = host = None
+ mpd_host_env = environ.get('MPD_HOST')
+ if mpd_host_env:
+ # If password is set:
+ # mpd_host_env = ['pass', 'host'] because MPD_HOST=pass@host
+ mpd_host_env = mpd_host_env.split('@')
+ mpd_host_env.reverse()
+ host = mpd_host_env[0]
+ if len(mpd_host_env) > 1 and mpd_host_env[1]:
+ passwd = mpd_host_env[1]
+ return (host, environ.get('MPD_PORT', None), passwd)
+
+def normalize_path(path):
+ """Get absolute path
+ """
+ if not isabs(path):
+ return normpath(join(getcwd(), path))
+ return path
+
+def exception_log():
+ """Log unknown exceptions"""
+ import logging
+ log = logging.getLogger('sima')
+ log.error('Unhandled Exception!!!')
+ log.error(''.join(traceback.format_exc()))
+ log.info('Please report the previous message'
+ ' along with some log entries right before the crash.')
+ log.info('thanks for your help :)')
+ log.info('Quiting now!')
+ sys.exit(1)
+
+
+# ArgParse Callbacks
+class Obsolete(Action):
+ # pylint: disable=R0903
+ """Deal with obsolete arguments
+ """
+ def __call__(self, parser, namespace, values, option_string=None):
+ raise ArgumentError(self, 'obsolete argument')
+
+class FileAction(Action):
+ """Generic class to inherit from for ARgPArse action on file/dir
+ """
+ # pylint: disable=R0903
+ def __call__(self, parser, namespace, values, option_string=None):
+ self._file = normalize_path(values)
+ self._dir = dirname(self._file)
+ self.parser = parser
+ self.checks()
+ setattr(namespace, self.dest, self._file)
+
+ def checks(self):
+ """control method
+ """
+ pass
+
+class Wfile(FileAction):
+ # pylint: disable=R0903
+ """Is file writable
+ """
+ def checks(self):
+ if not exists(self._dir):
+ #raise ArgumentError(self, '"{0}" does not exist'.format(self._dir))
+ self.parser.error('file does not exist: {0}'.format(self._dir))
+ if not exists(self._file):
+ # Is parent directory writable then
+ if not access(self._dir, W_OK):
+ self.parser.error('no write access to "{0}"'.format(self._dir))
+ else:
+ if not access(self._file, W_OK):
+ self.parser.error('no write access to "{0}"'.format(self._file))
+
+class Rfile(FileAction):
+ # pylint: disable=R0903
+ """Is file readable
+ """
+ def checks(self):
+ if not exists(self._file):
+ self.parser.error('file does not exist: {0}'.format(self._file))
+ if not isfile(self._file):
+ self.parser.error('not a file: {0}'.format(self._file))
+ if not access(self._file, R_OK):
+ self.parser.error('no read access to "{0}"'.format(self._file))
+
+class Wdir(FileAction):
+ # pylint: disable=R0903
+ """Is directory writable
+ """
+ def checks(self):
+ if not exists(self._file):
+ self.parser.error('directory does not exist: {0}'.format(self._file))
+ if not isdir(self._file):
+ self.parser.error('not a directory: {0}'.format(self._file))
+ if not access(self._file, W_OK):
+ self.parser.error('no write access to "{0}"'.format(self._file))
+
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab