diff --git a/.gitignore b/.gitignore
index b40dfdcc..8bacef32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,13 @@
+[Tt]est[Rr]esult*
+/cache
+/logs
+.project
+.pydevproject
+
+# coverage generated:
+/cover-html/
+.coverage
+.coveralls.yml
# Compiled source #
###################
@@ -61,9 +71,4 @@ Thumbs.db
obj/
[Rr]elease*/
_ReSharper*/
-[Tt]est[Rr]esult*
-/cache
-/logs
-.project
-.pydevproject
-.vscode
\ No newline at end of file
+.vscode
diff --git a/.travis.yml b/.travis.yml
index e6bb2338..165f65e3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,14 +13,25 @@ cache:
# http://about.travis-ci.org/docs/user/ci-environment/#Python-VM-images
python:
- "2.6"
- - "2.7"
+matrix:
+ include:
+ - python: "2.7"
+ env: SENDCOVERAGE=1
+
# pylint 1.4 does not run under python 2.6
install:
- - pip install pyOpenSSL
- - pip install pylint==1.3.1
- - pip install pyflakes
- - pip install pep8
+ - pip install pyOpenSSL
+ - pip install pylint==1.3.1
+ - pip install pyflakes
+ - pip install pep8
+ # coverage stuff:
+ - pip install coveralls
+ - pip install coverage
script:
- pep8 headphones
- pyflakes headphones
- - nosetests headphones
+ - nosetests
+
+after_success:
+ # coverage stuff:
+ - if [ $SENDCOVERAGE ]; then coveralls; fi
diff --git a/Headphones.py b/Headphones.py
index 0d31534e..dc956a43 100755
--- a/Headphones.py
+++ b/Headphones.py
@@ -152,7 +152,10 @@ def main():
headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db')
# Read config and start logging
- headphones.initialize(config_file)
+ try:
+ headphones.initialize(config_file)
+ except headphones.exceptions.SoftChrootError as e:
+ raise SystemExit('FATAL ERROR')
if headphones.DAEMON:
headphones.daemonize()
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index a1313d58..05109089 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -1095,7 +1095,7 @@
- Plex Token (for use with Plex Home)
+ Plex Token (for use with Plex Home)
diff --git a/headphones/__init__.py b/headphones/__init__.py
index eb48476d..92b274a9 100644
--- a/headphones/__init__.py
+++ b/headphones/__init__.py
@@ -29,7 +29,8 @@ from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from headphones import versioncheck, logger
import headphones.config
-
+from headphones.softchroot import SoftChroot
+import headphones.exceptions
# (append new extras to the end)
POSSIBLE_EXTRAS = [
@@ -74,6 +75,7 @@ started = False
DATA_DIR = None
CONFIG = None
+SOFT_CHROOT = None
DB_FILE = None
@@ -92,11 +94,11 @@ MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
UMASK = None
-
def initialize(config_file):
with INIT_LOCK:
global CONFIG
+ global SOFT_CHROOT
global _INITIALIZED
global CURRENT_VERSION
global LATEST_VERSION
@@ -136,6 +138,14 @@ def initialize(config_file):
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
verbose=VERBOSE)
+ try:
+ SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT))
+ if SOFT_CHROOT.isEnabled():
+ logger.info("Soft-chroot enabled for dir: %s", str(CONFIG.SOFT_CHROOT))
+ except exceptions.SoftChrootError as e:
+ logger.error("SoftChroot error: %s", e)
+ raise e
+
if not CONFIG.CACHE_DIR:
# Put the cache dir in the data dir for now
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py
new file mode 100644
index 00000000..f18b11e3
--- /dev/null
+++ b/headphones/albumart_test.py
@@ -0,0 +1,13 @@
+#import unittest
+#import mock
+from headphones.unittestcompat import TestCase
+
+import headphones.albumart
+
+# no tests...
+class AlbumArtTest(TestCase):
+ def test_nothing(self):
+ x = 100 - 2 * 50
+ if x:
+ headphones.albumart.getAlbumArt('asdf')
+ self.assertTrue(True)
diff --git a/headphones/config.py b/headphones/config.py
index 8362b934..97675305 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -15,6 +15,19 @@ def bool_int(value):
value = 0
return int(bool(value))
+class path(str):
+ """Internal 'marker' type for paths in config."""
+
+ @staticmethod
+ def __call__(val):
+ return path(val)
+
+ def __new__(cls, *args, **kw):
+ hstr = str.__new__(cls, *args, **kw)
+ return hstr
+
+ def __repr__(self):
+ return 'headphones.config.path(%s)' % self
_CONFIG_DEFINITIONS = {
'ADD_ALBUM_ART': (int, 'General', 0),
@@ -31,11 +44,11 @@ _CONFIG_DEFINITIONS = {
'AUTO_ADD_ARTISTS': (int, 'General', 1),
'BITRATE': (int, 'General', 192),
'BLACKHOLE': (int, 'General', 0),
- 'BLACKHOLE_DIR': (str, 'General', ''),
+ 'BLACKHOLE_DIR': (path, 'General', ''),
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
'BOXCAR_ONSNATCH': (int, 'Boxcar', 0),
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
- 'CACHE_DIR': (str, 'General', ''),
+ 'CACHE_DIR': (path, 'General', ''),
'CACHE_SIZEMB': (int, 'Advanced', 32),
'CHECK_GITHUB': (int, 'General', 1),
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
@@ -44,8 +57,8 @@ _CONFIG_DEFINITIONS = {
'CONFIG_VERSION': (str, 'General', '0'),
'CORRECT_METADATA': (int, 'General', 0),
'CUE_SPLIT': (int, 'General', 1),
- 'CUE_SPLIT_FLAC_PATH': (str, 'General', ''),
- 'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''),
+ 'CUE_SPLIT_FLAC_PATH': (path, 'General', ''),
+ 'CUE_SPLIT_SHNTOOL_PATH': (path, 'General', ''),
'CUSTOMAUTH': (int, 'General', 0),
'CUSTOMHOST': (str, 'General', 'localhost'),
'CUSTOMPASS': (str, 'General', ''),
@@ -53,12 +66,12 @@ _CONFIG_DEFINITIONS = {
'CUSTOMSLEEP': (int, 'General', 1),
'CUSTOMUSER': (str, 'General', ''),
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
- 'DESTINATION_DIR': (str, 'General', ''),
+ 'DESTINATION_DIR': (path, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0),
'DO_NOT_PROCESS_UNMATCHED': (int, 'General', 0),
- 'DOWNLOAD_DIR': (str, 'General', ''),
+ 'DOWNLOAD_DIR': (path, 'General', ''),
'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5),
- 'DOWNLOAD_TORRENT_DIR': (str, 'General', ''),
+ 'DOWNLOAD_TORRENT_DIR': (path, 'General', ''),
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
'EMAIL_ENABLED': (int, 'Email', 0),
'EMAIL_FROM': (str, 'Email', ''),
@@ -74,14 +87,14 @@ _CONFIG_DEFINITIONS = {
'EMBED_LYRICS': (int, 'General', 0),
'ENABLE_HTTPS': (int, 'General', 0),
'ENCODER': (str, 'General', 'ffmpeg'),
- 'ENCODERFOLDER': (str, 'General', ''),
+ 'ENCODERFOLDER': (path, 'General', ''),
'ENCODERLOSSLESS': (int, 'General', 1),
'ENCODEROUTPUTFORMAT': (str, 'General', 'mp3'),
'ENCODERQUALITY': (int, 'General', 2),
'ENCODERVBRCBR': (str, 'General', 'cbr'),
'ENCODER_MULTICORE': (int, 'General', 0),
'ENCODER_MULTICORE_COUNT': (int, 'General', 0),
- 'ENCODER_PATH': (str, 'General', ''),
+ 'ENCODER_PATH': (path, 'General', ''),
'EXTRAS': (str, 'General', ''),
'EXTRA_NEWZNABS': (list, 'Newznab', ''),
'EXTRA_TORZNABS': (list, 'Torznab', ''),
@@ -94,7 +107,7 @@ _CONFIG_DEFINITIONS = {
'FOLDER_PERMISSIONS': (str, 'General', '0755'),
'FREEZE_DB': (int, 'General', 0),
'GIT_BRANCH': (str, 'General', 'master'),
- 'GIT_PATH': (str, 'General', ''),
+ 'GIT_PATH': (path, 'General', ''),
'GIT_USER': (str, 'General', 'rembo10'),
'GROWL_ENABLED': (int, 'Growl', 0),
'GROWL_HOST': (str, 'Growl', ''),
@@ -103,8 +116,8 @@ _CONFIG_DEFINITIONS = {
'HEADPHONES_INDEXER': (bool_int, 'General', False),
'HPPASS': (str, 'General', ''),
'HPUSER': (str, 'General', ''),
- 'HTTPS_CERT': (str, 'General', ''),
- 'HTTPS_KEY': (str, 'General', ''),
+ 'HTTPS_CERT': (path, 'General', ''),
+ 'HTTPS_KEY': (path, 'General', ''),
'HTTP_HOST': (str, 'General', 'localhost'),
'HTTP_PASSWORD': (str, 'General', ''),
'HTTP_PORT': (int, 'General', 8181),
@@ -114,8 +127,8 @@ _CONFIG_DEFINITIONS = {
'IDTAG': (int, 'Beets', 0),
'IGNORE_CLEAN_RELEASES': (int, 'General', 0),
'IGNORED_WORDS': (str, 'General', ''),
- 'IGNORED_FOLDERS': (list, 'Advanced', []),
- 'IGNORED_FILES': (list, 'Advanced', []),
+ 'IGNORED_FOLDERS': (list, 'Advanced', []), # path
+ 'IGNORED_FILES': (list, 'Advanced', []), # path
'INCLUDE_EXTRAS': (int, 'General', 0),
'INTERFACE': (str, 'General', 'default'),
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
@@ -130,17 +143,17 @@ _CONFIG_DEFINITIONS = {
'LIBRARYSCAN_INTERVAL': (int, 'General', 300),
'LMS_ENABLED': (int, 'LMS', 0),
'LMS_HOST': (str, 'LMS', ''),
- 'LOG_DIR': (str, 'General', ''),
+ 'LOG_DIR': (path, 'General', ''),
'LOSSLESS_BITRATE_FROM': (int, 'General', 0),
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
- 'LOSSLESS_DESTINATION_DIR': (str, 'General', ''),
+ 'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
'MB_IGNORE_AGE': (int, 'General', 365),
'MININOVA': (int, 'Mininova', 0),
'MININOVA_RATIO': (str, 'Mininova', ''),
'MIRROR': (str, 'General', 'musicbrainz.org'),
'MOVE_FILES': (int, 'General', 0),
'MPC_ENABLED': (bool_int, 'MPC', False),
- 'MUSIC_DIR': (str, 'General', ''),
+ 'MUSIC_DIR': (path, 'General', ''),
'MUSIC_ENCODER': (int, 'General', 0),
'NEWZNAB': (int, 'Newznab', 0),
'NEWZNAB_APIKEY': (str, 'Newznab', ''),
@@ -223,6 +236,7 @@ _CONFIG_DEFINITIONS = {
'SAB_USERNAME': (str, 'SABnzbd', ''),
'SAMPLINGFREQUENCY': (int, 'General', 44100),
'SEARCH_INTERVAL': (int, 'General', 1440),
+ 'SOFT_CHROOT': (path, 'General', ''),
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
'SONGKICK_ENABLED': (int, 'Songkick', 1),
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
@@ -234,7 +248,7 @@ _CONFIG_DEFINITIONS = {
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
'SUBSONIC_USERNAME': (str, 'Subsonic', ''),
'SYNOINDEX_ENABLED': (int, 'Synoindex', 0),
- 'TORRENTBLACKHOLE_DIR': (str, 'General', ''),
+ 'TORRENTBLACKHOLE_DIR': (path, 'General', ''),
'TORRENT_DOWNLOADER': (int, 'General', 0),
'TORRENT_REMOVAL_INTERVAL': (int, 'General', 720),
'TORZNAB': (int, 'Torznab', 0),
@@ -295,7 +309,7 @@ class Config(object):
definition = _CONFIG_DEFINITIONS[key]
if len(definition) == 3:
definition_type, section, default = definition
- else:
+ elif len(definition) == 4:
definition_type, section, _, default = definition
return key, definition_type, section, ini_key, default
diff --git a/headphones/exceptions.py b/headphones/exceptions.py
index 5d0ddf52..107535c5 100644
--- a/headphones/exceptions.py
+++ b/headphones/exceptions.py
@@ -24,3 +24,9 @@ class NewzbinAPIThrottled(HeadphonesException):
"""
Newzbin has throttled us, deal with it
"""
+
+class SoftChrootError(HeadphonesException):
+ """
+ Fatal errors in SoftChroot module
+ """
+ pass
diff --git a/headphones/softchroot.py b/headphones/softchroot.py
new file mode 100644
index 00000000..80878548
--- /dev/null
+++ b/headphones/softchroot.py
@@ -0,0 +1,70 @@
+import os
+from headphones.exceptions import SoftChrootError
+
+class SoftChroot(object):
+ """ SoftChroot provides SOFT chrooting for UI
+
+ IMPORTANT: call methods of this class just in modules, which generates data for client UI. Try to avoid unnecessary usage.
+ """
+
+ enabled = False
+ chroot = None
+
+ def __init__(self, path):
+ if not path:
+ #disabled
+ return
+
+ path = path.strip()
+ if not path:
+ return
+
+ if (not os.path.exists(path) or
+ not os.path.isdir(path)):
+ raise SoftChrootError('No such directory: %s' % path)
+
+ path = path.rstrip(os.path.sep) + os.path.sep
+
+ self.enabled = True
+ self.chroot = path
+
+ def isEnabled(self):
+ return self.enabled
+
+ def getRoot(self):
+ return self.chroot
+
+ def apply(self, path):
+ if not self.enabled:
+ return path
+
+ if not path:
+ return path
+
+ p = path.strip()
+ if not p:
+ return path
+
+ if path.startswith(self.chroot):
+ p = os.path.sep + path[len(self.chroot):]
+ else:
+ p = os.path.sep
+
+ return p
+
+ def revoke(self, path):
+ if not self.enabled:
+ return path
+
+ if not path:
+ return path
+
+ p = path.strip()
+ if not p:
+ return path
+
+ if os.path.sep == p[0]:
+ p = p[1:]
+
+ p = self.chroot + p
+ return p
diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py
new file mode 100644
index 00000000..baa56703
--- /dev/null
+++ b/headphones/softchroot_test.py
@@ -0,0 +1,121 @@
+import os
+import mock
+from headphones.unittestcompat import TestCase, TestArgs
+#from mock import MagicMock
+
+from headphones.softchroot import SoftChroot
+from headphones.exceptions import SoftChrootError
+
+class SoftChrootTest(TestCase):
+ def test_create(self):
+ """ create headphones.SoftChroot """
+
+ cf = SoftChroot('/tmp/')
+ self.assertIsInstance(cf, SoftChroot)
+ self.assertTrue(cf.isEnabled())
+ self.assertEqual(cf.getRoot(), '/tmp/')
+
+ @TestArgs(
+ (None),
+ (''),
+ (' '),
+ )
+ def test_create_disabled(self, empty_path):
+ """ create DISABLED SoftChroot """
+
+ cf = SoftChroot(empty_path)
+ self.assertIsInstance(cf, SoftChroot)
+ self.assertFalse(cf.isEnabled())
+ self.assertIsNone(cf.getRoot())
+
+ def test_create_on_not_exists_dir(self):
+ """ create SoftChroot on non existent dir """
+
+ path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss')
+
+ cf = None
+ with self.assertRaises(SoftChrootError) as exc:
+ cf = SoftChroot(path)
+ self.assertIsNone(cf)
+
+ self.assertRegexpMatches(str(exc.exception), r'No such directory')
+ self.assertRegexpMatches(str(exc.exception), path)
+
+ @mock.patch('headphones.softchroot.os', wrap=os, name='OsMock')
+ def test_create_on_file(self, os_mock):
+ """ create SoftChroot on file, not a directory """
+
+ path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss')
+
+ os_mock.path.sep = os.path.sep
+ os_mock.path.isdir.side_effect = lambda x: x != path
+
+ cf = None
+ with self.assertRaises(SoftChrootError) as exc:
+ cf = SoftChroot(path)
+ self.assertIsNone(cf)
+
+ self.assertTrue(os_mock.path.isdir.called)
+
+ self.assertRegexpMatches(str(exc.exception), r'No such directory')
+ self.assertRegexpMatches(str(exc.exception), path)
+
+ @TestArgs(
+ (None, None),
+ ('', ''),
+ (' ', ' '),
+ ('/tmp/', '/'),
+ ('/tmp/asdf', '/asdf'),
+ )
+ def test_apply(self, p, e):
+ """ apply SoftChroot """
+ sc = SoftChroot('/tmp/')
+ a = sc.apply(p)
+ self.assertEqual(a, e)
+
+ @TestArgs(
+ ('/'),
+ ('/nonch/path/asdf'),
+ ('tmp/asdf'),
+ )
+ def test_apply_out_of_root(self, p):
+ """ apply SoftChroot to paths outside of the chroot """
+ sc = SoftChroot('/tmp/')
+ a = sc.apply(p)
+ self.assertEqual(a, '/')
+
+ @TestArgs(
+ (None, None),
+ ('', ''),
+ (' ', ' '),
+ ('/', '/tmp/'),
+ ('/asdf', '/tmp/asdf'),
+ ('/asdf/', '/tmp/asdf/'),
+ ('localdir/adf', '/tmp/localdir/adf'),
+ ('localdir/adf/', '/tmp/localdir/adf/'),
+ )
+ def test_revoke(self, p, e):
+ """ revoke SoftChroot """
+ sc = SoftChroot('/tmp/')
+ a = sc.revoke(p)
+ self.assertEqual(a, e)
+
+ @TestArgs(
+ (None),
+ (''),
+ (' '),
+ ('/tmp'),
+ ('/tmp/'),
+ ('/tmp/asdf'),
+ ('/tmp/localdir/adf'),
+ ('localdir/adf'),
+ ('localdir/adf/'),
+ )
+ def test_actions_on_disabled(self, p):
+ """ disabled SoftChroot should not change args on apply and revoke """
+ sc = SoftChroot(None)
+ a = sc.apply(p)
+ self.assertEqual(a, p)
+
+ r = sc.revoke(p)
+ self.assertEqual(r, p)
diff --git a/headphones/unittestcompat.py b/headphones/unittestcompat.py
new file mode 100644
index 00000000..8eb441f1
--- /dev/null
+++ b/headphones/unittestcompat.py
@@ -0,0 +1,90 @@
+import sys
+from unittest import TestCase as TC
+
+def _is26():
+ if sys.version_info[0] == 2 and sys.version_info[1] == 6:
+ return True
+ return False
+
+_dummy = _is26()
+
+def _d(f):
+ def decorate(self, *args, **kw):
+ if _dummy:
+ return self.assertTrue(True)
+ return f(self, *args, **kw)
+ return decorate
+
+class TestCase(TC):
+ """
+ Wrapper for python 2.6 stubs
+ """
+
+ def assertIsInstance(self, obj, cls, msg=None):
+ if not _dummy:
+ return super(TestCase, self).assertIsInstance(obj, cls, msg)
+ tst = isinstance(obj, cls)
+ return self.assertTrue(tst, msg)
+
+ @_d
+ def assertNotIsInstance(self, *args, **kw):
+ return super(TestCase, self).assertNotIsInstance(*args, **kw)
+
+ @_d
+ def assertIn(self, *args, **kw):
+ return super(TestCase, self).assertIn(*args, **kw)
+
+ @_d
+ def assertRegexpMatches(self, *args, **kw):
+ return super(TestCase, self).assertRegexpMatches(*args, **kw)
+
+ def assertIsNone(self, val, msg=None):
+ if not _dummy:
+ return super(TestCase, self).assertIsNone(val, msg)
+ tst = val is None
+ return super(TestCase, self).assertTrue(tst, msg)
+
+ def assertIsNotNone(self, val, msg=None):
+ if not _dummy:
+ return super(TestCase, self).assertIsNotNone(val, msg)
+ tst = val is not None
+ return super(TestCase, self).assertTrue(tst, msg)
+
+ class _TestCaseRaiseStub:
+ def __init__(self, exc, tc):
+ self.exc = exc
+ self.tc = tc
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, tp, value, traceback):
+ tst = tp is self.exc
+ self.tc.assertTrue(tst)
+ self.exception = value
+ return True
+
+ def assertRaises(self, exc, msg=None):
+ if not _dummy:
+ return super(TestCase, self).assertRaises(exc, msg)
+ return TestCase._TestCaseRaiseStub(exc, self)
+
+def TestArgs(*parameters):
+ def tuplify(x):
+ if not isinstance(x, tuple):
+ return (x,)
+ return x
+
+ def decorator(method, parameters=parameters):
+ for parameter in (tuplify(x) for x in parameters):
+
+ def method_for_parameter(self, method=method, parameter=parameter):
+ method(self, *parameter)
+ args_for_parameter = ",".join(repr(v) for v in parameter)
+ name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
+ frame = sys._getframe(1) # pylint: disable-msg=W0212
+ frame.f_locals[name_for_parameter] = method_for_parameter
+ frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')'
+ method_for_parameter.__name__ = name_for_parameter + '(' + args_for_parameter + ')'
+ return None
+ return decorator
diff --git a/headphones/webserve.py b/headphones/webserve.py
index 56d79ea1..ee1000de 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -1367,8 +1367,16 @@ class WebInterface(object):
"idtag": checked(headphones.CONFIG.IDTAG)
}
+ for k, v in config.iteritems():
+ if isinstance(v, headphones.config.path):
+ # need to apply SoftChroot to paths:
+ nv = headphones.SOFT_CHROOT.apply(v)
+ if v != nv:
+ config[k] = headphones.config.path(nv)
+
# Need to convert EXTRAS to a dictionary we can pass to the config:
# it'll come in as a string like 2,5,6,8
+
extra_munges = {
"dj-mix": "dj_mix",
"mixtape/street": "mixtape_street"
@@ -1435,6 +1443,17 @@ class WebInterface(object):
kwargs[plain_config] = kwargs[use_config]
del kwargs[use_config]
+ for k, v in kwargs.iteritems():
+ # TODO : HUGE crutch. It is all because there is no way to deal with options...
+ _conf = headphones.CONFIG._define(k)
+ conftype = _conf[1]
+
+ #print '===>', conftype
+ if conftype is headphones.config.path:
+ nv = headphones.SOFT_CHROOT.revoke(v)
+ if nv != v:
+ kwargs[k] = nv
+
# Check if encoderoutputformat is set multiple times
if len(kwargs['encoderoutputformat'][-1]) > 1:
kwargs['encoderoutputformat'] = kwargs['encoderoutputformat'][-1]
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..e47a22ee
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,12 @@
+[nosetests]
+verbosity=2
+tests=headphones
+#rednose=1
+#exclude-dir=lib
+
+with-coverage=1
+cover-branches=1
+
+cover-html=1
+cover-html-dir=cover-html
+cover-package=headphones