From c77f43be5e43a176890fbf1bb5ef3964c3c225b9 Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Tue, 25 Oct 2016 19:21:16 +1100 Subject: [PATCH 1/6] Accept partial release date for search --- headphones/searcher.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index cda9a822..1efb657e 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -201,14 +201,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False, continue if headphones.CONFIG.WAIT_UNTIL_RELEASE_DATE and album['ReleaseDate']: - try: - release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d") - except: - logger.warn( - "No valid date for: %s. Skipping automatic search" % album['AlbumTitle']) + release_date = strptime_musicbrainz(album['ReleaseDate']) + if not release_date: + logger.warn("No valid date for: %s. Skipping automatic search" % + album['AlbumTitle']) continue - if release_date > datetime.datetime.today(): + elif release_date > datetime.datetime.today(): logger.info("Skipping: %s. Waiting for release date of: %s" % ( album['AlbumTitle'], album['ReleaseDate'])) continue @@ -237,6 +236,26 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False, logger.info('Search for wanted albums complete') +def strptime_musicbrainz(date_str): + """ + Release date as returned by Musicbrainz may contain the full date (Year-Month-Day) + but it may as well be just Year-Month or even just the year. + + Args: + date_str: the date as a string (ex: "2003-05-01", "2003-03", "2003") + + Returns: + The more accurate datetime object we can create or None if parse failed + """ + acceptable_formats = ('%Y-%m-%d', '%Y-%m', '%Y') + for date_format in acceptable_formats: + try: + return datetime.datetime.strptime(date_str, date_format) + except: + pass + return None + + def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or headphones.CONFIG.NEWZNAB or From 92601e0eb8a9926b24b7718558a1d92e32be824a Mon Sep 17 00:00:00 2001 From: William Friesen Date: Sat, 19 Nov 2016 12:15:22 +1100 Subject: [PATCH 2/6] Use HTML escaping for password fields Fixes rembo10/headphones#2474 --- data/interfaces/default/config.html | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index baf39586..99d1d4ec 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -56,7 +56,7 @@ - +
@@ -173,7 +173,7 @@ - +
- +
Note: With Transmission, you can specify a different download directory for downloads sent from Headphones. @@ -379,7 +379,7 @@
- +
@@ -400,7 +400,7 @@
- +
Note: With Deluge, you can specify a different download directory for downloads sent from Headphones. @@ -466,7 +466,7 @@
- +
Don't have an account? Sign up! @@ -622,7 +622,7 @@
- +
@@ -642,7 +642,7 @@
- +
@@ -755,7 +755,7 @@
- +
@@ -980,7 +980,7 @@
- +
@@ -1006,7 +1006,7 @@
- +
@@ -1028,7 +1028,7 @@
- +
@@ -1142,7 +1142,7 @@ Username of your Plex client API (blank for none)
- + Password of your Plex client API (blank for none)
@@ -1242,7 +1242,7 @@
- +
@@ -1642,7 +1642,7 @@
-
+
@@ -1655,7 +1655,7 @@
-
+
Get an Account!
From dff1ac2e41381ccc1b2e6ec7cfa13df32442ad0d Mon Sep 17 00:00:00 2001 From: massiliattak Date: Sun, 20 Nov 2016 20:15:37 +0100 Subject: [PATCH 3/6] Update T411 new domain ( t411.li ) --- headphones/searcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headphones/searcher.py b/headphones/searcher.py index 1efb657e..ac938972 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -1801,7 +1801,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, if headphones.CONFIG.TQUATTRECENTONZE: username = headphones.CONFIG.TQUATTRECENTONZE_USER password = headphones.CONFIG.TQUATTRECENTONZE_PASSWORD - API_URL = "http://api.t411.ch" + API_URL = "http://api.t411.li" AUTH_URL = API_URL + '/auth' DL_URL = API_URL + '/torrents/download/' provider = "t411" From b6b33e1b1e4969e5e359037f9236ce4b57419b17 Mon Sep 17 00:00:00 2001 From: Denzo Date: Tue, 29 Nov 2016 20:49:52 +0100 Subject: [PATCH 4/6] + Added support for PassTheHeadphones.me % Removed hardcoding for What.CD in pygazelle API, making it easy to add other Gazelle-based trackers + Added URL parameter for What.CD and PTH, in case they come back under a new domain name --- data/interfaces/default/config.html | 29 ++++++++ headphones/config.py | 6 ++ headphones/searcher.py | 109 +++++++++++++++++++++++++++- headphones/webserve.py | 6 ++ lib/pygazelle/api.py | 8 +- 5 files changed, 152 insertions(+), 6 deletions(-) diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index baf39586..d6018ff0 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -644,6 +644,10 @@ +
+ + +
@@ -651,6 +655,30 @@
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
@@ -2412,6 +2440,7 @@ initConfigCheckbox("#use_waffles"); initConfigCheckbox("#use_rutracker"); initConfigCheckbox("#use_whatcd"); + initConfigCheckbox("#use_pth"); initConfigCheckbox("#use_strike"); initConfigCheckbox("#api_enabled"); initConfigCheckbox("#enable_https"); diff --git a/headphones/config.py b/headphones/config.py index e825cc68..46efed19 100644 --- a/headphones/config.py +++ b/headphones/config.py @@ -293,6 +293,12 @@ _CONFIG_DEFINITIONS = { 'WHATCD_PASSWORD': (str, 'What.cd', ''), 'WHATCD_RATIO': (str, 'What.cd', ''), 'WHATCD_USERNAME': (str, 'What.cd', ''), + 'WHATCD_URL': (str, 'What.cd', 'https://what.cd'), + 'PTH': (int, 'PassTheHeadphones.me', 0), + 'PTH_PASSWORD': (str, 'PassTheHeadphones.me', ''), + 'PTH_RATIO': (str, 'PassTheHeadphones.me', ''), + 'PTH_USERNAME': (str, 'PassTheHeadphones.me', ''), + 'PTH_URL': (str, 'PassTheHeadphones.me', 'https://passtheheadphones.me'), 'XBMC_ENABLED': (int, 'XBMC', 0), 'XBMC_HOST': (str, 'XBMC', ''), 'XBMC_NOTIFY': (int, 'XBMC', 0), diff --git a/headphones/searcher.py b/headphones/searcher.py index cda9a822..0f4c7c33 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -38,7 +38,6 @@ from headphones import logger, db, helpers, classes, sab, nzbget, request from headphones import utorrent, transmission, notifiers, rutracker, deluge from bencode import bencode, bdecode - # Magnet to torrent services, for Black hole. Stolen from CouchPotato. TORRENT_TO_MAGNET_SERVICES = [ # 'https://zoink.it/torrent/%s.torrent', @@ -163,6 +162,8 @@ def get_seed_ratio(provider): seed_ratio = headphones.CONFIG.KAT_RATIO elif provider == 'What.cd': seed_ratio = headphones.CONFIG.WHATCD_RATIO + elif provider == 'PassTheHeadphones.Me': + seed_ratio = headphones.CONFIG.PTH_RATIO elif provider == 'The Pirate Bay': seed_ratio = headphones.CONFIG.PIRATEBAY_RATIO elif provider == 'Old Pirate Bay': @@ -255,6 +256,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False): headphones.CONFIG.WAFFLES or headphones.CONFIG.RUTRACKER or headphones.CONFIG.WHATCD or + headphones.CONFIG.PTH or headphones.CONFIG.STRIKE) results = [] @@ -1485,7 +1487,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, try: logger.info(u"Attempting to log in to What.cd...") gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME, - headphones.CONFIG.WHATCD_PASSWORD) + headphones.CONFIG.WHATCD_PASSWORD, + headphones.CONFIG.WHATCD_URL) gazelle._login() except Exception as e: gazelle = None @@ -1545,6 +1548,106 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, provider, 'torrent', True)) + # PassTheHeadphones.me - Using same logic as What.CD as it's also Gazelle, so should really make this into something reusable + if headphones.CONFIG.PTH: + provider = "PassTheHeadphones.me" + providerurl = "https://passtheheadphones.me/" + + 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( + u"Your preferred bitrate is not one of the available What.cd 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 gazelle or not gazelle.logged_in(): + try: + logger.info(u"Attempting to log in to PassTheHeadphones.me...") + gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.PTH_USERNAME, + headphones.CONFIG.PTH_PASSWORD, + headphones.CONFIG.PTH_URL) + gazelle._login() + except Exception as e: + gazelle = None + logger.error(u"PassTheHeadphones credentials incorrect or site is down. Error: %s %s" % ( + e.__class__.__name__, str(e))) + + if gazelle and gazelle.logged_in(): + logger.info(u"Searching %s..." % provider) + all_torrents = [] + for search_format in search_formats: + if usersearchterm: + all_torrents.extend( + gazelle.search_torrents(searchstr=usersearchterm, format=search_format, + encoding=bitrate_string)['results']) + else: + all_torrents.extend(gazelle.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(u"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( + u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents)) + + # sort by times d/l'd + if not len(match_torrents): + logger.info(u"No results found from %s for %s after filtering" % (provider, term)) + elif len(match_torrents) > 1: + logger.info(u"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( + u"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 + resultlist.append((torrent.file_path, + torrent.size, + gazelle.generate_torrent_link(torrent.id), + provider, + 'torrent', True)) + # Pirate Bay if headphones.CONFIG.PIRATEBAY: provider = "The Pirate Bay" @@ -1888,6 +1991,8 @@ def preprocess(resultlist): headers['User-Agent'] = USER_AGENT elif result[3] == 'What.cd': headers['User-Agent'] = 'Headphones' + elif result[3] == 'PassTheHeadphones.me': + headers['User-Agent'] = 'Headphones' elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay": headers[ 'User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36' diff --git a/headphones/webserve.py b/headphones/webserve.py index 9b09feba..368a5327 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -1228,6 +1228,12 @@ class WebInterface(object): "whatcd_username": headphones.CONFIG.WHATCD_USERNAME, "whatcd_password": headphones.CONFIG.WHATCD_PASSWORD, "whatcd_ratio": headphones.CONFIG.WHATCD_RATIO, + "whatcd_url": headphones.CONFIG.WHATCD_URL, + "use_pth": checked(headphones.CONFIG.PTH), + "pth_username": headphones.CONFIG.PTH_USERNAME, + "pth_password": headphones.CONFIG.PTH_PASSWORD, + "pth_ratio": headphones.CONFIG.PTH_RATIO, + "pth_url": headphones.CONFIG.PTH_URL, "use_strike": checked(headphones.CONFIG.STRIKE), "strike_ratio": headphones.CONFIG.STRIKE_RATIO, "use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE), diff --git a/lib/pygazelle/api.py b/lib/pygazelle/api.py index b3dba2bb..fae8f418 100644 --- a/lib/pygazelle/api.py +++ b/lib/pygazelle/api.py @@ -42,7 +42,7 @@ class GazelleAPI(object): 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3'} - def __init__(self, username=None, password=None): + def __init__(self, username=None, password=None, url=None): self.session = requests.session() self.session.headers = self.default_headers self.username = username @@ -59,7 +59,7 @@ class GazelleAPI(object): self.cached_torrents = {} self.cached_requests = {} self.cached_categories = {} - self.site = "https://what.cd/" + self.site = url + "/" self.past_request_timestamps = [] def wait_for_rate_limit(self): @@ -95,7 +95,7 @@ class GazelleAPI(object): self.wait_for_rate_limit() - loginpage = 'https://what.cd/login.php' + loginpage = self.site + 'login.php' data = {'username': self.username, 'password': self.password, 'keeplogged': '1'} @@ -122,7 +122,7 @@ class GazelleAPI(object): Pass an action and relevant arguments for that action. """ def make_request(action, **kwargs): - ajaxpage = 'ajax.php' + ajaxpage = '/ajax.php' content = self.unparsed_request(ajaxpage, action, **kwargs) try: if not isinstance(content, text_type): From 352b009e9128726a59e94020e3a88c1a7acad6d9 Mon Sep 17 00:00:00 2001 From: Denzo Date: Tue, 29 Nov 2016 20:54:20 +0100 Subject: [PATCH 5/6] # Removed slash from ajax.php --- lib/pygazelle/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pygazelle/api.py b/lib/pygazelle/api.py index fae8f418..92aac04e 100644 --- a/lib/pygazelle/api.py +++ b/lib/pygazelle/api.py @@ -122,7 +122,7 @@ class GazelleAPI(object): Pass an action and relevant arguments for that action. """ def make_request(action, **kwargs): - ajaxpage = '/ajax.php' + ajaxpage = 'ajax.php' content = self.unparsed_request(ajaxpage, action, **kwargs) try: if not isinstance(content, text_type): From 0ecfe499fcdbcdcfdc2647ee2eb6b869857d1fce Mon Sep 17 00:00:00 2001 From: rembo10 Date: Thu, 1 Dec 2016 15:59:11 +0000 Subject: [PATCH 6/6] Updated changelog for v0.5.18 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 087baab7..80846884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v0.5.18 +Released 01 December 2016 + +Highlights: +* Added: PassTheHeadphones support +* Fixed: Special characters in password fields breaking on config page +* Improved: Updated t411 url + +The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.17...v0.5.18). + + ## v0.5.17 Released 10 November 2016