From 78b3802f8f4f5660eb2e96269d71f6fe36b19225 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 4 May 2015 20:50:16 -0700 Subject: [PATCH 01/30] Added option to ignore clean/censored releases (#2198) --- data/interfaces/default/config.html | 6 ++++++ headphones/config.py | 1 + headphones/searcher.py | 7 +++++++ headphones/webserve.py | 3 ++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index c6925195..4b5c5ff3 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -724,6 +724,12 @@ Results without these words in the title will be filtered out. You can use OR: 'flac OR lossless OR alac, vinyl' +
+ +
diff --git a/headphones/config.py b/headphones/config.py index b27d48da..d3e3e301 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -105,6 +105,7 @@ _CONFIG_DEFINITIONS = { 'HTTP_ROOT': (str, 'General', '/'), 'HTTP_USERNAME': (str, 'General', ''), 'IDTAG': (int, 'Beets', 0), + 'IGNORE_CLEAN_RELEASES': (int, 'General', 0), 'IGNORED_WORDS': (str, 'General', ''), 'IGNORED_FOLDERS': (list, 'Advanced', []), 'IGNORED_FILES': (list, 'Advanced', []), diff --git a/headphones/searcher.py b/headphones/searcher.py index 549e2e05..5f38905f 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -999,6 +999,13 @@ def verifyresult(title, artistterm, term, lossless): logger.info("Removed '%s' from results because it doesn't contain required word: '%s'", title, each_word) return False + if headphones.CONFIG.IGNORE_CLEAN_RELEASES: + for each_word in ['clean','edited','censored']: + logger.debug("Checking if '%s' is in search result: '%s'", each_word, title) + if each_word.lower() in title.lower() and each_word.lower() not in term.lower(): + logger.info("Removed '%s' from results because it contains clean album word: '%s'", title, each_word) + return False + tokens = re.split('\W', term, re.IGNORECASE | re.UNICODE) for token in tokens: diff --git a/headphones/webserve.py b/headphones/webserve.py index 26b6ac76..84420d4d 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1005,6 +1005,7 @@ class WebInterface(object): "preferred_words": headphones.CONFIG.PREFERRED_WORDS, "ignored_words": headphones.CONFIG.IGNORED_WORDS, "required_words": headphones.CONFIG.REQUIRED_WORDS, + "ignore_clean_releases": checked(headphones.CONFIG.IGNORE_CLEAN_RELEASES), "torrentblackhole_dir": headphones.CONFIG.TORRENTBLACKHOLE_DIR, "download_torrent_dir": headphones.CONFIG.DOWNLOAD_TORRENT_DIR, "numberofseeders": headphones.CONFIG.NUMBEROFSEEDERS, @@ -1205,7 +1206,7 @@ class WebInterface(object): checked_configs = [ "launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", "use_newznab", "newznab_enabled", "use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_mininova", "use_waffles", "use_rutracker", - "use_whatcd", "preferred_bitrate_allow_lossless", "detect_bitrate", "freeze_db", "cue_split", "move_files", "rename_files", + "use_whatcd", "preferred_bitrate_allow_lossless", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files", "rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics", "replace_existing_folders", "file_underscores", "include_extras", "autowant_upcoming", "autowant_all", "autowant_manually_added", "keep_torrent_files", "music_encoder", "encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", "growl_onsnatch", "prowl_enabled", From c6b0f5807482ac7d2e6eabdb888d90010947f264 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 4 May 2015 20:55:00 -0700 Subject: [PATCH 02/30] Moved the 'freeze db' option to advanced->misc. tab --- data/interfaces/default/config.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 4b5c5ff3..991c004a 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -742,10 +742,6 @@
-
+
+ +
From 9d425d33ab2d54992b6caaa5d628bd961ce9a6a9 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 4 May 2015 20:58:53 -0700 Subject: [PATCH 03/30] Added button to toggle verbose/debug logging --- data/interfaces/default/logs.html | 1 + 1 file changed, 1 insertion(+) diff --git a/data/interfaces/default/logs.html b/data/interfaces/default/logs.html index 8caa608e..c82f79c4 100644 --- a/data/interfaces/default/logs.html +++ b/data/interfaces/default/logs.html @@ -7,6 +7,7 @@ From 898f814dcee6fbcd9e8b78f5bda0e6fe5a00c158 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 4 May 2015 21:09:10 -0700 Subject: [PATCH 04/30] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae7feca..042039ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v0.5.6 +Released xx xxx 2015 + +Highlights: +* Added: Filter out clean/edited/censored releases (#2198) +* Added: Button on the log page to toggle verbose/debug logging +* Improved: Moved the 'freeze db' option to the advanced->misc. tab + +The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6). + ## v0.5.5 Released 04 May 2015 From 70b1574aa60a0cb729575e60aa4147e38f11478d Mon Sep 17 00:00:00 2001 From: rembo10 Date: Wed, 6 May 2015 00:02:11 -0700 Subject: [PATCH 05/30] Updated sab.py to use the requests library. Should fix #2208 --- headphones/sab.py | 133 ++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 87 deletions(-) diff --git a/headphones/sab.py b/headphones/sab.py index 45565f77..c7ff286c 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -20,19 +20,21 @@ import MultipartPostHandler import headphones import cookielib -import urllib2 import httplib -import urllib -import ast from headphones.common import USER_AGENT -from headphones import logger -from headphones import helpers +from headphones import logger, helpers, request -def sendNZB(nzb): +def sab_api_call(request_type=None, params={}, **kwargs): - params = {} + if not headphones.CONFIG.SAB_HOST.startswith('http'): + headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST + + if headphones.CONFIG.SAB_HOST.endswith('/'): + headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1] + + url = headphones.CONFIG.SAB_HOST + "/" + "api?" if headphones.CONFIG.SAB_USERNAME: params['ma_username'] = headphones.CONFIG.SAB_USERNAME @@ -40,9 +42,28 @@ def sendNZB(nzb): params['ma_password'] = headphones.CONFIG.SAB_PASSWORD if headphones.CONFIG.SAB_APIKEY: params['apikey'] = headphones.CONFIG.SAB_APIKEY - if headphones.CONFIG.SAB_CATEGORY: + + if request_type=='send_nzb' and headphones.CONFIG.SAB_CATEGORY: params['cat'] = headphones.CONFIG.SAB_CATEGORY + params['output']='json' + + logger.info("Trying to send nzb to SABnzbd") + response = request.request_json(url, params=params, **kwargs) + + if not response: + logger.error("Error connecting to SABnzbd on url: %s" % url) + return False + if response['status'] == False and response['error']: + logger.error("Error connecting to SABnzbd: %s" % response['error']) + return False + else: + logger.info("Successfully connected to SABnzbd") + return response + +def sendNZB(nzb): + + params = {} # 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 @@ -62,102 +83,40 @@ def sendNZB(nzb): # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler nzbdata = helpers.latinToAscii(nzb.extraInfo[0]) params['mode'] = 'addfile' - multiPartParams = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)} + files = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)} + headers = {'User-Agent': USER_AGENT} - if not headphones.CONFIG.SAB_HOST.startswith('http'): - headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST + if nzb.resultType == "nzb": + response = sab_api_call('send_nzb', params=params) + elif nzb.resultType == "nzbdata": + cookies = cookielib.CookieJar() + response = sab_api_call('send_nzb', params=params, method="post", files=files, cookies=cookies, headers=headers) - if headphones.CONFIG.SAB_HOST.endswith('/'): - headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1] - - url = headphones.CONFIG.SAB_HOST + "/" + "api?" + urllib.urlencode(params) - - 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) as e: - logger.error(u"Unable to connect to SAB with URL: %s" % url) - return False - - except httplib.InvalidURL as e: - logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.CONFIG.SAB_HOST) - return False - - except Exception as e: - logger.error(u"Error: " + str(e)) - return False - - if f is None: + if not response: logger.info(u"No data returned from SABnzbd, NZB not sent") return False - try: - result = f.readlines() - except Exception as 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") + if response['status']: + logger.info(u"NZB sent to SABnzbd 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) + logger.error(u"Error sending NZB to SABnzbd: %s" % response['error']) return False def checkConfig(): params = {'mode': 'get_config', - 'section': 'misc' + 'section': 'misc', } - if headphones.CONFIG.SAB_USERNAME: - params['ma_username'] = headphones.CONFIG.SAB_USERNAME - if headphones.CONFIG.SAB_PASSWORD: - params['ma_password'] = headphones.CONFIG.SAB_PASSWORD - if headphones.CONFIG.SAB_APIKEY: - params['apikey'] = headphones.CONFIG.SAB_APIKEY - - if not headphones.CONFIG.SAB_HOST.startswith('http'): - headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST - - if headphones.CONFIG.SAB_HOST.endswith('/'): - headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1] - - url = headphones.CONFIG.SAB_HOST + "/" + "api?" + urllib.urlencode(params) - - try: - f = urllib.urlopen(url).read() - except Exception: + config_options = sab_api_call(params=params) + + if not config_options: logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)") return (0, 0) - config_options = ast.literal_eval(f) - - replace_spaces = config_options['misc']['replace_spaces'] - replace_dots = config_options['misc']['replace_dots'] + replace_spaces = config_options['config']['misc']['replace_spaces'] + replace_dots = config_options['config']['misc']['replace_dots'] return (replace_spaces, replace_dots) From 1a39066cfce8d9f23ef5800827ac2c91821856a3 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Wed, 6 May 2015 00:04:35 -0700 Subject: [PATCH 06/30] Updated changelog to reflect the sab.py https fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 042039ab..ddfe656a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Released xx xxx 2015 Highlights: * Added: Filter out clean/edited/censored releases (#2198) * Added: Button on the log page to toggle verbose/debug logging +* Fixed: Connecting to SABnzbd over https with python >= 2.7.9 * Improved: Moved the 'freeze db' option to the advanced->misc. tab The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6). From 93fca1351017ebaa034c772ee47cd0288d1dc466 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 11 May 2015 17:46:24 -0700 Subject: [PATCH 07/30] Don't check for status when doing sabnzbd api call --- headphones/sab.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/headphones/sab.py b/headphones/sab.py index c7ff286c..23669110 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -48,17 +48,14 @@ def sab_api_call(request_type=None, params={}, **kwargs): params['output']='json' - logger.info("Trying to send nzb to SABnzbd") + logger.info("Attempting to connect to SABnzbd on url: %s" % url) response = request.request_json(url, params=params, **kwargs) if not response: logger.error("Error connecting to SABnzbd on url: %s" % url) return False - if response['status'] == False and response['error']: - logger.error("Error connecting to SABnzbd: %s" % response['error']) - return False else: - logger.info("Successfully connected to SABnzbd") + logger.info("Successfully connected to SABnzbd on url: %s" % url) return response def sendNZB(nzb): From 0347ca1bcc0a42578771738e67fa156568daa9ef Mon Sep 17 00:00:00 2001 From: Ade Date: Sat, 16 May 2015 09:02:38 +1200 Subject: [PATCH 08/30] Various - Email SSL - History, show Folder Name when hovering over Status - Cuesplit, allow wav, ape to be split - mb, possible fix for #2181 --- data/interfaces/default/config.html | 3 ++ data/interfaces/default/history.html | 45 +++++++++++++++------------- headphones/config.py | 1 + headphones/cuesplit.py | 25 +++++++--------- headphones/helpers.py | 2 +- headphones/mb.py | 4 +-- headphones/notifiers.py | 5 +++- headphones/postprocessor.py | 8 +---- headphones/webserve.py | 3 +- 9 files changed, 49 insertions(+), 47 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 991c004a..3b1ddb6f 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -1134,6 +1134,9 @@
+
+ +
diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index 0b7cd6ee..e27b024b 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -32,32 +32,35 @@ - %for item in history: - <% - if item['Status'] == 'Processed': - grade = 'A' - elif item['Status'] == 'Snatched': - grade = 'C' - elif item['Status'] == 'Unprocessed': - grade = 'X' - elif item['Status'] == 'Frozen': - grade = 'X' - else: - grade = 'U' + %for item in history: + <% + if item['Status'] == 'Processed': + grade = 'A' + elif item['Status'] == 'Snatched': + grade = 'C' + elif item['Status'] == 'Unprocessed': + grade = 'X' + elif item['Status'] == 'Frozen': + grade = 'X' + else: + grade = 'U' - fileid = 'unknown' - if item['URL'].find('nzb') != -1: - fileid = 'nzb' - if item['URL'].find('torrent') != -1: - fileid = 'torrent' - if item['URL'].find('rutracker') != -1: - fileid = 'torrent' - %> + fileid = 'unknown' + if item['URL'].find('nzb') != -1: + fileid = 'nzb' + if item['URL'].find('torrent') != -1: + fileid = 'torrent' + if item['URL'].find('rutracker') != -1: + fileid = 'torrent' + + folder = 'Folder: ' + item['FolderName'] + + %> ${item['DateAdded']} ${cgi.escape(item['Title'], quote=True)} [${fileid}][album page] ${helpers.bytes_to_mb(item['Size'])} - ${item['Status']} + ${item['Status']} [retry][new] diff --git a/headphones/config.py b/headphones/config.py index d3e3e301..73b66765 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -64,6 +64,7 @@ _CONFIG_DEFINITIONS = { 'EMAIL_SMTP_USER': (str, 'Email', ''), 'EMAIL_SMTP_PASSWORD': (str, 'Email', ''), 'EMAIL_SMTP_PORT': (int, 'Email', 25), + 'EMAIL_SSL': (int, 'Email', 0), 'EMAIL_TLS': (int, 'Email', 0), 'EMAIL_ONSNATCH': (int, 'Email', 0), 'EMBED_ALBUM_ART': (int, 'General', 0), diff --git a/headphones/cuesplit.py b/headphones/cuesplit.py index d9b3c7bc..55112368 100755 --- a/headphones/cuesplit.py +++ b/headphones/cuesplit.py @@ -62,9 +62,7 @@ WAVE_FILE_TYPE_BY_EXTENSION = { '.flac': 'Free Lossless Audio Codec' } -# TODO: Only alow flac for now -#SHNTOOL_COMPATIBLE = ('Waveform Audio', 'WavPack', 'Free Lossless Audio Codec') -SHNTOOL_COMPATIBLE = ('Free Lossless Audio Codec') +#SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio") # TODO: Make this better! # this module-level variable is bad. :( @@ -109,10 +107,10 @@ def split_baby(split_file, split_cmd): env['PATH'] += os.pathsep + headphones.CONFIG.CUE_SPLIT_FLAC_PATH process = subprocess.Popen(split_cmd, startupinfo=startupinfo, - - stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE, - stderr=subprocess.PIPE, env=env) + stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) stdout, stderr = process.communicate() + if process.returncode: logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace')) out = stdout if stdout else stderr @@ -592,11 +590,10 @@ def split(albumpath): splitter = 'shntool' if splitter == 'shntool' and not check_splitter(splitter): - raise ValueError('Command not found, ensure shntool with FLAC or xld (OS X) installed') + raise ValueError('Command not found, ensure shntool or xld installed') - # Determine if file can be split (only flac allowed for shntool) - if 'xld' in splitter and wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys() or \ - wave.type not in SHNTOOL_COMPATIBLE: + # Determine if file can be split + if wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys(): raise ValueError('Cannot split, audio file has unsupported extension') # Split with xld @@ -640,7 +637,7 @@ def split(albumpath): cmd.extend(['-f']) cmd.extend([SPLIT_FILE_NAME]) cmd.extend(['-o']) - cmd.extend(['flac']) + cmd.extend([wave.name_ext.lstrip('.')]) cmd.extend([wave.name]) split = split_baby(wave.name, cmd) os.remove(SPLIT_FILE_NAME) @@ -652,9 +649,9 @@ def split(albumpath): logger.info('Tagging %s...', t.name) t.tag() - # rename FLAC files - if split and CUE_META.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)): - for t in base_dir.tracks(ext='.flac', split=True): + # rename files + if split and CUE_META.count_tracks() == len(base_dir.tracks(ext=wave.name_ext, split=True)): + for t in base_dir.tracks(ext=wave.name_ext, split=True): if t.name != t.filename(): logger.info('Renaming %s to %s...', t.name, t.filename()) os.rename(t.name, t.filename()) diff --git a/headphones/helpers.py b/headphones/helpers.py index 1a319ab5..238b6dea 100644 --- a/headphones/helpers.py +++ b/headphones/helpers.py @@ -519,7 +519,7 @@ def get_downloaded_track_list(albumpath): return downloaded_track_list -def preserve_torrent_direcory(albumpath): +def preserve_torrent_directory(albumpath): """ Copy torrent directory to headphones-modified to keep files for seeding. """ diff --git a/headphones/mb.py b/headphones/mb.py index 3a150977..b0ddddce 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -218,7 +218,7 @@ def getArtist(artistid, extrasonly=False): artist_dict = {} artist = None try: - limit = 200 + limit = 100 with mb_lock: artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] newRgs = None @@ -288,7 +288,7 @@ def getArtist(artistid, extrasonly=False): mb_extras_list = [] try: - limit = 200 + limit = 100 newRgs = None while newRgs is None or len(newRgs) >= limit: with mb_lock: diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 9631660d..4c6ec1f6 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -843,7 +843,10 @@ class Email(object): message['To'] = headphones.CONFIG.EMAIL_TO try: - mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT) + if (headphones.CONFIG.EMAIL_SSL): + mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT) + else: + mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT) if (headphones.CONFIG.EMAIL_TLS): mailserver.starttls() diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index d6df01c7..8354c9ae 100755 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -188,15 +188,9 @@ def verify(albumid, albumpath, Kind=None, forced=False): # Split cue if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list): if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent": - albumpath = helpers.preserve_torrent_direcory(albumpath) + albumpath = helpers.preserve_torrent_directory(albumpath) if albumpath and helpers.cue_split(albumpath): downloaded_track_list = helpers.get_downloaded_track_list(albumpath) - else: - myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid]) - processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath) - if not processed: - renameUnprocessedFolder(albumpath, tag="Unprocessed") - return # test #1: metadata - usually works logger.debug('Verifying metadata...') diff --git a/headphones/webserve.py b/headphones/webserve.py index 84420d4d..1608d32f 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1167,6 +1167,7 @@ class WebInterface(object): "email_smtp_user": headphones.CONFIG.EMAIL_SMTP_USER, "email_smtp_password": headphones.CONFIG.EMAIL_SMTP_PASSWORD, "email_smtp_port": int(headphones.CONFIG.EMAIL_SMTP_PORT), + "email_ssl": checked(headphones.CONFIG.EMAIL_SSL), "email_tls": checked(headphones.CONFIG.EMAIL_TLS), "email_onsnatch": checked(headphones.CONFIG.EMAIL_ONSNATCH), "idtag": checked(headphones.CONFIG.IDTAG) @@ -1214,7 +1215,7 @@ class WebInterface(object): "nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled", "pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", "twitter_enabled", "twitter_onsnatch", "osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled", - "mpc_enabled", "email_enabled", "email_tls", "email_onsnatch", "customauth", "idtag" + "mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", "customauth", "idtag" ] for checked_config in checked_configs: if checked_config not in kwargs: From ccff70d9c4956656b2abb05c21786cf7dd52ec78 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Fri, 15 May 2015 20:58:09 -0700 Subject: [PATCH 09/30] Capitalized Headphones.py in osx init script --- init-scripts/init.osx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/init-scripts/init.osx b/init-scripts/init.osx index 226b8057..ba176e20 100644 --- a/init-scripts/init.osx +++ b/init-scripts/init.osx @@ -7,7 +7,7 @@ ProgramArguments /usr/bin/python - /Applications/Headphones/headphones.py + /Applications/Headphones/Headphones.py --quiet --daemon --nolaunch @@ -15,4 +15,4 @@ RunAtLoad - \ No newline at end of file + From 1bbf9a23189619ec973bc5748faf377dff112749 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 16 May 2015 19:32:40 -0700 Subject: [PATCH 10/30] Updated osx init script to prevent crashing on yosemite --- init-scripts/init.osx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/init-scripts/init.osx b/init-scripts/init.osx index ba176e20..c9960449 100644 --- a/init-scripts/init.osx +++ b/init-scripts/init.osx @@ -3,16 +3,16 @@ Label - com.headphones.headphones + headphones ProgramArguments + /usr/bin/python /Applications/Headphones/Headphones.py - --quiet - --daemon - --nolaunch RunAtLoad + KeepAlive + From f808dd131bf53bc0f7466a05d7bb58f3344e5cb4 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 16 May 2015 19:40:13 -0700 Subject: [PATCH 11/30] Fixed a couple log messages in sab.py to make it a little less redundant and fixed some log grammar in requests.py --- headphones/request.py | 2 +- headphones/sab.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/headphones/request.py b/headphones/request.py index f8e9ca8a..a8b9e84d 100644 --- a/headphones/request.py +++ b/headphones/request.py @@ -95,7 +95,7 @@ def request_response(url, method="get", auto_raise=True, "host is up and running.") except requests.Timeout: logger.error( - "Request timed out. The remote host did not respond timely.") + "Request timed out. The remote host did not respond in a timely manner.") except requests.HTTPError as e: if e.response is not None: if e.response.status_code >= 500: diff --git a/headphones/sab.py b/headphones/sab.py index 23669110..681906e3 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -48,14 +48,13 @@ def sab_api_call(request_type=None, params={}, **kwargs): params['output']='json' - logger.info("Attempting to connect to SABnzbd on url: %s" % url) response = request.request_json(url, params=params, **kwargs) if not response: - logger.error("Error connecting to SABnzbd on url: %s" % url) + logger.error("Error connecting to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) return False else: - logger.info("Successfully connected to SABnzbd on url: %s" % url) + logger.info("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) return response def sendNZB(nzb): From 5a433c42e7f04be9566a83ba5e23a139a79c1d06 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 16 May 2015 20:26:05 -0700 Subject: [PATCH 12/30] Moved kickass search to json, updated some searcher log messages --- headphones/searcher.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index 5f38905f..441deb9f 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -481,7 +481,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe categories = "3030" # Request results - logger.info('Parsing results from Headphones Indexer') + logger.info('Searching Headphones Indexer with search term: %s' % term) headers = {'User-Agent': USER_AGENT} params = { @@ -552,7 +552,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe categories = categories + ",4050" # Request results - logger.info('Parsing results from %s', newznab_host[0]) + logger.info('Parsing results from %s using search term: %s' % (newznab_host[0],term)) headers = {'User-Agent': USER_AGENT} params = { @@ -600,9 +600,6 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe categories = "3030" logger.info("Album type is audiobook/spokenword. Using audiobook category") - # Request results - logger.info('Requesting from nzbs.org') - headers = {'User-Agent': USER_AGENT} params = { "t": "search", @@ -618,7 +615,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe timeout=5 ) - logger.info('Parsing results from nzbs.org') + logger.info('Parsing results from nzbs.org using search term: %s' % term) # Process feed if data: if not len(data.entries): @@ -650,7 +647,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe logger.info("Album type is audiobook/spokenword. Searching all music categories") # Request results - logger.info('Parsing results from omgwtfnzbs') + logger.info('Parsing results from omgwtfnzbs using search term: %s' % term) headers = {'User-Agent': USER_AGENT} params = { @@ -1106,7 +1103,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose providerurl = fix_url("https://kickass.to") # Build URL - providerurl = providerurl + "/usearch/" + ka_term + providerurl = providerurl + "/json.php?" # Pick category for torrents if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: @@ -1120,28 +1117,27 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose maxsize = 300000000 # Requesting content - logger.info("Searching KAT using term: %s", ka_term) + logger.info("Searching %s using term: %s" % (provider,ka_term)) params = { - "categories[0]": "music", + "q": ka_term + "+category:music", "field": "seeders", - "sorder": "desc", - "rss": "1" + "sorder": "desc" } - data = request.request_feed(url=providerurl, params=params) + data = request.request_json(url=providerurl, params=params) # Process feed if data: - if not len(data.entries): - logger.info("No results found") + if not data['list']: + logger.info("No results found on %s using search term: %s" % (provider, ka_term)) else: - for item in data.entries: + for item in data['list']: try: rightformat = True title = item['title'] - seeders = item['torrent_seeds'] - url = item['links'][1]['href'] - size = int(item['links'][1]['length']) + seeders = item['seeds'] + url = item['torrentLink'] + size = int(item['size']) if format == "2": torrent = request.request_content(url) @@ -1190,7 +1186,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose query_items.append('bitrate:"%s"' % bitrate) # Requesting content - logger.info('Parsing results from Waffles') + logger.info('Parsing results from Waffles.fm') params = { "uid": headphones.CONFIG.WAFFLES_UID, @@ -1378,7 +1374,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose rows = data.select('table tbody tr') if not rows: - logger.info("No results found") + logger.info("No results found from The Pirate Bay using term: %s" % tpb_term) else: for item in rows: try: From 6e4cec829a0e4afe774c250022458b167e2c1527 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 16 May 2015 20:40:49 -0700 Subject: [PATCH 13/30] Updated some sabnzbd logging --- headphones/sab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/headphones/sab.py b/headphones/sab.py index 681906e3..79a67a0e 100644 --- a/headphones/sab.py +++ b/headphones/sab.py @@ -54,7 +54,7 @@ def sab_api_call(request_type=None, params={}, **kwargs): logger.error("Error connecting to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) return False else: - logger.info("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) + logger.debug("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) return response def sendNZB(nzb): @@ -82,6 +82,7 @@ def sendNZB(nzb): files = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)} headers = {'User-Agent': USER_AGENT} + logger.info("Attempting to connect to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST) if nzb.resultType == "nzb": response = sab_api_call('send_nzb', params=params) elif nzb.resultType == "nzbdata": From 0fb9234ebc713f0b49f14cfbd1f38bccaebaec42 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sat, 16 May 2015 20:47:55 -0700 Subject: [PATCH 14/30] Updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddfe656a..c760eee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ Highlights: * Added: Filter out clean/edited/censored releases (#2198) * Added: Button on the log page to toggle verbose/debug logging * Fixed: Connecting to SABnzbd over https with python >= 2.7.9 +* Fixed: Email Notifications with SSL +* Fixed: Don't limit musicbrainz results to first 100 +* Improved: Cuesplit, allow wav, ape to be split * Improved: Moved the 'freeze db' option to the advanced->misc. tab +* Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6). From dbeb31d4fe19196a5a95927c688a304bf06e06b1 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sun, 17 May 2015 13:53:08 -0700 Subject: [PATCH 15/30] Initial commit for fetching metacritic scores. Needs better presentation --- data/interfaces/default/artist.html | 5 ++- headphones/__init__.py | 12 +++++- headphones/importer.py | 5 ++- headphones/metacritic.py | 59 +++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 headphones/metacritic.py diff --git a/data/interfaces/default/artist.html b/data/interfaces/default/artist.html index 01e43f68..a19b1146 100644 --- a/data/interfaces/default/artist.html +++ b/data/interfaces/default/artist.html @@ -70,6 +70,7 @@ Name Date Type + Score Status Have Bitrate @@ -125,6 +126,7 @@ ${album['AlbumTitle']} ${album['ReleaseDate']} ${album['Type']} + ${album['CriticScore']}/${album['UserScore']} ${album['Status']} %if album['Status'] == 'Skipped' or album['Status'] == 'Ignored': [want] @@ -218,7 +220,7 @@ $("li:gt(4)", this).hide(); /* :gt() is zero-indexed */ $("li:nth-child(5)", this).after("
  • More...
  • "); /* :nth-child() is one-indexed */ }); - $("li.more a").live("click", function() { + $("li.more").on("click", 'a', function() { var li = $(this).parents("li:first"); li.parent().children().show(); li.remove(); @@ -283,6 +285,7 @@ { "sType": "date" }, null, null, + null, { "sType": "title-numeric"}, null, null diff --git a/headphones/__init__.py b/headphones/__init__.py index 349416b8..a3c359e6 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -357,7 +357,7 @@ def dbcheck(): # Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is # releasegroup id) 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, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT, SearchTerm TEXT)') + '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, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT, SearchTerm TEXT, CriticScore TEXT, UserScore TEXT)') # Format here means mp3, flac, etc. 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, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)') @@ -583,6 +583,16 @@ def dbcheck(): except sqlite3.OperationalError: c.execute('ALTER TABLE albums ADD COLUMN SearchTerm TEXT DEFAULT NULL') + try: + c.execute('SELECT CriticScore from albums') + except sqlite3.OperationalError: + c.execute('ALTER TABLE albums ADD COLUMN CriticScore TEXT DEFAULT NULL') + + try: + c.execute('SELECT UserScore from albums') + except sqlite3.OperationalError: + c.execute('ALTER TABLE albums ADD COLUMN UserScore TEXT DEFAULT NULL') + conn.commit() c.close() diff --git a/headphones/importer.py b/headphones/importer.py index c02f508f..16e5efac 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Headphones. If not, see . -from headphones import logger, helpers, db, mb, lastfm +from headphones import logger, helpers, db, mb, lastfm, metacritic from beets.mediafile import MediaFile @@ -488,6 +488,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): logger.info(u"Seeing if we need album art for: %s" % artist['artist_name']) cache.getThumb(ArtistID=artistid) + logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name']) + metacritic.update(artist['artist_name'], artist['releasegroups']) + if errors: logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name'])) else: diff --git a/headphones/metacritic.py b/headphones/metacritic.py new file mode 100644 index 00000000..5df3d601 --- /dev/null +++ b/headphones/metacritic.py @@ -0,0 +1,59 @@ +# This file is part of Headphones. +# +# Headphones 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. +# +# Headphones 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 Headphones. If not, see . + +import re +import headphones + +from headphones import db, helpers, logger, request + +def update(artist_name,release_groups): + """ Pretty simple and crude function to find the artist page on metacritic, + then parse that page to get critic & user scores for albums""" + + # First let's modify the artist name to fit the metacritic convention. + # We could just do a search, then take the top result, but at least this will + # cut down on api calls. If it's ineffective then we'll switch to search + + replacements = {" & " : " "} + pattern = re.compile(r'\b(' + '|'.join(replacements.keys()) + r')\b') + mc_artist_name = pattern.sub(lambda x: replacements[x.group()], artist_name) + + mc_artist_name = artist_name.lower().replace(" ","-") + + url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100" + + res = request.request_soup(url, parser='html.parser') + + try: + rows = res.tbody.find_all('tr') + except: + logger.info("Unable to get metacritic scores for: %s" % artist_name) + return + + myDB = db.DBConnection() + + for row in rows: + title = row.a.string + for rg in release_groups: + if rg['title'].lower() == title.lower(): + scores = row.find_all("span") + critic_score = scores[0].string + user_score = scores[1].string + controlValueDict = {"AlbumID": rg['id']} + newValueDict = {'CriticScore':critic_score,'UserScore':user_score} + myDB.upsert("albums", newValueDict, controlValueDict) + + + From 18af7d6392b6baf17264e29b4ac10021b18a86ab Mon Sep 17 00:00:00 2001 From: rembo10 Date: Sun, 17 May 2015 13:54:02 -0700 Subject: [PATCH 16/30] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c760eee7..b687dc82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Released xx xxx 2015 Highlights: +* Added: Metacritic scores * Added: Filter out clean/edited/censored releases (#2198) * Added: Button on the log page to toggle verbose/debug logging * Fixed: Connecting to SABnzbd over https with python >= 2.7.9 From 93dbf13d88efa117ef92b40fd3ff0c3897a10d51 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 19 May 2015 16:23:43 -0700 Subject: [PATCH 17/30] Few metacritic changes. Formatting on artist page, css & fixed replacements in artist name --- data/interfaces/default/artist.html | 2 +- data/interfaces/default/css/style.css | 2 ++ headphones/metacritic.py | 7 +++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/data/interfaces/default/artist.html b/data/interfaces/default/artist.html index a19b1146..638044ea 100644 --- a/data/interfaces/default/artist.html +++ b/data/interfaces/default/artist.html @@ -70,7 +70,7 @@ Name Date Type - Score + Metacritic Status Have Bitrate diff --git a/data/interfaces/default/css/style.css b/data/interfaces/default/css/style.css index 0fd4a4ea..e4922d60 100644 --- a/data/interfaces/default/css/style.css +++ b/data/interfaces/default/css/style.css @@ -1445,6 +1445,7 @@ div#artistheader h2 a { #album_table td#albumname, #album_table td#reldate, #album_table td#type, +#album_table td#score, #track_table td#duration, #upcoming_table td#select, #upcoming_table td#status, @@ -1455,6 +1456,7 @@ div#artistheader h2 a { } #album_table td#status, #album_table td#bitrate, +#album_table td#score, #album_table td#albumformat, #album_table td#wantlossless { font-size: 13px; diff --git a/headphones/metacritic.py b/headphones/metacritic.py index 5df3d601..f6fe9289 100644 --- a/headphones/metacritic.py +++ b/headphones/metacritic.py @@ -26,11 +26,10 @@ def update(artist_name,release_groups): # We could just do a search, then take the top result, but at least this will # cut down on api calls. If it's ineffective then we'll switch to search - replacements = {" & " : " "} - pattern = re.compile(r'\b(' + '|'.join(replacements.keys()) + r')\b') - mc_artist_name = pattern.sub(lambda x: replacements[x.group()], artist_name) + replacements = {" & " : " ", "." : ""} + mc_artist_name = helpers.replace_all(artist_name.lower(),replacements) - mc_artist_name = artist_name.lower().replace(" ","-") + mc_artist_name = mc_artist_name.replace(" ","-") url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100" From 4c16b53c2a1fe636267bc2dab9a86c768501102c Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 19 May 2015 17:50:54 -0700 Subject: [PATCH 18/30] SSL for headphones indexer --- headphones/searcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index 441deb9f..2d346799 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -493,7 +493,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe } data = request.request_feed( - url="http://indexer.codeshy.com/api", + url="https://indexer.codeshy.com/api", params=params, headers=headers, auth=(headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS) ) From 4005becd0a816faae22f42743c810251b0c7cfe5 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 19 May 2015 18:06:44 -0700 Subject: [PATCH 19/30] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b687dc82..fbd6a65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Highlights: * Improved: Cuesplit, allow wav, ape to be split * Improved: Moved the 'freeze db' option to the advanced->misc. tab * Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results +* Improved: SSL for headphones indexer The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6). From 885d1cc77cd63147f0db6e65fbfd7cb6cc07b29f Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 19 May 2015 18:15:05 -0700 Subject: [PATCH 20/30] Updated the codeshy.com links to point to https --- data/interfaces/default/config.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 3b1ddb6f..50dc6ae4 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -429,7 +429,7 @@
    @@ -1486,7 +1486,7 @@ From db1519bcea861576accfce5ddb770158c4012834 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 19 May 2015 20:26:50 -0700 Subject: [PATCH 21/30] Updated musicbrainz library --- lib/musicbrainzngs/__init__.py | 1 + lib/musicbrainzngs/caa.py | 177 +++++++++++++++++++++++++++ lib/musicbrainzngs/mbxml.py | 115 ++++++++++++------ lib/musicbrainzngs/musicbrainz.py | 191 +++++++++++++++++++++--------- 4 files changed, 396 insertions(+), 88 deletions(-) create mode 100644 lib/musicbrainzngs/caa.py diff --git a/lib/musicbrainzngs/__init__.py b/lib/musicbrainzngs/__init__.py index 36962ef5..22fed80d 100644 --- a/lib/musicbrainzngs/__init__.py +++ b/lib/musicbrainzngs/__init__.py @@ -1 +1,2 @@ from musicbrainzngs.musicbrainz import * +from musicbrainzngs.caa import * diff --git a/lib/musicbrainzngs/caa.py b/lib/musicbrainzngs/caa.py new file mode 100644 index 00000000..c43a5bea --- /dev/null +++ b/lib/musicbrainzngs/caa.py @@ -0,0 +1,177 @@ +# This file is part of the musicbrainzngs library +# Copyright (C) Alastair Porter, Wieland Hoffmann, and others +# This file is distributed under a BSD-2-Clause type license. +# See the COPYING file for more information. + +__all__ = [ + 'set_caa_hostname', 'get_image_list', 'get_release_group_image_list', + 'get_release_group_image_front', 'get_image_front', 'get_image_back', + 'get_image' + ] + +import json + +from musicbrainzngs import compat +from musicbrainzngs import musicbrainz + +hostname = "coverartarchive.org" + + +def set_caa_hostname(new_hostname): + """Set the base hostname for Cover Art Archive requests. + Defaults to 'coverartarchive.org'.""" + global hostname + hostname = new_hostname + + +def _caa_request(mbid, imageid=None, size=None, entitytype="release"): + """ Make a CAA request. + + :param imageid: ``front``, ``back`` or a number from the listing obtained + with :meth:`get_image_list`. + :type imageid: str + + :param size: 250, 500 + :type size: str or None + + :param entitytype: ``release`` or ``release-group`` + :type entitytype: str + """ + # Construct the full URL for the request, including hostname and + # query string. + path = [entitytype, mbid] + if imageid and size: + path.append("%s-%s" % (imageid, size)) + elif imageid: + path.append(imageid) + url = compat.urlunparse(( + 'http', + hostname, + '/%s' % '/'.join(path), + '', + '', + '' + )) + musicbrainz._log.debug("GET request for %s" % (url, )) + + # Set up HTTP request handler and URL opener. + httpHandler = compat.HTTPHandler(debuglevel=0) + handlers = [httpHandler] + + opener = compat.build_opener(*handlers) + + # Make request. + req = musicbrainz._MusicbrainzHttpRequest("GET", url, None) + # Useragent isn't needed for CAA, but we'll add it if it exists + if musicbrainz._useragent != "": + req.add_header('User-Agent', musicbrainz._useragent) + musicbrainz._log.debug("requesting with UA %s" % musicbrainz._useragent) + + resp = musicbrainz._safe_read(opener, req, None) + + # TODO: The content type declared by the CAA for JSON files is + # 'applicaiton/octet-stream'. This is not useful to detect whether the + # content is JSON, so default to decoding JSON if no imageid was supplied. + # http://tickets.musicbrainz.org/browse/CAA-75 + if imageid: + # If we asked for an image, return the image + return resp + else: + # Otherwise it's json + return json.loads(resp) + + +def get_image_list(releaseid): + """Get the list of cover art associated with a release. + + The return value is the deserialized response of the `JSON listing + `_ + returned by the Cover Art Archive API. + + If an error occurs then a musicbrainz.ResponseError will + be raised with one of the following HTTP codes: + + * 400: `Releaseid` is not a valid UUID + * 404: No release exists with an MBID of `releaseid` + * 503: Ratelimit exceeded + """ + return _caa_request(releaseid) + + +def get_release_group_image_list(releasegroupid): + """Get the list of cover art associated with a release group. + + The return value is the deserialized response of the `JSON listing + `_ + returned by the Cover Art Archive API. + + If an error occurs then a musicbrainz.ResponseError will + be raised with one of the following HTTP codes: + + * 400: `Releaseid` is not a valid UUID + * 404: No release exists with an MBID of `releaseid` + * 503: Ratelimit exceeded + """ + return _caa_request(releasegroupid, entitytype="release-group") + + +def get_release_group_image_front(releasegroupid, size=None): + """Download the front cover art for a release group. + The `size` argument and the possible error conditions are the same as for + :meth:`get_image`. + """ + return get_image(releasegroupid, "front", size=size, + entitytype="release-group") + + +def get_image_front(releaseid, size=None): + """Download the front cover art for a release. + The `size` argument and the possible error conditions are the same as for + :meth:`get_image`. + """ + return get_image(releaseid, "front", size=size) + + +def get_image_back(releaseid, size=None): + """Download the back cover art for a release. + The `size` argument and the possible error conditions are the same as for + :meth:`get_image`. + """ + return get_image(releaseid, "back", size=size) + + +def get_image(mbid, coverid, size=None, entitytype="release"): + """Download cover art for a release. The coverart file to download + is specified by the `coverid` argument. + + If `size` is not specified, download the largest copy present, which can be + very large. + + If an error occurs then a musicbrainz.ResponseError will be raised with one + of the following HTTP codes: + + * 400: `Releaseid` is not a valid UUID or `coverid` is invalid + * 404: No release exists with an MBID of `releaseid` + * 503: Ratelimit exceeded + + :param coverid: ``front``, ``back`` or a number from the listing obtained with + :meth:`get_image_list` + :type coverid: int or str + + :param size: 250, 500 or None. If it is None, the largest available picture + will be downloaded. If the image originally uploaded to the + Cover Art Archive was smaller than the requested size, only + the original image will be returned. + :type size: str or None + + :param entitytype: The type of entity for which to download the cover art. + This is either ``release`` or ``release-group``. + :type entitytype: str + :return: The binary image data + :type: str + """ + if isinstance(coverid, int): + coverid = "%d" % (coverid, ) + if isinstance(size, int): + size = "%d" % (size, ) + return _caa_request(mbid, coverid, size=size, entitytype=entitytype) diff --git a/lib/musicbrainzngs/mbxml.py b/lib/musicbrainzngs/mbxml.py index 03998143..49a4a02e 100644 --- a/lib/musicbrainzngs/mbxml.py +++ b/lib/musicbrainzngs/mbxml.py @@ -36,6 +36,22 @@ NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2", "http://musicbrainz.org/ns/ext#-2.0": "ext"} _log = logging.getLogger("musicbrainzngs") +def get_error_message(error): + """ Given an error XML message from the webservice containing + xy, return a list + of [x, y]""" + try: + tree = util.bytes_to_elementtree(error) + root = tree.getroot() + errors = [] + if root.tag == "error": + for ch in root: + if ch.tag == "text": + errors.append(ch.text) + return errors + except ET.ParseError: + return None + def make_artist_credit(artists): names = [] for artist in artists: @@ -123,6 +139,7 @@ def parse_message(message): "place": parse_place, "release": parse_release, "release-group": parse_release_group, + "series": parse_series, "recording": parse_recording, "work": parse_work, "url": parse_url, @@ -138,6 +155,7 @@ def parse_message(message): "place-list": parse_place_list, "release-list": parse_release_list, "release-group-list": parse_release_group_list, + "series-list": parse_series_list, "recording-list": parse_recording_list, "work-list": parse_work_list, "url-list": parse_url_list, @@ -297,7 +315,7 @@ def parse_relation_list(rl): def parse_relation(relation): result = {} attribs = ["type", "type-id"] - elements = ["target", "direction", "begin", "end", "ended"] + elements = ["target", "direction", "begin", "end", "ended", "ordering-key"] inner_els = {"area": parse_area, "artist": parse_artist, "label": parse_label, @@ -305,6 +323,7 @@ def parse_relation(relation): "recording": parse_recording, "release": parse_release, "release-group": parse_release_group, + "series": parse_series, "attribute-list": parse_element_list, "work": parse_work, "target": parse_relation_target @@ -324,6 +343,8 @@ def parse_release(release): "label-info-list": parse_label_info_list, "medium-list": parse_medium_list, "release-group": parse_release_group, + "tag-list": parse_tag_list, + "user-tag-list": parse_tag_list, "relation-list": parse_relation_list, "annotation": parse_annotation, "cover-art-archive": parse_caa, @@ -408,6 +429,22 @@ def parse_recording(recording): return result +def parse_series_list(sl): + return [parse_series(s) for s in sl] + +def parse_series(series): + result = {} + attribs = ["id", "type", "ext:score"] + elements = ["name", "disambiguation"] + inner_els = {"alias-list": parse_alias_list, + "relation-list": parse_relation_list, + "annotation": parse_annotation} + + result.update(parse_attributes(attribs, series)) + result.update(parse_elements(elements, inner_els, series)) + + return result + def parse_external_id_list(pl): return [parse_attributes(["id"], p)["id"] for p in pl] @@ -427,13 +464,28 @@ def parse_work(work): "alias-list": parse_alias_list, "iswc-list": parse_element_list, "relation-list": parse_relation_list, - "annotation": parse_response_message} + "annotation": parse_response_message, + "attribute-list": parse_work_attribute_list + } result.update(parse_attributes(attribs, work)) result.update(parse_elements(elements, inner_els, work)) return result +def parse_work_attribute_list(wal): + return [parse_work_attribute(wa) for wa in wal] + +def parse_work_attribute(wa): + result = {} + attribs = ["type"] + + result.update(parse_attributes(attribs, wa)) + result["attribute"] = wa.text + + return result + + def parse_url_list(ul): return [parse_url(u) for u in ul] @@ -617,45 +669,40 @@ def make_barcode_request(release2barcode): return ET.tostring(root, "utf-8") -def make_tag_request(artist2tags, recording2tags): +def make_tag_request(**kwargs): NS = "http://musicbrainz.org/ns/mmd-2.0#" root = ET.Element("{%s}metadata" % NS) - rec_list = ET.SubElement(root, "{%s}recording-list" % NS) - for rec, tags in recording2tags.items(): - rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS) - rec_xml.set("{%s}id" % NS, rec) - taglist = ET.SubElement(rec_xml, "{%s}user-tag-list" % NS) - for tag in tags: - usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS) - name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS) - name_xml.text = tag - art_list = ET.SubElement(root, "{%s}artist-list" % NS) - for art, tags in artist2tags.items(): - art_xml = ET.SubElement(art_list, "{%s}artist" % NS) - art_xml.set("{%s}id" % NS, art) - taglist = ET.SubElement(art_xml, "{%s}user-tag-list" % NS) - for tag in tags: - usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS) - name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS) - name_xml.text = tag + for entity_type in ['artist', 'label', 'place', 'recording', 'release', 'release_group', 'work']: + entity_tags = kwargs.pop(entity_type + '_tags', None) + if entity_tags is not None: + e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-'))) + for e, tags in entity_tags.items(): + e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-'))) + e_xml.set("{%s}id" % NS, e) + taglist = ET.SubElement(e_xml, "{%s}user-tag-list" % NS) + for tag in tags: + usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS) + name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS) + name_xml.text = tag + if kwargs.keys(): + raise TypeError("make_tag_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0]) return ET.tostring(root, "utf-8") -def make_rating_request(artist2rating, recording2rating): +def make_rating_request(**kwargs): NS = "http://musicbrainz.org/ns/mmd-2.0#" root = ET.Element("{%s}metadata" % NS) - rec_list = ET.SubElement(root, "{%s}recording-list" % NS) - for rec, rating in recording2rating.items(): - rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS) - rec_xml.set("{%s}id" % NS, rec) - rating_xml = ET.SubElement(rec_xml, "{%s}user-rating" % NS) - rating_xml.text = str(rating) - art_list = ET.SubElement(root, "{%s}artist-list" % NS) - for art, rating in artist2rating.items(): - art_xml = ET.SubElement(art_list, "{%s}artist" % NS) - art_xml.set("{%s}id" % NS, art) - rating_xml = ET.SubElement(art_xml, "{%s}user-rating" % NS) - rating_xml.text = str(rating) + for entity_type in ['artist', 'label', 'recording', 'release_group', 'work']: + entity_ratings = kwargs.pop(entity_type + '_ratings', None) + if entity_ratings is not None: + e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-'))) + for e, rating in entity_ratings.items(): + e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-'))) + e_xml.set("{%s}id" % NS, e) + rating_xml = ET.SubElement(e_xml, "{%s}user-rating" % NS) + rating_xml.text = str(rating) + if kwargs.keys(): + raise TypeError("make_rating_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0]) return ET.tostring(root, "utf-8") diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py index d7e5e74f..57450270 100644 --- a/lib/musicbrainzngs/musicbrainz.py +++ b/lib/musicbrainzngs/musicbrainz.py @@ -11,25 +11,25 @@ import socket import hashlib import locale import sys -import base64 +import json import xml.etree.ElementTree as etree from xml.parsers import expat -from warnings import warn, simplefilter +from warnings import warn from musicbrainzngs import mbxml from musicbrainzngs import util from musicbrainzngs import compat +import base64 + _version = "0.6devMODIFIED" _log = logging.getLogger("musicbrainzngs") -# turn on DeprecationWarnings below -simplefilter(action="once", category=DeprecationWarning) - +LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' # Constants for validation. -RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'url', 'work'] +RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'series', 'url', 'work'] RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES] TAG_INCLUDES = ["tags", "user-tags"] RATING_INCLUDES = ["ratings", "user-ratings"] @@ -43,6 +43,9 @@ VALID_INCLUDES = { ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES, 'annotation': [ + ], + 'instrument': [ + ], 'label': [ "releases", # Subqueries @@ -59,20 +62,23 @@ VALID_INCLUDES = { "artists", "labels", "recordings", "release-groups", "media", "artist-credits", "discids", "puids", "isrcs", "recording-level-rels", "work-level-rels", "annotation", "aliases" - ] + RELATION_INCLUDES, + ] + TAG_INCLUDES + RELATION_INCLUDES, 'release-group': [ "artists", "releases", "discids", "media", "artist-credits", "annotation", "aliases" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, + 'series': [ + "annotation", "aliases" + ] + RELATION_INCLUDES, 'work': [ "artists", # Subqueries "aliases", "annotation" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'url': RELATION_INCLUDES, - 'discid': [ + 'discid': [ # Discid should be the same as release "artists", "labels", "recordings", "release-groups", "media", "artist-credits", "discids", "puids", "isrcs", - "recording-level-rels", "work-level-rels" + "recording-level-rels", "work-level-rels", "annotation", "aliases" ] + RELATION_INCLUDES, 'isrc': ["artists", "releases", "puids", "isrcs"], 'iswc': ["artists"], @@ -93,7 +99,7 @@ VALID_RELEASE_TYPES = [ "nat", "album", "single", "ep", "broadcast", "other", # primary types "compilation", "soundtrack", "spokenword", "interview", "audiobook", - "live", "remix", "dj-mix", "mixtape/street", "demo", # secondary types + "live", "remix", "dj-mix", "mixtape/street", # secondary types ] #: These can be used to filter whenever releases or release-groups are involved VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"] @@ -101,6 +107,10 @@ VALID_SEARCH_FIELDS = { 'annotation': [ 'entity', 'name', 'text', 'type' ], + 'area': [ + 'aid', 'area', 'alias', 'begin', 'comment', 'end', 'ended', + 'iso', 'iso1', 'iso2', 'iso3', 'type' + ], 'artist': [ 'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment', 'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type', @@ -133,12 +143,20 @@ VALID_SEARCH_FIELDS = { 'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks', 'tracksmedium', 'type' ], + 'series': [ + 'alias', 'comment', 'sid', 'series', 'type' + ], 'work': [ 'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag', 'type', 'wid', 'work', 'workaccent' ], } +# Constants +class AUTH_YES: pass +class AUTH_NO: pass +class AUTH_IFSET: pass + # Exceptions. @@ -280,7 +298,7 @@ def auth(u, p): global user, password user = u password = p - + def hpauth(u, p): """Set the username and password to be used in subsequent queries to the MusicBrainz XML API that require authentication. @@ -559,28 +577,33 @@ def set_format(fmt="xml"): """Sets the format that should be returned by the Web Service. The server currently supports `xml` and `json`. - When you set the format to anything different from the default, - you need to provide your own parser with :func:`set_parser`. + This method will set a default parser for the specified format, + but you can modify it with :func:`set_parser`. .. warning:: The json format used by the server is different from the json format returned by the `musicbrainzngs` internal parser - when using the `xml` format! + when using the `xml` format! This format may change at any time. """ global ws_format - if fmt not in ["xml", "json"]: - raise ValueError("invalid format: %s" % fmt) - else: + if fmt == "xml": ws_format = fmt + set_parser() # set to default + elif fmt == "json": + ws_format = fmt + warn("The json format is non-official and may change at any time") + set_parser(json.loads) + else: + raise ValueError("invalid format: %s" % fmt) @_rate_limit -def _mb_request(path, method='GET', auth_required=False, client_required=False, - args=None, data=None, body=None): +def _mb_request(path, method='GET', auth_required=AUTH_NO, + client_required=False, args=None, data=None, body=None): """Makes a request for the specified `path` (endpoint) on /ws/2 on the globally-specified hostname. Parses the responses and returns the resulting object. `auth_required` and `client_required` control - whether exceptions should be raised if the client and - username/password are left unspecified, respectively. + whether exceptions should be raised if the username/password and + client are left unspecified, respectively. """ global parser_fun @@ -626,11 +649,19 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False, handlers = [httpHandler] # Add credentials if required. - if auth_required: + add_auth = False + if auth_required == AUTH_YES: _log.debug("Auth required for %s" % url) if not user: raise UsageError("authorization required; " "use auth(user, pass) first") + add_auth = True + + if auth_required == AUTH_IFSET and user: + _log.debug("Using auth for %s because user and pass is set" % url) + add_auth = True + + if add_auth: passwordMgr = _RedirectPasswordMgr() authHandler = _DigestAuthHandler(passwordMgr) authHandler.add_password("musicbrainz.org", (), user, password) @@ -641,6 +672,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False, # Make request. req = _MusicbrainzHttpRequest(method, url, data) req.add_header('User-Agent', _useragent) + # Add headphones credentials if mb_auth: @@ -658,16 +690,19 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False, return parser_fun(resp) -def _is_auth_required(entity, includes): - """ Some calls require authentication. This returns - True if a call does, False otherwise - """ - if "user-tags" in includes or "user-ratings" in includes: - return True - elif entity.startswith("collection"): - return True - else: - return False +def _get_auth_type(entity, id, includes): + """ Some calls require authentication. This returns + True if a call does, False otherwise + """ + if "user-tags" in includes or "user-ratings" in includes: + return AUTH_YES + elif entity.startswith("collection"): + if not id: + return AUTH_YES + else: + return AUTH_IFSET + else: + return AUTH_NO def _do_mb_query(entity, id, includes=[], params={}): """Make a single GET call to the MusicBrainz XML API. `entity` is a @@ -681,7 +716,7 @@ def _do_mb_query(entity, id, includes=[], params={}): if not isinstance(includes, list): includes = [includes] _check_includes(entity, includes) - auth_required = _is_auth_required(entity, includes) + auth_required = _get_auth_type(entity, id, includes) args = dict(params) if len(includes) > 0: inc = " ".join(includes) @@ -704,8 +739,8 @@ def _do_mb_search(entity, query='', fields={}, if query: clean_query = util._unicode(query) if fields: - clean_query = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', - r'\\\1', clean_query) + clean_query = re.sub(LUCENE_SPECIAL, r'\\\1', + clean_query) if strict: query_parts.append('"%s"' % clean_query) else: @@ -721,11 +756,11 @@ def _do_mb_search(entity, query='', fields={}, elif key == "puid": warn("PUID support was removed from server\n" "the 'puid' field is ignored", - DeprecationWarning, stacklevel=2) + Warning, stacklevel=2) # Escape Lucene's special characters. value = util._unicode(value) - value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\\/])', r'\\\1', value) + value = re.sub(LUCENE_SPECIAL, r'\\\1', value) if value: if strict: query_parts.append('%s:"%s"' % (key, value)) @@ -752,18 +787,18 @@ def _do_mb_search(entity, query='', fields={}, def _do_mb_delete(path): """Send a DELETE request for the specified object. """ - return _mb_request(path, 'DELETE', True, True) + return _mb_request(path, 'DELETE', AUTH_YES, True) def _do_mb_put(path): """Send a PUT request for the specified object. """ - return _mb_request(path, 'PUT', True, True) + return _mb_request(path, 'PUT', AUTH_YES, True) def _do_mb_post(path, body): """Perform a single POST call for an endpoint with a specified request body. """ - return _mb_request(path, 'POST', True, True, body=body) + return _mb_request(path, 'POST', AUTH_YES, True, body=body) # The main interface! @@ -788,6 +823,15 @@ def get_artist_by_id(id, includes=[], release_status=[], release_type=[]): release_status, release_type) return _do_mb_query("artist", id, includes, params) +@_docstring('instrument') +def get_instrument_by_id(id, includes=[], release_status=[], release_type=[]): + """Get the instrument with the MusicBrainz `id` as a dict with an 'artist' key. + + *Available includes*: {includes}""" + params = _check_filter_and_make_params("instrument", includes, + release_status, release_type) + return _do_mb_query("instrument", id, includes, params) + @_docstring('label') def get_label_by_id(id, includes=[], release_status=[], release_type=[]): """Get the label with the MusicBrainz `id` as a dict with a 'label' key. @@ -836,6 +880,13 @@ def get_release_group_by_id(id, includes=[], release_status, release_type) return _do_mb_query("release-group", id, includes, params) +@_docstring('series') +def get_series_by_id(id, includes=[]): + """Get the series with the MusicBrainz `id` as a dict with a 'series' key. + + *Available includes*: {includes}""" + return _do_mb_query("series", id, includes) + @_docstring('work') def get_work_by_id(id, includes=[]): """Get the work with the MusicBrainz `id` as a dict with a 'work' key. @@ -860,6 +911,13 @@ def search_annotations(query='', limit=None, offset=None, strict=False, **fields *Available search fields*: {fields}""" return _do_mb_search('annotation', query, fields, limit, offset, strict) +@_docstring('area') +def search_areas(query='', limit=None, offset=None, strict=False, **fields): + """Search for areas and return a dict with an 'area-list' key. + + *Available search fields*: {fields}""" + return _do_mb_search('area', query, fields, limit, offset, strict) + @_docstring('artist') def search_artists(query='', limit=None, offset=None, strict=False, **fields): """Search for artists and return a dict with an 'artist-list' key. @@ -898,6 +956,13 @@ def search_release_groups(query='', limit=None, offset=None, *Available search fields*: {fields}""" return _do_mb_search('release-group', query, fields, limit, offset, strict) +@_docstring('series') +def search_series(query='', limit=None, offset=None, strict=False, **fields): + """Search for series and return a dict with a 'series-list' key. + + *Available search fields*: {fields}""" + return _do_mb_search('series', query, fields, limit, offset, strict) + @_docstring('work') def search_works(query='', limit=None, offset=None, strict=False, **fields): """Search for works and return a dict with a 'work-list' key. @@ -907,18 +972,25 @@ def search_works(query='', limit=None, offset=None, strict=False, **fields): # Lists of entities -@_docstring('release') -def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True): - """Search for releases with a :musicbrainz:`Disc ID`. +@_docstring('discid') +def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None): + """Search for releases with a :musicbrainz:`Disc ID` or table of contents. When a `toc` is provided and no release with the disc ID is found, a fuzzy search by the toc is done. The `toc` should have to same format as :attr:`discid.Disc.toc_string`. + When a `toc` is provided, the format of the discid itself is not + checked server-side, so any value may be passed if searching by only + `toc` is desired. If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does, the CD Stub will be returned. Prevent this from happening by passing `cdstubs=False`. + By default only results that match a format that allows discids + (e.g. CD) are included. To include all media formats, pass + `media_format='all'`. + The result is a dict with either a 'disc' , a 'cdstub' key or a 'release-list' (fuzzy match with TOC). A 'disc' has a 'release-list' and a 'cdstub' key has direct 'artist' @@ -931,6 +1003,8 @@ def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True): params["toc"] = toc if not cdstubs: params["cdstubs"] = "no" + if media_format: + params["media-format"] = media_format return _do_mb_query("discid", id, includes, params) @_docstring('recording') @@ -940,7 +1014,7 @@ def get_recordings_by_echoprint(echoprint, includes=[], release_status=[], (not available on server)""" warn("Echoprints were never introduced\n" "and will not be found (404)", - DeprecationWarning, stacklevel=2) + Warning, stacklevel=2) raise ResponseError(cause=compat.HTTPError( None, 404, "Not Found", None, None)) @@ -951,7 +1025,7 @@ def get_recordings_by_puid(puid, includes=[], release_status=[], (not available on server)""" warn("PUID support was removed from the server\n" "and no PUIDs will be found (404)", - DeprecationWarning, stacklevel=2) + Warning, stacklevel=2) raise ResponseError(cause=compat.HTTPError( None, 404, "Not Found", None, None)) @@ -1116,7 +1190,7 @@ def submit_puids(recording_puids): """ warn("PUID support was dropped at the server\n" "nothing will be submitted", - DeprecationWarning, stacklevel=2) + Warning, stacklevel=2) return {'message': {'text': 'OK'}} def submit_echoprints(recording_echoprints): @@ -1125,7 +1199,7 @@ def submit_echoprints(recording_echoprints): """ warn("Echoprints were never introduced\n" "nothing will be submitted", - DeprecationWarning, stacklevel=2) + Warning, stacklevel=2) return {'message': {'text': 'OK'}} def submit_isrcs(recording_isrcs): @@ -1139,20 +1213,29 @@ def submit_isrcs(recording_isrcs): query = mbxml.make_isrc_request(rec2isrcs) return _do_mb_post("recording", query) -def submit_tags(artist_tags={}, recording_tags={}): +def submit_tags(**kwargs): """Submit user tags. - Artist or recording parameters are of the form: + Takes parameters named e.g. 'artist_tags', 'recording_tags', etc., + and of the form: {entity_id1: [tag1, ...], ...} + + The user's tags for each entity will be set to that list, adding or + removing tags as necessary. Submitting an empty list for an entity + will remove all tags for that entity by the user. """ - query = mbxml.make_tag_request(artist_tags, recording_tags) + query = mbxml.make_tag_request(**kwargs) return _do_mb_post("tag", query) -def submit_ratings(artist_ratings={}, recording_ratings={}): - """ Submit user ratings. - Artist or recording parameters are of the form: +def submit_ratings(**kwargs): + """Submit user ratings. + Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc., + and of the form: {entity_id1: rating, ...} + + Ratings are numbers from 0-100, at intervals of 20 (20 per 'star'). + Submitting a rating of 0 will remove the user's rating. """ - query = mbxml.make_rating_request(artist_ratings, recording_ratings) + query = mbxml.make_rating_request(**kwargs) return _do_mb_post("rating", query) def add_releases_to_collection(collection, releases=[]): From 658cb0403f822d450d08d9a308d930fcbe698abd Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 19 May 2015 23:53:14 -0700 Subject: [PATCH 22/30] Added support for series (initial stages - needs some cleanup) --- CHANGELOG.md | 1 + data/interfaces/default/base.html | 1 + data/interfaces/default/searchresults.html | 17 +++++- headphones/__init__.py | 7 ++- headphones/importer.py | 11 +++- headphones/mb.py | 66 +++++++++++++++++++++- headphones/webserve.py | 11 +++- 7 files changed, 106 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd6a65a..9d91fdfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Released xx xxx 2015 Highlights: * Added: Metacritic scores +* Added: Series support (e.g. Cafe Del Mar, Now That's What I Call Music, etc) * Added: Filter out clean/edited/censored releases (#2198) * Added: Button on the log page to toggle verbose/debug logging * Fixed: Connecting to SABnzbd over https with python >= 2.7.9 diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index 7bed4ac3..536d4430 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -62,6 +62,7 @@ diff --git a/data/interfaces/default/searchresults.html b/data/interfaces/default/searchresults.html index 81737326..8fea4992 100644 --- a/data/interfaces/default/searchresults.html +++ b/data/interfaces/default/searchresults.html @@ -17,8 +17,12 @@ Date Score - %else: + %elif type == 'artist': Artist Name + Score + %else: + Series Name + Type Score %endif @@ -40,8 +44,10 @@ %if type == 'album':
    - %else: + %elif type == 'artist':
    + %else: + %endif %if type == 'album': ${result['title']} @@ -52,9 +58,14 @@
    ${result['score']}
    ${result['albumid']} - %else: + %elif type == 'artist': ${result['uniquename']}
    ${result['score']}
    + + %else: + ${result['uniquename']} + ${result['type']} +
    ${result['score']}
    %endif diff --git a/headphones/__init__.py b/headphones/__init__.py index a3c359e6..6a45e259 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -352,7 +352,7 @@ 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, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT)') + '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, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT, Type TEXT)') # ReleaseFormat here means CD,Digital,Vinyl, etc. If using the default # Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is # releasegroup id) @@ -593,6 +593,11 @@ def dbcheck(): except sqlite3.OperationalError: c.execute('ALTER TABLE albums ADD COLUMN UserScore TEXT DEFAULT NULL') + try: + c.execute('SELECT Type from artists') + except sqlite3.OperationalError: + c.execute('ALTER TABLE artists ADD COLUMN Type TEXT DEFAULT NULL') + conn.commit() c.close() diff --git a/headphones/importer.py b/headphones/importer.py index 16e5efac..dc6e490b 100644 --- a/headphones/importer.py +++ b/headphones/importer.py @@ -111,7 +111,7 @@ def addArtistIDListToDB(artistidlist): addArtisttoDB(artistid) -def addArtisttoDB(artistid, extrasonly=False, forcefull=False): +def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"): # Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums from headphones import cache @@ -142,12 +142,19 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False): "Status": "Loading", "IncludeExtras": headphones.CONFIG.INCLUDE_EXTRAS, "Extras": headphones.CONFIG.EXTRAS} + if type=="series": + newValueDict['Type'] = "series" else: newValueDict = {"Status": "Loading"} + if dbartist["Type"] == "series": + type = "series" myDB.upsert("artists", newValueDict, controlValueDict) - artist = mb.getArtist(artistid, extrasonly) + if type=="series": + artist = mb.getSeries(artistid) + else: + artist = mb.getArtist(artistid, extrasonly) if artist and artist.get('artist_name') in blacklisted_special_artist_names: logger.warn('Cannot import blocked special purpose artist: %s' % artist.get('artist_name')) diff --git a/headphones/mb.py b/headphones/mb.py index b0ddddce..5aa71143 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -135,7 +135,6 @@ def findArtist(name, limit=1): }) return artistlist - def findRelease(name, limit=1, artist=None): releaselist = [] releaseResults = None @@ -213,6 +212,39 @@ def findRelease(name, limit=1, artist=None): }) return releaselist +def findSeries(name, limit=1): + serieslist = [] + seriesResults = None + + chars = set('!?*-') + if any((c in chars) for c in name): + name = '"' + name + '"' + + criteria = {'series': name.lower()} + + with mb_lock: + try: + seriesResults = musicbrainzngs.search_series(limit=limit, **criteria)['series-list'] + except musicbrainzngs.WebServiceError as e: + logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) + mb_lock.snooze(5) + + if not seriesResults: + return False + for result in seriesResults: + if 'disambiguation' in result: + uniquename = unicode(result['name'] + " (" + result['disambiguation'] + ")") + else: + uniquename = unicode(result['name']) + serieslist.append({ + 'uniquename': uniquename, + 'name': unicode(result['name']), + 'type': unicode(result['type']), + 'id': unicode(result['id']), + 'url': unicode("http://musicbrainz.org/series/" + result['id']),#probably needs to be changed + 'score': int(result['ext:score']) + }) + return serieslist def getArtist(artistid, extrasonly=False): artist_dict = {} @@ -316,6 +348,38 @@ def getArtist(artistid, extrasonly=False): artist_dict['releasegroups'] = releasegroups return artist_dict +def getSeries(seriesid): + series_dict = {} + series = None + try: + with mb_lock: + series = musicbrainzngs.get_series_by_id(seriesid,includes=['release-group-rels'])['series'] + except musicbrainzngs.WebServiceError as e: + logger.warn('Attempt to retrieve series information from MusicBrainz failed for seriesid: %s (%s)' % (seriesid, str(e))) + mb_lock.snooze(5) + except Exception as e: + pass + + if not series: + return False + + if 'disambiguation' in series: + series_dict['artist_name'] = unicode(series['name'] + " (" + unicode(series['disambiguation']) + ")") + else: + series_dict['artist_name'] = unicode(series['name']) + + releasegroups = [] + + for rg in series['release_group-relation-list']: + releasegroup = rg['release-group'] + releasegroups.append({ + 'title':releasegroup['title'], + 'date':releasegroup['first-release-date'], + 'id':releasegroup['id'], + 'type':rg['type'] + }) + series_dict['releasegroups'] = releasegroups + return series_dict def getReleaseGroup(rgid): """ diff --git a/headphones/webserve.py b/headphones/webserve.py index 1608d32f..5a41dc3b 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -145,8 +145,10 @@ class WebInterface(object): raise cherrypy.HTTPRedirect("home") if type == 'artist': searchresults = mb.findArtist(name, limit=100) - else: + elif type == 'album': searchresults = mb.findRelease(name, limit=100) + else: + searchresults = mb.findSeries(name, limit=100) return serve_template(templatename="searchresults.html", title='Search Results for: "' + name + '"', searchresults=searchresults, name=name, type=type) @cherrypy.expose @@ -156,6 +158,13 @@ class WebInterface(object): thread.join(1) raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % artistid) + @cherrypy.expose + def addSeries(self, seriesid): + thread = threading.Thread(target=importer.addArtisttoDB, args=[seriesid, False, False, "series"]) + thread.start() + thread.join(1) + raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % seriesid) + @cherrypy.expose def getExtras(self, ArtistID, newstyle=False, **kwargs): # if calling this function without the newstyle, they're using the old format From 39f07025c691e9201915c332bf808914636287e8 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Wed, 20 May 2015 10:26:04 -0700 Subject: [PATCH 23/30] Removed a check from postprocessor normalizedAlbumArtist/artistTitleCount, fixed some logging --- headphones/postprocessor.py | 2 +- headphones/searcher.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index 8354c9ae..d5351234 100755 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -49,7 +49,7 @@ def checkFolder(): download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR album_path = os.path.join(download_dir, album['FolderName']).encode(headphones.SYS_ENCODING, 'replace') - logger.info("Checking if %s exists" % album_path) + logger.debug("Checking if %s exists" % album_path) if os.path.exists(album_path): logger.info('Found "' + album['FolderName'] + '" in ' + album['Kind'] + ' download folder. Verifying....') diff --git a/headphones/searcher.py b/headphones/searcher.py index 2d346799..70d05a0d 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -321,8 +321,10 @@ def more_filtering(results, album, albumlength, new): normalizedResultTitle = removeDisallowedFilenameChars(result[0]) artistTitleCount = normalizedResultTitle.count(normalizedAlbumArtist) - if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2: - continue + # WHAT DOES THIS DO? + #if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2: + # logger.info("Removing %s from %s" % (result[0], result[3])) + # continue if low_size_limit and (int(result[1]) < low_size_limit): logger.info("%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(low_size_limit)) @@ -400,7 +402,7 @@ def sort_search_results(resultlist, album, new, albumlength): finallist = sorted(newlist, key=lambda title: (-title[5], title[6])) if not len(finallist) and len(flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS: - logger.info("Since there were no appropriate lossy matches (and at least one lossless match, going to use lossless instead") + logger.info("Since there were no appropriate lossy matches (and at least one lossless match), going to use lossless instead") finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])), reverse=True) except Exception: logger.exception('Unhandled exception') From a7632cc1e5f6172c34d1f6da4eb580a2460e55c2 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Thu, 21 May 2015 18:36:58 -0700 Subject: [PATCH 24/30] For series, use series name + year as search term --- headphones/searcher.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index 70d05a0d..1b58151f 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -443,6 +443,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe # Use the provided search term if available, otherwise build a search term if album['SearchTerm']: term = album['SearchTerm'] + elif album['Type'] == 'part of': + term = cleanalbum + " " + year else: # FLAC usually doesn't have a year for some reason so leave it out. # Various Artist albums might be listed as VA, so I'll leave that out too @@ -1050,7 +1052,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose # of these torrent providers are just using cleanartist/cleanalbum terms if album['SearchTerm']: term = album['SearchTerm'] - + elif album['Type'] == 'part of': + term = cleanalbum + " " + year else: # FLAC usually doesn't have a year for some reason so I'll leave it out # Various Artist albums might be listed as VA, so I'll leave that out too From cc2aceb3d80a7421e7b76ffa6d5e64cbf5d32ab7 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Thu, 21 May 2015 19:14:58 -0700 Subject: [PATCH 25/30] Fix for nzbget connection url (#2135) --- CHANGELOG.md | 1 + headphones/nzbget.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d91fdfb..633c6400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Highlights: * Fixed: Connecting to SABnzbd over https with python >= 2.7.9 * Fixed: Email Notifications with SSL * Fixed: Don't limit musicbrainz results to first 100 +* Fixed: nzbget url fix * Improved: Cuesplit, allow wav, ape to be split * Improved: Moved the 'freeze db' option to the advanced->misc. tab * Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results diff --git a/headphones/nzbget.py b/headphones/nzbget.py index 4ed18b7e..6a341a73 100644 --- a/headphones/nzbget.py +++ b/headphones/nzbget.py @@ -32,20 +32,20 @@ from headphones import logger def sendNZB(nzb): addToTop = False - nzbgetXMLrpc = "%(username)s:%(password)s@%(host)s/xmlrpc" + nzbgetXMLrpc = "%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc" - if headphones.CONFIG.NZBGET_HOST is None: + if not headphones.CONFIG.NZBGET_HOST: logger.error(u"No NZBget host found in configuration. Please configure it.") return False if headphones.CONFIG.NZBGET_HOST.startswith('https://'): - nzbgetXMLrpc = 'https://' + nzbgetXMLrpc - headphones.CONFIG.NZBGET_HOST.replace('https://', '', 1) + protocol = 'https' + host = headphones.CONFIG.NZBGET_HOST.replace('https://', '', 1) else: - nzbgetXMLrpc = 'http://' + nzbgetXMLrpc - headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1) + protocol = 'http' + host = headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1) - url = nzbgetXMLrpc % {"host": headphones.CONFIG.NZBGET_HOST, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD} + url = nzbgetXMLrpc % {"protocol": protocol, "host": host, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD} nzbGetRPC = xmlrpclib.ServerProxy(url) try: From 206ea72a27ffde9c49b3e976fb960f517bf040b0 Mon Sep 17 00:00:00 2001 From: Ade Date: Sat, 23 May 2015 08:59:56 +1200 Subject: [PATCH 26/30] mb demo --- lib/musicbrainzngs/musicbrainz.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py index 57450270..c765dcf5 100644 --- a/lib/musicbrainzngs/musicbrainz.py +++ b/lib/musicbrainzngs/musicbrainz.py @@ -100,6 +100,7 @@ VALID_RELEASE_TYPES = [ "album", "single", "ep", "broadcast", "other", # primary types "compilation", "soundtrack", "spokenword", "interview", "audiobook", "live", "remix", "dj-mix", "mixtape/street", # secondary types + "demo", # headphones ] #: These can be used to filter whenever releases or release-groups are involved VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"] From 84bca81a9abfe1ee5e004455fcd135490601a2c4 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Fri, 22 May 2015 20:23:17 -0700 Subject: [PATCH 27/30] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633c6400..64e278e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Highlights: * Improved: Moved the 'freeze db' option to the advanced->misc. tab * Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results * Improved: SSL for headphones indexer +* Improved: Disable update dialog box if check_github is diabled The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6). From fa85639efb07f2bd3a9f7fc4916e9f9fff3b33b3 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Fri, 22 May 2015 20:25:40 -0700 Subject: [PATCH 28/30] Hide update bar if github checking is turned off --- data/interfaces/default/base.html | 4 ++-- headphones/__init__.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index 536d4430..b123fe18 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -31,12 +31,12 @@
    - % if not headphones.CURRENT_VERSION: + % if headphones.CONFIG.CHECK_GITHUB and not headphones.CURRENT_VERSION:
    You're running an unknown version of Headphones. Update or Close
    - % elif headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win': + % elif headphones.CONFIG.CHECK_GITHUB and headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
    A newer version is available. You're ${headphones.COMMITS_BEHIND} commits behind. Update or Close
    diff --git a/headphones/__init__.py b/headphones/__init__.py index 6a45e259..ef2d4283 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -176,7 +176,7 @@ def initialize(config_file): version_lock_file, e) # Check for new versions - if CONFIG.CHECK_GITHUB_ON_STARTUP: + if CONFIG.CHECK_GITHUB and CONFIG.CHECK_GITHUB_ON_STARTUP: try: LATEST_VERSION = versioncheck.checkGithub() except: @@ -288,11 +288,12 @@ def initialize_scheduler(): schedule_job(updater.dbUpdate, 'MusicBrainz Update', hours=hours, minutes=0) #Update check - if CONFIG.CHECK_GITHUB_INTERVAL: - minutes = CONFIG.CHECK_GITHUB_INTERVAL - else: - minutes = 0 - schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes) + if CONFIG.CHECK_GITHUB: + if CONFIG.CHECK_GITHUB_INTERVAL: + minutes = CONFIG.CHECK_GITHUB_INTERVAL + else: + minutes = 0 + schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes) # Remove Torrent + data if Post Processed and finished Seeding minutes = CONFIG.TORRENT_REMOVAL_INTERVAL From a3524ef8aa50c9dbe76ce6e23e071ad61ec24977 Mon Sep 17 00:00:00 2001 From: Ade Date: Tue, 2 Jun 2015 09:02:29 +1200 Subject: [PATCH 29/30] OS X Notification fix --- headphones/notifiers.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/headphones/notifiers.py b/headphones/notifiers.py index 4c6ec1f6..5592a347 100644 --- a/headphones/notifiers.py +++ b/headphones/notifiers.py @@ -732,23 +732,32 @@ class OSX_NOTIFY(object): self.objc = __import__("objc") self.AppKit = __import__("AppKit") except: + logger.warn('OS X Notification: Cannot import objc or AppKit') return False def swizzle(self, cls, SEL, func): - old_IMP = cls.instanceMethodForSelector_(SEL) + old_IMP = getattr(cls, SEL, None) + if old_IMP is None: + old_IMP = cls.instanceMethodForSelector_(SEL) def wrapper(self, *args, **kwargs): return func(self, old_IMP, *args, **kwargs) - new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector, - signature=old_IMP.signature) - self.objc.classAddMethod(cls, SEL, new_IMP) + + new_IMP = self.objc.selector( + wrapper, + selector=old_IMP.selector, + signature=old_IMP.signature + ) + self.objc.classAddMethod(cls, SEL.encode(), new_IMP) def notify(self, title, subtitle=None, text=None, sound=True, image=None): try: - self.swizzle(self.objc.lookUpClass('NSBundle'), - b'bundleIdentifier', - self.swizzled_bundleIdentifier) + self.swizzle( + self.objc.lookUpClass('NSBundle'), + 'bundleIdentifier', + self.swizzled_bundleIdentifier + ) NSUserNotification = self.objc.lookUpClass('NSUserNotification') NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter') From 4b8222d7922395571bcf47ce09aeceeb3a8b8fcc Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 8 Jun 2015 11:37:51 -0700 Subject: [PATCH 30/30] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e278e4..62bf7df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## v0.5.6 -Released xx xxx 2015 +Released 08 June 2015 Highlights: * Added: Metacritic scores @@ -12,6 +12,7 @@ Highlights: * Fixed: Email Notifications with SSL * Fixed: Don't limit musicbrainz results to first 100 * Fixed: nzbget url fix +* Fixed: OSX Notifications * Improved: Cuesplit, allow wav, ape to be split * Improved: Moved the 'freeze db' option to the advanced->misc. tab * Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results