Merge branch 'develop'

This commit is contained in:
rembo10
2014-05-15 21:10:41 -07:00
19 changed files with 645 additions and 278 deletions

View File

@@ -77,16 +77,16 @@ def main():
args = parser.parse_args()
if args.verbose:
headphones.VERBOSE = 2
elif args.quiet:
headphones.VERBOSE = 0
headphones.VERBOSE = True
if args.quiet:
headphones.QUIET = True
if args.daemon:
if sys.platform == 'win32':
print "Daemonize not supported under Windows, starting normally"
else:
headphones.DAEMON=True
headphones.VERBOSE = False
headphones.DAEMON = True
headphones.QUIET = True
if args.pidfile:
headphones.PIDFILE = str(args.pidfile)
@@ -101,7 +101,7 @@ def main():
try:
file(headphones.PIDFILE, 'w').write("pid\n")
except IOError, e:
raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno))
raise SystemExit("Unable to write PID file: %s [%d]", e.strerror, e.errno)
else:
logger.warn("Not running in daemon mode. PID file creation disabled.")

View File

@@ -240,7 +240,8 @@
refreshTable();
$('#artistnamelink').text(data["ArtistName"]);
if (loadingMessage == false){
showMsg("Getting artist information",true);
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
showArtistMsg("Getting artist information");
loadingMessage = true;
}
if (spinner_active == false){
@@ -255,8 +256,7 @@
else{
$('#artistnamespinner').remove()
$('#loadingtext').remove()
$('#ajaxMsg').empty()
$('#ajaxMsg').removeAttr('style')
$('#ajaxMsg2').remove()
spinner_active = false
loadingtext_active = false
loadingMessage = false
@@ -303,7 +303,7 @@
resetFilters("albums");
setTimeout(function(){
initFancybox();
},1500)
},1500);
}
$(document).ready(function() {

View File

@@ -29,7 +29,7 @@
</head>
<body>
<div id="container">
<div id="ajaxMsg"></div>
<div id="ajaxMsg" class="ajaxMsg"></div>
% if not headphones.CURRENT_VERSION:
<div id="updatebar">
You're running an unknown version of Headphones. <a href="update">Update</a> or

View File

@@ -470,7 +470,14 @@
<div class="row radio clearfix">
<input type="radio" name="preferred_quality" id="preferred_quality0" value="0" ${config['pref_qual_0']}><label>Highest Quality excluding Lossless</label>
<input type="radio" name="preferred_quality" id="preferred_quality1" value="1" ${config['pref_qual_1']}><label>Highest Quality including Lossless</label>
<input type="radio" name="preferred_quality" id="preferred_quality3" value="3" ${config['pref_qual_3']}><label>Lossless Only</label>
<input type="radio" name="preferred_quality" id="lossless_only" value="3" ${config['pref_qual_3']}><label>Lossless Only</label>
<div id="lossless_only_options">
<span style="padding-left: 20px">
Reject if target size is not in bitrate range: \
<input type="text" class="override-float" name="lossless_bitrate_from" value="${config['lossless_bitrate_from']}" size="4" title="e.g. if album length = 40 mins, from = 350 kbps, then min target size = 102.5 mb, anything less will be rejected">to\
<input type="text" class="override-float" name="lossless_bitrate_to" value="${config['lossless_bitrate_to']}" size="5" title="e.g. if album length = 40 mins, to = 2000 kbps, then max target size = 586 mb, anything greater will be rejected">kbps
</span>
</div>
<input type="radio" id="preferred_bitrate" name="preferred_quality" value="2" ${config['pref_qual_2']}>Preferred Bitrate: <input type="text" class="override-float" name="preferred_bitrate" value="${config['pref_bitrate']}" size="3">kbps<br>
<div id="preferred_bitrate_options">
Reject if <strong>less than</strong> <input type="text" class="override-float" name="preferred_bitrate_low_buffer" value="${config['pref_bitrate_low']}" size="3">% or <strong>more than</strong> <input type="text" class="override-float" name="preferred_bitrate_high_buffer" value="${config['pref_bitrate_high']}" size="3">% of the target size (leave blank for no limit)<br><br>
@@ -496,7 +503,7 @@
<div class="row">
<label>Preferred Words</label>
<input type="text" name="preferred_words" value="${config['preferred_words']}" size="50">
<small>Results with these words in the title will be preferred over results without them</small>
<small>Results with these words in the title will be preferred over results without them (search provider names can also be entered)</small>
</div>
<div class="row">
<label>Required Words</label>
@@ -1587,6 +1594,15 @@
}
});
if ($("#lossless_only").is(":checked"))
{
$("#lossless_only_options").show();
}
else
{
$("#lossless_only_options").hide();
}
if ($("#preferred_bitrate").is(":checked"))
{
$("#preferred_bitrate_options").show();
@@ -1632,18 +1648,22 @@
if ($("#preferred_bitrate").is(":checked"))
{
$("#preferred_bitrate_options").slideDown("fast");
$("#lossless_only_options").slideUp("fast");
}
if ($("#preferred_quality0").is(":checked"))
{
$("#preferred_bitrate_options").slideUp("fast");
$("#lossless_only_options").slideUp("fast");
}
if ($("#preferred_quality1").is(":checked"))
{
$("#preferred_bitrate_options").slideUp("fast");
$("#lossless_only_options").slideUp("fast");
}
if ($("#preferred_quality3").is(":checked"))
if ($("#lossless_only").is(":checked"))
{
$("#preferred_bitrate_options").slideUp("fast");
$("#lossless_only_options").slideDown("fast");
$("#preferred_bitrate_options").slideUp("fast");
}
if ($("#nzb_downloader_sabnzbd").is(":checked"))
{

View File

@@ -545,7 +545,7 @@ footer {
position: relative;
top: 4px;
}
#ajaxMsg {
.ajaxMsg {
border: 1px solid #cccccc;
background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
background-image: linear-gradient(#ffffff, #eeeeee) !important;
@@ -576,16 +576,16 @@ footer {
-o-opacity: 0.8 !important;
opacity: 0.8 !important;
}
#ajaxMsg .msg {
.ajaxMsg .msg {
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
line-height: normal;
padding-left: 20px;
}
#ajaxMsg .loader {
.ajaxMsg .loader {
position: relative;
top: 2px;
}
#ajaxMsg.success {
.ajaxMsg .success {
background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
@@ -595,7 +595,7 @@ footer {
padding: 15px 10px;
text-align: left;
}
#ajaxMsg.error {
.ajaxMsg .error {
background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
@@ -605,7 +605,7 @@ footer {
padding: 15px 10px;
text-align: left;
}
#ajaxMsg .ui-icon {
.ajaxMsg .ui-icon {
display: inline-block;
margin-left: -20px;
top: 2px;

View File

@@ -228,6 +228,21 @@ function showMsg(msg,loader,timeout,ms) {
}
}
function showArtistMsg(msg) {
var feedback = $("#ajaxMsg2");
update = $("#updatebar");
if ( update.is(":visible") ) {
var height = update.height() + 35;
feedback.css("bottom",height + "px");
} else {
feedback.removeAttr("style");
}
feedback.fadeIn();
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
feedback.css("padding","14px 10px")
$(feedback).prepend(message);
}
function doAjaxCall(url,elem,reload,form) {
// Set Message
feedback = $("#ajaxMsg");

View File

@@ -35,12 +35,12 @@
%if type == 'album':
<td id="albumname"><a href="${result['albumurl']}">${result['title']}</a></td>
%endif
<td id="artistname"><a href="${result['url']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="score"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></td>
%if type == 'album':
%if type == 'album':
<td id="add"><a href="addReleaseById?rid=${result['albumid']}"><i class="fa fa-plus"></i> Add this album</a></td>
%else:
<td id="add"><a href="addArtist?artistid=${result['id']}"><i class="fa fa-plus"></i> Add this artist</a></td>
<td id="add"><a href="${result['url']}"></i> View on MusicBrainz</a></td>
%endif
</tr>
%endfor
@@ -57,10 +57,10 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
function getArt() {
$("table#searchresults_table tr td#albumart img").each(function(){
$("table#searchresults_table tr td#albumart img").each(function(){
var id = $(this).attr('title');
var image = $(this);
if ( !image.hasClass('done') ) {
@@ -76,7 +76,7 @@
"bDestroy": true,
"aoColumnDefs": [
{ 'bSortable': false, 'aTargets': [ 0,3 ] }
],
],
"oLanguage": {
"sLengthMenu":"Show _MENU_ results per page",
"sEmptyTable": "No results",
@@ -91,7 +91,7 @@
resetFilters("album");
}
$(document).ready(function(){
initThisPage();
initThisPage();
});
$(window).load(function(){
initFancybox();

View File

@@ -38,7 +38,8 @@ SIGNAL = None
SYS_PLATFORM = None
SYS_ENCODING = None
VERBOSE = 1
QUIET = False
VERBOSE = False
DAEMON = False
CREATEPID = False
PIDFILE= None
@@ -103,6 +104,8 @@ PREFERRED_BITRATE_HIGH_BUFFER = None
PREFERRED_BITRATE_LOW_BUFFER = None
PREFERRED_BITRATE_ALLOW_LOSSLESS = False
DETECT_BITRATE = False
LOSSLESS_BITRATE_FROM = None
LOSSLESS_BITRATE_TO = None
ADD_ARTISTS = False
CORRECT_METADATA = False
MOVE_FILES = False
@@ -248,7 +251,7 @@ PLEX_UPDATE = False
PLEX_NOTIFY = False
NMA_ENABLED = False
NMA_APIKEY = None
NMA_PRIORITY = None
NMA_PRIORITY = 0
NMA_ONSNATCH = None
PUSHALOT_ENABLED = False
PUSHALOT_APIKEY = None
@@ -292,6 +295,8 @@ JOURNAL_MODE = None
UMASK = None
VERIFY_SSL_CERT = True
def CheckSection(sec):
""" Check if INI section exists, if not create it """
try:
@@ -338,7 +343,7 @@ def initialize():
with INIT_LOCK:
global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \
global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, QUIET, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \
HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, GIT_USER, GIT_BRANCH, DO_NOT_OVERRIDE_GIT_BRANCH, \
CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \
LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, ADD_ARTISTS, CORRECT_METADATA, MOVE_FILES, \
@@ -358,9 +363,9 @@ def initialize():
PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \
MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \
XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \
PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, LOSSLESS_BITRATE_FROM, LOSSLESS_BITRATE_TO, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, VERIFY_SSL_CERT
if __INITIALIZED__:
@@ -438,6 +443,8 @@ def initialize():
PREFERRED_BITRATE_LOW_BUFFER = check_setting_int(CFG, 'General', 'preferred_bitrate_low_buffer', '')
PREFERRED_BITRATE_ALLOW_LOSSLESS = bool(check_setting_int(CFG, 'General', 'preferred_bitrate_allow_lossless', 0))
DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0))
LOSSLESS_BITRATE_FROM = check_setting_int(CFG, 'General', 'lossless_bitrate_from', '')
LOSSLESS_BITRATE_TO = check_setting_int(CFG, 'General', 'lossless_bitrate_to', '')
ADD_ARTISTS = bool(check_setting_int(CFG, 'General', 'auto_add_artists', 1))
CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0))
MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0))
@@ -643,6 +650,8 @@ def initialize():
ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80)
VERIFY_SSL_CERT = bool(check_setting_int(CFG, 'Advanced', 'verify_ssl_cert', 1))
# update folder formats in the config & bump up config version
if CONFIG_VERSION == '0':
from headphones.helpers import replace_all
@@ -713,8 +722,9 @@ def initialize():
if VERBOSE:
sys.stderr.write('Unable to create the log directory. Logging to screen only.\n')
# Start the logger, silence console logging if we need to
logger.initLogger(verbose=VERBOSE)
# Start the logger, disable console if needed
logger.initLogger(console=not QUIET, verbose=VERBOSE)
logger.initLogger(console=not QUIET, verbose=False)
if not CACHE_DIR:
# Put the cache dir in the data dir for now
@@ -862,6 +872,8 @@ def config_write():
new_config['General']['preferred_bitrate_low_buffer'] = PREFERRED_BITRATE_LOW_BUFFER
new_config['General']['preferred_bitrate_allow_lossless'] = int(PREFERRED_BITRATE_ALLOW_LOSSLESS)
new_config['General']['detect_bitrate'] = int(DETECT_BITRATE)
new_config['General']['lossless_bitrate_from'] = LOSSLESS_BITRATE_FROM
new_config['General']['lossless_bitrate_to'] = LOSSLESS_BITRATE_TO
new_config['General']['auto_add_artists'] = int(ADD_ARTISTS)
new_config['General']['correct_metadata'] = int(CORRECT_METADATA)
new_config['General']['move_files'] = int(MOVE_FILES)
@@ -1014,7 +1026,7 @@ def config_write():
new_config['NMA'] = {}
new_config['NMA']['nma_enabled'] = int(NMA_ENABLED)
new_config['NMA']['nma_apikey'] = NMA_APIKEY
new_config['NMA']['nma_priority'] = NMA_PRIORITY
new_config['NMA']['nma_priority'] = int(NMA_PRIORITY)
new_config['NMA']['nma_onsnatch'] = int(NMA_ONSNATCH)
new_config['Pushalot'] = {}
@@ -1092,6 +1104,7 @@ def config_write():
new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT
new_config['Advanced']['cache_sizemb'] = CACHE_SIZEMB
new_config['Advanced']['journal_mode'] = JOURNAL_MODE
new_config['Advanced']['verify_ssl_cert'] = int(VERIFY_SSL_CERT)
new_config.write()

View File

@@ -231,22 +231,32 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
if not forcefull:
new_release_group = False
try:
check_release_date = rg_exists['ReleaseDate']
except TypeError:
check_release_date = None
new_release_group = True
if check_release_date:
if check_release_date[0] is None:
if new_release_group:
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras)
else:
if check_release_date is None or check_release_date == u"None":
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras,True)
else:
if len(check_release_date[0]) == 10:
release_date = check_release_date[0]
elif len(check_release_date[0]) == 7:
release_date = check_release_date[0]+"-31"
elif len(check_release_date[0]) == 4:
release_date = check_release_date[0]+"-12-31"
if len(check_release_date) == 10:
release_date = check_release_date
elif len(check_release_date) == 7:
release_date = check_release_date+"-31"
elif len(check_release_date) == 4:
release_date = check_release_date+"-12-31"
else:
release_date = today
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
@@ -256,9 +266,6 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
logger.info("[%s] Skipping: %s (Release Date >%s Days)" % (artist['artist_name'], rg['title'], pause_delta))
skip_log = 1
new_releases = 0
else:
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras)
if force_repackage == 1:
new_releases = -1
@@ -496,7 +503,7 @@ def finalize_update(artistid, artistname, errors=False):
myDB = db.DBConnection()
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid]))
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [artistid]))
#havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artistname]))
@@ -582,6 +589,7 @@ def addReleaseById(rid):
controlValueDict = {"AlbumID": rgid}
newValueDict = {"ArtistID": release_dict['artist_id'],
"ReleaseID": rgid,
"ArtistName": release_dict['artist_name'],
"AlbumTitle": release_dict['rg_title'],
"AlbumASIN": release_dict['asin'],

View File

@@ -43,19 +43,32 @@ class LogListHandler(logging.Handler):
headphones.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName))
def initLogger(verbose=1):
def initLogger(console=False, verbose=False):
"""
Setup logging for Headphones. It uses the logger instance with the name
'headphones'. Three log handlers are added:
* RotatingFileHandler: for the file headphones.log
* LogListHandler: for Web UI
* StreamHandler: for console (if verbose > 0)
* StreamHandler: for console (if console)
Console logging is only enabled if console is set to True.
"""
# Close and remove old handlers. This is required to reinit the loggers
# at runtime
for handler in logger.handlers[:]:
# Just make sure it is cleaned up.
if isinstance(handler, handlers.RotatingFileHandler):
handler.close()
elif isinstance(handler, logging.StreamHandler):
handler.flush()
logger.removeHandler(handler)
# Configure the logger to accept all messages
logger.propagate = False
logger.setLevel(logging.DEBUG if verbose == 2 else logging.INFO)
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
# Setup file logger
filename = os.path.join(headphones.LOG_DIR, FILENAME)
@@ -74,7 +87,7 @@ def initLogger(verbose=1):
logger.addHandler(loglist_handler)
# Setup console logger
if verbose:
if console:
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
console_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter)

