diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index d88cbfdd..84f3770f 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -708,6 +708,10 @@
+
+ + +
diff --git a/headphones/config.py b/headphones/config.py index 9679981f..7f29d50e 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -306,6 +306,7 @@ _CONFIG_DEFINITIONS = { 'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1), 'WAIT_UNTIL_RELEASE_DATE': (int, 'General', 0), 'REDACTED': (int, 'Redacted', 0), + 'REDACTED_APIKEY': (str, 'Redacted', ''), 'REDACTED_USERNAME': (str, 'Redacted', ''), 'REDACTED_PASSWORD': (str, 'Redacted', ''), 'REDACTED_RATIO': (str, 'Redacted', ''), diff --git a/headphones/searcher.py b/headphones/searcher.py index 65d486e7..f47e1fa7 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -68,12 +68,9 @@ TORRENT_TO_MAGNET_SERVICES = [ 'https://www.seedpeer.me/torrent/%s' ] -# Persistent Orpheus.network API object -orpheusobj = None ruobj = None -# Persistent RED API object -redobj = None - +# Persistent Orpheus.network and RED API objects +gazelleobjs = {} def fix_url(s, charset="utf-8"): @@ -1330,7 +1327,7 @@ def verifyresult(title, artistterm, term, lossless): dumbtoken = replace_all(token, dic) if not has_token(title, dumbtoken): logger.info( - "Removed from results: %s (missing tokens: [%s, %s, %s])", + "Removed from results: %s (missing tokens: [%s, %s, %s])", title, token, cleantoken, dumbtoken) return False @@ -1553,10 +1550,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, if rulist: resultlist.extend(rulist) - if headphones.CONFIG.ORPHEUS: - provider = "Orpheus.network" - providerurl = "https://orpheus.network/" + # RED, Orpheus.network and potentially other Gazelle API based trackers. + def _search_torrent_gazelle(provider, providerurl, username=None, password=None, apikey=None, try_use_fltoken=False): + global gazelleobjs bitrate = None bitrate_string = bitrate @@ -1578,7 +1575,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, bitrate_string = encoding_string if bitrate_string not in gazelleencoding.ALL_ENCODINGS: logger.info( - "Your preferred bitrate is not one of the available Orpheus.network filters, so not using it as a search parameter.") + f"Your preferred bitrate is not one of the available { provider } filters, so not using it as a search parameter.") maxsize = 10000000000 elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless search_formats = [gazelleformat.FLAC, gazelleformat.MP3] @@ -1587,64 +1584,75 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, search_formats = [gazelleformat.MP3] maxsize = 300000000 - if not orpheusobj or not orpheusobj.logged_in(): + gazelleobj = gazelleobjs.get(provider, None) + if not gazelleobj or not gazelleobj.logged_in(): try: - logger.info("Attempting to log in to Orpheus.network...") - orpheusobj = gazelleapi.GazelleAPI(headphones.CONFIG.ORPHEUS_USERNAME, - headphones.CONFIG.ORPHEUS_PASSWORD, - headphones.CONFIG.ORPHEUS_URL) - orpheusobj._login() + logger.info(f"Attempting to log in to {provider}...") + if apikey: + gazelleobj = gazelleapi.GazelleAPI(apikey=apikey, + url=providerurl) + elif username and password: + gazelleobj = gazelleapi.GazelleAPI(username=username, + password=password, + url=providerurl) + else: + raise(f"Neither apikey nor username/password provided for provider {provider}.") + gazelleobj._login() except Exception as e: - orpheusobj = None - logger.error("Orpheus.network credentials incorrect or site is down. Error: %s %s" % ( - e.__class__.__name__, str(e))) + gazelleobj = None + logger.error("%s credentials incorrect or site is down. Error: %s %s" % ( + provider, e.__class__.__name__, str(e))) + gazelleobjs[provider] = gazelleobj - if orpheusobj and orpheusobj.logged_in(): + if gazelleobj and gazelleobj.logged_in(): logger.info("Searching %s..." % provider) all_torrents = [] album_type = "" # Specify release types to filter by - if album['Type'] == 'Album': - album_type = [gazellerelease_type.ALBUM] - if album['Type'] == 'Soundtrack': - album_type = [gazellerelease_type.SOUNDTRACK] - if album['Type'] == 'EP': - album_type = [gazellerelease_type.EP] - # No musicbrainz match for this type - # if album['Type'] == 'Anthology': - # album_type = [gazellerelease_type.ANTHOLOGY] - if album['Type'] == 'Compilation': - album_type = [gazellerelease_type.COMPILATION] - if album['Type'] == 'DJ-mix': - album_type = [gazellerelease_type.DJ_MIX] - if album['Type'] == 'Single': - album_type = [gazellerelease_type.SINGLE] - if album['Type'] == 'Live': - album_type = [gazellerelease_type.LIVE_ALBUM] - if album['Type'] == 'Remix': - album_type = [gazellerelease_type.REMIX] - if album['Type'] == 'Bootleg': - album_type = [gazellerelease_type.BOOTLEG] - if album['Type'] == 'Interview': - album_type = [gazellerelease_type.INTERVIEW] - if album['Type'] == 'Mixtape/Street': - album_type = [gazellerelease_type.MIXTAPE] - if album['Type'] == 'Other': - album_type = [gazellerelease_type.UNKNOWN] + gazelle_release_type_mapping = { + 'Album': [gazellerelease_type.ALBUM], + 'Soundtrack': [gazellerelease_type.SOUNDTRACK], + 'EP': [gazellerelease_type.EP], + # No musicbrainz match for this type + # 'Anthology': [gazellerelease_type.ANTHOLOGY], + 'Compilation': [gazellerelease_type.COMPILATION], + 'DJ-mix': [gazellerelease_type.DJ_MIX], + 'Single': [gazellerelease_type.SINGLE], + 'Live': [gazellerelease_type.LIVE_ALBUM], + 'Remix': [gazellerelease_type.REMIX], + 'Bootleg': [gazellerelease_type.BOOTLEG], + 'Interview': [gazellerelease_type.INTERVIEW], + 'Mixtape/Street': [gazellerelease_type.MIXTAPE], + 'Other': [gazellerelease_type.UNKNOWN], + } + + album_type = gazelle_release_type_mapping.get( + album['Type'], + gazellerelease_type.UNKNOWN + ) for search_format in search_formats: if usersearchterm: all_torrents.extend( - orpheusobj.search_torrents(searchstr=usersearchterm, format=search_format, - encoding=bitrate_string, releasetype=album_type)['results']) + gazelleobj.search_torrents( + searchstr=usersearchterm, + format=search_format, + encoding=bitrate_string, + releasetype=album_type + )['results'] + ) else: - all_torrents.extend(orpheusobj.search_torrents(artistname=semi_clean_artist_term, - groupname=semi_clean_album_term, - format=search_format, - encoding=bitrate_string, - releasetype=album_type)['results']) + all_torrents.extend( + gazelleobj.search_torrents( + artistname=semi_clean_artist_term, + groupname=semi_clean_album_term, + format=search_format, + encoding=bitrate_string, + releasetype=album_type) + ['results'] + ) # filter on format, size, and num seeders logger.info("Filtering torrents by format, maximum size, and minimum seeders...") @@ -1654,145 +1662,71 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, logger.info( "Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) - # sort by times d/l'd + # Sort by quality and seeders if not len(match_torrents): logger.info("No results found from %s for %s after filtering" % (provider, term)) elif len(match_torrents) > 1: logger.info("Found %d matching releases from %s for %s - %s after filtering" % (len(match_torrents), provider, artistterm, albumterm)) - logger.info('Sorting torrents by number of seeders...') - match_torrents.sort(key=lambda x: int(x.seeders), reverse=True) - if gazelleformat.MP3 in search_formats: - logger.info('Sorting torrents by seeders...') - match_torrents.sort(key=lambda x: int(x.seeders), reverse=True) if search_formats and None not in search_formats: - match_torrents.sort( - key=lambda x: int(search_formats.index(x.format))) # prefer lossless - # if bitrate: - # match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True) - # match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True) + logger.info('Sorting torrents by format and number of seeders...') + match_torrents.sort(key=lambda x: (search_formats.index(x.format), -int(x.seeders))) + else: + logger.info('Sorting torrents by number of seeders...') + match_torrents.sort(key=lambda x: int(x.seeders), reverse=True) logger.info( - "New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) + "New order: %s" % + ", ".join(repr(torrent) for torrent in match_torrents) + ) + results = [] for torrent in match_torrents: if not torrent.file_path: torrent.group.update_group_data() # will load the file_path for the individual torrents - resultlist.append( + + use_fltoken = try_use_fltoken and torrent.can_use_token + + results.append( Result( torrent.file_path, torrent.size, - orpheusobj.generate_torrent_link(torrent.id), + gazelleobj.generate_torrent_link(torrent.id, use_fltoken), provider, 'torrent', True ) ) + return results + + if headphones.CONFIG.ORPHEUS: + provider = "Orpheus.network" + providerurl = "https://orpheus.network/" + + resultlist.extend( + _search_torrent_gazelle( + provider, + providerurl, + username=headphones.CONFIG.ORPHEUS_USERNAME, + password=headphones.CONFIG.ORPHEUS_PASSWORD, + try_use_fltoken=False, + ) + ) + - # Redacted - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable if headphones.CONFIG.REDACTED: provider = "Redacted" - providerurl = "https://redacted.ch" + providerurl = "https://redacted.sh" - bitrate = None - bitrate_string = bitrate - - if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly: # Lossless Only mode - search_formats = [gazelleformat.FLAC] - maxsize = 10000000000 - elif headphones.CONFIG.PREFERRED_QUALITY == 2: # Preferred quality mode - search_formats = [None] # should return all - bitrate = headphones.CONFIG.PREFERRED_BITRATE - if bitrate: - if 225 <= int(bitrate) < 256: - bitrate = 'V0' - elif 200 <= int(bitrate) < 225: - bitrate = 'V1' - elif 175 <= int(bitrate) < 200: - bitrate = 'V2' - for encoding_string in gazelleencoding.ALL_ENCODINGS: - if re.search(bitrate, encoding_string, flags=re.I): - bitrate_string = encoding_string - if bitrate_string not in gazelleencoding.ALL_ENCODINGS: - logger.info( - "Your preferred bitrate is not one of the available RED filters, so not using it as a search parameter.") - maxsize = 10000000000 - elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless - search_formats = [gazelleformat.FLAC, gazelleformat.MP3] - maxsize = 10000000000 - else: # Highest quality excluding lossless - search_formats = [gazelleformat.MP3] - maxsize = 300000000 - - if not redobj or not redobj.logged_in(): - try: - logger.info("Attempting to log in to Redacted...") - redobj = gazelleapi.GazelleAPI(headphones.CONFIG.REDACTED_USERNAME, - headphones.CONFIG.REDACTED_PASSWORD, - providerurl) - redobj._login() - except Exception as e: - redobj = None - logger.error("Redacted credentials incorrect or site is down. Error: %s %s" % ( - e.__class__.__name__, str(e))) - - if redobj and redobj.logged_in(): - logger.info("Searching %s..." % provider) - all_torrents = [] - for search_format in search_formats: - if usersearchterm: - all_torrents.extend( - redobj.search_torrents(searchstr=usersearchterm, format=search_format, - encoding=bitrate_string)['results']) - else: - all_torrents.extend(redobj.search_torrents(artistname=semi_clean_artist_term, - groupname=semi_clean_album_term, - format=search_format, - encoding=bitrate_string)['results']) - - # filter on format, size, and num seeders - logger.info("Filtering torrents by format, maximum size, and minimum seeders...") - match_torrents = [t for t in all_torrents if - t.size <= maxsize and t.seeders >= minimumseeders] - - logger.info( - "Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) - - # sort by times d/l'd - if not len(match_torrents): - logger.info("No results found from %s for %s after filtering" % (provider, term)) - elif len(match_torrents) > 1: - logger.info("Found %d matching releases from %s for %s - %s after filtering" % - (len(match_torrents), provider, artistterm, albumterm)) - logger.info( - "Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string) - match_torrents.sort(key=lambda x: int(x.snatched), reverse=True) - if gazelleformat.MP3 in search_formats: - # sort by size after rounding to nearest 10MB...hacky, but will favor highest quality - match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)), - reverse=True) - if search_formats and None not in search_formats: - match_torrents.sort( - key=lambda x: int(search_formats.index(x.format))) # prefer lossless - # if bitrate: - # match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True) - # match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True) - logger.info( - "New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) - - for torrent in match_torrents: - if not torrent.file_path: - torrent.group.update_group_data() # will load the file_path for the individual torrents - use_token = headphones.CONFIG.REDACTED_USE_FLTOKEN and torrent.can_use_token - resultlist.append( - Result( - torrent.file_path, - torrent.size, - redobj.generate_torrent_link(torrent.id, use_token), - provider, - 'torrent', - True - ) - ) + resultlist.extend( + _search_torrent_gazelle( + provider, + providerurl, + username=headphones.CONFIG.REDACTED_USERNAME, + password=headphones.CONFIG.REDACTED_PASSWORD, + apikey=headphones.CONFIG.REDACTED_APIKEY, + try_use_fltoken=headphones.CONFIG.REDACTED_USE_FLTOKEN, + ) + ) # PIRATE BAY diff --git a/headphones/webserve.py b/headphones/webserve.py index eacabfaa..7de97ddd 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1242,6 +1242,7 @@ class WebInterface(object): "orpheus_ratio": headphones.CONFIG.ORPHEUS_RATIO, "orpheus_url": headphones.CONFIG.ORPHEUS_URL, "use_redacted": checked(headphones.CONFIG.REDACTED), + "redacted_apikey": headphones.CONFIG.REDACTED_APIKEY, "redacted_username": headphones.CONFIG.REDACTED_USERNAME, "redacted_password": headphones.CONFIG.REDACTED_PASSWORD, "redacted_ratio": headphones.CONFIG.REDACTED_RATIO, diff --git a/lib/pygazelle/api.py b/lib/pygazelle/api.py index 866e7baf..dd864ba6 100644 --- a/lib/pygazelle/api.py +++ b/lib/pygazelle/api.py @@ -41,11 +41,12 @@ class GazelleAPI(object): 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3'} - def __init__(self, username=None, password=None, url=None): + def __init__(self, apikey=None, username=None, password=None, url=None): self.session = requests.session() self.session.headers = self.default_headers self.username = username self.password = password + self.apikey = apikey self.authkey = None self.passkey = None self.userid = None @@ -94,14 +95,17 @@ class GazelleAPI(object): self.wait_for_rate_limit() - loginpage = self.site + 'login.php' - data = {'username': self.username, - 'password': self.password, - 'keeplogged': '1'} - r = self.session.post(loginpage, data=data, timeout=self.default_timeout, headers=self.default_headers) - self.past_request_timestamps.append(time.time()) - if r.status_code != 200: - raise LoginException("Login returned status code %s" % r.status_code) + if self.apikey is not None: + self.session.headers["Authorization"] = self.apikey + else: + loginpage = self.site + 'login.php' + data = {'username': self.username, + 'password': self.password, + 'keeplogged': '1'} + r = self.session.post(loginpage, data=data, timeout=self.default_timeout, headers=self.default_headers) + self.past_request_timestamps.append(time.time()) + if r.status_code != 200: + raise LoginException("Login returned status code %s" % r.status_code) try: accountinfo = self.request('index', autologin=False)