From 8f70eb9b03fc82e9ad7953b9b8c9c6b3b1eb1f48 Mon Sep 17 00:00:00 2001 From: sbuser Date: Mon, 1 Aug 2011 07:35:40 -0500 Subject: [PATCH 1/2] Added support for Newzbin. This required a bunch of new functions and etc as Newzbin doesn't allow for GET authentication with a hash. They want you to HTTP auth. I started creating some classes that I copied from SickBeard to extract away things in the future. Added USER_AGENT --- Headphones.py | 176 +++++----- headphones/__init__.py | 658 +++++++++++++++++++------------------ headphones/searcher.py | 702 ++++++++++++++++++++++++---------------- headphones/templates.py | 26 ++ headphones/webserve.py | 8 +- 5 files changed, 879 insertions(+), 691 deletions(-) mode change 100755 => 100644 Headphones.py diff --git a/Headphones.py b/Headphones.py old mode 100755 new mode 100644 index f8053ff4..ccef9ec1 --- a/Headphones.py +++ b/Headphones.py @@ -8,98 +8,98 @@ import headphones from headphones import webstart, logger try: - import argparse + import argparse except ImportError: - import lib.argparse as argparse - + import lib.argparse as argparse + def main(): - # Fixed paths to Headphones - if hasattr(sys, 'frozen'): - headphones.FULL_PATH = os.path.abspath(sys.executable) - else: - headphones.FULL_PATH = os.path.abspath(__file__) - - headphones.PROG_DIR = os.path.dirname(headphones.FULL_PATH) - headphones.ARGS = sys.argv[1:] - - # Set up and gather command line arguments - parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+') + # Fixed paths to Headphones + if hasattr(sys, 'frozen'): + headphones.FULL_PATH = os.path.abspath(sys.executable) + else: + headphones.FULL_PATH = os.path.abspath(__file__) + + headphones.PROG_DIR = os.path.dirname(headphones.FULL_PATH) + headphones.ARGS = sys.argv[1:] + + # Set up and gather command line arguments + parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+') - parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging') - parser.add_argument('-d', '--daemon', action='store_true', help='Run as a daemon') - parser.add_argument('-p', '--port', type=int, help='Force Headphones to run on a specified port') - parser.add_argument('--datadir', help='Specify a directory where to store your data files') - parser.add_argument('--config', help='Specify a config file to use') - parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup') - - args = parser.parse_args() - - if args.quiet: - headphones.QUIET=True - - if args.daemon: - headphones.DAEMON=True - headphones.QUIET=True - - if args.datadir: - headphones.DATA_DIR = args.datadir - else: - headphones.DATA_DIR = headphones.PROG_DIR - - if args.config: - headphones.CONFIG_FILE = args.config - else: - headphones.CONFIG_FILE = os.path.join(headphones.DATA_DIR, 'config.ini') - - # Try to create the DATA_DIR if it doesn't exist - if not os.path.exists(headphones.DATA_DIR): - try: - os.makedirs(headphones.DATA_DIR) - except OSError: - raise SystemExit('Could not create data directory: ' + headphones.DATA_DIR + '. Exiting....') - - # Make sure the DATA_DIR is writeable - if not os.access(headphones.DATA_DIR, os.W_OK): - raise SystemExit('Cannot write to the data directory: ' + headphones.DATA_DIR + '. Exiting...') - - # Put the database in the DATA_DIR - headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db') - - headphones.CFG = ConfigObj(headphones.CONFIG_FILE) - - # Read config & start logging - headphones.initialize() - - if headphones.DAEMON: - headphones.daemonize() - - # Force the http port if neccessary - if args.port: - http_port = args.port - logger.info('Starting Headphones on foced port: %i' % http_port) - else: - http_port = int(headphones.HTTP_PORT) - - # Try to start the server. - webstart.initialize({ - 'http_port': http_port, - 'http_host': headphones.HTTP_HOST, - 'http_root': headphones.HTTP_ROOT, - 'http_username': headphones.HTTP_USERNAME, - 'http_password': headphones.HTTP_PASSWORD, - }) - - logger.info('Starting Headphones on port: %i' % http_port) - - if headphones.LAUNCH_BROWSER and not args.nolaunch: - headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT) - - # Start the background threads - headphones.start() - - return + parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging') + parser.add_argument('-d', '--daemon', action='store_true', help='Run as a daemon') + parser.add_argument('-p', '--port', type=int, help='Force Headphones to run on a specified port') + parser.add_argument('--datadir', help='Specify a directory where to store your data files') + parser.add_argument('--config', help='Specify a config file to use') + parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup') + + args = parser.parse_args() + + if args.quiet: + headphones.QUIET=True + + if args.daemon: + headphones.DAEMON=True + headphones.QUIET=True + + if args.datadir: + headphones.DATA_DIR = args.datadir + else: + headphones.DATA_DIR = headphones.PROG_DIR + + if args.config: + headphones.CONFIG_FILE = args.config + else: + headphones.CONFIG_FILE = os.path.join(headphones.DATA_DIR, 'config.ini') + + # Try to create the DATA_DIR if it doesn't exist + if not os.path.exists(headphones.DATA_DIR): + try: + os.makedirs(headphones.DATA_DIR) + except OSError: + raise SystemExit('Could not create data directory: ' + headphones.DATA_DIR + '. Exiting....') + + # Make sure the DATA_DIR is writeable + if not os.access(headphones.DATA_DIR, os.W_OK): + raise SystemExit('Cannot write to the data directory: ' + headphones.DATA_DIR + '. Exiting...') + + # Put the database in the DATA_DIR + headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db') + + headphones.CFG = ConfigObj(headphones.CONFIG_FILE) + + # Read config & start logging + headphones.initialize() + + if headphones.DAEMON: + headphones.daemonize() + + # Force the http port if neccessary + if args.port: + http_port = args.port + logger.info('Starting Headphones on foced port: %i' % http_port) + else: + http_port = int(headphones.HTTP_PORT) + + # Try to start the server. + webstart.initialize({ + 'http_port': http_port, + 'http_host': headphones.HTTP_HOST, + 'http_root': headphones.HTTP_ROOT, + 'http_username': headphones.HTTP_USERNAME, + 'http_password': headphones.HTTP_PASSWORD, + }) + + logger.info('Starting Headphones on port: %i' % http_port) + + if headphones.LAUNCH_BROWSER and not args.nolaunch: + headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT) + + # Start the background threads + headphones.start() + + return if __name__ == "__main__": - main() + main() diff --git a/headphones/__init__.py b/headphones/__init__.py index eb8a5dd0..84023254 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -11,7 +11,8 @@ from lib.configobj import ConfigObj import cherrypy -from headphones import updater, searcher, importer, versioncheck, logger, postprocessor +from headphones import updater, searcher, importer, versioncheck, logger, postprocessor, version, sab +from headphones.common import * FULL_PATH = None PROG_DIR = None @@ -92,8 +93,13 @@ NZBSORG = False NZBSORG_UID = None NZBSORG_HASH = None +NEWZBIN = False +NEWZBIN_UID = None +NEWZBIN_PASSWORD = None + LASTFM_USERNAME = None + def CheckSection(sec): """ Check if INI section exists, if not create it """ try: @@ -138,351 +144,361 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True): else: logger.debug(item_name + " -> ******") return my_val - + def initialize(): - with INIT_LOCK: - - global __INITIALIZED__, FULL_PATH, PROG_DIR, QUIET, DAEMON, DATA_DIR, CONFIG_FILE, CFG, LOG_DIR, CACHE_DIR, \ - HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, LAUNCH_BROWSER, GIT_PATH, \ - CURRENT_VERSION, LATEST_VERSION, MUSIC_DIR, DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, \ - CORRECT_METADATA, MOVE_FILES, RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, CLEANUP_FILES, INCLUDE_EXTRAS, \ - ADD_ALBUM_ART, EMBED_ALBUM_ART, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, NZB_SEARCH_INTERVAL, \ - LIBRARYSCAN_INTERVAL, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \ - NZBMATRIX, NZBMATRIX_USERNAME, NZBMATRIX_APIKEY, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, \ - NZBSORG, NZBSORG_UID, NZBSORG_HASH, LASTFM_USERNAME - - if __INITIALIZED__: - return False - - # Make sure all the config sections exist - CheckSection('General') - CheckSection('SABnzbd') - CheckSection('NZBMatrix') - CheckSection('Newznab') - CheckSection('NZBsorg') - - # Set global variables based on config file or use defaults - try: - HTTP_PORT = check_setting_int(CFG, 'General', 'http_port', 8181) - except: - HTTP_PORT = 8181 - - if HTTP_PORT < 21 or HTTP_PORT > 65535: - HTTP_PORT = 8181 - - HTTP_HOST = check_setting_str(CFG, 'General', 'http_host', '0.0.0.0') - HTTP_USERNAME = check_setting_str(CFG, 'General', 'http_username', '') - HTTP_PASSWORD = check_setting_str(CFG, 'General', 'http_password', '') - HTTP_ROOT = check_setting_str(CFG, 'General', 'http_root', '/') - LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) - GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') - LOG_DIR = check_setting_str(CFG, 'General', 'log_dir', '') - - MUSIC_DIR = check_setting_str(CFG, 'General', 'music_dir', '') - DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '') - PREFERRED_QUALITY = check_setting_int(CFG, 'General', 'preferred_quality', 0) - PREFERRED_BITRATE = check_setting_int(CFG, 'General', 'preferred_bitrate', '') - DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0)) - CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0)) - MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0)) - RENAME_FILES = bool(check_setting_int(CFG, 'General', 'rename_files', 0)) - FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', 'artist/album [year]') - FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', 'tracknumber artist - album [year]- title') - CLEANUP_FILES = bool(check_setting_int(CFG, 'General', 'cleanup_files', 0)) - ADD_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'add_album_art', 0)) - EMBED_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'embed_album_art', 0)) - DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'download_dir', '') - BLACKHOLE = bool(check_setting_int(CFG, 'General', 'blackhole', 0)) - BLACKHOLE_DIR = check_setting_str(CFG, 'General', 'blackhole_dir', '') - USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', '') - INCLUDE_EXTRAS = bool(check_setting_int(CFG, 'General', 'include_extras', 0)) - - NZB_SEARCH_INTERVAL = check_setting_int(CFG, 'General', 'nzb_search_interval', 360) - LIBRARYSCAN_INTERVAL = check_setting_int(CFG, 'General', 'libraryscan_interval', 180) - - SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') - SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') - SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '') - SAB_APIKEY = check_setting_str(CFG, 'SABnzbd', 'sab_apikey', '') - SAB_CATEGORY = check_setting_str(CFG, 'SABnzbd', 'sab_category', '') - - NZBMATRIX = bool(check_setting_int(CFG, 'NZBMatrix', 'nzbmatrix', 0)) - NZBMATRIX_USERNAME = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_username', '') - NZBMATRIX_APIKEY = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_apikey', '') - - NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0)) - NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '') - NEWZNAB_APIKEY = check_setting_str(CFG, 'Newznab', 'newznab_apikey', '') - - NZBSORG = bool(check_setting_int(CFG, 'NZBsorg', 'nzbsorg', 0)) - NZBSORG_UID = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_uid', '') - NZBSORG_HASH = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_hash', '') - - LASTFM_USERNAME = check_setting_str(CFG, 'General', 'lastfm_username', '') - - if not LOG_DIR: - LOG_DIR = os.path.join(DATA_DIR, 'logs') - - if not os.path.exists(LOG_DIR): - try: - os.makedirs(LOG_DIR) - except OSError: - if not QUIET: - print 'Unable to create the log directory. Logging to screen only.' - - # Start the logger, silence console logging if we need to - logger.headphones_log.initLogger(quiet=QUIET) - - # Update some old config code: - if FOLDER_FORMAT == '%artist/%album/%track': - FOLDER_FORMAT = 'artist/album [year]' - if FILE_FORMAT == '%tracknumber %artist - %album - %title': - FILE_FORMAT = 'tracknumber artist - album - title' - - # Put the cache dir in the data dir for now - CACHE_DIR = os.path.join(DATA_DIR, 'cache') - if not os.path.exists(CACHE_DIR): - try: - os.makedirs(CACHE_DIR) - except OSError: - logger.error('Could not create cache dir. Check permissions of datadir: ' + DATA_DIR) - - # Initialize the database - logger.info('Checking to see if the database has all tables....') - try: - dbcheck() - except Exception, e: - logger.error("Can't connect to the database: %s" % e) - - # Get the currently installed version - returns None, 'win32' or the git hash - # Also sets INSTALL_TYPE variable to 'win', 'git' or 'source' - CURRENT_VERSION = versioncheck.getVersion() - - # Check for new versions - LATEST_VERSION = versioncheck.checkGithub() + with INIT_LOCK: + + global __INITIALIZED__, FULL_PATH, PROG_DIR, QUIET, DAEMON, DATA_DIR, CONFIG_FILE, CFG, LOG_DIR, CACHE_DIR, \ + HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, LAUNCH_BROWSER, GIT_PATH, \ + CURRENT_VERSION, LATEST_VERSION, MUSIC_DIR, DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, \ + CORRECT_METADATA, MOVE_FILES, RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, CLEANUP_FILES, INCLUDE_EXTRAS, \ + ADD_ALBUM_ART, EMBED_ALBUM_ART, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, NZB_SEARCH_INTERVAL, \ + LIBRARYSCAN_INTERVAL, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \ + NZBMATRIX, NZBMATRIX_USERNAME, NZBMATRIX_APIKEY, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, \ + NZBSORG, NZBSORG_UID, NZBSORG_HASH, NEWZBIN, NEWZBIN_UID, NEWZBIN_PASSWORD, LASTFM_USERNAME + + if __INITIALIZED__: + return False + + # Make sure all the config sections exist + CheckSection('General') + CheckSection('SABnzbd') + CheckSection('NZBMatrix') + CheckSection('Newznab') + CheckSection('NZBsorg') + CheckSection('Newzbin') + + # Set global variables based on config file or use defaults + try: + HTTP_PORT = check_setting_int(CFG, 'General', 'http_port', 8181) + except: + HTTP_PORT = 8181 + + if HTTP_PORT < 21 or HTTP_PORT > 65535: + HTTP_PORT = 8181 + + HTTP_HOST = check_setting_str(CFG, 'General', 'http_host', '0.0.0.0') + HTTP_USERNAME = check_setting_str(CFG, 'General', 'http_username', '') + HTTP_PASSWORD = check_setting_str(CFG, 'General', 'http_password', '') + HTTP_ROOT = check_setting_str(CFG, 'General', 'http_root', '/') + LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) + GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') + LOG_DIR = check_setting_str(CFG, 'General', 'log_dir', '') + + MUSIC_DIR = check_setting_str(CFG, 'General', 'music_dir', '') + DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '') + PREFERRED_QUALITY = check_setting_int(CFG, 'General', 'preferred_quality', 0) + PREFERRED_BITRATE = check_setting_int(CFG, 'General', 'preferred_bitrate', '') + DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0)) + CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0)) + MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0)) + RENAME_FILES = bool(check_setting_int(CFG, 'General', 'rename_files', 0)) + FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', 'artist/album [year]') + FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', 'tracknumber artist - album [year]- title') + CLEANUP_FILES = bool(check_setting_int(CFG, 'General', 'cleanup_files', 0)) + ADD_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'add_album_art', 0)) + EMBED_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'embed_album_art', 0)) + DOWNLOAD_DIR = check_setting_str(CFG, 'General', 'download_dir', '') + BLACKHOLE = bool(check_setting_int(CFG, 'General', 'blackhole', 0)) + BLACKHOLE_DIR = check_setting_str(CFG, 'General', 'blackhole_dir', '') + USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', '') + INCLUDE_EXTRAS = bool(check_setting_int(CFG, 'General', 'include_extras', 0)) + + NZB_SEARCH_INTERVAL = check_setting_int(CFG, 'General', 'nzb_search_interval', 360) + LIBRARYSCAN_INTERVAL = check_setting_int(CFG, 'General', 'libraryscan_interval', 180) + + SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') + SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') + SAB_PASSWORD = check_setting_str(CFG, 'SABnzbd', 'sab_password', '') + SAB_APIKEY = check_setting_str(CFG, 'SABnzbd', 'sab_apikey', '') + SAB_CATEGORY = check_setting_str(CFG, 'SABnzbd', 'sab_category', '') + + NZBMATRIX = bool(check_setting_int(CFG, 'NZBMatrix', 'nzbmatrix', 0)) + NZBMATRIX_USERNAME = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_username', '') + NZBMATRIX_APIKEY = check_setting_str(CFG, 'NZBMatrix', 'nzbmatrix_apikey', '') + + NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0)) + NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '') + NEWZNAB_APIKEY = check_setting_str(CFG, 'Newznab', 'newznab_apikey', '') + + NZBSORG = bool(check_setting_int(CFG, 'NZBsorg', 'nzbsorg', 0)) + NZBSORG_UID = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_uid', '') + NZBSORG_HASH = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_hash', '') - __INITIALIZED__ = True - return True - + NEWZBIN = bool(check_setting_int(CFG, 'Newzbin', 'newzbin', 0)) + NEWZBIN_UID = check_setting_str(CFG, 'Newzbin', 'newzbin_uid', '') + NEWZBIN_PASSWORD = check_setting_str(CFG, 'Newzbin', 'newzbin_password', '') + + LASTFM_USERNAME = check_setting_str(CFG, 'General', 'lastfm_username', '') + + if not LOG_DIR: + LOG_DIR = os.path.join(DATA_DIR, 'logs') + + if not os.path.exists(LOG_DIR): + try: + os.makedirs(LOG_DIR) + except OSError: + if not QUIET: + print 'Unable to create the log directory. Logging to screen only.' + + # Start the logger, silence console logging if we need to + logger.headphones_log.initLogger(quiet=QUIET) + + # Update some old config code: + if FOLDER_FORMAT == '%artist/%album/%track': + FOLDER_FORMAT = 'artist/album [year]' + if FILE_FORMAT == '%tracknumber %artist - %album - %title': + FILE_FORMAT = 'tracknumber artist - album - title' + + # Put the cache dir in the data dir for now + CACHE_DIR = os.path.join(DATA_DIR, 'cache') + if not os.path.exists(CACHE_DIR): + try: + os.makedirs(CACHE_DIR) + except OSError: + logger.error('Could not create cache dir. Check permissions of datadir: ' + DATA_DIR) + + # Initialize the database + logger.info('Checking to see if the database has all tables....') + try: + dbcheck() + except Exception, e: + logger.error("Can't connect to the database: %s" % e) + + # Get the currently installed version - returns None, 'win32' or the git hash + # Also sets INSTALL_TYPE variable to 'win', 'git' or 'source' + CURRENT_VERSION = versioncheck.getVersion() + + # Check for new versions + LATEST_VERSION = versioncheck.checkGithub() + + __INITIALIZED__ = True + return True + def daemonize(): - if threading.activeCount() != 1: - logger.warn('There are %r active threads. Daemonizing may cause \ - strange behavior.' % threading.enumerate()) - - sys.stdout.flush() - sys.stderr.flush() - - # Do first fork - try: - pid = os.fork() - if pid == 0: - pass - else: - # Exit the parent process - logger.debug('Forking once...') - os._exit(0) - except OSError, e: - sys.exit("1st fork failed: %s [%d]" % (e.strerror, e.errno)) - - os.setsid() + if threading.activeCount() != 1: + logger.warn('There are %r active threads. Daemonizing may cause \ + strange behavior.' % threading.enumerate()) + + sys.stdout.flush() + sys.stderr.flush() + + # Do first fork + try: + pid = os.fork() + if pid == 0: + pass + else: + # Exit the parent process + logger.debug('Forking once...') + os._exit(0) + except OSError, e: + sys.exit("1st fork failed: %s [%d]" % (e.strerror, e.errno)) + + os.setsid() - # Do second fork - try: - pid = os.fork() - if pid > 0: - logger.debug('Forking twice...') - os._exit(0) # Exit second parent process - except OSError, e: - sys.exit("2nd fork failed: %s [%d]" % (e.strerror, e.errno)) + # Do second fork + try: + pid = os.fork() + if pid > 0: + logger.debug('Forking twice...') + os._exit(0) # Exit second parent process + except OSError, e: + sys.exit("2nd fork failed: %s [%d]" % (e.strerror, e.errno)) - os.chdir("/") - os.umask(0) - - si = open('/dev/null', "r") - so = open('/dev/null', "a+") - se = open('/dev/null', "a+") - - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) - - logger.info('Daemonized to PID: %s' % os.getpid()) - + os.chdir("/") + os.umask(0) + + si = open('/dev/null', "r") + so = open('/dev/null', "a+") + se = open('/dev/null', "a+") + + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + logger.info('Daemonized to PID: %s' % os.getpid()) + def launch_browser(host, port, root): - if host == '0.0.0.0': - host = 'localhost' - - try: - webbrowser.open('http://%s:%i%s' % (host, port, root)) - except Exception, e: - logger.error('Could not launch browser: %s' % e) + if host == '0.0.0.0': + host = 'localhost' + + try: + webbrowser.open('http://%s:%i%s' % (host, port, root)) + except Exception, e: + logger.error('Could not launch browser: %s' % e) def config_write(): - new_config = ConfigObj() - new_config.filename = CONFIG_FILE + new_config = ConfigObj() + new_config.filename = CONFIG_FILE - new_config['General'] = {} - new_config['General']['http_port'] = HTTP_PORT - new_config['General']['http_host'] = HTTP_HOST - new_config['General']['http_username'] = HTTP_USERNAME - new_config['General']['http_password'] = HTTP_PASSWORD - new_config['General']['http_root'] = HTTP_ROOT - new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) - new_config['General']['log_dir'] = LOG_DIR - new_config['General']['git_path'] = GIT_PATH + new_config['General'] = {} + new_config['General']['http_port'] = HTTP_PORT + new_config['General']['http_host'] = HTTP_HOST + new_config['General']['http_username'] = HTTP_USERNAME + new_config['General']['http_password'] = HTTP_PASSWORD + new_config['General']['http_root'] = HTTP_ROOT + new_config['General']['launch_browser'] = int(LAUNCH_BROWSER) + new_config['General']['log_dir'] = LOG_DIR + new_config['General']['git_path'] = GIT_PATH - new_config['General']['music_dir'] = MUSIC_DIR - new_config['General']['destination_dir'] = DESTINATION_DIR - new_config['General']['preferred_quality'] = PREFERRED_QUALITY - new_config['General']['preferred_bitrate'] = PREFERRED_BITRATE - new_config['General']['detect_bitrate'] = int(DETECT_BITRATE) - new_config['General']['correct_metadata'] = int(CORRECT_METADATA) - new_config['General']['move_files'] = int(MOVE_FILES) - new_config['General']['rename_files'] = int(RENAME_FILES) - new_config['General']['folder_format'] = FOLDER_FORMAT - new_config['General']['file_format'] = FILE_FORMAT - new_config['General']['cleanup_files'] = int(CLEANUP_FILES) - new_config['General']['add_album_art'] = int(ADD_ALBUM_ART) - new_config['General']['embed_album_art'] = int(EMBED_ALBUM_ART) - new_config['General']['download_dir'] = DOWNLOAD_DIR - new_config['General']['blackhole'] = int(BLACKHOLE) - new_config['General']['blackhole_dir'] = BLACKHOLE_DIR - new_config['General']['usenet_retention'] = USENET_RETENTION - new_config['General']['include_extras'] = int(INCLUDE_EXTRAS) - - new_config['General']['nzb_search_interval'] = NZB_SEARCH_INTERVAL - new_config['General']['libraryscan_interval'] = LIBRARYSCAN_INTERVAL + new_config['General']['music_dir'] = MUSIC_DIR + new_config['General']['destination_dir'] = DESTINATION_DIR + new_config['General']['preferred_quality'] = PREFERRED_QUALITY + new_config['General']['preferred_bitrate'] = PREFERRED_BITRATE + new_config['General']['detect_bitrate'] = int(DETECT_BITRATE) + new_config['General']['correct_metadata'] = int(CORRECT_METADATA) + new_config['General']['move_files'] = int(MOVE_FILES) + new_config['General']['rename_files'] = int(RENAME_FILES) + new_config['General']['folder_format'] = FOLDER_FORMAT + new_config['General']['file_format'] = FILE_FORMAT + new_config['General']['cleanup_files'] = int(CLEANUP_FILES) + new_config['General']['add_album_art'] = int(ADD_ALBUM_ART) + new_config['General']['embed_album_art'] = int(EMBED_ALBUM_ART) + new_config['General']['download_dir'] = DOWNLOAD_DIR + new_config['General']['blackhole'] = int(BLACKHOLE) + new_config['General']['blackhole_dir'] = BLACKHOLE_DIR + new_config['General']['usenet_retention'] = USENET_RETENTION + new_config['General']['include_extras'] = int(INCLUDE_EXTRAS) + + new_config['General']['nzb_search_interval'] = NZB_SEARCH_INTERVAL + new_config['General']['libraryscan_interval'] = LIBRARYSCAN_INTERVAL - new_config['SABnzbd'] = {} - new_config['SABnzbd']['sab_host'] = SAB_HOST - new_config['SABnzbd']['sab_username'] = SAB_USERNAME - new_config['SABnzbd']['sab_password'] = SAB_PASSWORD - new_config['SABnzbd']['sab_apikey'] = SAB_APIKEY - new_config['SABnzbd']['sab_category'] = SAB_CATEGORY + new_config['SABnzbd'] = {} + new_config['SABnzbd']['sab_host'] = SAB_HOST + new_config['SABnzbd']['sab_username'] = SAB_USERNAME + new_config['SABnzbd']['sab_password'] = SAB_PASSWORD + new_config['SABnzbd']['sab_apikey'] = SAB_APIKEY + new_config['SABnzbd']['sab_category'] = SAB_CATEGORY - new_config['NZBMatrix'] = {} - new_config['NZBMatrix']['nzbmatrix'] = int(NZBMATRIX) - new_config['NZBMatrix']['nzbmatrix_username'] = NZBMATRIX_USERNAME - new_config['NZBMatrix']['nzbmatrix_apikey'] = NZBMATRIX_APIKEY + new_config['NZBMatrix'] = {} + new_config['NZBMatrix']['nzbmatrix'] = int(NZBMATRIX) + new_config['NZBMatrix']['nzbmatrix_username'] = NZBMATRIX_USERNAME + new_config['NZBMatrix']['nzbmatrix_apikey'] = NZBMATRIX_APIKEY - new_config['Newznab'] = {} - new_config['Newznab']['newznab'] = int(NEWZNAB) - new_config['Newznab']['newznab_host'] = NEWZNAB_HOST - new_config['Newznab']['newznab_apikey'] = NEWZNAB_APIKEY + new_config['Newznab'] = {} + new_config['Newznab']['newznab'] = int(NEWZNAB) + new_config['Newznab']['newznab_host'] = NEWZNAB_HOST + new_config['Newznab']['newznab_apikey'] = NEWZNAB_APIKEY - new_config['NZBsorg'] = {} - new_config['NZBsorg']['nzbsorg'] = int(NZBSORG) - new_config['NZBsorg']['nzbsorg_uid'] = NZBSORG_UID - new_config['NZBsorg']['nzbsorg_hash'] = NZBSORG_HASH - - new_config['General']['lastfm_username'] = LASTFM_USERNAME - - new_config.write() + new_config['NZBsorg'] = {} + new_config['NZBsorg']['nzbsorg'] = int(NZBSORG) + new_config['NZBsorg']['nzbsorg_uid'] = NZBSORG_UID + new_config['NZBsorg']['nzbsorg_hash'] = NZBSORG_HASH + + new_config['Newzbin'] = {} + new_config['Newzbin']['newzbin'] = int(NEWZBIN) + new_config['Newzbin']['newzbin_uid'] = NEWZBIN_UID + new_config['Newzbin']['newzbin_password'] = NEWZBIN_PASSWORD + + new_config['General']['lastfm_username'] = LASTFM_USERNAME + + new_config.write() - + def start(): - - global __INITIALIZED__, started - - if __INITIALIZED__: - - # Start our scheduled background tasks + + global __INITIALIZED__, started + + if __INITIALIZED__: + + # Start our scheduled background tasks - SCHED.add_cron_job(updater.dbUpdate, hour=4, minute=0, second=0) - SCHED.add_interval_job(searcher.searchNZB, minutes=NZB_SEARCH_INTERVAL) - SCHED.add_interval_job(importer.scanMusic, minutes=LIBRARYSCAN_INTERVAL) - SCHED.add_interval_job(versioncheck.checkGithub, minutes=300) - SCHED.add_interval_job(postprocessor.checkFolder, minutes=5) + SCHED.add_cron_job(updater.dbUpdate, hour=4, minute=0, second=0) + SCHED.add_interval_job(searcher.searchNZB, minutes=NZB_SEARCH_INTERVAL) + SCHED.add_interval_job(importer.scanMusic, minutes=LIBRARYSCAN_INTERVAL) + SCHED.add_interval_job(versioncheck.checkGithub, minutes=300) + SCHED.add_interval_job(postprocessor.checkFolder, minutes=5) - SCHED.start() - - started = True - + SCHED.start() + + started = True + def dbcheck(): - conn=sqlite3.connect(DB_FILE) - c=conn.cursor() - c.execute('CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER)') - c.execute('CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT)') - c.execute('CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER)') - c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT)') - c.execute('CREATE TABLE IF NOT EXISTS have (ArtistName TEXT, AlbumTitle TEXT, TrackNumber TEXT, TrackTitle TEXT, TrackLength TEXT, BitRate TEXT, Genre TEXT, Date TEXT, TrackID TEXT)') - c.execute('CREATE TABLE IF NOT EXISTS lastfmcloud (ArtistName TEXT, ArtistID TEXT, Count INTEGER)') - - try: - c.execute('SELECT IncludeExtras from artists') - except sqlite3.OperationalError: - c.execute('ALTER TABLE artists ADD COLUMN IncludeExtras INTEGER DEFAULT 0') - - try: - c.execute('SELECT LatestAlbum from artists') - except sqlite3.OperationalError: - c.execute('ALTER TABLE artists ADD COLUMN LatestAlbum TEXT') - - try: - c.execute('SELECT ReleaseDate from artists') - except sqlite3.OperationalError: - c.execute('ALTER TABLE artists ADD COLUMN ReleaseDate TEXT') - - try: - c.execute('SELECT AlbumID from artists') - except sqlite3.OperationalError: - c.execute('ALTER TABLE artists ADD COLUMN AlbumID TEXT') - - try: - c.execute('SELECT HaveTracks from artists') - except sqlite3.OperationalError: - c.execute('ALTER TABLE artists ADD COLUMN HaveTracks INTEGER DEFAULT 0') - - try: - c.execute('SELECT TotalTracks from artists') - except sqlite3.OperationalError: - c.execute('ALTER TABLE artists ADD COLUMN TotalTracks INTEGER DEFAULT 0') - - try: - c.execute('SELECT Type from albums') - except sqlite3.OperationalError: - c.execute('ALTER TABLE albums ADD COLUMN Type TEXT DEFAULT "Album"') + conn=sqlite3.connect(DB_FILE) + c=conn.cursor() + c.execute('CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER)') + c.execute('CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT)') + c.execute('CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER)') + c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT)') + c.execute('CREATE TABLE IF NOT EXISTS have (ArtistName TEXT, AlbumTitle TEXT, TrackNumber TEXT, TrackTitle TEXT, TrackLength TEXT, BitRate TEXT, Genre TEXT, Date TEXT, TrackID TEXT)') + c.execute('CREATE TABLE IF NOT EXISTS lastfmcloud (ArtistName TEXT, ArtistID TEXT, Count INTEGER)') + + try: + c.execute('SELECT IncludeExtras from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN IncludeExtras INTEGER DEFAULT 0') + + try: + c.execute('SELECT LatestAlbum from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN LatestAlbum TEXT') + + try: + c.execute('SELECT ReleaseDate from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN ReleaseDate TEXT') + + try: + c.execute('SELECT AlbumID from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN AlbumID TEXT') + + try: + c.execute('SELECT HaveTracks from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN HaveTracks INTEGER DEFAULT 0') + + try: + c.execute('SELECT TotalTracks from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN TotalTracks INTEGER DEFAULT 0') + + try: + c.execute('SELECT Type from albums') + except sqlite3.OperationalError: + c.execute('ALTER TABLE albums ADD COLUMN Type TEXT DEFAULT "Album"') - try: - c.execute('SELECT TrackNumber from tracks') - except sqlite3.OperationalError: - c.execute('ALTER TABLE tracks ADD COLUMN TrackNumber INTEGER') - - try: - c.execute('SELECT FolderName from snatched') - except sqlite3.OperationalError: - c.execute('ALTER TABLE snatched ADD COLUMN FolderName TEXT') - - conn.commit() - c.close() + try: + c.execute('SELECT TrackNumber from tracks') + except sqlite3.OperationalError: + c.execute('ALTER TABLE tracks ADD COLUMN TrackNumber INTEGER') + + try: + c.execute('SELECT FolderName from snatched') + except sqlite3.OperationalError: + c.execute('ALTER TABLE snatched ADD COLUMN FolderName TEXT') + + conn.commit() + c.close() - + def shutdown(restart=False, update=False): - - cherrypy.engine.exit() - SCHED.shutdown(wait=False) - - config_write() - - if update: - try: - versioncheck.update() - except Exception, e: - logger.warn('Headphones failed to update: %s. Restarting.' % e) - - if restart: - - popen_list = [sys.executable, FULL_PATH] - popen_list += ARGS - if '--nolaunch' not in popen_list: - popen_list += ['--nolaunch'] - logger.info('Restarting Headphones with ' + str(popen_list)) - subprocess.Popen(popen_list, cwd=os.getcwd()) - - os._exit(0) \ No newline at end of file + + cherrypy.engine.exit() + SCHED.shutdown(wait=False) + + config_write() + + if update: + try: + versioncheck.update() + except Exception, e: + logger.warn('Headphones failed to update: %s. Restarting.' % e) + + if restart: + + popen_list = [sys.executable, FULL_PATH] + popen_list += ARGS + if '--nolaunch' not in popen_list: + popen_list += ['--nolaunch'] + logger.info('Restarting Headphones with ' + str(popen_list)) + subprocess.Popen(popen_list, cwd=os.getcwd()) + + os._exit(0) \ No newline at end of file diff --git a/headphones/searcher.py b/headphones/searcher.py index 970e0ec2..05a5159f 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -1,299 +1,439 @@ import urllib import lib.feedparser as feedparser from xml.dom import minidom -import os, re +from xml.parsers.expat import ExpatError +import os, re, time import headphones -from headphones import logger, db, helpers +from headphones import logger, db, helpers, classes, sab + +class NewzbinDownloader(urllib.FancyURLopener): + + def __init__(self): + urllib.FancyURLopener.__init__(self) + + def http_error_default(self, url, fp, errcode, errmsg, headers): + + # if newzbin is throttling us, wait seconds and try again + if errcode == 400: + + newzbinErrCode = int(headers.getheader('X-DNZB-RCode')) + + if newzbinErrCode == 450: + rtext = str(headers.getheader('X-DNZB-RText')) + result = re.search("wait (\d+) seconds", rtext) + + elif newzbinErrCode == 401: + logger.info("Newzbin error 401") + #raise exceptions.AuthException("Newzbin username or password incorrect") + + elif newzbinErrCode == 402: + #raise exceptions.AuthException("Newzbin account not premium status, can't download NZBs") + logger.info("Newzbin error 402") + + logger.info("Newzbin throttled our NZB downloading, pausing for " + result.group(1) + "seconds") + + time.sleep(int(result.group(1))) + + #raise exceptions.NewzbinAPIThrottled() + +#this should be in a class somewhere +def getNewzbinURL(url): + + myOpener = classes.AuthURLOpener(headphones.NEWZBIN_UID, headphones.NEWZBIN_PASSWORD) + try: + f = myOpener.openit(url) + except (urllib.ContentTooShortError, IOError), e: + logger.info("Error loading search results: ContentTooShortError ") + return None + + data = f.read() + f.close() + + return data def searchNZB(albumid=None, new=False): - myDB = db.DBConnection() - - if albumid: - results = myDB.select('SELECT ArtistName, AlbumTitle, AlbumID, ReleaseDate from albums WHERE Status="Wanted" AND AlbumID=?', [albumid]) - else: - results = myDB.select('SELECT ArtistName, AlbumTitle, AlbumID, ReleaseDate from albums WHERE Status="Wanted"') - new = True - - for albums in results: - - albumid = albums[2] - reldate = albums[3] - - try: - year = reldate[:4] - except TypeError: - year = '' - - dic = {'...':'', ' & ':' ', ' = ': ' ', '?':'', '$':'s', ' + ':' ', '"':'', ',':''} + myDB = db.DBConnection() + + if albumid: + results = myDB.select('SELECT ArtistName, AlbumTitle, AlbumID, ReleaseDate from albums WHERE Status="Wanted" AND AlbumID=?', [albumid]) + else: + results = myDB.select('SELECT ArtistName, AlbumTitle, AlbumID, ReleaseDate from albums WHERE Status="Wanted"') + new = True + + for albums in results: + + albumid = albums[2] + reldate = albums[3] + + try: + year = reldate[:4] + except TypeError: + year = '' + + dic = {'...':'', ' & ':' ', ' = ': ' ', '?':'', '$':'s', ' + ':' ', '"':'', ',':''} - cleanartistalbum = helpers.latinToAscii(helpers.replace_all(albums[0]+' '+albums[1], dic)) + cleanartistalbum = helpers.latinToAscii(helpers.replace_all(albums[0]+' '+albums[1], dic)) - # FLAC usually doesn't have a year for some reason so I'll leave it out: - term = re.sub('[\.\-]', ' ', '%s' % (cleanartistalbum)).encode('utf-8') - altterm = re.sub('[\.\-]', ' ', '%s %s' % (cleanartistalbum, year)).encode('utf-8') - - # Only use the year if the term could return a bunch of different albums, i.e. self-titled albums - if albums[0] in albums[1] or len(albums[0]) < 4 or len(albums[1]) < 4: - term = altterm - - logger.info("Searching for %s since it was marked as wanted" % term) - - resultlist = [] - - if headphones.NZBMATRIX: + # FLAC usually doesn't have a year for some reason so I'll leave it out: + term = re.sub('[\.\-]', ' ', '%s' % (cleanartistalbum)).encode('utf-8') + altterm = re.sub('[\.\-]', ' ', '%s %s' % (cleanartistalbum, year)).encode('utf-8') + + # Only use the year if the term could return a bunch of different albums, i.e. self-titled albums + if albums[0] in albums[1] or len(albums[0]) < 4 or len(albums[1]) < 4: + term = altterm + + logger.info("Searching for %s since it was marked as wanted" % term) + + resultlist = [] + + if headphones.NZBMATRIX: + provider = "nzbmatrix" + if headphones.PREFERRED_QUALITY == 3: + categories = "23" + maxsize = 10000000000 + elif headphones.PREFERRED_QUALITY: + categories = "23,22" + maxsize = 2000000000 + else: + categories = "22" + maxsize = 300000000 + + + params = { "page": "download", + "username": headphones.NZBMATRIX_USERNAME, + "apikey": headphones.NZBMATRIX_APIKEY, + "subcat": categories, + "age": headphones.USENET_RETENTION, + "english": 1, + "ssl": 1, + "scenename": 1, + "term": term + } + + searchURL = "http://rss.nzbmatrix.com/rss.php?" + urllib.urlencode(params) + logger.info(u"Parsing results from "+searchURL) + d = feedparser.parse(searchURL) + + for item in d.entries: + try: + url = item.link + title = item.title + size = int(item.links[1]['length']) + if size < maxsize: + resultlist.append((title, size, url, provider)) + logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) + else: + logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) + + except AttributeError, e: + logger.info(u"No results found from NZBMatrix for %s" % term) + + if headphones.NEWZNAB: + provider = "newznab" + if headphones.PREFERRED_QUALITY == 3: + categories = "3040" + maxsize = 10000000000 + elif headphones.PREFERRED_QUALITY: + categories = "3040,3010" + maxsize = 2000000000 + else: + categories = "3010" + maxsize = 300000000 - if headphones.PREFERRED_QUALITY == 3: - categories = "23" - maxsize = 10000000000 - elif headphones.PREFERRED_QUALITY: - categories = "23,22" - maxsize = 2000000000 - else: - categories = "22" - maxsize = 300000000 - - - params = { "page": "download", - "username": headphones.NZBMATRIX_USERNAME, - "apikey": headphones.NZBMATRIX_APIKEY, - "subcat": categories, - "age": headphones.USENET_RETENTION, - "english": 1, - "ssl": 1, - "scenename": 1, - "term": term - } - - searchURL = "http://rss.nzbmatrix.com/rss.php?" + urllib.urlencode(params) - logger.info(u"Parsing results from "+searchURL) - d = feedparser.parse(searchURL) - - for item in d.entries: - try: - url = item.link - title = item.title - size = int(item.links[1]['length']) - if size < maxsize: - resultlist.append((title, size, url)) - logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) - else: - logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) - - except AttributeError, e: - logger.info(u"No results found from NZBMatrix for %s" % term) - - if headphones.NEWZNAB: - - if headphones.PREFERRED_QUALITY == 3: - categories = "3040" - maxsize = 10000000000 - elif headphones.PREFERRED_QUALITY: - categories = "3040,3010" - maxsize = 2000000000 - else: - categories = "3010" - maxsize = 300000000 + params = { "t": "search", + "apikey": headphones.NEWZNAB_APIKEY, + "cat": categories, + "maxage": headphones.USENET_RETENTION, + "q": term + } + + searchURL = headphones.NEWZNAB_HOST + '/api?' + urllib.urlencode(params) + logger.info(u"Parsing results from "+searchURL) + + d = feedparser.parse(searchURL) + + if not len(d.entries): + logger.info(u"No results found from %s for %s" % (headphones.NEWZNAB_HOST, term)) + pass + + else: + for item in d.entries: + try: + url = item.link + title = item.title + size = int(item.links[1]['length']) + if size < maxsize: + resultlist.append((title, size, url, provider)) + logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) + else: + logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) + + except Exception, e: + logger.error(u"An unknown error occured trying to parse the feed: %s" % e) + + if headphones.NZBSORG: + provider = "nzbsorg" + if headphones.PREFERRED_QUALITY == 3: + categories = "5" + maxsize = 10000000000 + term = term + ' flac' + elif headphones.PREFERRED_QUALITY: + categories = "5" + maxsize = 2000000000 + else: + categories = "5" + maxsize = 300000000 - params = { "t": "search", - "apikey": headphones.NEWZNAB_APIKEY, - "cat": categories, - "maxage": headphones.USENET_RETENTION, - "q": term - } - - searchURL = headphones.NEWZNAB_HOST + '/api?' + urllib.urlencode(params) - logger.info(u"Parsing results from "+searchURL) - - d = feedparser.parse(searchURL) - - if not len(d.entries): - logger.info(u"No results found from %s for %s" % (headphones.NEWZNAB_HOST, term)) - pass - - else: - for item in d.entries: - try: - url = item.link - title = item.title - size = int(item.links[1]['length']) - if size < maxsize: - resultlist.append((title, size, url)) - logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) - else: - logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) - - except Exception, e: - logger.error(u"An unknown error occured trying to parse the feed: %s" % e) - - if headphones.NZBSORG: - - if headphones.PREFERRED_QUALITY == 3: - categories = "5" - maxsize = 10000000000 - term = term + ' flac' - elif headphones.PREFERRED_QUALITY: - categories = "5" - maxsize = 2000000000 - else: - categories = "5" - maxsize = 300000000 + params = { "action": "search", + "dl": 1, + "catid": categories, + "i": headphones.NZBSORG_UID, + "h": headphones.NZBSORG_HASH, + "age": headphones.USENET_RETENTION, + "q": term + } + + searchURL = 'https://secure.nzbs.org/rss.php?' + urllib.urlencode(params) + + #data = urllib.urlopen(searchURL).read() + data = urllib.urlopen(searchURL).read() + + logger.info(u"Parsing results from "+searchURL) + + try: + d = minidom.parseString(data) + node = d.documentElement + items = d.getElementsByTagName("item") + except ExpatError: + logger.error('Unable to get the NZBs.org feed. Check that your settings are correct - post a bug if they are') + items = None + + if len(items): + + for item in items: + + sizenode = item.getElementsByTagName("report:size")[0].childNodes + titlenode = item.getElementsByTagName("title")[0].childNodes + linknode = item.getElementsByTagName("link")[0].childNodes + + for node in sizenode: + size = int(node.data) + for node in titlenode: + title = node.data + for node in linknode: + url = node.data + + if size < maxsize: + resultlist.append((title, size, url, provider)) + logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) + else: + logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) + + else: + + logger.info('No results found from NZBs.org for %s' % term) - params = { "action": "search", - "dl": 1, - "catid": categories, - "i": headphones.NZBSORG_UID, - "h": headphones.NZBSORG_HASH, - "age": headphones.USENET_RETENTION, - "q": term - } - - searchURL = 'https://secure.nzbs.org/rss.php?' + urllib.urlencode(params) - - data = urllib.urlopen(searchURL).read() - - logger.info(u"Parsing results from "+searchURL) - - try: - d = minidom.parseString(data) - node = d.documentElement - items = d.getElementsByTagName("item") - except ExpatError: - logger.error('Unable to get the NZBs.org feed. Check that your settings are correct - post a bug if they are') - items = None - - if len(items): - - for item in items: - - sizenode = item.getElementsByTagName("report:size")[0].childNodes - titlenode = item.getElementsByTagName("title")[0].childNodes - linknode = item.getElementsByTagName("link")[0].childNodes - - for node in sizenode: - size = int(node.data) - for node in titlenode: - title = node.data - for node in linknode: - url = node.data - - if size < maxsize: - resultlist.append((title, size, url)) - logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) - else: - logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) - - else: - - logger.info('No results found from NZBs.org for %s' % term) - - if len(resultlist): - - if headphones.PREFERRED_QUALITY == 2 and headphones.PREFERRED_BITRATE: + if headphones.NEWZBIN: + provider = "newzbin" + providerurl = "https:/www.newzbin.com/" + if headphones.PREFERRED_QUALITY == 3: + categories = "7" #music + format = "2" #flac + maxsize = 10000000000 + elif headphones.PREFERRED_QUALITY: + categories = "7" #music + format = "10" #mp3+flac + maxsize = 2000000000 + else: + categories = "7" #music + format = "8" #mp3 + maxsize = 300000000 - logger.debug('Target bitrate: %s kbps' % headphones.PREFERRED_BITRATE) + params = { + "fpn": "p", + 'u_nfo_posts_only': 0, + 'u_url_posts_only': 0, + 'u_comment_posts_only': 0, + 'u_show_passworded': 0, + "searchaction": "Search", + #"dl": 1, + "category": categories, + "retention": headphones.USENET_RETENTION, + "ps_rb_audio_format": format, + "feed": "rss", + "u_post_results_amt": 50, #this can default to a high number per user + "hauth": 1, + "q": term + } + searchURL = providerurl + "search/?%s" % urllib.urlencode(params) + data = getNewzbinURL(searchURL) + if data: + logger.info(u"Parsing results from "+searchURL) + + try: + d = minidom.parseString(data) + node = d.documentElement + items = d.getElementsByTagName("item") + except ExpatError: + logger.info('Unable to get the NEWZBIN feed. Check that your settings are correct - post a bug if they are') + items = None + + if len(items): + + for item in items: + + sizenode = item.getElementsByTagName("report:size")[0].childNodes + titlenode = item.getElementsByTagName("title")[0].childNodes + linknode = item.getElementsByTagName("link")[0].childNodes + + for node in sizenode: + size = int(node.data) + for node in titlenode: + title = node.data + for node in linknode: + url = node.data + + #exract the reportid from the link nodes + id_regex = re.escape(providerurl) + 'browse/post/(\d+)/' + id_match = re.match(id_regex, url) + if not id_match: + logger.info("Didn't find a valid Newzbin reportid in linknode") + else: + url = id_match.group(1) #we have to make a post request later, need the id + if size < maxsize and url: + resultlist.append((title, size, url, provider)) + logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size))) + else: + logger.info('%s is larger than the maxsize for this category, skipping. (Size: %i bytes)' % (title, size)) + + else: + logger.info('No results found from NEWZBIN for %s' % term) + + if len(resultlist): + + if headphones.PREFERRED_QUALITY == 2 and headphones.PREFERRED_BITRATE: - tracks = myDB.select('SELECT TrackDuration from tracks WHERE AlbumID=?', [albumid]) + logger.debug('Target bitrate: %s kbps' % headphones.PREFERRED_BITRATE) - try: - albumlength = sum([pair[0] for pair in tracks]) - logger.debug('Album length = %s' % albumlength) - targetsize = albumlength/1000 * int(headphones.PREFERRED_BITRATE) * 128 - logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize)) - - newlist = [] + tracks = myDB.select('SELECT TrackDuration from tracks WHERE AlbumID=?', [albumid]) - for result in resultlist: - delta = abs(targetsize - result[1]) - newlist.append((result[0], result[1], result[2], delta)) - - nzblist = sorted(newlist, key=lambda title: title[3]) - - except Exception, e: - - logger.debug('Error: %s' % str(e)) - logger.info('No track information for %s - %s. Defaulting to highest quality' % (albums[0], albums[1])) - - nzblist = sorted(resultlist, key=lambda title: title[1], reverse=True) - - else: - - nzblist = sorted(resultlist, key=lambda title: title[1], reverse=True) - - - if new: - # Checks to see if it's already downloaded - i = 0 - - while i < len(nzblist): - alreadydownloaded = myDB.select('SELECT * from snatched WHERE URL=?', [nzblist[i][2]]) - - if len(alreadydownloaded) >= 1: - logger.info('%s has already been downloaded. Skipping.' % nzblist[i][0]) - i += 1 - - else: - bestqual = nzblist[i] - break - - try: - x = bestqual[0] - except UnboundLocalError: - logger.info('No more matches for %s' % term) - return - - else: - bestqual = nzblist[0] - - - logger.info(u"Found best result: %s (%s) - %s" % (bestqual[0], bestqual[2], helpers.bytes_to_mb(bestqual[1]))) - - downloadurl = bestqual[2] - nzb_folder_name = '%s - %s [%s]' % (helpers.latinToAscii(albums[0]).encode('UTF-8'), helpers.latinToAscii(albums[1]).encode('UTF-8'), year) + try: + albumlength = sum([pair[0] for pair in tracks]) + logger.debug('Album length = %s' % albumlength) + targetsize = albumlength/1000 * int(headphones.PREFERRED_BITRATE) * 128 + logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize)) + + newlist = [] - if headphones.SAB_HOST and not headphones.BLACKHOLE: - linkparams = {} - - linkparams["mode"] = "addurl" - - if headphones.SAB_APIKEY: - linkparams["apikey"] = headphones.SAB_APIKEY - if headphones.SAB_USERNAME: - linkparams["ma_username"] = headphones.SAB_USERNAME - if headphones.SAB_PASSWORD: - linkparams["ma_password"] = headphones.SAB_PASSWORD - if headphones.SAB_CATEGORY: - linkparams["cat"] = headphones.SAB_CATEGORY - - linkparams["name"] = downloadurl - - linkparams["nzbname"] = nzb_folder_name - - saburl = 'http://' + headphones.SAB_HOST + '/sabnzbd/api?' + urllib.urlencode(linkparams) - logger.info(u"Sending link to SABNZBD: " + saburl) - - try: - urllib.urlopen(saburl) - - except: - logger.error(u"Unable to send link. Are you sure the host address is correct?") - break - - myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) - myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) - - - elif headphones.BLACKHOLE: - - nzb_name = nzb_folder_name + '.nzb' - download_path = os.path.join(headphones.BLACKHOLE_DIR, nzb_name) - - try: - urllib.urlretrieve(downloadurl, download_path) - except Exception, e: - logger.error('Couldn\'t retrieve NZB: %s' % e) - break - - myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) - myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) \ No newline at end of file + for result in resultlist: + delta = abs(targetsize - result[1]) + newlist.append((result[0], result[1], result[2], delta)) + + nzblist = sorted(newlist, key=lambda title: title[3]) + + except Exception, e: + + logger.debug('Error: %s' % str(e)) + logger.info('No track information for %s - %s. Defaulting to highest quality' % (albums[0], albums[1])) + + nzblist = sorted(resultlist, key=lambda title: title[1], reverse=True) + + else: + + nzblist = sorted(resultlist, key=lambda title: title[1], reverse=True) + + + if new: + # Checks to see if it's already downloaded + i = 0 + + while i < len(nzblist): + alreadydownloaded = myDB.select('SELECT * from snatched WHERE URL=?', [nzblist[i][2]]) + + if len(alreadydownloaded) >= 1: + logger.info('%s has already been downloaded. Skipping.' % nzblist[i][0]) + i += 1 + + else: + bestqual = nzblist[i] + break + + try: + x = bestqual[0] + except UnboundLocalError: + logger.info('No more matches for %s' % term) + return + + else: + bestqual = nzblist[0] + + + logger.info(u"Found best result: %s (%s) - %s" % (bestqual[0], bestqual[2], helpers.bytes_to_mb(bestqual[1]))) + + if bestqual[3] == "newzbin": + #logger.info("Found a newzbin result") + reportid = bestqual[2] + params = urllib.urlencode({"username": headphones.NEWZBIN_UID, "password": headphones.NEWZBIN_PASSWORD, "reportid": reportid}) + url = providerurl + "/api/dnzb/" + urllib._urlopener = NewzbinDownloader() + data = urllib.urlopen(url, data=params).read() + nzb = classes.NZBDataSearchResult() + nzb.extraInfo.append(data) + nzb_folder_name = '%s - %s [%s]' % (helpers.latinToAscii(albums[0]).encode('UTF-8'), helpers.latinToAscii(albums[1]).encode('UTF-8'), year) + nzb.name = nzb_folder_name + logger.info(u"Sending FILE to SABNZBD: " + nzb.name) + sab.sendNZB(nzb) + + myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) + else: + downloadurl = bestqual[2] + nzb_folder_name = '%s - %s [%s]' % (helpers.latinToAscii(albums[0]).encode('UTF-8'), helpers.latinToAscii(albums[1]).encode('UTF-8'), year) + + if headphones.SAB_HOST and not headphones.BLACKHOLE: + linkparams = {} + + linkparams["mode"] = "addurl" + + if headphones.SAB_APIKEY: + linkparams["apikey"] = headphones.SAB_APIKEY + if headphones.SAB_USERNAME: + linkparams["ma_username"] = headphones.SAB_USERNAME + if headphones.SAB_PASSWORD: + linkparams["ma_password"] = headphones.SAB_PASSWORD + if headphones.SAB_CATEGORY: + linkparams["cat"] = headphones.SAB_CATEGORY + + linkparams["name"] = downloadurl + + linkparams["nzbname"] = nzb_folder_name + + saburl = 'http://' + headphones.SAB_HOST + '/sabnzbd/api?' + urllib.urlencode(linkparams) + logger.info(u"Sending link to SABNZBD: " + saburl) + + try: + urllib.urlopen(saburl) + + except: + logger.error(u"Unable to send link. Are you sure the host address is correct?") + break + + myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) + + + elif headphones.BLACKHOLE: + + nzb_name = nzb_folder_name + '.nzb' + download_path = os.path.join(headphones.BLACKHOLE_DIR, nzb_name) + + try: + urllib.urlretrieve(downloadurl, download_path) + except Exception, e: + logger.error('Couldn\'t retrieve NZB: %s' % e) + break + + myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]]) + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name]) \ No newline at end of file diff --git a/headphones/templates.py b/headphones/templates.py index 40671190..a5efb9cf 100644 --- a/headphones/templates.py +++ b/headphones/templates.py @@ -258,6 +258,32 @@ configform = form = '''