View File

@@ -27,7 +27,7 @@ import time
from xml.dom import minidom
from httplib import HTTPSConnection
from urllib import urlencode
from lib.pynma import pynma
import lib.oauth2 as oauth
import lib.pythontwitter as twitter
@@ -380,34 +380,40 @@ class Plex:
logger.warn('Error sending notification request to Plex Media Server')
class NMA:
def notify(self, artist=None, album=None, snatched=None):
title = 'Headphones'
api = headphones.NMA_APIKEY
nma_priority = headphones.NMA_PRIORITY
def __init__(self):
logger.debug(u"NMA title: " + title)
logger.debug(u"NMA API: " + api)
logger.debug(u"NMA Priority: " + str(nma_priority))
self.apikey = headphones.NMA_APIKEY
self.priority = headphones.NMA_PRIORITY
def _send(self, data):
return request.request_content('https://www.notifymyandroid.com/publicapi/notify', data=data)
def notify(self, artist=None, album=None, snatched_nzb=None):
apikey = self.apikey
priority = self.priority
if snatched_nzb:
event = snatched_nzb + " snatched!"
description = "Headphones has snatched: " + snatched_nzb + " and has sent it to SABnzbd+"
if snatched:
event = snatched + " snatched!"
message = "Headphones has snatched: " + snatched
else:
event = artist + ' - ' + album + ' complete!'
description = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
data = { 'apikey': apikey, 'application':'Headphones', 'event': event, 'description': description, 'priority': priority}
logger.debug(u"NMA event: " + event)
logger.debug(u"NMA message: " + message)
logger.info('Sending notification request to NotifyMyAndroid')
request = self._send(data)
batch = False
if not request:
logger.warn('Error sending notification request to NotifyMyAndroid')
p = pynma.PyNMA()
keys = api.split(',')
p.addkey(keys)
if len(keys) > 1: batch = True
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch)
if not response[api][u'code'] == u'200':
logger.error(u'Could not send notification to NotifyMyAndroid')
return False
else:
return True
class PUSHBULLET:

