From 5efbd561d6694ab3c4cf064611ff1fba123774e3 Mon Sep 17 00:00:00 2001
From: Hypsometric <25771958+hypsometric@users.noreply.github.com>
Date: Mon, 26 Aug 2024 19:38:44 +0200
Subject: [PATCH 1/3] Allow the use of apikey with GazelleAPI & Redacted
---
data/interfaces/default/config.html | 4 ++++
headphones/config.py | 1 +
headphones/searcher.py | 11 +++++------
headphones/webserve.py | 1 +
lib/pygazelle/api.py | 22 +++++++++++++---------
5 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index d88cbfdd4..84f3770f1 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 9679981f9..7f29d50e2 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -306,6 +306,7 @@ def __repr__(self):
'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 65d486e77..a2cd9eeb3 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -1590,9 +1590,9 @@ def set_proxy(proxy_url):
if not orpheusobj or not orpheusobj.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 = gazelleapi.GazelleAPI(username=headphones.CONFIG.ORPHEUS_USERNAME,
+ password=headphones.CONFIG.ORPHEUS_PASSWORD,
+ url=headphones.CONFIG.ORPHEUS_URL)
orpheusobj._login()
except Exception as e:
orpheusobj = None
@@ -1726,9 +1726,8 @@ def set_proxy(proxy_url):
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 = gazelleapi.GazelleAPI(apikey=headphones.CONFIG.REDACTED_APIKEY,
+ url=providerurl)
redobj._login()
except Exception as e:
redobj = None
diff --git a/headphones/webserve.py b/headphones/webserve.py
index 8722cc380..e85503f5a 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -1242,6 +1242,7 @@ def config(self):
"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 866e7baf4..dd864ba6f 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 @@ def _login(self):
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)
From 95bd0a57ff17af9979847bd253b61f78dccb2e96 Mon Sep 17 00:00:00 2001
From: Hypsometric <25771958+hypsometric@users.noreply.github.com>
Date: Tue, 18 Mar 2025 20:27:10 +0100
Subject: [PATCH 2/3] Refactor common Orpheus & Redacted code
Orpheus & Redacted being based on Gazelle API, their code was the same
(with the exception of release type filtering).
Merging both will make maintenance easy and brings Redacted on par with
Orpheus regarding release type filtering.
---
headphones/searcher.py | 252 ++++++++++++++++-------------------------
1 file changed, 95 insertions(+), 157 deletions(-)
diff --git a/headphones/searcher.py b/headphones/searcher.py
index a2cd9eeb3..5b77edad5 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -68,12 +68,9 @@
'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 set_proxy(proxy_url):
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 set_proxy(proxy_url):
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 set_proxy(proxy_url):
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(username=headphones.CONFIG.ORPHEUS_USERNAME,
- password=headphones.CONFIG.ORPHEUS_PASSWORD,
- url=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...")
@@ -1674,124 +1682,54 @@ def set_proxy(proxy_url):
logger.info(
"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
- # 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"
-
- 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(apikey=headphones.CONFIG.REDACTED_APIKEY,
- url=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]
+ if headphones.CONFIG.ORPHEUS:
+ provider = "Orpheus.network"
+ providerurl = "https://orpheus.network/"
- logger.info(
- "Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
+ resultlist.extend(
+ _search_torrent_gazelle(
+ provider,
+ providerurl,
+ username=headphones.CONFIG.ORPHEUS_USERNAME,
+ password=headphones.CONFIG.ORPHEUS_PASSWORD,
+ try_use_fltoken=False,
+ )
+ )
- # 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
- )
- )
+ if headphones.CONFIG.REDACTED:
+ provider = "Redacted"
+ providerurl = "https://redacted.sh"
+
+ 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
From 26a609585144e5314b2d093fdef8bef1f206a337 Mon Sep 17 00:00:00 2001
From: Hypsometric <25771958+hypsometric@users.noreply.github.com>
Date: Tue, 18 Mar 2025 20:55:21 +0100
Subject: [PATCH 3/3] Make Gazelle torrents sorting clearer
Sort by format and most seeders if `search_formats` has data.
Sort only by most seeders else.
---
headphones/searcher.py | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/headphones/searcher.py b/headphones/searcher.py
index 5b77edad5..f47e1fa76 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -1662,25 +1662,22 @@ def _search_torrent_gazelle(provider, providerurl, username=None, password=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: