diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index d38fa710..5ee7a95b 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -470,7 +470,14 @@
- + +
+ + Reject if target size is not in bitrate range: \ + to\ + kbps + +
Preferred Bitrate: kbps
Reject if less than % or more than % of the target size (leave blank for no limit)

@@ -496,7 +503,7 @@
- Results with these words in the title will be preferred over results without them + Results with these words in the title will be preferred over results without them (search provider names can also be entered)
@@ -1587,6 +1594,15 @@ } }); + if ($("#lossless_only").is(":checked")) + { + $("#lossless_only_options").show(); + } + else + { + $("#lossless_only_options").hide(); + } + if ($("#preferred_bitrate").is(":checked")) { $("#preferred_bitrate_options").show(); @@ -1632,18 +1648,22 @@ if ($("#preferred_bitrate").is(":checked")) { $("#preferred_bitrate_options").slideDown("fast"); + $("#lossless_only_options").slideUp("fast"); } if ($("#preferred_quality0").is(":checked")) { $("#preferred_bitrate_options").slideUp("fast"); + $("#lossless_only_options").slideUp("fast"); } if ($("#preferred_quality1").is(":checked")) { $("#preferred_bitrate_options").slideUp("fast"); + $("#lossless_only_options").slideUp("fast"); } - if ($("#preferred_quality3").is(":checked")) + if ($("#lossless_only").is(":checked")) { - $("#preferred_bitrate_options").slideUp("fast"); + $("#lossless_only_options").slideDown("fast"); + $("#preferred_bitrate_options").slideUp("fast"); } if ($("#nzb_downloader_sabnzbd").is(":checked")) { diff --git a/headphones/__init__.py b/headphones/__init__.py index 9aa42ba1..0fff644b 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -103,6 +103,8 @@ PREFERRED_BITRATE_HIGH_BUFFER = None PREFERRED_BITRATE_LOW_BUFFER = None PREFERRED_BITRATE_ALLOW_LOSSLESS = False DETECT_BITRATE = False +LOSSLESS_BITRATE_FROM = None +LOSSLESS_BITRATE_TO = None ADD_ARTISTS = False CORRECT_METADATA = False MOVE_FILES = False @@ -358,7 +360,7 @@ def initialize(): PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \ MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \ XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ - PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ + PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, LOSSLESS_BITRATE_FROM, LOSSLESS_BITRATE_TO, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \ PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED @@ -438,6 +440,8 @@ def initialize(): PREFERRED_BITRATE_LOW_BUFFER = check_setting_int(CFG, 'General', 'preferred_bitrate_low_buffer', '') PREFERRED_BITRATE_ALLOW_LOSSLESS = bool(check_setting_int(CFG, 'General', 'preferred_bitrate_allow_lossless', 0)) DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0)) + LOSSLESS_BITRATE_FROM = check_setting_int(CFG, 'General', 'lossless_bitrate_from', '') + LOSSLESS_BITRATE_TO = check_setting_int(CFG, 'General', 'lossless_bitrate_to', '') ADD_ARTISTS = bool(check_setting_int(CFG, 'General', 'auto_add_artists', 1)) CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0)) MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0)) @@ -862,6 +866,8 @@ def config_write(): new_config['General']['preferred_bitrate_low_buffer'] = PREFERRED_BITRATE_LOW_BUFFER new_config['General']['preferred_bitrate_allow_lossless'] = int(PREFERRED_BITRATE_ALLOW_LOSSLESS) new_config['General']['detect_bitrate'] = int(DETECT_BITRATE) + new_config['General']['lossless_bitrate_from'] = LOSSLESS_BITRATE_FROM + new_config['General']['lossless_bitrate_to'] = LOSSLESS_BITRATE_TO new_config['General']['auto_add_artists'] = int(ADD_ARTISTS) new_config['General']['correct_metadata'] = int(CORRECT_METADATA) new_config['General']['move_files'] = int(MOVE_FILES) diff --git a/headphones/searcher.py b/headphones/searcher.py index 0a33df12..c26b5826 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -151,11 +151,20 @@ def sort_search_results(resultlist, album, new): # Add a priority if it has any of the preferred words temp_list = [] + preferred_words = None + if headphones.PREFERRED_WORDS: + preferred_words = helpers.split_string(headphones.PREFERRED_WORDS) for result in resultlist: - if headphones.PREFERRED_WORDS and any(word.lower() in result[0].lower() for word in helpers.split_string(headphones.PREFERRED_WORDS)): - temp_list.append((result[0],result[1],result[2],result[3],result[4],1)) - else: - temp_list.append((result[0],result[1],result[2],result[3],result[4],0)) + priority = 0 + if preferred_words: + if any(word.lower() in result[0].lower() for word in preferred_words): + priority = 1 + # add a search provider priority (weighted based on position) + i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()), None) + if i != None: + priority += round((len(preferred_words) - i) / float(len(preferred_words)),2) + + temp_list.append((result[0],result[1],result[2],result[3],result[4],priority)) resultlist = temp_list @@ -218,6 +227,34 @@ def sort_search_results(resultlist, album, new): finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True) + # lossless - ignore results if target size outside bitrate range + elif headphones.PREFERRED_QUALITY == 3 and (headphones.LOSSLESS_BITRATE_FROM or headphones.LOSSLESS_BITRATE_TO): + + finallist = [] + tracks = myDB.select('SELECT TrackDuration from tracks WHERE AlbumID=?', [album['AlbumID']]) + + if len(tracks): + + albumlength = sum([pair[0] for pair in tracks]) + mintargetsize = 0 + maxtargetsize = 0 + if headphones.LOSSLESS_BITRATE_FROM: + mintargetsize = albumlength/1000 * int(headphones.LOSSLESS_BITRATE_FROM) * 128 + if headphones.LOSSLESS_BITRATE_TO: + maxtargetsize = albumlength/1000 * int(headphones.LOSSLESS_BITRATE_TO) * 128 + + if mintargetsize > 0 or maxtargetsize > 0: + for i, result in reversed(list(enumerate(resultlist))): + if int(result[1]) < mintargetsize and mintargetsize > 0 or int(result[1]) > maxtargetsize and maxtargetsize > 0: + if int(result[1]) < mintargetsize: + logger.info("%s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(mintargetsize)) + else: + logger.info("%s is too large for this album - not considering it. (Size: %s, Maxsize: %s)", result[0], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(maxtargetsize)) + del resultlist[i] + + if len (resultlist): + finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True) + else: finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True) diff --git a/headphones/searcher_rutracker.py b/headphones/searcher_rutracker.py index 8837d205..3c799f24 100644 --- a/headphones/searcher_rutracker.py +++ b/headphones/searcher_rutracker.py @@ -18,6 +18,7 @@ from tempfile import mkdtemp class Rutracker(): logged_in = False + # Stores a number of login attempts to prevent recursion. #login_counter = 0 @@ -29,7 +30,7 @@ class Rutracker(): def login(self, login, password): """Implements tracker login procedure.""" - + self.logged_in = False if login is None or password is None: @@ -51,7 +52,6 @@ class Rutracker(): pass # Check if we're logged in - for cookie in self.cookiejar: if cookie.name == 'bb_data': self.logged_in = True @@ -64,7 +64,6 @@ class Rutracker(): """ # Build search url - searchterm = '' if artist != 'Various Artists': searchterm = artist @@ -82,8 +81,7 @@ class Rutracker(): else: format = '+mp3||aac' - # sort by size, descending. - + # sort by size, descending. sort = '&o=7&s=2' searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort) @@ -111,25 +109,21 @@ class Rutracker(): #logger.debug (soup.prettify()) # Title - for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}): title = link.get_text() titles.append(title) # Download URL - for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}): url = link.get('href') urls.append(url) # Seeders - for link in soup.find_all('b', attrs={'class' : 'seedmed'}): seeder = link.get_text() seeders.append(seeder) # Size - for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}): size = link.u.string sizes.append(size) @@ -138,30 +132,33 @@ class Rutracker(): pass # Combine lists - torrentlist = zip(titles, urls, seeders, sizes) # return if nothing found - if not torrentlist: return False - - # get headphones track count for album, return if not found - - myDB = db.DBConnection() - tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) - hptrackcount = len(tracks) - - if not hptrackcount: - logger.info('headphones track info not found, cannot compare to torrent') - return False - - # Return all valid entries, ignored, required words now checked in searcher.py - - #unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd'] - formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif'] - deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive'] + # don't bother checking track counts anymore, let searcher filter instead + # leave code in just in case + check_track_count = False + + if check_track_count: + + # get headphones track count for album, return if not found + myDB = db.DBConnection() + tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) + hptrackcount = len(tracks) + + if not hptrackcount: + logger.info('headphones track info not found, cannot compare to torrent') + return False + + # Return all valid entries, ignored, required words now checked in searcher.py + + #unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd'] + + formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif'] + deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive'] for torrent in torrentlist: @@ -169,105 +166,102 @@ class Rutracker(): url = torrent[1] seeders = torrent[2] size = torrent[3] - - title = returntitle.lower() - - if int(size) <= maxsize and int(seeders) >= minseeders: - - # Check torrent info - - torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t'] - self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)) - - # Debug - #for cookie in self.cookiejar: - # logger.debug ('Cookie: %s' % cookie) - - try: - page = self.opener.open(url) - torrent = page.read() - if torrent: - decoded = bencode.bdecode(torrent) - metainfo = decoded['info'] - page.close () - except Exception, e: - logger.error('Error getting torrent: %s' % e) - return False - - # get torrent track count and check for cue - - trackcount = 0 - cuecount = 0 - - if 'files' in metainfo: # multi - for pathfile in metainfo['files']: - path = pathfile['path'] - for file in path: - if any(file.lower().endswith('.' + x.lower()) for x in formatlist): - trackcount += 1 - if '.cue' in file: - cuecount += 1 - - #Torrent topic page - - topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id - logger.debug ('torrent title: %s' % title) - logger.debug ('headphones trackcount: %s' % hptrackcount) - logger.debug ('rutracker trackcount: %s' % trackcount) - # If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s) - # This is for the case where we have a single .flac/.wav which can be split by cue - # Not great, but shouldn't be doing this too often - - totallogcount = 0 - if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount: - page = self.opener.open(topicurl, timeout=60) - soup = BeautifulSoup(page.read()) - findtoc = soup.find_all(text='TOC of the extracted CD') - if not findtoc: - findtoc = soup.find_all(text='TOC извлечённого CD') - for toc in findtoc: - logcount = 0 - for toccontent in toc.find_all_next(text=True): - cut_string = toccontent.split('|') - new_string = cut_string[0].lstrip().rstrip() - if new_string == '1' or new_string == '01': - logcount = 1 - elif logcount > 0: - if new_string.isdigit(): - logcount += 1 - else: - break - totallogcount = totallogcount + logcount - - if totallogcount > 0: - trackcount = totallogcount - logger.debug ('rutracker logtrackcount: %s' % totallogcount) - - # If torrent track count = hp track count then return torrent, - # if greater, check for deluxe/special/foreign editions - # if less, then allow if it's a single track with a cue - - valid = False - - if trackcount == hptrackcount: + if int(size) <= maxsize and int(seeders) >= minseeders: + + #Torrent topic page + torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t'] + topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id + + # add to list + if not check_track_count: valid = True - elif trackcount > hptrackcount: - if any(deluxe in title for deluxe in deluxelist): + else: + + # Check torrent info + self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)) + + # Debug + #for cookie in self.cookiejar: + # logger.debug ('Cookie: %s' % cookie) + + try: + page = self.opener.open(url) + torrent = page.read() + if torrent: + decoded = bencode.bdecode(torrent) + metainfo = decoded['info'] + page.close () + except Exception, e: + logger.error('Error getting torrent: %s' % e) + return False + + # get torrent track count and check for cue + trackcount = 0 + cuecount = 0 + + if 'files' in metainfo: # multi + for pathfile in metainfo['files']: + path = pathfile['path'] + for file in path: + if any(file.lower().endswith('.' + x.lower()) for x in formatlist): + trackcount += 1 + if '.cue' in file: + cuecount += 1 + + title = returntitle.lower() + logger.debug ('torrent title: %s' % title) + logger.debug ('headphones trackcount: %s' % hptrackcount) + logger.debug ('rutracker trackcount: %s' % trackcount) + + # If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s) + # This is for the case where we have a single .flac/.wav which can be split by cue + # Not great, but shouldn't be doing this too often + totallogcount = 0 + if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount: + page = self.opener.open(topicurl, timeout=60) + soup = BeautifulSoup(page.read()) + findtoc = soup.find_all(text='TOC of the extracted CD') + if not findtoc: + findtoc = soup.find_all(text='TOC извлечённого CD') + for toc in findtoc: + logcount = 0 + for toccontent in toc.find_all_next(text=True): + cut_string = toccontent.split('|') + new_string = cut_string[0].lstrip().rstrip() + if new_string == '1' or new_string == '01': + logcount = 1 + elif logcount > 0: + if new_string.isdigit(): + logcount += 1 + else: + break + totallogcount = totallogcount + logcount + + if totallogcount > 0: + trackcount = totallogcount + logger.debug ('rutracker logtrackcount: %s' % totallogcount) + + # If torrent track count = hp track count then return torrent, + # if greater, check for deluxe/special/foreign editions + # if less, then allow if it's a single track with a cue + valid = False + + if trackcount == hptrackcount: valid = True + elif trackcount > hptrackcount: + if any(deluxe in title for deluxe in deluxelist): + valid = True # Add to list - if valid: rulist.append((returntitle, size, topicurl)) else: if topicurl: logger.info(u'Torrent found with %s tracks but the selected headphones release has %s tracks, skipping for rutracker.org' % (topicurl, trackcount, hptrackcount)) - else: logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (returntitle, int(size), int(seeders))) - return rulist def get_torrent(self, url, savelocation=None): diff --git a/headphones/transmission.py b/headphones/transmission.py index e2e321b3..17ad434e 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -30,7 +30,14 @@ from headphones import logger, notifiers, request def addTorrent(link): method = 'torrent-add' - arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR} + + if link.endswith('.torrent'): + f = open(link,'rb') + metainfo = str(base64.b64encode(f.read())) + f.close() + arguments = {'metainfo': metainfo, 'download-dir':headphones.DOWNLOAD_TORRENT_DIR} + else: + arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR} response = torrentAction(method,arguments) diff --git a/headphones/webserve.py b/headphones/webserve.py index 6db4c1f3..709de4f0 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1000,6 +1000,8 @@ class WebInterface(object): "pref_bitrate_low" : headphones.PREFERRED_BITRATE_LOW_BUFFER, "pref_bitrate_allow_lossless" : checked(headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS), "detect_bitrate" : checked(headphones.DETECT_BITRATE), + "lossless_bitrate_from" : headphones.LOSSLESS_BITRATE_FROM, + "lossless_bitrate_to" : headphones.LOSSLESS_BITRATE_TO, "move_files" : checked(headphones.MOVE_FILES), "rename_files" : checked(headphones.RENAME_FILES), "correct_metadata" : checked(headphones.CORRECT_METADATA), @@ -1136,8 +1138,8 @@ class WebInterface(object): xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None, pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0, osx_notify_enabled=0, osx_notify_onsnatch=0, osx_notify_app=None, boxcar_enabled=0, boxcar_onsnatch=0, boxcar_token=None, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None, - preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None, - plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0, + preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, lossless_bitrate_from=None, lossless_bitrate_to=None, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None, + file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0, songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, mpc_enabled=False, **kwargs ): headphones.HTTP_HOST = http_host @@ -1216,6 +1218,8 @@ class WebInterface(object): headphones.PREFERRED_BITRATE_LOW_BUFFER = preferred_bitrate_low_buffer headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS = preferred_bitrate_allow_lossless headphones.DETECT_BITRATE = detect_bitrate + headphones.LOSSLESS_BITRATE_FROM = lossless_bitrate_from + headphones.LOSSLESS_BITRATE_TO = lossless_bitrate_to headphones.MOVE_FILES = move_files headphones.CORRECT_METADATA = correct_metadata headphones.RENAME_FILES = rename_files