View File

@@ -5,6 +5,7 @@ from bs4 import BeautifulSoup
import requests
import feedparser
import headphones
def request_response(url, method="get", auto_raise=True, whitelist_status_code=None, **kwargs):
"""
@@ -17,9 +18,13 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
if whitelist_status_code and type(whitelist_status_code) != list:
whitelist_status_code = [whitelist_status_code]
# Disable verification of SSL certificates if requested. Note: this could
# pose a security issue!
kwargs["verify"] = headphones.VERIFY_SSL_CERT
# Map method to the request.XXX method. This is a simple hack, but it allows
# requests to apply more magic per method. See lib/requests/api.py.
request_method = getattr(requests, method)
request_method = getattr(requests, method.lower())
try:
# Request the URL
@@ -45,7 +50,15 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
logger.error("Request timed out.")
except requests.HTTPError, e:
if e.response is not None:
logger.error("Request raise HTTP error with status code: %d", e.response.status_code)
if e.response.status_code >= 500:
cause = "remote server error"
elif e.response.status_code >= 400:
cause = "local request error"
else:
# I don't think we will end up here, but for completeness
cause = "unknown"
logger.error("Request raise HTTP error with status code %d (%s).", e.response.status_code, cause)
else:
logger.error("Request raised HTTP error.")
except requests.RequestException, e:

View File

@@ -21,6 +21,8 @@ from lib.pygazelle import encoding as gazelleencoding
from lib.pygazelle import format as gazelleformat
from lib.pygazelle import media as gazellemedia
from xml.dom import minidom
from base64 import b16encode, b32decode
from hashlib import sha1
import os, re, time
import string
@@ -28,12 +30,13 @@ import shutil
import requests
import subprocess
import headphones
from headphones.common import USER_AGENT
from headphones import logger, db, helpers, classes, sab, nzbget, request
from headphones import utorrent, transmission, notifiers
import lib.bencode as bencode
from lib.bencode import bencode as bencode, bdecode
import headphones.searcher_rutracker as rutrackersearch
rutracker = rutrackersearch.Rutracker()
@@ -148,11 +151,20 @@ def sort_search_results(resultlist, album, new):
# Add a priority if it has any of the preferred words
temp_list = []
preferred_words = None
if headphones.PREFERRED_WORDS:
preferred_words = helpers.split_string(headphones.PREFERRED_WORDS)
for result in resultlist:
if headphones.PREFERRED_WORDS and any(word.lower() in result[0].lower() for word in helpers.split_string(headphones.PREFERRED_WORDS)):
temp_list.append((result[0],result[1],result[2],result[3],result[4],1))
else:
temp_list.append((result[0],result[1],result[2],result[3],result[4],0))
priority = 0
if preferred_words:
if any(word.lower() in result[0].lower() for word in preferred_words):
priority = 1
# add a search provider priority (weighted based on position)
i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()), None)
if i != None:
priority += round((len(preferred_words) - i) / float(len(preferred_words)),2)
temp_list.append((result[0],result[1],result[2],result[3],result[4],priority))
resultlist = temp_list
@@ -215,6 +227,34 @@ def sort_search_results(resultlist, album, new):
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
# lossless - ignore results if target size outside bitrate range
elif headphones.PREFERRED_QUALITY == 3 and (headphones.LOSSLESS_BITRATE_FROM or headphones.LOSSLESS_BITRATE_TO):
finallist = []
tracks = myDB.select('SELECT TrackDuration from tracks WHERE AlbumID=?', [album['AlbumID']])
if len(tracks):
albumlength = sum([pair[0] for pair in tracks])
mintargetsize = 0
maxtargetsize = 0
if headphones.LOSSLESS_BITRATE_FROM:
mintargetsize = albumlength/1000 * int(headphones.LOSSLESS_BITRATE_FROM) * 128
if headphones.LOSSLESS_BITRATE_TO:
maxtargetsize = albumlength/1000 * int(headphones.LOSSLESS_BITRATE_TO) * 128
if mintargetsize > 0 or maxtargetsize > 0:
for i, result in reversed(list(enumerate(resultlist))):
if int(result[1]) < mintargetsize and mintargetsize > 0 or int(result[1]) > maxtargetsize and maxtargetsize > 0:
if int(result[1]) < mintargetsize:
logger.info("%s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(mintargetsize))
else:
logger.info("%s is too large for this album - not considering it. (Size: %s, Maxsize: %s)", result[0], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(maxtargetsize))
del resultlist[i]
if len (resultlist):
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
else:
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
@@ -534,8 +574,7 @@ def searchNZB(album, new=False, losslessOnly=False):
data = request.request_json(
url='http://api.omgwtfnzbs.org/json/',
params=params, headers=headers,
validator=lambda x: type(x) == dict
params=params, headers=headers
)
# Parse response
@@ -659,7 +698,7 @@ def send_to_downloader(data, bestqual, album):
#Open the fresh torrent file again so we can extract the proper torrent name
#Used later in post-processing.
with open(download_path, 'rb') as fp:
torrent_info = bencode.bdecode(fp.read())
torrent_info = bdecode(fp.read())
folder_name = torrent_info['info'].get('name', '')
logger.info('Torrent folder name: %s' % folder_name)
@@ -696,16 +735,18 @@ def send_to_downloader(data, bestqual, album):
except Exception, e:
logger.exception("Unhandled exception")
else:
else:# if headphones.TORRENT_DOWNLOADER == 2:
logger.info("Sending torrent to uTorrent")
# rutracker needs cookies to be set, pass the .torrent file instead of url
if bestqual[3] == 'rutracker.org':
file_or_url = rutracker.get_torrent(bestqual[2])
else:
file_or_url = bestqual[2]
folder_name = utorrent.addTorrent(file_or_url)
_hash = CalculateTorrentHash(file_or_url, data)
folder_name = utorrent.addTorrent(file_or_url, _hash)
if folder_name:
logger.info('Torrent folder name: %s' % folder_name)
@@ -757,7 +798,7 @@ def send_to_downloader(data, bestqual, album):
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
logger.info(u"Sending NMA notification")
nma = notifiers.NMA()
nma.notify(snatched_nzb=name)
nma.notify(snatched=name)
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH:
logger.info(u"Sending Pushalot notification")
pushalot = notifiers.PUSHALOT()
@@ -1400,8 +1441,8 @@ def preprocess(resultlist):
for result in resultlist:
if result[4] == 'torrent':
#Get out of here if we're using Transmission or uTorrent
if headphones.TORRENT_DOWNLOADER != 0:
#Get out of here if we're using Transmission
if headphones.TORRENT_DOWNLOADER == 1: ## if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
return True, result
# get outta here if rutracker
if result[3] == 'rutracker.org':
@@ -1451,3 +1492,19 @@ def preprocess(resultlist):
continue
return (None, None)
def CalculateTorrentHash(link, data):
if link.startswith('magnet'):
tor_hash = re.findall('urn:btih:([\w]{32,40})', link)[0]
if len(tor_hash) == 32:
tor_hash = b16encode(b32decode(tor_hash)).lower()
else:
info = bdecode(data)["info"]
tor_hash = sha1(bencode(info)).hexdigest()
logger.debug('Torrent Hash: ' + str(tor_hash))
return tor_hash

View File

@@ -18,6 +18,7 @@ from tempfile import mkdtemp
class Rutracker():
logged_in = False
# Stores a number of login attempts to prevent recursion.
#login_counter = 0
@@ -29,7 +30,7 @@ class Rutracker():
def login(self, login, password):
"""Implements tracker login procedure."""
self.logged_in = False
if login is None or password is None:
@@ -51,7 +52,6 @@ class Rutracker():
pass
# Check if we're logged in
for cookie in self.cookiejar:
if cookie.name == 'bb_data':
self.logged_in = True
@@ -64,7 +64,6 @@ class Rutracker():
"""
# Build search url
searchterm = ''
if artist != 'Various Artists':
searchterm = artist
@@ -82,8 +81,7 @@ class Rutracker():
else:
format = '+mp3||aac'
# sort by size, descending.
# sort by size, descending.
sort = '&o=7&s=2'
searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort)
@@ -111,25 +109,21 @@ class Rutracker():
#logger.debug (soup.prettify())
# Title
for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}):
title = link.get_text()
titles.append(title)
# Download URL
for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}):
url = link.get('href')
urls.append(url)
# Seeders
for link in soup.find_all('b', attrs={'class' : 'seedmed'}):
seeder = link.get_text()
seeders.append(seeder)
# Size
for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}):
size = link.u.string
sizes.append(size)
@@ -138,30 +132,33 @@ class Rutracker():
pass
# Combine lists
torrentlist = zip(titles, urls, seeders, sizes)
# return if nothing found
if not torrentlist:
return False
# get headphones track count for album, return if not found
myDB = db.DBConnection()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
hptrackcount = len(tracks)
if not hptrackcount:
logger.info('headphones track info not found, cannot compare to torrent')
return False
# Return all valid entries, ignored, required words now checked in searcher.py
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif']
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive']
# don't bother checking track counts anymore, let searcher filter instead
# leave code in just in case
check_track_count = False
if check_track_count:
# get headphones track count for album, return if not found
myDB = db.DBConnection()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
hptrackcount = len(tracks)
if not hptrackcount:
logger.info('headphones track info not found, cannot compare to torrent')
return False
# Return all valid entries, ignored, required words now checked in searcher.py
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif']
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive']
for torrent in torrentlist:
@@ -169,105 +166,102 @@ class Rutracker():
url = torrent[1]
seeders = torrent[2]
size = torrent[3]
title = returntitle.lower()
if int(size) <= maxsize and int(seeders) >= minseeders:
# Check torrent info
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
# Debug
#for cookie in self.cookiejar:
# logger.debug ('Cookie: %s' % cookie)
try:
page = self.opener.open(url)
torrent = page.read()
if torrent:
decoded = bencode.bdecode(torrent)
metainfo = decoded['info']
page.close ()
except Exception, e:
logger.error('Error getting torrent: %s' % e)
return False
# get torrent track count and check for cue
trackcount = 0
cuecount = 0
if 'files' in metainfo: # multi
for pathfile in metainfo['files']:
path = pathfile['path']
for file in path:
if any(file.lower().endswith('.' + x.lower()) for x in formatlist):
trackcount += 1
if '.cue' in file:
cuecount += 1
#Torrent topic page
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
logger.debug ('torrent title: %s' % title)
logger.debug ('headphones trackcount: %s' % hptrackcount)
logger.debug ('rutracker trackcount: %s' % trackcount)
# If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s)
# This is for the case where we have a single .flac/.wav which can be split by cue
# Not great, but shouldn't be doing this too often
totallogcount = 0
if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount:
page = self.opener.open(topicurl, timeout=60)
soup = BeautifulSoup(page.read())
findtoc = soup.find_all(text='TOC of the extracted CD')
if not findtoc:
findtoc = soup.find_all(text='TOC извлечённого CD')
for toc in findtoc:
logcount = 0
for toccontent in toc.find_all_next(text=True):
cut_string = toccontent.split('|')
new_string = cut_string[0].lstrip().rstrip()
if new_string == '1' or new_string == '01':
logcount = 1
elif logcount > 0:
if new_string.isdigit():
logcount += 1
else:
break
totallogcount = totallogcount + logcount
if totallogcount > 0:
trackcount = totallogcount
logger.debug ('rutracker logtrackcount: %s' % totallogcount)
# If torrent track count = hp track count then return torrent,
# if greater, check for deluxe/special/foreign editions
# if less, then allow if it's a single track with a cue
valid = False
if trackcount == hptrackcount:
if int(size) <= maxsize and int(seeders) >= minseeders:
#Torrent topic page
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
# add to list
if not check_track_count:
valid = True
elif trackcount > hptrackcount:
if any(deluxe in title for deluxe in deluxelist):
else:
# Check torrent info
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
# Debug
#for cookie in self.cookiejar:
# logger.debug ('Cookie: %s' % cookie)
try:
page = self.opener.open(url)
torrent = page.read()
if torrent:
decoded = bencode.bdecode(torrent)
metainfo = decoded['info']
page.close ()
except Exception, e:
logger.error('Error getting torrent: %s' % e)
return False
# get torrent track count and check for cue
trackcount = 0
cuecount = 0
if 'files' in metainfo: # multi
for pathfile in metainfo['files']:
path = pathfile['path']
for file in path:
if any(file.lower().endswith('.' + x.lower()) for x in formatlist):
trackcount += 1
if '.cue' in file:
cuecount += 1
title = returntitle.lower()
logger.debug ('torrent title: %s' % title)
logger.debug ('headphones trackcount: %s' % hptrackcount)
logger.debug ('rutracker trackcount: %s' % trackcount)
# If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s)
# This is for the case where we have a single .flac/.wav which can be split by cue
# Not great, but shouldn't be doing this too often
totallogcount = 0
if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount:
page = self.opener.open(topicurl, timeout=60)
soup = BeautifulSoup(page.read())
findtoc = soup.find_all(text='TOC of the extracted CD')
if not findtoc:
findtoc = soup.find_all(text='TOC извлечённого CD')
for toc in findtoc:
logcount = 0
for toccontent in toc.find_all_next(text=True):
cut_string = toccontent.split('|')
new_string = cut_string[0].lstrip().rstrip()
if new_string == '1' or new_string == '01':
logcount = 1
elif logcount > 0:
if new_string.isdigit():
logcount += 1
else:
break
totallogcount = totallogcount + logcount
if totallogcount > 0:
trackcount = totallogcount
logger.debug ('rutracker logtrackcount: %s' % totallogcount)
# If torrent track count = hp track count then return torrent,
# if greater, check for deluxe/special/foreign editions
# if less, then allow if it's a single track with a cue
valid = False
if trackcount == hptrackcount:
valid = True
elif trackcount > hptrackcount:
if any(deluxe in title for deluxe in deluxelist):
valid = True
# Add to list
if valid:
rulist.append((returntitle, size, topicurl))
else:
if topicurl:
logger.info(u'<a href="%s">Torrent</a> found with %s tracks but the selected headphones release has %s tracks, skipping for rutracker.org' % (topicurl, trackcount, hptrackcount))
else:
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (returntitle, int(size), int(seeders)))
return rulist
def get_torrent(self, url, savelocation=None):

