diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1037f8e0..087baab7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## v0.5.17
+Released 10 November 2016
+
+Highlights:
+* Added: t411 support
+* Fixed: Rutracker login
+* Fixed: Deluge empty password
+* Fixed: FreeBSD init script
+* Improved: Musicbrainz searching
+
+The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.16...v0.5.17).
+
## v0.5.16
Released 10 June 2016
diff --git a/Headphones.py b/Headphones.py
index dc956a43..e4506797 100755
--- a/Headphones.py
+++ b/Headphones.py
@@ -81,6 +81,8 @@ def main():
help='Prevent browser from launching on startup')
parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
+ parser.add_argument(
+ '--host', help='Specify a host (default - localhost)')
args = parser.parse_args()
@@ -170,6 +172,13 @@ def main():
else:
http_port = int(headphones.CONFIG.HTTP_PORT)
+ # Force the http host if neccessary
+ if args.host:
+ http_host = args.host
+ logger.info('Using forced web server host: %s', http_host)
+ else:
+ http_host = headphones.CONFIG.HTTP_HOST
+
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if headphones.CONFIG.ENABLE_HTTPS:
@@ -183,7 +192,7 @@ def main():
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': http_port,
- 'http_host': headphones.CONFIG.HTTP_HOST,
+ 'http_host': http_host,
'http_root': headphones.CONFIG.HTTP_ROOT,
'http_proxy': headphones.CONFIG.HTTP_PROXY,
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index a22a0ea7..baf39586 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -743,6 +743,22 @@
+
+
@@ -2400,6 +2416,7 @@
initConfigCheckbox("#api_enabled");
initConfigCheckbox("#enable_https");
initConfigCheckbox("#customauth");
+ initConfigCheckbox("#use_tquattrecentonze");
$('#twitterStep1').click(function () {
diff --git a/headphones/config.py b/headphones/config.py
index 5b819007..e825cc68 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -274,6 +274,9 @@ _CONFIG_DEFINITIONS = {
'TWITTER_PASSWORD': (str, 'Twitter', ''),
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
'TWITTER_USERNAME': (str, 'Twitter', ''),
+ 'TQUATTRECENTONZE': (int, 'tquattrecentonze', 0),
+ 'TQUATTRECENTONZE_PASSWORD': (str, 'tquattrecentonze', ''),
+ 'TQUATTRECENTONZE_USER': (str, 'tquattrecentonze', ''),
'UPDATE_DB_INTERVAL': (int, 'General', 24),
'USENET_RETENTION': (int, 'General', '1500'),
'UTORRENT_HOST': (str, 'uTorrent', ''),
diff --git a/headphones/crier.py b/headphones/crier.py
new file mode 100644
index 00000000..2399f6a9
--- /dev/null
+++ b/headphones/crier.py
@@ -0,0 +1,36 @@
+import pprint
+import sys
+import threading
+import traceback
+
+from headphones import logger
+
+
+def cry():
+ """
+ Logs thread traces.
+ """
+ tmap = {}
+ main_thread = None
+ # get a map of threads by their ID so we can print their names
+ # during the traceback dump
+ for t in threading.enumerate():
+ if t.ident:
+ tmap[t.ident] = t
+ else:
+ main_thread = t
+
+ # Loop over each thread's current frame, writing info about it
+ for tid, frame in sys._current_frames().iteritems():
+ thread = tmap.get(tid, main_thread)
+
+ lines = []
+ lines.append('%s\n' % thread.getName())
+ lines.append('========================================\n')
+ lines += traceback.format_stack(frame)
+ lines.append('========================================\n')
+ lines.append('LOCAL VARIABLES:\n')
+ lines.append('========================================\n')
+ lines.append(pprint.pformat(frame.f_locals))
+ lines.append('\n\n')
+ logger.info("".join(lines))
diff --git a/headphones/deluge.py b/headphones/deluge.py
index f97063d3..6c105054 100644
--- a/headphones/deluge.py
+++ b/headphones/deluge.py
@@ -269,7 +269,8 @@ def _get_auth():
delugeweb_host = headphones.CONFIG.DELUGE_HOST
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
- logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
+ if len(delugeweb_password) > 0:
+ logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
if not delugeweb_host.startswith('http'):
delugeweb_host = 'http://%s' % delugeweb_host
diff --git a/headphones/helpers.py b/headphones/helpers.py
index ff062d57..af1a13e0 100644
--- a/headphones/helpers.py
+++ b/headphones/helpers.py
@@ -251,6 +251,7 @@ _XLATE_SPECIAL = {
# Translation table.
# Cover additional special characters processing normalization.
u"'": '', # replace apostrophe with nothing
+ u"’": '', # replace musicbrainz style apostrophe with nothing
u'&': ' and ', # expand & to ' and '
}
diff --git a/headphones/mb.py b/headphones/mb.py
index 397e5008..faba6b8a 100644
--- a/headphones/mb.py
+++ b/headphones/mb.py
@@ -91,10 +91,6 @@ def findArtist(name, limit=1):
artistlist = []
artistResults = None
- chars = set('!?*-')
- if any((c in chars) for c in name):
- name = '"' + name + '"'
-
criteria = {'artist': name.lower()}
with mb_lock:
@@ -156,16 +152,13 @@ def findRelease(name, limit=1, artist=None):
if not artist and ':' in name:
name, artist = name.rsplit(":", 1)
- chars = set('!?*-')
- if any((c in chars) for c in name):
- name = '"' + name + '"'
- if artist and any((c in chars) for c in artist):
- artist = '"' + artist + '"'
+ criteria = {'release': name.lower()}
+ if artist:
+ criteria['artist'] = artist.lower()
with mb_lock:
try:
- releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[
- 'release-list']
+ releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
except musicbrainzngs.WebServiceError as e: # need to update exceptions
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
mb_lock.snooze(5)
@@ -234,10 +227,6 @@ def findSeries(name, limit=1):
serieslist = []
seriesResults = None
- chars = set('!?*-')
- if any((c in chars) for c in name):
- name = '"' + name + '"'
-
criteria = {'series': name.lower()}
with mb_lock:
@@ -759,19 +748,12 @@ def findArtistbyAlbum(name):
def findAlbumID(artist=None, album=None):
results = None
- chars = set('!?*-')
try:
if album and artist:
- if any((c in chars) for c in album):
- album = '"' + album + '"'
- if any((c in chars) for c in artist):
- artist = '"' + artist + '"'
criteria = {'release': album.lower()}
criteria['artist'] = artist.lower()
else:
- if any((c in chars) for c in album):
- album = '"' + album + '"'
criteria = {'release': album.lower()}
with mb_lock:
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
diff --git a/headphones/rutracker.py b/headphones/rutracker.py
index af8e947e..b80685cf 100644
--- a/headphones/rutracker.py
+++ b/headphones/rutracker.py
@@ -44,28 +44,30 @@ class Rutracker(object):
logger.info("Attempting to log in to rutracker...")
try:
- r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
+ r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
# try again
- if 'bb_data' not in r.cookies.keys():
+ if not self.has_bb_data_cookie(r):
time.sleep(10)
- r = self.session.post(loginpage, data=post_params, timeout=self.timeout)
- if r.status_code != 200:
- logger.error("rutracker login returned status code %s" % r.status_code)
- self.loggedin = False
+ r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
+ if self.has_bb_data_cookie(r):
+ self.loggedin = True
+ logger.info("Successfully logged in to rutracker")
else:
- if 'bb_data' in r.cookies.keys():
- self.loggedin = True
- logger.info("Successfully logged in to rutracker")
- else:
- logger.error(
- "Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
- self.loggedin = False
+ logger.error(
+ "Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
+ self.loggedin = False
return self.loggedin
except Exception as e:
logger.error("Unknown error logging in to rutracker: %s" % e)
self.loggedin = False
return self.loggedin
+ def has_bb_data_cookie(self, response):
+ if 'bb_data' in response.cookies.keys():
+ return True
+ # Rutracker randomly send a 302 redirect code, cookie may be present in response history
+ return next(('bb_data' in r.cookies.keys() for r in response.history), False)
+
def searchurl(self, artist, album, year, format):
"""
Return the search url
diff --git a/headphones/searcher.py b/headphones/searcher.py
index c8f16184..cda9a822 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -14,6 +14,7 @@
# along with Headphones. If not, see .
# NZBGet support added by CurlyMo as a part of XBian - XBMC on the Raspberry Pi
+# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
from base64 import b16encode, b32decode
from hashlib import sha1
@@ -24,6 +25,7 @@ import datetime
import subprocess
import unicodedata
import urlparse
+from json import loads
import os
import re
@@ -813,6 +815,19 @@ def send_to_downloader(data, bestqual, album):
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
download_path = os.path.join(headphones.CONFIG.TORRENTBLACKHOLE_DIR, torrent_name)
+ # Blackhole for t411
+ if bestqual[2].lower().startswith("http://api.t411"):
+ if headphones.CONFIG.MAGNET_LINKS == 2:
+ try:
+ url = bestqual[2].split('TOKEN')[0]
+ token = bestqual[2].split('TOKEN')[1]
+ data = request.request_content(url, headers={'Authorization': token})
+ torrent_to_file(download_path, data)
+ logger.info('Successfully converted magnet to torrent file')
+ except Exception as e:
+ logger.error("Error converting magnet link: %s" % str(e))
+ return
+
if bestqual[2].lower().startswith("magnet:"):
if headphones.CONFIG.MAGNET_LINKS == 1:
try:
@@ -1763,6 +1778,77 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
resultlist.append((title, size, url, provider, 'torrent', match))
except Exception as e:
logger.exception("Unhandled exception in Mininova Parser")
+ # t411
+ if headphones.CONFIG.TQUATTRECENTONZE:
+ username = headphones.CONFIG.TQUATTRECENTONZE_USER
+ password = headphones.CONFIG.TQUATTRECENTONZE_PASSWORD
+ API_URL = "http://api.t411.ch"
+ AUTH_URL = API_URL + '/auth'
+ DL_URL = API_URL + '/torrents/download/'
+ provider = "t411"
+ t411_term = term.replace(" ", "%20")
+ SEARCH_URL = API_URL + '/torrents/search/' + t411_term + "?limit=15&cid=395&subcat=623"
+ headers_login = {'username': username, 'password': password}
+
+ # Requesting content
+ logger.info('Parsing results from t411 using search term: %s' % term)
+ req = request.request_content(AUTH_URL, method='post', data=headers_login)
+
+ if len(req.split('"')) == 9:
+ token = req.split('"')[7]
+ headers_auth = {'Authorization': token}
+ logger.info('t411 - User %s logged in' % username)
+ else:
+ logger.info('t411 - Login error : %s' % req.split('"')[3])
+
+ # Quality
+ if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
+ providerurl = fix_url(SEARCH_URL + "&term[16][]=529&term[16][]=1184")
+ elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
+ providerurl = fix_url(SEARCH_URL + "&term[16][]=685&term[16][]=527&term[16][]=1070&term[16][]=528&term[16][]=1167&term[16][]=1166&term[16][]=530&term[16][]=529&term[16][]=1184&term[16][]=532&term[16][]=533&term[16][]=1085&term[16][]=534&term[16][]=535&term[16][]=1069&term[16][]=537&term[16][]=538")
+ elif headphones.CONFIG.PREFERRED_QUALITY == 0:
+ providerurl = fix_url(SEARCH_URL + "&term[16][]=685&term[16][]=527&term[16][]=1070&term[16][]=528&term[16][]=1167&term[16][]=1166&term[16][]=530&term[16][]=532&term[16][]=533&term[16][]=1085&term[16][]=534&term[16][]=535&term[16][]=1069&term[16][]=537&term[16][]=538")
+ else:
+ providerurl = fix_url(SEARCH_URL)
+
+ # Tracker search
+ req = request.request_content(providerurl, headers=headers_auth)
+ req = loads(req)
+ total = req['total']
+
+ # Process feed
+ if total == '0':
+ logger.info("No results found from t411 for %s" % term)
+ else:
+ logger.info('Found %s results from t411' % total)
+ torrents = req['torrents']
+ for torrent in torrents:
+ try:
+ title = torrent['name']
+ if torrent['seeders'] < minimumseeders:
+ logger.info('Skipping torrent %s : seeders below minimum set' % title)
+ continue
+ id = torrent['id']
+ size = int(torrent['size'])
+ data = request.request_content(DL_URL + id, headers=headers_auth)
+
+ # Blackhole
+ if headphones.CONFIG.TORRENT_DOWNLOADER == 0 and headphones.CONFIG.MAGNET_LINKS == 2:
+ url = DL_URL + id + 'TOKEN' + token
+ resultlist.append((title, size, url, provider, 'torrent', True))
+
+ # Build magnet
+ else:
+ metadata = bdecode(data)
+ hashcontents = bencode(metadata['info'])
+ digest = sha1(hashcontents).hexdigest()
+ trackers = [metadata["announce"]][0]
+ url = 'magnet:?xt=urn:btih:%s&tr=%s' % (digest, trackers)
+ resultlist.append((title, size, url, provider, 'torrent', True))
+
+ except Exception as e:
+ logger.error("Error converting magnet link: %s" % str(e))
+ return
# attempt to verify that this isn't a substring result
# when looking for "Foo - Foo" we don't want "Foobar"
diff --git a/headphones/webserve.py b/headphones/webserve.py
index f4a06c2b..9b09feba 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -14,6 +14,7 @@
# along with Headphones. If not, see .
# NZBGet support added by CurlyMo as a part of XBian - XBMC on the Raspberry Pi
+# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
from operator import itemgetter
import threading
@@ -28,7 +29,7 @@ import urllib2
import os
import re
-from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers
+from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers, crier
from headphones.helpers import checked, radio, today, clean_name
from mako.lookup import TemplateLookup
from mako import exceptions
@@ -69,6 +70,11 @@ class WebInterface(object):
artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return serve_template(templatename="index.html", title="Home", artists=artists)
+ @cherrypy.expose
+ def threads(self):
+ crier.cry()
+ raise cherrypy.HTTPRedirect("home")
+
@cherrypy.expose
def artistPage(self, ArtistID):
myDB = db.DBConnection()
@@ -1224,6 +1230,9 @@ class WebInterface(object):
"whatcd_ratio": headphones.CONFIG.WHATCD_RATIO,
"use_strike": checked(headphones.CONFIG.STRIKE),
"strike_ratio": headphones.CONFIG.STRIKE_RATIO,
+ "use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE),
+ "tquattrecentonze_user": headphones.CONFIG.TQUATTRECENTONZE_USER,
+ "tquattrecentonze_password": headphones.CONFIG.TQUATTRECENTONZE_PASSWORD,
"pref_qual_0": radio(headphones.CONFIG.PREFERRED_QUALITY, 0),
"pref_qual_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1),
"pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2),
@@ -1420,7 +1429,7 @@ class WebInterface(object):
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
"use_mininova", "use_waffles", "use_rutracker",
- "use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate",
+ "use_whatcd", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless", "detect_bitrate",
"ignore_clean_releases", "freeze_db", "cue_split", "move_files",
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
"embed_album_art", "embed_lyrics",
diff --git a/init-scripts/init.freebsd b/init-scripts/init.freebsd
index 85d2cf14..bd4cca2a 100755
--- a/init-scripts/init.freebsd
+++ b/init-scripts/init.freebsd
@@ -1,7 +1,8 @@
#!/bin/sh
#
# PROVIDE: headphones
-# REQUIRE: DAEMON sabnzbd
+# REQUIRE: DAEMON
+# BEFORE: LOGIN
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
@@ -15,56 +16,34 @@
# as root.
# headphones_dir: Directory where Headphones lives.
# Default: /usr/local/headphones
-# headphones_chdir: Change to this directory before running Headphones.
-# Default is same as headphones_dir.
# headphones_pid: The name of the pidfile to create.
# Default is headphones.pid in headphones_dir.
-PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
name="headphones"
rcvar=${name}_enable
-load_rc_config ${name}
-
: "${headphones_enable:="NO"}"
: "${headphones_user:="_sabnzbd"}"
: "${headphones_dir:="/usr/local/headphones"}"
-: "${headphones_chdir:="${headphones_dir}"}"
-: "${headphones_pid:="${headphones_dir}/headphones.pid"}"
+: "${headphones_conf:="/usr/local/headphones/config.ini"}"
-status_cmd="${name}_status"
-stop_cmd="${name}_stop"
+command="${headphones_dir}/Headphones.py"
+command_interpreter="/usr/bin/python"
+pidfile="/var/run/headphones/headphones.pid"
+start_precmd="headphones_start_precmd"
+headphones_flags="--daemon --nolaunch --pidfile $pidfile --config $headphones_conf $headphones_flags"
-command="/usr/sbin/daemon"
-command_args="-f -p ${headphones_pid} python ${headphones_dir}/Headphones.py ${headphones_flags} --quiet --nolaunch"
+headphones_start_precmd() {
+ if [ $($ID -u) != 0 ]; then
+ err 1 "Must be root."
+ fi
-# Ensure user is root when running this script.
-if [ "$(id -u)" != "0" ]; then
- echo "Oops, you should be root before running this!"
- exit 1
-fi
-
-verify_headphones_pid() {
- # Make sure the pid corresponds to the Headphones process.
- pid=$(cat "${headphones_pid}" 2>/dev/null)
- pgrep -F "${headphones_pid}" -q "python ${headphones_dir}/Headphones.py"
- return $?
-}
-
-# Try to stop Headphones cleanly by calling shutdown over http.
-headphones_stop() {
- echo "Stopping $name"
- verify_headphones_pid
- if [ -n "${pid}" ]; then
- wait_for_pids "${pid}"
- echo "Stopped"
- fi
-}
-
-headphones_status() {
- verify_headphones_pid && echo "$name is running as ${pid}" || echo "$name is not running"
+ if [ ! -d /var/run/headphones ]; then
+ install -do $headphones_user /var/run/headphones
+ fi
}
+load_rc_config ${name}
run_rc_command "$1"
diff --git a/init-scripts/init.ubuntu b/init-scripts/init.ubuntu
index c4c0b8dc..47f2f2b4 100755
--- a/init-scripts/init.ubuntu
+++ b/init-scripts/init.ubuntu
@@ -33,6 +33,7 @@
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for headphones, i.e. " --config=/home/headphones/config.ini"
## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
+## HP_HOST= #$HOST_OPTS, host for the webserver, overrides value in config.ini
##
## EXAMPLE if want to run as different user
## add HP_USER=username to /etc/default/headphones
@@ -105,7 +106,12 @@ load_settings() {
PORT_OPTS=" --port=${HP_PORT} "
}
- DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
+ # Host config
+ [ -n "$HP_HOST" ] && {
+ HOST_OPTS=" --host=${HP_HOST} "
+ }
+
+ DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS} ${HOST_OPTS} ${EXTRA_DAEMON_OPTS}"
SETTINGS_LOADED=TRUE
fi
diff --git a/lib/cherrypy/wsgiserver/wsgiserver2.py b/lib/cherrypy/wsgiserver/wsgiserver2.py
index b4919cf5..ee1f8214 100644
--- a/lib/cherrypy/wsgiserver/wsgiserver2.py
+++ b/lib/cherrypy/wsgiserver/wsgiserver2.py
@@ -99,12 +99,14 @@ DEFAULT_BUFFER_SIZE = -1
class FauxSocket(object):
-
"""Faux socket with the minimal interface required by pypy"""
def _reuse(self):
pass
+ def _drop(self):
+ pass
+
_fileobject_uses_str_type = isinstance(
socket._fileobject(FauxSocket())._rbuf, basestring)
del FauxSocket # this class is not longer required for anything.