From 188d907e103bc3b4737be2a788801a90ecfe1133 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 11:02:25 +0200 Subject: [PATCH 1/8] GET, POST, HEAD etc are in lower case in requests.py --- headphones/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/request.py b/headphones/request.py index 9892592e..bbb517a9 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -19,7 +19,7 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N # Map method to the request.XXX method. This is a simple hack, but it allows # requests to apply more magic per method. See lib/requests/api.py. - request_method = getattr(requests, method) + request_method = getattr(requests, method.lower()) try: # Request the URL From 63d3ab1cc1442aa93390e385466eb9797b15326c Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 11:06:02 +0200 Subject: [PATCH 2/8] Advanced option to turn off remote SSL certificate validation --- headphones/__init__.py | 7 ++++++- headphones/request.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/headphones/__init__.py b/headphones/__init__.py index 380a31bf..821fb202 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -292,6 +292,8 @@ JOURNAL_MODE = None UMASK = None +VERIFY_SSL_CERT = True + def CheckSection(sec): """ Check if INI section exists, if not create it """ try: @@ -360,7 +362,7 @@ def initialize(): XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \ - PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED + PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, VERIFY_SSL_CERT if __INITIALIZED__: @@ -643,6 +645,8 @@ def initialize(): ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80) + VERIFY_SSL_CERT = bool(check_setting_int(CFG, 'Advanced', 'verify_ssl_cert', True)) + # update folder formats in the config & bump up config version if CONFIG_VERSION == '0': from headphones.helpers import replace_all @@ -1092,6 +1096,7 @@ def config_write(): new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT new_config['Advanced']['cache_sizemb'] = CACHE_SIZEMB new_config['Advanced']['journal_mode'] = JOURNAL_MODE + new_config['Advanced']['verify_ssl_cert'] = VERIFY_SSL_CERT new_config.write() diff --git a/headphones/request.py b/headphones/request.py index bbb517a9..612d8679 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -5,6 +5,7 @@ from bs4 import BeautifulSoup import requests import feedparser +import headphones def request_response(url, method="get", auto_raise=True, whitelist_status_code=None, **kwargs): """ @@ -17,6 +18,10 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N if whitelist_status_code and type(whitelist_status_code) != list: whitelist_status_code = [whitelist_status_code] + # Disable verification of SSL certificates if requested. Note: this could + # pose a security issue! + kwargs["verify"] = headphones.VERIFY_SSL_CERT + # Map method to the request.XXX method. This is a simple hack, but it allows # requests to apply more magic per method. See lib/requests/api.py. request_method = getattr(requests, method.lower()) From e30cb2620049ddf50a8d7b88b544d03cd7383706 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 11:11:57 +0200 Subject: [PATCH 3/8] Clarified meaning of HTTP error codes. Will show if it's local or remote error --- headphones/request.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/headphones/request.py b/headphones/request.py index 612d8679..4edb8b2f 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -50,7 +50,15 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N logger.error("Request timed out.") except requests.HTTPError, e: if e.response is not None: - logger.error("Request raise HTTP error with status code: %d", e.response.status_code) + if e.response.status_code >= 500: + cause = "remote server error" + elif e.response.status_code >= 400: + cause = "local request error" + else: + # I don't think we will end up here, but for completeness + cause = "unknown" + + logger.error("Request raise HTTP error with status code %d (%s).", e.response.status_code, cause) else: logger.error("Request raised HTTP error.") except requests.RequestException, e: From 9b2594355fb9ab029139481a1a590314c60f61a2 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 11:16:58 +0200 Subject: [PATCH 4/8] Let the --quiet option don't silence --verbose if we want verbose --- Headphones.py | 6 +++--- headphones/__init__.py | 9 +++++---- headphones/logger.py | 10 ++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Headphones.py b/Headphones.py index 480caf08..ce67b2b1 100755 --- a/Headphones.py +++ b/Headphones.py @@ -77,9 +77,9 @@ def main(): args = parser.parse_args() if args.verbose: - headphones.VERBOSE = 2 - elif args.quiet: - headphones.VERBOSE = 0 + headphones.VERBOSE = True + if args.quiet: + headphones.QUIET = True if args.daemon: if sys.platform == 'win32': diff --git a/headphones/__init__.py b/headphones/__init__.py index 821fb202..3b785efc 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -38,7 +38,8 @@ SIGNAL = None SYS_PLATFORM = None SYS_ENCODING = None -VERBOSE = 1 +QUIET = True +VERBOSE = False DAEMON = False CREATEPID = False PIDFILE= None @@ -340,7 +341,7 @@ def initialize(): with INIT_LOCK: - global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \ + global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, QUIET, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \ HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, GIT_USER, GIT_BRANCH, DO_NOT_OVERRIDE_GIT_BRANCH, \ CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \ LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, ADD_ARTISTS, CORRECT_METADATA, MOVE_FILES, \ @@ -717,8 +718,8 @@ def initialize(): if VERBOSE: sys.stderr.write('Unable to create the log directory. Logging to screen only.\n') - # Start the logger, silence console logging if we need to - logger.initLogger(verbose=VERBOSE) + # Start the logger, disable console if needed + logger.initLogger(console=not QUIET, verbose=VERBOSE) if not CACHE_DIR: # Put the cache dir in the data dir for now diff --git a/headphones/logger.py b/headphones/logger.py index b2ea73db..0f2baf98 100644 --- a/headphones/logger.py +++ b/headphones/logger.py @@ -43,19 +43,21 @@ class LogListHandler(logging.Handler): headphones.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName)) -def initLogger(verbose=1): +def initLogger(console=False, verbose=False): """ Setup logging for Headphones. It uses the logger instance with the name 'headphones'. Three log handlers are added: * RotatingFileHandler: for the file headphones.log * LogListHandler: for Web UI - * StreamHandler: for console (if verbose > 0) + * StreamHandler: for console (if console) + + Console logging is only enabled if console is set to True. """ # Configure the logger to accept all messages logger.propagate = False - logger.setLevel(logging.DEBUG if verbose == 2 else logging.INFO) + logger.setLevel(logging.DEBUG if verbose else logging.INFO) # Setup file logger filename = os.path.join(headphones.LOG_DIR, FILENAME) @@ -74,7 +76,7 @@ def initLogger(verbose=1): logger.addHandler(loglist_handler) # Setup console logger - if verbose: + if console: console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') console_handler = logging.StreamHandler() console_handler.setFormatter(console_formatter) From 45868bc523213ed0afaa1272cefc1da1a3adf201 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 11:45:28 +0200 Subject: [PATCH 5/8] Toggle VERBOSE at runtime. Just surf to /toggleVerbose. Comes in handy for the ones who don't want to change startup scripts --- Headphones.py | 2 +- headphones/__init__.py | 3 ++- headphones/logger.py | 11 +++++++++++ headphones/webserve.py | 8 ++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Headphones.py b/Headphones.py index ce67b2b1..1a7c1df7 100755 --- a/Headphones.py +++ b/Headphones.py @@ -101,7 +101,7 @@ def main(): try: file(headphones.PIDFILE, 'w').write("pid\n") except IOError, e: - raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) + raise SystemExit("Unable to write PID file: %s [%d]", e.strerror, e.errno) else: logger.warn("Not running in daemon mode. PID file creation disabled.") diff --git a/headphones/__init__.py b/headphones/__init__.py index 3b785efc..ac685164 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -38,7 +38,7 @@ SIGNAL = None SYS_PLATFORM = None SYS_ENCODING = None -QUIET = True +QUIET = False VERBOSE = False DAEMON = False CREATEPID = False @@ -720,6 +720,7 @@ def initialize(): # Start the logger, disable console if needed logger.initLogger(console=not QUIET, verbose=VERBOSE) + logger.initLogger(console=not QUIET, verbose=False) if not CACHE_DIR: # Put the cache dir in the data dir for now diff --git a/headphones/logger.py b/headphones/logger.py index 0f2baf98..4a21f3f8 100644 --- a/headphones/logger.py +++ b/headphones/logger.py @@ -55,6 +55,17 @@ def initLogger(console=False, verbose=False): Console logging is only enabled if console is set to True. """ + # Close and remove old handlers. This is required to reinit the loggers + # at runtime + for handler in logger.handlers[:]: + # Just make sure it is cleaned up. + if isinstance(handler, handlers.RotatingFileHandler): + handler.close() + elif isinstance(handler, logging.StreamHandler): + handler.flush() + + logger.removeHandler(handler) + # Configure the logger to accept all messages logger.propagate = False logger.setLevel(logging.DEBUG if verbose else logging.INFO) diff --git a/headphones/webserve.py b/headphones/webserve.py index 6db4c1f3..b4dd0c9a 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -736,6 +736,14 @@ class WebInterface(object): raise cherrypy.HTTPRedirect("logs") clearLogs.exposed = True + def toggleVerbose(self): + headphones.VERBOSE = not headphones.VERBOSE + logger.initLogger(not headphones.QUIET, headphones.VERBOSE) + logger.info("Verbose toggled, set to %s", headphones.VERBOSE) + logger.debug("If you read this message, debug logging is available") + raise cherrypy.HTTPRedirect("logs") + toggleVerbose.exposed = True + def getLog(self,iDisplayStart=0,iDisplayLength=100,iSortCol_0=0,sSortDir_0="desc",sSearch="",**kwargs): iDisplayStart = int(iDisplayStart) From f92f8928b08d0d4be062099a2fe849ed8dc8b12e Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 11:48:57 +0200 Subject: [PATCH 6/8] Remove the validator, since it returns a dict on error and a list on results. Validator always expected a dict --- headphones/searcher.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index 1cc78b84..6dc6741f 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -537,8 +537,7 @@ def searchNZB(album, new=False, losslessOnly=False): data = request.request_json( url='http://api.omgwtfnzbs.org/json/', - params=params, headers=headers, - validator=lambda x: type(x) == dict + params=params, headers=headers ) # Parse response From 674617c0f80cb1f46c8f62e84d34851cab7bf5b4 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 12:09:26 +0200 Subject: [PATCH 7/8] Ensure correct types --- headphones/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/headphones/__init__.py b/headphones/__init__.py index ac685164..a136efa8 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -646,7 +646,7 @@ def initialize(): ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80) - VERIFY_SSL_CERT = bool(check_setting_int(CFG, 'Advanced', 'verify_ssl_cert', True)) + VERIFY_SSL_CERT = bool(check_setting_int(CFG, 'Advanced', 'verify_ssl_cert', 1)) # update folder formats in the config & bump up config version if CONFIG_VERSION == '0': @@ -1098,7 +1098,7 @@ def config_write(): new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT new_config['Advanced']['cache_sizemb'] = CACHE_SIZEMB new_config['Advanced']['journal_mode'] = JOURNAL_MODE - new_config['Advanced']['verify_ssl_cert'] = VERIFY_SSL_CERT + new_config['Advanced']['verify_ssl_cert'] = int(VERIFY_SSL_CERT) new_config.write() From fe2834f4a65b0c398d4430e9f78159db763dc639 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Mon, 12 May 2014 12:12:40 +0200 Subject: [PATCH 8/8] Be quiet when running daemon --- Headphones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Headphones.py b/Headphones.py index 1a7c1df7..10ab2e44 100755 --- a/Headphones.py +++ b/Headphones.py @@ -85,8 +85,8 @@ def main(): if sys.platform == 'win32': print "Daemonize not supported under Windows, starting normally" else: - headphones.DAEMON=True - headphones.VERBOSE = False + headphones.DAEMON = True + headphones.QUIET = True if args.pidfile: headphones.PIDFILE = str(args.pidfile)