View File

@@ -30,7 +30,14 @@ from headphones import logger, notifiers, request
def addTorrent(link):
method = 'torrent-add'
arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR}
if link.endswith('.torrent'):
f = open(link,'rb')
metainfo = str(base64.b64encode(f.read()))
f.close()
arguments = {'metainfo': metainfo, 'download-dir':headphones.DOWNLOAD_TORRENT_DIR}
else:
arguments = {'filename': link, 'download-dir': headphones.DOWNLOAD_TORRENT_DIR}
response = torrentAction(method,arguments)

View File

@@ -13,86 +13,154 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import re
import os
import time
import base64
import urllib, urllib2, urlparse, cookielib
import json, re, os, time
import headphones
import simplejson as json
from headphones import logger
from headphones import logger, notifiers, request
class utorrentclient(object):
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
# This is just a simple script to send torrents to transmission. The
# intention is to turn this into a class where we can check the state
# of the download, set the download dir, etc.
# TODO: Store the session id so we don't need to make 2 calls
# Store torrent id so we can check up on it
def __init__(self, base_url = None, username = None, password = None,):
host = headphones.UTORRENT_HOST
if not host.startswith('http'):
host = 'http://' + host
if host.endswith('/'):
host = host[:-1]
if host.endswith('/gui'):
host = host[:-4]
self.base_url = host
self.username = headphones.UTORRENT_USERNAME
self.password = headphones.UTORRENT_PASSWORD
self.opener = self._make_opener('uTorrent', self.base_url, self.username, self.password)
self.token = self._get_token()
#TODO refresh token, when necessary
def _make_opener(self, realm, base_url, username, password):
"""uTorrent API need HTTP Basic Auth and cookie support for token verify."""
auth = urllib2.HTTPBasicAuthHandler()
auth.add_password(realm=realm,uri=base_url,user=username,passwd=password)
opener = urllib2.build_opener(auth)
urllib2.install_opener(opener)
cookie_jar = cookielib.CookieJar()
cookie_handler = urllib2.HTTPCookieProcessor(cookie_jar)
handlers = [auth, cookie_handler]
opener = urllib2.build_opener(*handlers)
return opener
def _get_token(self):
url = urlparse.urljoin(self.base_url, 'gui/token.html')
try:
response = self.opener.open(url)
except urllib2.HTTPError as err:
logger.debug('URL: ' + str(url))
logger.debug('Error getting Token. uTorrent responded with error: ' + str(err))
match = re.search(utorrentclient.TOKEN_REGEX, response.read())
return match.group(1)
def list(self, **kwargs):
params = [('list', '1')]
params += kwargs.items()
return self._action(params)
def add_url(self, url):
#can receive magnet or normal .torrent link
params = [('action', 'add-url'), ('s', url)]
return self._action(params)
def start(self, *hashes):
params = [('action', 'start'), ]
for hash in hashes:
params.append(('hash', hash))
return self._action(params)
def stop(self, *hashes):
params = [('action', 'stop'), ]
for hash in hashes:
params.append(('hash', hash))
return self._action(params)
def pause(self, *hashes):
params = [('action', 'pause'), ]
for hash in hashes:
params.append(('hash', hash))
return self._action(params)
def forcestart(self, *hashes):
params = [('action', 'forcestart'), ]
for hash in hashes:
params.append(('hash', hash))
return self._action(params)
def getfiles(self, hash):
params = [('action', 'getfiles'), ('hash', hash)]
return self._action(params)
def getprops(self, hash):
params = [('action', 'getprops'), ('hash', hash)]
return self._action(params)
def setprops(self, hash, s, v):
params = [('action', 'setprops'), ('hash', hash), ("s", s), ("v", v)]
return self._action(params)
def setprio(self, hash, priority, *files):
params = [('action', 'setprio'), ('hash', hash), ('p', str(priority))]
for file_index in files:
params.append(('f', str(file_index)))
return self._action(params)
def _action(self, params, body=None, content_type=None):
url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params)
request = urllib2.Request(url)
if body:
request.add_data(body)
request.add_header('Content-length', len(body))
if content_type:
request.add_header('Content-type', content_type)
try:
response = self.opener.open(request)
return response.code, json.loads(response.read())
except urllib2.HTTPError as err:
logger.debug('URL: ' + str(url))
logger.debug('uTorrent webUI raised the following error: ' + str(err))
def addTorrent(link):
host = headphones.UTORRENT_HOST
username = headphones.UTORRENT_USERNAME
password = headphones.UTORRENT_PASSWORD
def labelTorrent(hash):
label = headphones.UTORRENT_LABEL
token = ''
uTorrentClient = utorrentclient()
settinglabel = True
while settinglabel:
torrentList = uTorrentClient.list()
for torrent in torrentList[1].get('torrents'):
if (torrent[0].lower() == hash):
uTorrentClient.setprops(hash,'label',label)
settinglabel = False
return True
if not host.startswith('http'):
host = 'http://' + host
if host.endswith('/'):
host = host[:-1]
def dirTorrent(hash):
uTorrentClient = utorrentclient()
torrentList = uTorrentClient.list()
for torrent in torrentList[1].get('torrents'):
if (torrent[0].lower() == hash):
return torrent[26]
return False
if host.endswith('/gui'):
host = host + '/'
else:
host = host + '/gui/'
# Retrieve session id
auth = (username, password) if username and password else None
token_request = request.request_response(host + 'token.html', auth=auth)
token = re.findall('<div.*?>(.*?)</', token_request.content)[0]
guid = token_request.cookies['GUID']
cookies = dict(GUID = guid)
if link.startswith("magnet") or link.startswith("http") or link.endswith(".torrent"):
params = {'action':'add-url', 's':link, 'token':token}
response = request.request_json(host, params=params, auth=auth, cookies=cookies)
else:
params = {'action':'add-file', 'token':token}
files = {'torrent_file':{'music.torrent' : link}}
response = request.request_json(host, method="post", params=params, files=files, auth=auth, cookies=cookies)
if not response:
logger.error("Error sending torrent to uTorrent")
return
if link.startswith('magnet'):
tor_hash = re.findall('urn:btih:([\w]{32,40})', link)[0]
if len(tor_hash) == 32:
tor_hash = b16encode(b32decode(tor_hash)).lower()
else:
info = bdecode(link.content)["info"]
tor_hash = sha1(bencode(info)).hexdigest()
params = {'action':'setprops', 'hash':tor_hash,'s':'label', 'v':label, 'token':token}
response = request.request_json(host, params=params, auth=auth, cookies=cookies)
if not response:
logger.error("Error setting torrent label in uTorrent")
return
# folder info can probably be cleaned up with getprops
folder = None
params = {'list':'1', 'token':token}
response = request.request_json(host, params=params, auth=auth, cookies=cookies)
if not response:
logger.error("Error getting torrent information from uTorrent")
return
for torrent in response['torrents']:
folder = os.path.basename(torrent[26])
return folder
def addTorrent(link, hash):
uTorrentClient = utorrentclient()
uTorrentClient.add_url(link)
labelTorrent(hash)
return dirTorrent(hash)