+ + + + +
+ +

Newzbin:

+ + + +
+ +

+ Newzbin UID:
+ +

+ + + +
+ +

+ Newzbin Password:
+ +

+ diff --git a/headphones/webserve.py b/headphones/webserve.py index 31f22821..f20f5a97 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -565,6 +565,9 @@ class WebInterface(object): checked(headphones.NZBSORG), headphones.NZBSORG_UID, headphones.NZBSORG_HASH, + checked(headphones.NEWZBIN), + headphones.NEWZBIN_UID, + headphones.NEWZBIN_PASSWORD, radio(headphones.PREFERRED_QUALITY, 0), radio(headphones.PREFERRED_QUALITY, 1), radio(headphones.PREFERRED_QUALITY, 3), @@ -592,7 +595,7 @@ class WebInterface(object): def configUpdate(self, http_host='0.0.0.0', http_username=None, http_port=8181, http_password=None, launch_browser=0, sab_host=None, sab_username=None, sab_apikey=None, sab_password=None, sab_category=None, download_dir=None, blackhole=0, blackhole_dir=None, usenet_retention=None, nzbmatrix=0, nzbmatrix_username=None, nzbmatrix_apikey=None, newznab=0, newznab_host=None, newznab_apikey=None, - nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, + nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, newzbin=0, newzbin_uid=None, newzbin_password=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0, rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, embed_album_art=0, destination_dir=None, folder_format=None, file_format=None, include_extras=0, log_dir=None): headphones.HTTP_HOST = http_host @@ -618,6 +621,9 @@ class WebInterface(object): headphones.NZBSORG = nzbsorg headphones.NZBSORG_UID = nzbsorg_uid headphones.NZBSORG_HASH = nzbsorg_hash + headphones.NEWZBIN = newzbin + headphones.NEWZBIN_UID = newzbin_uid + headphones.NEWZBIN_PASSWORD = newzbin_password headphones.PREFERRED_QUALITY = int(preferred_quality) headphones.PREFERRED_BITRATE = preferred_bitrate headphones.DETECT_BITRATE = detect_bitrate From b00c7535b432a8997a32885aca66579d9d49ab06 Mon Sep 17 00:00:00 2001 From: sbuser Date: Mon, 1 Aug 2011 16:47:54 -0500 Subject: [PATCH 2/2] Trying to get old newzbin back for rembo... --- headphones/classes.py | 131 +++++++++++++++++++++++++++++ headphones/common.py | 162 ++++++++++++++++++++++++++++++++++++ headphones/sab.py | 123 +++++++++++++++++++++++++++ lib/MultipartPostHandler.py | 88 ++++++++++++++++++++ 4 files changed, 504 insertions(+) create mode 100644 headphones/classes.py create mode 100644 headphones/common.py create mode 100644 headphones/sab.py create mode 100644 lib/MultipartPostHandler.py diff --git a/headphones/classes.py b/headphones/classes.py new file mode 100644 index 00000000..5dc37eb6 --- /dev/null +++ b/headphones/classes.py @@ -0,0 +1,131 @@ +# Author: Nic Wolfe +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see . + + + +import headphones + +import urllib +import datetime + +from common import USER_AGENT + +class HeadphonesURLopener(urllib.FancyURLopener): + version = USER_AGENT + +class AuthURLOpener(HeadphonesURLopener): + """ + URLOpener class that supports http auth without needing interactive password entry. + If the provided username/password don't work it simply fails. + + user: username to use for HTTP auth + pw: password to use for HTTP auth + """ + def __init__(self, user, pw): + self.username = user + self.password = pw + + # remember if we've tried the username/password before + self.numTries = 0 + + # call the base class + urllib.FancyURLopener.__init__(self) + + def prompt_user_passwd(self, host, realm): + """ + Override this function and instead of prompting just give the + username/password that were provided when the class was instantiated. + """ + + # if this is the first try then provide a username/password + if self.numTries == 0: + self.numTries = 1 + return (self.username, self.password) + + # if we've tried before then return blank which cancels the request + else: + return ('', '') + + # this is pretty much just a hack for convenience + def openit(self, url): + self.numTries = 0 + return HeadphonesURLopener.open(self, url) + +class SearchResult: + """ + Represents a search result from an indexer. + """ + + def __init__(self): + self.provider = -1 + + # URL to the NZB/torrent file + self.url = "" + + # used by some providers to store extra info associated with the result + self.extraInfo = [] + + # quality of the release + self.quality = -1 + + # release name + self.name = "" + + def __str__(self): + + if self.provider == None: + return "Invalid provider, unable to print self" + + myString = self.provider.name + " @ " + self.url + "\n" + myString += "Extra Info:\n" + for extra in self.extraInfo: + myString += " " + extra + "\n" + return myString + +class NZBSearchResult(SearchResult): + """ + Regular NZB result with an URL to the NZB + """ + resultType = "nzb" + +class NZBDataSearchResult(SearchResult): + """ + NZB result where the actual NZB XML data is stored in the extraInfo + """ + resultType = "nzbdata" + +class TorrentSearchResult(SearchResult): + """ + Torrent result with an URL to the torrent + """ + resultType = "torrent" + +class Proper: + def __init__(self, name, url, date): + self.name = name + self.url = url + self.date = date + self.provider = None + self.quality = -1 + + self.tvdbid = -1 + self.season = -1 + self.episode = -1 + + def __str__(self): + return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid) diff --git a/headphones/common.py b/headphones/common.py new file mode 100644 index 00000000..cebebcb3 --- /dev/null +++ b/headphones/common.py @@ -0,0 +1,162 @@ +''' +Created on Aug 1, 2011 + +@author: Michael +''' +import platform, operator, os, re + +from headphones import version + +#Identify Our Application +USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')' + +### Notification Types +NOTIFY_SNATCH = 1 +NOTIFY_DOWNLOAD = 2 + +notifyStrings = {} +notifyStrings[NOTIFY_SNATCH] = "Started Download" +notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished" + +### Release statuses +UNKNOWN = -1 # should never happen +UNAIRED = 1 # releases that haven't dropped yet +SNATCHED = 2 # qualified with quality +WANTED = 3 # releases we don't have but want to get +DOWNLOADED = 4 # qualified with quality +SKIPPED = 5 # releases we don't want +ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats) +IGNORED = 7 # releases that you don't want included in your download stats +SNATCHED_PROPER = 9 # qualified with quality + +class Quality: + + NONE = 0 + B192 = 1<<1 # 2 + VBR = 1<<2 # 4 + B256 = 1<<3 # 8 + B320 = 1<<4 #16 + FLAC = 1<<5 #32 + + # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere + UNKNOWN = 1<<15 + + qualityStrings = {NONE: "N/A", + UNKNOWN: "Unknown", + B192: "MP3 192", + VBR: "MP3 VBR", + B256: "MP3 256", + B320: "MP3 320", + FLAC: "Flac"} + + statusPrefixes = {DOWNLOADED: "Downloaded", + SNATCHED: "Snatched"} + + @staticmethod + def _getStatusStrings(status): + toReturn = {} + for x in Quality.qualityStrings.keys(): + toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")" + return toReturn + + @staticmethod + def combineQualities(anyQualities, bestQualities): + anyQuality = 0 + bestQuality = 0 + if anyQualities: + anyQuality = reduce(operator.or_, anyQualities) + if bestQualities: + bestQuality = reduce(operator.or_, bestQualities) + return anyQuality | (bestQuality<<16) + + @staticmethod + def splitQuality(quality): + anyQualities = [] + bestQualities = [] + for curQual in Quality.qualityStrings.keys(): + if curQual & quality: + anyQualities.append(curQual) + if curQual<<16 & quality: + bestQualities.append(curQual) + + return (anyQualities, bestQualities) + + @staticmethod + def nameQuality(name): + + name = os.path.basename(name) + + # if we have our exact text then assume we put it there + for x in Quality.qualityStrings: + if x == Quality.UNKNOWN: + continue + + regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W' + regex_match = re.search(regex, name, re.I) + if regex_match: + return x + + checkName = lambda list, func: func([re.search(x, name, re.I) for x in list]) + + #TODO: fix quality checking here + if checkName(["mp3", "192"], any) and not checkName(["flac"], all): + return Quality.B192 + elif checkName(["mp3", "256"], any) and not checkName(["flac"], all): + return Quality.B256 + elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all): + return Quality.VBR + elif checkName(["mp3", "320"], any) and not checkName(["flac"], all): + return Quality.B320 + else: + return Quality.UNKNOWN + + @staticmethod + def assumeQuality(name): + + if name.lower().endswith(".mp3"): + return Quality.MP3 + elif name.lower().endswith(".flac"): + return Quality.LOSSLESS + else: + return Quality.UNKNOWN + + @staticmethod + def compositeStatus(status, quality): + return status + 100 * quality + + @staticmethod + def qualityDownloaded(status): + return (status - DOWNLOADED) / 100 + + @staticmethod + def splitCompositeStatus(status): + """Returns a tuple containing (status, quality)""" + for x in sorted(Quality.qualityStrings.keys(), reverse=True): + if status > x*100: + return (status-x*100, x) + + return (Quality.NONE, status) + + @staticmethod + def statusFromName(name, assume=True): + quality = Quality.nameQuality(name) + if assume and quality == Quality.UNKNOWN: + quality = Quality.assumeQuality(name) + return Quality.compositeStatus(DOWNLOADED, quality) + + DOWNLOADED = None + SNATCHED = None + SNATCHED_PROPER = None + +Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] +Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] + +MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], []) +LOSSLESS = Quality.combineQualities([Quality.FLAC], []) +ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], []) + +qualityPresets = (MP3, LOSSLESS, ANY) +qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)", + LOSSLESS: "Lossless (flac)", + ANY: "Any"} \ No newline at end of file diff --git a/headphones/sab.py b/headphones/sab.py new file mode 100644 index 00000000..bc9d8053 --- /dev/null +++ b/headphones/sab.py @@ -0,0 +1,123 @@ +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see . + + + +import urllib, httplib +import datetime + +import headphones + +from lib import MultipartPostHandler +import urllib2, cookielib + +from headphones.common import USER_AGENT +from headphones import logger + + +def sendNZB(nzb): + + params = {} + + if headphones.SAB_USERNAME != None: + params['ma_username'] = headphones.SAB_USERNAME + if headphones.SAB_PASSWORD != None: + params['ma_password'] = headphones.SAB_PASSWORD + if headphones.SAB_APIKEY != None: + params['apikey'] = headphones.SAB_APIKEY + if headphones.SAB_CATEGORY != None: + params['cat'] = headphones.SAB_CATEGORY + + +# # if released recently make it high priority +# for curEp in nzb.episodes: +# if datetime.date.today() - curEp.airdate <= datetime.timedelta(days=7): +# params['priority'] = 1 + + # if it's a normal result we just pass SAB the URL + if nzb.resultType == "nzb": + # for newzbin results send the ID to sab specifically + if nzb.provider.getID() == 'newzbin': + id = nzb.provider.getIDFromURL(nzb.url) + if not id: + logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url)) + return False + params['mode'] = 'addid' + params['name'] = id + else: + params['mode'] = 'addurl' + params['name'] = nzb.url + + # if we get a raw data result we want to upload it to SAB + elif nzb.resultType == "nzbdata": + params['mode'] = 'addfile' + multiPartParams = {"nzbfile": (nzb.name+".nzb", nzb.extraInfo[0])} + + url = "http://" + headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params) + + logger.info(u"Sending NZB to SABnzbd") + + logger.info(u"URL: " + url) + + try: + + if nzb.resultType == "nzb": + f = urllib.urlopen(url) + elif nzb.resultType == "nzbdata": + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), + MultipartPostHandler.MultipartPostHandler) + + req = urllib2.Request(url, + multiPartParams, + headers={'User-Agent': USER_AGENT}) + + f = opener.open(req) + + except (EOFError, IOError), e: + logger.info(u"Unable to connect to SAB: ") + return False + + except httplib.InvalidURL, e: + logger.info(u"Invalid SAB host, check your config: ") + return False + + if f == None: + logger.info(u"No data returned from SABnzbd, NZB not sent") + return False + + try: + result = f.readlines() + except Exception, e: + logger.info(u"Error trying to get result from SAB, NZB not sent: ") + return False + + if len(result) == 0: + logger.info(u"No data returned from SABnzbd, NZB not sent") + return False + + sabText = result[0].strip() + + logger.info(u"Result text from SAB: " + sabText) + + if sabText == "ok": + logger.info(u"NZB sent to SAB successfully") + return True + elif sabText == "Missing authentication": + logger.info(u"Incorrect username/password sent to SAB, NZB not sent") + return False + else: + logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText) + return False \ No newline at end of file diff --git a/lib/MultipartPostHandler.py b/lib/MultipartPostHandler.py new file mode 100644 index 00000000..82fa59c6 --- /dev/null +++ b/lib/MultipartPostHandler.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +#### +# 06/2010 Nic Wolfe +# 02/2006 Will Holcomb +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# + +import urllib +import urllib2 +import mimetools, mimetypes +import os, sys + +# Controls how sequences are uncoded. If true, elements may be given multiple values by +# assigning a sequence. +doseq = 1 + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) in (file, list, tuple): + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError, "not a valid non-string sequence or mapping object", traceback + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data; boundary=%s' % boundary + if(request.has_header('Content-Type') + and request.get_header('Content-Type').find('multipart/form-data') != 0): + print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + return request + + @staticmethod + def multipart_encode(vars, files, boundary = None, buffer = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buffer is None: + buffer = '' + for(key, value) in vars: + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"' % key + buffer += '\r\n\r\n' + value + '\r\n' + for(key, fd) in files: + + # allow them to pass in a file or a tuple with name & data + if type(fd) == file: + name_in = fd.name + fd.seek(0) + data_in = fd.read() + elif type(fd) in (tuple, list): + name_in, data_in = fd + + filename = os.path.basename(name_in) + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buffer += 'Content-Type: %s\r\n' % contenttype + # buffer += 'Content-Length: %s\r\n' % file_size + buffer += '\r\n' + data_in + '\r\n' + buffer += '--%s--\r\n\r\n' % boundary + return boundary, buffer + + https_request = http_request \ No newline at end of file