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
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index baf39586..5bc2edea 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -56,7 +56,7 @@
-
+
@@ -173,7 +173,7 @@
-
+
@@ -359,7 +359,7 @@
Transmission Password
-
+
Note: With Transmission, you can specify a different download directory for downloads sent from Headphones.
@@ -379,7 +379,7 @@
uTorrent Password
-
+
uTorrent Label
@@ -400,7 +400,7 @@
Deluge Password
-
+
Note: With Deluge, you can specify a different download directory for downloads sent from Headphones.
@@ -466,7 +466,7 @@
Password
-
+
Password
-
+
Seed Ratio
@@ -642,7 +642,11 @@
Password
-
+
+
+
+ URL
+
Seed Ratio
@@ -651,6 +655,30 @@
+
+
@@ -980,7 +1008,7 @@
SMTP User
- SMTP Password
+ SMTP Password
SMTP Port
@@ -1006,7 +1034,7 @@
Growl Host:Port
- Growl Password
+ Growl Password
Notify on snatch?
@@ -1028,7 +1056,7 @@
Username
- Password
+ Password
Update Library
@@ -1142,7 +1170,7 @@
Username of your Plex client API (blank for none)
- Plex Password
+ Plex Password
Password of your Plex client API (blank for none)
@@ -1242,7 +1270,7 @@
Subsonic Username
- Subsonic Password
+ Subsonic Password
@@ -1642,7 +1670,7 @@
Username
- Password
+ Password
@@ -1655,7 +1683,7 @@
Username
@@ -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..7b19eb26 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':
@@ -201,14 +202,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 +237,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
@@ -255,6 +275,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 +1506,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 +1567,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"
@@ -1782,7 +1904,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"
@@ -1888,6 +2010,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..92aac04e 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'}