View File

@@ -736,6 +736,14 @@ class WebInterface(object):
raise cherrypy.HTTPRedirect("logs")
clearLogs.exposed = True
def toggleVerbose(self):
headphones.VERBOSE = not headphones.VERBOSE
logger.initLogger(not headphones.QUIET, headphones.VERBOSE)
logger.info("Verbose toggled, set to %s", headphones.VERBOSE)
logger.debug("If you read this message, debug logging is available")
raise cherrypy.HTTPRedirect("logs")
toggleVerbose.exposed = True
def getLog(self,iDisplayStart=0,iDisplayLength=100,iSortCol_0=0,sSortDir_0="desc",sSearch="",**kwargs):
iDisplayStart = int(iDisplayStart)
@@ -1000,6 +1008,8 @@ class WebInterface(object):
"pref_bitrate_low" : headphones.PREFERRED_BITRATE_LOW_BUFFER,
"pref_bitrate_allow_lossless" : checked(headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS),
"detect_bitrate" : checked(headphones.DETECT_BITRATE),
"lossless_bitrate_from" : headphones.LOSSLESS_BITRATE_FROM,
"lossless_bitrate_to" : headphones.LOSSLESS_BITRATE_TO,
"move_files" : checked(headphones.MOVE_FILES),
"rename_files" : checked(headphones.RENAME_FILES),
"correct_metadata" : checked(headphones.CORRECT_METADATA),
@@ -1136,8 +1146,8 @@ class WebInterface(object):
xbmc_update=0, xbmc_notify=0, nma_enabled=False, nma_apikey=None, nma_priority=0, nma_onsnatch=0, pushalot_enabled=False, pushalot_apikey=None, pushalot_onsnatch=0, synoindex_enabled=False, lms_enabled=0, lms_host=None,
pushover_enabled=0, pushover_onsnatch=0, pushover_keys=None, pushover_priority=0, pushover_apitoken=None, pushbullet_enabled=0, pushbullet_onsnatch=0, pushbullet_apikey=None, pushbullet_deviceid=None, twitter_enabled=0, twitter_onsnatch=0,
osx_notify_enabled=0, osx_notify_onsnatch=0, osx_notify_app=None, boxcar_enabled=0, boxcar_onsnatch=0, boxcar_token=None, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None,
preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None, file_permissions=None, folder_permissions=None,
plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0,
preferred_bitrate_high_buffer=None, preferred_bitrate_low_buffer=None, preferred_bitrate_allow_lossless=0, lossless_bitrate_from=None, lossless_bitrate_to=None, cache_sizemb=None, enable_https=0, https_cert=None, https_key=None,
file_permissions=None, folder_permissions=None, plex_enabled=0, plex_server_host=None, plex_client_host=None, plex_username=None, plex_password=None, plex_update=0, plex_notify=0,
songkick_enabled=0, songkick_apikey=None, songkick_location=None, songkick_filter_enabled=0, encoder_multicore=False, encoder_multicore_count=0, mpc_enabled=False, **kwargs ):
headphones.HTTP_HOST = http_host
@@ -1216,6 +1226,8 @@ class WebInterface(object):
headphones.PREFERRED_BITRATE_LOW_BUFFER = preferred_bitrate_low_buffer
headphones.PREFERRED_BITRATE_ALLOW_LOSSLESS = preferred_bitrate_allow_lossless
headphones.DETECT_BITRATE = detect_bitrate
headphones.LOSSLESS_BITRATE_FROM = lossless_bitrate_from
headphones.LOSSLESS_BITRATE_TO = lossless_bitrate_to
headphones.MOVE_FILES = move_files
headphones.CORRECT_METADATA = correct_metadata
headphones.RENAME_FILES = rename_files

4
lib/pynma/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
from pynma import PyNMA

137
lib/pynma/pynma.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/python
from xml.dom.minidom import parseString
from httplib import HTTPSConnection
from urllib import urlencode
__version__ = "0.1"
API_SERVER = 'nma.usk.bz'
ADD_PATH = '/publicapi/notify'
USER_AGENT="PyNMA/v%s"%__version__
def uniq_preserve(seq): # Dave Kirby
# Order preserving
seen = set()
return [x for x in seq if x not in seen and not seen.add(x)]
def uniq(seq):
# Not order preserving
return {}.fromkeys(seq).keys()
class PyNMA(object):
"""PyNMA(apikey=[], developerkey=None)
takes 2 optional arguments:
- (opt) apykey: might me a string containing 1 key or an array of keys
- (opt) developerkey: where you can store your developer key
"""
def __init__(self, apikey=[], developerkey=None):
self._developerkey = None
self.developerkey(developerkey)
if apikey:
if type(apikey) == str:
apikey = [apikey]
self._apikey = uniq(apikey)
def addkey(self, key):
"Add a key (register ?)"
if type(key) == str:
if not key in self._apikey:
self._apikey.append(key)
elif type(key) == list:
for k in key:
if not k in self._apikey:
self._apikey.append(k)
def delkey(self, key):
"Removes a key (unregister ?)"
if type(key) == str:
if key in self._apikey:
self._apikey.remove(key)
elif type(key) == list:
for k in key:
if key in self._apikey:
self._apikey.remove(k)
def developerkey(self, developerkey):
"Sets the developer key (and check it has the good length)"
if type(developerkey) == str and len(developerkey) == 48:
self._developerkey = developerkey
def push(self, application="", event="", description="", url="", priority=0, batch_mode=False):
"""Pushes a message on the registered API keys.
takes 5 arguments:
- (req) application: application name [256]
- (req) event: event name [1000]
- (req) description: description [10000]
- (opt) url: url [512]
- (opt) priority: from -2 (lowest) to 2 (highest) (def:0)
- (opt) batch_mode: call API 5 by 5 (def:False)
Warning: using batch_mode will return error only if all API keys are bad
cf: http://nma.usk.bz/api.php
"""
datas = {
'application': application[:256].encode('utf8'),
'event': event[:1024].encode('utf8'),
'description': description[:10000].encode('utf8'),
'priority': priority
}
if url:
datas['url'] = url[:512]
if self._developerkey:
datas['developerkey'] = self._developerkey
results = {}
if not batch_mode:
for key in self._apikey:
datas['apikey'] = key
res = self.callapi('POST', ADD_PATH, datas)
results[key] = res
else:
for i in range(0, len(self._apikey), 5):
datas['apikey'] = ",".join(self._apikey[i:i+5])
res = self.callapi('POST', ADD_PATH, datas)
results[datas['apikey']] = res
return results
def callapi(self, method, path, args):
headers = { 'User-Agent': USER_AGENT }
if method == "POST":
headers['Content-type'] = "application/x-www-form-urlencoded"
http_handler = HTTPSConnection(API_SERVER)
http_handler.request(method, path, urlencode(args), headers)
resp = http_handler.getresponse()
try:
res = self._parse_reponse(resp.read())
except Exception, e:
res = {'type': "pynmaerror",
'code': 600,
'message': str(e)
}
pass
return res
def _parse_reponse(self, response):
root = parseString(response).firstChild
for elem in root.childNodes:
if elem.nodeType == elem.TEXT_NODE: continue
if elem.tagName == 'success':
res = dict(elem.attributes.items())
res['message'] = ""
res['type'] = elem.tagName
return res
if elem.tagName == 'error':
res = dict(elem.attributes.items())
res['message'] = elem.firstChild.nodeValue
res['type'] = elem.tagName
return res