mirror of
https://github.com/SickGear/SickGear.git
synced 2025-01-05 17:43:37 +00:00
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
commit
de07881ada
35 changed files with 981 additions and 680 deletions
|
@ -257,7 +257,7 @@ def main():
|
|||
sys.exit("PID file: " + sickbeard.PIDFILE + " already exists. Exiting.")
|
||||
|
||||
# The pidfile is only useful in daemon mode, make sure we can write the file properly
|
||||
if sickbeard.CREATEPID:
|
||||
if sickbeard.CREATEPID and not sickbeard.restarted:
|
||||
if sickbeard.DAEMON:
|
||||
pid_dir = os.path.dirname(sickbeard.PIDFILE)
|
||||
if not os.access(pid_dir, os.F_OK):
|
||||
|
@ -331,7 +331,7 @@ def main():
|
|||
|
||||
sickbeard.showList = []
|
||||
|
||||
if sickbeard.DAEMON and not sickbeard.restarted:
|
||||
if sickbeard.DAEMON:
|
||||
daemonize()
|
||||
|
||||
# Use this PID for everything
|
||||
|
|
|
@ -1184,6 +1184,20 @@
|
|||
<span class="component-desc">Get your key at: <a href="http://trakt.tv/settings/api" rel="noreferrer" onclick="window.open('${sickbeard.ANON_REDIRECT}' + this.href, '_blank'); return false;">http://trakt.tv/settings/api</a></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<input type="checkbox" class="enabler" name="trakt_sync" id="trakt_sync" #if $sickbeard.TRAKT_SYNC then "checked=\"checked\"" else ""# />
|
||||
<label class="clearfix" for="trakt_sync">
|
||||
<span class="component-title">Sync Libraries:</span>
|
||||
<span class="component-desc">Syncs SickRage's show library with your trakt.tv show library</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<input type="checkbox" class="enabler" name="trakt_use_recommended" id="trakt_use_recommended" #if $sickbeard.TRAKT_USE_RECOMMENDED then "checked=\"checked\"" else ""# />
|
||||
<label class="clearfix" for="trakt_use_recommended">
|
||||
<span class="component-title">Use Recommended Shows:</span>
|
||||
<span class="component-desc">Adds ability to add recommended shows from trakt.tv</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-pair">
|
||||
<input type="checkbox" class="enabler" name="trakt_use_watchlist" id="trakt_use_watchlist" #if $sickbeard.TRAKT_USE_WATCHLIST then "checked=\"checked\"" else ""# />
|
||||
<label class="clearfix" for="trakt_use_watchlist">
|
||||
|
|
|
@ -28,7 +28,19 @@
|
|||
</a>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
|
||||
#if $sickbeard.TRAKT_USE_RECOMMENDED:
|
||||
<a href="$sbRoot/home/addShows/recommendedShows/" id="btnNewShow" class="btn btn-large">
|
||||
<div class="button"><img src="$sbRoot/images/add-new32.png" height="32" width="32" alt="Add Recommended Shows"/></div>
|
||||
<div class="buttontext">
|
||||
<h2>Add Recommended Shows</h2>
|
||||
<p>For shows that you haven't downloaded yet, this option recommends shows to add based on your Trakt.tv watch list, creates a directory for its episodes, and adds it to SickRage. *** Trakt.tv must be enabled ***</p>
|
||||
</div>
|
||||
</a>
|
||||
#end if
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<a href="$sbRoot/home/addShows/existingShows/" id="btnExistingShow" class="btn btn-large">
|
||||
<div class="button"><img src="$sbRoot/images/add-existing32.png" height="32" width="32" alt="Add Existing Shows"/></div>
|
||||
<div class="buttontext">
|
||||
|
|
64
gui/slick/interfaces/default/home_recommendedShows.tmpl
Normal file
64
gui/slick/interfaces/default/home_recommendedShows.tmpl
Normal file
|
@ -0,0 +1,64 @@
|
|||
#import os.path
|
||||
#import json
|
||||
#import sickbeard
|
||||
#set global $header="Recommended Shows"
|
||||
#set global $title="Recommended Shows"
|
||||
|
||||
#set global $sbPath="../.."
|
||||
|
||||
#set global $statpath="../.."#
|
||||
#set global $topmenu="home"#
|
||||
#import os.path
|
||||
|
||||
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="$sbRoot/css/formwizard.css?$sbPID" />
|
||||
<script type="text/javascript" src="$sbRoot/js/formwizard.js?$sbPID"></script>
|
||||
<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script>
|
||||
<script type="text/javascript" src="$sbRoot/js/recommendedShows.js?$sbPID"></script>
|
||||
<script type="text/javascript" src="$sbRoot/js/addShowOptions.js?$sbPID"></script>
|
||||
|
||||
#if $varExists('header')
|
||||
<h1 class="header">$header</h1>
|
||||
#else
|
||||
<h1 class="title">$title</h1>
|
||||
#end if
|
||||
|
||||
<div id="displayText"></div>
|
||||
<br />
|
||||
|
||||
<form id="recommendedShowsForm" method="post" action="$sbRoot/home/addShows/addRecommendedShow" accept-charset="utf-8">
|
||||
<fieldset class="sectionwrap">
|
||||
<legend class="legendStep">Select a recommended show</legend>
|
||||
|
||||
<div class="stepDiv">
|
||||
<div id="searchResults" style="height: 225px; overflow: auto;"><br/></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sectionwrap">
|
||||
<legend class="legendStep">Pick the parent folder</legend>
|
||||
|
||||
<div class="stepDiv">
|
||||
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_rootDirs.tmpl")
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sectionwrap">
|
||||
<legend class="legendStep">Customize options</legend>
|
||||
|
||||
<div class="stepDiv">
|
||||
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_addShowOptions.tmpl")
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<div style="width: 800px; text-align:">
|
||||
<input class="btn" type="button" id="addShowButton" value="Add Show" disabled="disabled" />
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="$sbRoot/js/rootDirs.js?$sbPID"></script>
|
||||
|
||||
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")
|
|
@ -37,7 +37,7 @@ $(document).ready(function() {
|
|||
$('.dir_check').each(function(i,w){
|
||||
if ($(w).is(':checked')) {
|
||||
if (url.length)
|
||||
url += '&'
|
||||
url += '&';
|
||||
url += 'rootDir=' + encodeURIComponent($(w).attr('id'));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,24 +1,39 @@
|
|||
var message_url = sbRoot + '/ui/get_messages';
|
||||
$.pnotify.defaults.pnotify_width = "340px";
|
||||
$.pnotify.defaults.pnotify_history = false;
|
||||
$.pnotify.defaults.pnotify_delay = 4000;
|
||||
var message_url = sbRoot + '/ui/get_messages/';
|
||||
$.pnotify.defaults.width = "400px";
|
||||
$.pnotify.defaults.styling = "jqueryui";
|
||||
$.pnotify.defaults.history = false;
|
||||
$.pnotify.defaults.shadow = false;
|
||||
$.pnotify.defaults.delay = 4000;
|
||||
$.pnotify.defaults.maxonscreen = 5;
|
||||
|
||||
function check_notifications() {
|
||||
$.getJSON(message_url, function(data){
|
||||
$.each(data, function(name,data){
|
||||
$.pnotify({
|
||||
pnotify_type: data.type,
|
||||
pnotify_hide: data.type == 'notice',
|
||||
pnotify_title: data.title,
|
||||
pnotify_text: data.message
|
||||
var poll_interval = 5000;
|
||||
$.ajax({
|
||||
url: message_url,
|
||||
success: function (data) {
|
||||
poll_interval = 5000;
|
||||
$.each(data, function (name, data) {
|
||||
$.pnotify({
|
||||
type: data.type,
|
||||
hide: data.type == 'notice',
|
||||
title: data.title,
|
||||
text: data.message
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
error: function () {
|
||||
poll_interval = 15000;
|
||||
},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
complete: function () {
|
||||
setTimeout(check_notifications, poll_interval);
|
||||
},
|
||||
timeout: 15000 // timeout every 15 secs
|
||||
});
|
||||
|
||||
setTimeout(check_notifications, 3000)
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
$(document).ready(function () {
|
||||
|
||||
check_notifications();
|
||||
|
||||
|
|
144
gui/slick/js/recommendedShows.js
Normal file
144
gui/slick/js/recommendedShows.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
$(document).ready(function () {
|
||||
function getRecommendedShows() {
|
||||
$.getJSON(sbRoot + '/home/addShows/getRecommendedShows', {}, function (data) {
|
||||
var firstResult = true;
|
||||
var resultStr = '<fieldset>\n<legend>Recommended Shows:</legend>\n';
|
||||
var checked = '';
|
||||
|
||||
if (data.results.length === 0) {
|
||||
resultStr += '<b>No recommended shows found, update your watched shows list on trakt.tv.</b>';
|
||||
} else {
|
||||
$.each(data.results, function (index, obj) {
|
||||
if (firstResult) {
|
||||
checked = ' checked';
|
||||
firstResult = false;
|
||||
} else {
|
||||
checked = '';
|
||||
}
|
||||
|
||||
var whichSeries = obj.join('|');
|
||||
|
||||
resultStr += '<input type="radio" id="whichSeries" name="whichSeries" value="' + whichSeries + '"' + checked + ' /> ';
|
||||
resultStr += '<a href="' + obj[1] + '" onclick=\"window.open(this.href, \'_blank\'); return false;\" ><b>' + obj[2] + '</b></a>';
|
||||
|
||||
if (obj[4] !== null) {
|
||||
var startDate = new Date(obj[4]);
|
||||
var today = new Date();
|
||||
if (startDate > today) {
|
||||
resultStr += ' (will debut on ' + obj[4] + ')';
|
||||
} else {
|
||||
resultStr += ' (started on ' + obj[4] + ')';
|
||||
}
|
||||
}
|
||||
|
||||
if (obj[0] !== null) {
|
||||
resultStr += ' [' + obj[0] + ']';
|
||||
}
|
||||
|
||||
if (obj[3] !== null) {
|
||||
resultStr += '<br />' + obj[3];
|
||||
}
|
||||
|
||||
resultStr += '<p /><br />';
|
||||
});
|
||||
resultStr += '</ul>';
|
||||
}
|
||||
resultStr += '</fieldset>';
|
||||
$('#searchResults').html(resultStr);
|
||||
updateSampleText();
|
||||
myform.loadsection(0);
|
||||
});
|
||||
}
|
||||
|
||||
$('#addShowButton').click(function () {
|
||||
// if they haven't picked a show don't let them submit
|
||||
if (!$("input:radio[name='whichSeries']:checked").val() && !$("input:hidden[name='whichSeries']").val().length) {
|
||||
alert('You must choose a show to continue');
|
||||
return false;
|
||||
}
|
||||
|
||||
$('#recommendedShowsForm').submit();
|
||||
});
|
||||
|
||||
$('#qualityPreset').change(function () {
|
||||
myform.loadsection(2);
|
||||
});
|
||||
|
||||
var myform = new formtowizard({
|
||||
formid: 'recommendedShowsForm',
|
||||
revealfx: ['slide', 500],
|
||||
oninit: function () {
|
||||
getRecommendedShows();
|
||||
updateSampleText();
|
||||
}
|
||||
});
|
||||
|
||||
function goToStep(num) {
|
||||
$('.step').each(function () {
|
||||
if ($.data(this, 'section') + 1 == num) {
|
||||
$(this).click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSampleText() {
|
||||
// if something's selected then we have some behavior to figure out
|
||||
|
||||
var show_name, sep_char;
|
||||
// if they've picked a radio button then use that
|
||||
if ($('input:radio[name=whichSeries]:checked').length) {
|
||||
show_name = $('input:radio[name=whichSeries]:checked').val().split('|')[2];
|
||||
} else {
|
||||
show_name = '';
|
||||
}
|
||||
|
||||
var sample_text = 'Adding show <b>' + show_name + '</b> into <b>';
|
||||
|
||||
// if we have a root dir selected, figure out the path
|
||||
if ($("#rootDirs option:selected").length) {
|
||||
var root_dir_text = $('#rootDirs option:selected').val();
|
||||
if (root_dir_text.indexOf('/') >= 0) {
|
||||
sep_char = '/';
|
||||
} else if (root_dir_text.indexOf('\\') >= 0) {
|
||||
sep_char = '\\';
|
||||
} else {
|
||||
sep_char = '';
|
||||
}
|
||||
|
||||
if (root_dir_text.substr(sample_text.length - 1) != sep_char) {
|
||||
root_dir_text += sep_char;
|
||||
}
|
||||
root_dir_text += '<i>||</i>' + sep_char;
|
||||
|
||||
sample_text += root_dir_text;
|
||||
} else if ($('#fullShowPath').length && $('#fullShowPath').val().length) {
|
||||
sample_text += $('#fullShowPath').val();
|
||||
} else {
|
||||
sample_text += 'unknown dir.';
|
||||
}
|
||||
|
||||
sample_text += '</b>';
|
||||
|
||||
// if we have a show name then sanitize and use it for the dir name
|
||||
if (show_name.length) {
|
||||
$.get(sbRoot + '/home/addShows/sanitizeFileName', {name: show_name}, function (data) {
|
||||
$('#displayText').html(sample_text.replace('||', data));
|
||||
});
|
||||
// if not then it's unknown
|
||||
} else {
|
||||
$('#displayText').html(sample_text.replace('||', '??'));
|
||||
}
|
||||
|
||||
// also toggle the add show button
|
||||
if (($("#rootDirs option:selected").length || ($('#fullShowPath').length && $('#fullShowPath').val().length)) &&
|
||||
($('input:radio[name=whichSeries]:checked').length)) {
|
||||
$('#addShowButton').attr('disabled', false);
|
||||
} else {
|
||||
$('#addShowButton').attr('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
$('#rootDirText').change(updateSampleText);
|
||||
$('#whichSeries').live('change', updateSampleText);
|
||||
|
||||
});
|
|
@ -728,32 +728,36 @@ class Tvdb:
|
|||
log().debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc(self.config['url_seriesBanner'] % (sid))
|
||||
banners = {}
|
||||
for cur_banner in bannersEt['banner'] if bannersEt else []:
|
||||
bid = cur_banner['id']
|
||||
btype = cur_banner['bannertype']
|
||||
btype2 = cur_banner['bannertype2']
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
|
||||
for k, v in cur_banner.items():
|
||||
if k is None or v is None:
|
||||
try:
|
||||
for cur_banner in bannersEt['banner']:
|
||||
bid = cur_banner['id']
|
||||
btype = cur_banner['bannertype']
|
||||
btype2 = cur_banner['bannertype2']
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
|
||||
k, v = k.lower(), v.lower()
|
||||
banners[btype][btype2][bid][k] = v
|
||||
for k, v in cur_banner.items():
|
||||
if k is None or v is None:
|
||||
continue
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
log().debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
k, v = k.lower(), v.lower()
|
||||
banners[btype][btype2][bid][k] = v
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
log().debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
except:
|
||||
pass
|
||||
|
||||
self._setShowData(sid, "_banners", banners)
|
||||
|
||||
|
@ -785,17 +789,21 @@ class Tvdb:
|
|||
actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
|
||||
|
||||
cur_actors = Actors()
|
||||
for curActorItem in actorsEt["actor"] if actorsEt else []:
|
||||
curActor = Actor()
|
||||
for k, v in curActorItem.items():
|
||||
k = k.lower()
|
||||
if v is not None:
|
||||
if k == "image":
|
||||
v = self.config['url_artworkPrefix'] % (v)
|
||||
else:
|
||||
v = self._cleanData(v)
|
||||
curActor[k] = v
|
||||
cur_actors.append(curActor)
|
||||
try:
|
||||
for curActorItem in actorsEt["actor"]:
|
||||
curActor = Actor()
|
||||
for k, v in curActorItem.items():
|
||||
k = k.lower()
|
||||
if v is not None:
|
||||
if k == "image":
|
||||
v = self.config['url_artworkPrefix'] % (v)
|
||||
else:
|
||||
v = self._cleanData(v)
|
||||
curActor[k] = v
|
||||
cur_actors.append(curActor)
|
||||
except:
|
||||
pass
|
||||
|
||||
self._setShowData(sid, '_actors', cur_actors)
|
||||
|
||||
def _getShowData(self, sid, language, seriesSearch=False):
|
||||
|
|
|
@ -35,7 +35,7 @@ from providers import ezrss, tvtorrents, btn, newznab, womble, thepiratebay, tor
|
|||
from sickbeard.config import CheckSection, check_setting_int, check_setting_str, check_setting_float, ConfigMigrator, \
|
||||
naming_ep_type
|
||||
from sickbeard import searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser, \
|
||||
subtitles, traktWatchListChecker
|
||||
subtitles, traktChecker
|
||||
from sickbeard import helpers, db, exceptions, show_queue, search_queue, scheduler, show_name_helpers
|
||||
from sickbeard import logger
|
||||
from sickbeard import naming
|
||||
|
@ -85,7 +85,7 @@ searchQueueScheduler = None
|
|||
properFinderScheduler = None
|
||||
autoPostProcesserScheduler = None
|
||||
subtitlesFinderScheduler = None
|
||||
traktWatchListCheckerScheduler = None
|
||||
traktCheckerScheduler = None
|
||||
|
||||
showList = None
|
||||
loadingShowList = None
|
||||
|
@ -354,6 +354,8 @@ TRAKT_REMOVE_WATCHLIST = False
|
|||
TRAKT_USE_WATCHLIST = False
|
||||
TRAKT_METHOD_ADD = 0
|
||||
TRAKT_START_PAUSED = False
|
||||
TRAKT_USE_RECOMMENDED = False
|
||||
TRAKT_SYNC = False
|
||||
|
||||
USE_PYTIVO = False
|
||||
PYTIVO_NOTIFY_ONSNATCH = False
|
||||
|
@ -444,7 +446,7 @@ def initialize(consoleLogging=True):
|
|||
TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_LABEL, TORRENT_VERIFY_CERT, \
|
||||
USE_XBMC, XBMC_ALWAYS_ON, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \
|
||||
XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, BACKLOG_FREQUENCY, \
|
||||
USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktWatchListCheckerScheduler, \
|
||||
USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, \
|
||||
USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \
|
||||
PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \
|
||||
showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SORT_ARTICLE, showList, loadingShowList, \
|
||||
|
@ -754,10 +756,9 @@ def initialize(consoleLogging=True):
|
|||
USE_PUSHOVER = bool(check_setting_int(CFG, 'Pushover', 'use_pushover', 0))
|
||||
PUSHOVER_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsnatch', 0))
|
||||
PUSHOVER_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_ondownload', 0))
|
||||
PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(
|
||||
check_setting_int(CFG, 'Pushover', 'pushover_notify_onsubtitledownload', 0))
|
||||
PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Pushover', 'pushover_notify_onsubtitledownload', 0))
|
||||
PUSHOVER_USERKEY = check_setting_str(CFG, 'Pushover', 'pushover_userkey', '')
|
||||
PUSHOVER_APIKEY = check_setting_str(CFG, 'Pushover', 'pushover_apikey', '')
|
||||
PUSHOVER_APIKEY = check_setting_str(CFG, 'Pushover', 'pushover_apikey', '')
|
||||
USE_LIBNOTIFY = bool(check_setting_int(CFG, 'Libnotify', 'use_libnotify', 0))
|
||||
LIBNOTIFY_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_onsnatch', 0))
|
||||
LIBNOTIFY_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_ondownload', 0))
|
||||
|
@ -792,6 +793,8 @@ def initialize(consoleLogging=True):
|
|||
TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0))
|
||||
TRAKT_METHOD_ADD = check_setting_str(CFG, 'Trakt', 'trakt_method_add', "0")
|
||||
TRAKT_START_PAUSED = bool(check_setting_int(CFG, 'Trakt', 'trakt_start_paused', 0))
|
||||
TRAKT_USE_RECOMMENDED = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_recommended', 0))
|
||||
TRAKT_SYNC = bool(check_setting_int(CFG, 'Trakt', 'trakt_sync', 0))
|
||||
|
||||
CheckSection(CFG, 'pyTivo')
|
||||
USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0))
|
||||
|
@ -1006,9 +1009,9 @@ def initialize(consoleLogging=True):
|
|||
threadName="POSTPROCESSER",
|
||||
silent=not PROCESS_AUTOMATICALLY)
|
||||
|
||||
traktWatchListCheckerScheduler = scheduler.Scheduler(traktWatchListChecker.TraktChecker(),
|
||||
traktCheckerScheduler = scheduler.Scheduler(traktChecker.TraktChecker(),
|
||||
cycleTime=datetime.timedelta(hours=1),
|
||||
threadName="TRAKTWATCHLIST",
|
||||
threadName="TRAKTCHECKER",
|
||||
silent=not USE_TRAKT)
|
||||
|
||||
subtitlesFinderScheduler = scheduler.Scheduler(subtitles.SubtitlesFinder(),
|
||||
|
@ -1122,7 +1125,7 @@ def start():
|
|||
global __INITIALIZED__, maintenanceScheduler, backlogSearchScheduler, \
|
||||
showUpdateScheduler, versionCheckScheduler, showQueueScheduler, \
|
||||
properFinderScheduler, autoPostProcesserScheduler, searchQueueScheduler, \
|
||||
subtitlesFinderScheduler, USE_SUBTITLES,traktWatchListCheckerScheduler, \
|
||||
subtitlesFinderScheduler, USE_SUBTITLES,traktCheckerScheduler, \
|
||||
dailySearchScheduler, started
|
||||
|
||||
with INIT_LOCK:
|
||||
|
@ -1163,8 +1166,8 @@ def start():
|
|||
if USE_SUBTITLES:
|
||||
subtitlesFinderScheduler.thread.start()
|
||||
|
||||
# start the trakt watchlist
|
||||
traktWatchListCheckerScheduler.thread.start()
|
||||
# start the trakt checker
|
||||
traktCheckerScheduler.thread.start()
|
||||
|
||||
started = True
|
||||
|
||||
|
@ -1173,7 +1176,7 @@ def halt():
|
|||
global __INITIALIZED__, maintenanceScheduler, backlogSearchScheduler, \
|
||||
showUpdateScheduler, versionCheckScheduler, showQueueScheduler, \
|
||||
properFinderScheduler, autoPostProcesserScheduler, searchQueueScheduler, \
|
||||
subtitlesFinderScheduler, traktWatchListCheckerScheduler, \
|
||||
subtitlesFinderScheduler, traktCheckerScheduler, \
|
||||
dailySearchScheduler, started
|
||||
|
||||
with INIT_LOCK:
|
||||
|
@ -1240,10 +1243,10 @@ def halt():
|
|||
except:
|
||||
pass
|
||||
|
||||
traktWatchListCheckerScheduler.abort = True
|
||||
logger.log(u"Waiting for the TRAKTWATCHLIST thread to exit")
|
||||
traktCheckerScheduler.abort = True
|
||||
logger.log(u"Waiting for the TRAKTCHECKER thread to exit")
|
||||
try:
|
||||
traktWatchListCheckerScheduler.thread.join(10)
|
||||
traktCheckerScheduler.thread.join(10)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -1665,6 +1668,8 @@ def save_config():
|
|||
new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST)
|
||||
new_config['Trakt']['trakt_method_add'] = TRAKT_METHOD_ADD
|
||||
new_config['Trakt']['trakt_start_paused'] = int(TRAKT_START_PAUSED)
|
||||
new_config['Trakt']['trakt_use_recommended'] = int(TRAKT_USE_RECOMMENDED)
|
||||
new_config['Trakt']['trakt_sync'] = int(TRAKT_SYNC)
|
||||
|
||||
new_config['pyTivo'] = {}
|
||||
new_config['pyTivo']['use_pytivo'] = int(USE_PYTIVO)
|
||||
|
|
|
@ -263,9 +263,9 @@ def download_file(url, filename):
|
|||
|
||||
def findCertainShow(showList, indexerid=None):
|
||||
if indexerid:
|
||||
results = filter(lambda x: x.indexerid == indexerid, showList)
|
||||
results = filter(lambda x: int(x.indexerid) == int(indexerid), showList)
|
||||
else:
|
||||
results = filter(lambda x: x.indexerid == indexerid, showList)
|
||||
results = filter(lambda x: int(x.indexerid) == int(indexerid), showList)
|
||||
|
||||
if len(results) == 0:
|
||||
return None
|
||||
|
|
|
@ -46,7 +46,7 @@ class TraktNotifier:
|
|||
|
||||
# URL parameters
|
||||
data = {
|
||||
'indexer_id': ep_obj.show.indexerid,
|
||||
'tvdb_id': ep_obj.show.indexerid,
|
||||
'title': ep_obj.show.name,
|
||||
'year': ep_obj.show.startyear,
|
||||
'episodes': [{
|
||||
|
|
|
@ -21,6 +21,7 @@ import generic
|
|||
|
||||
from sickbeard import logger
|
||||
from sickbeard import tvcache
|
||||
from sickbeard.exceptions import AuthException
|
||||
|
||||
|
||||
class WombleProvider(generic.NZBProvider):
|
||||
|
@ -40,13 +41,40 @@ class WombleCache(tvcache.TVCache):
|
|||
# only poll Womble's Index every 15 minutes max
|
||||
self.minTime = 15
|
||||
|
||||
def _getRSSData(self):
|
||||
url = self.provider.url + 'rss/?sec=TV-x264&fr=false'
|
||||
logger.log(u"Womble's Index cache update URL: " + url, logger.DEBUG)
|
||||
return self.getRSSFeed(url)
|
||||
def updateCache(self):
|
||||
|
||||
# delete anything older then 7 days
|
||||
logger.log(u"Clearing " + self.provider.name + " cache")
|
||||
self._clearCache()
|
||||
|
||||
data = None
|
||||
if not self.shouldUpdate():
|
||||
for url in [self.provider.url + 'rss/?sec=tv-sd&fr=false', self.provider.url + 'rss/?sec=tv-hd&fr=false']:
|
||||
logger.log(u"Womble's Index cache update URL: " + url, logger.DEBUG)
|
||||
data = self.getRSSFeed(url)
|
||||
|
||||
# As long as we got something from the provider we count it as an update
|
||||
if not data:
|
||||
return []
|
||||
|
||||
# By now we know we've got data and no auth errors, all we need to do is put it in the database
|
||||
cl = []
|
||||
for item in data.entries:
|
||||
|
||||
ci = self._parseItem(item)
|
||||
if ci is not None:
|
||||
cl.append(ci)
|
||||
|
||||
if cl:
|
||||
myDB = self._getDB()
|
||||
myDB.mass_action(cl)
|
||||
|
||||
# set last updated
|
||||
if data:
|
||||
self.setLastUpdate()
|
||||
|
||||
def _checkAuth(self, data):
|
||||
return data != 'Invalid Link'
|
||||
|
||||
|
||||
provider = WombleProvider()
|
||||
|
||||
|
|
62
sickbeard/rssfeeds.py
Normal file
62
sickbeard/rssfeeds.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
import os
|
||||
import threading
|
||||
import urllib
|
||||
import urlparse
|
||||
import re
|
||||
import time
|
||||
import sickbeard
|
||||
|
||||
from sickbeard import logger
|
||||
from sickbeard import encodingKludge as ek
|
||||
from sickbeard.exceptions import ex
|
||||
from lib.shove import Shove
|
||||
from lib.feedcache import cache
|
||||
|
||||
feed_lock = threading.Lock()
|
||||
|
||||
class RSSFeeds:
|
||||
def __init__(self, db_name):
|
||||
try:
|
||||
self.fs = self.fs = Shove('sqlite:///' + ek.ek(os.path.join, sickbeard.CACHE_DIR, db_name + '.db'), compress=True)
|
||||
self.fc = cache.Cache(self.fs)
|
||||
except Exception, e:
|
||||
logger.log(u"RSS error: " + ex(e), logger.ERROR)
|
||||
raise
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
self.fc = None
|
||||
self.fs.close()
|
||||
|
||||
def clearCache(self, age=None):
|
||||
if not self.fc:
|
||||
return
|
||||
|
||||
self.fc.purge(age)
|
||||
|
||||
def getRSSFeed(self, url, post_data=None):
|
||||
if not self.fc:
|
||||
return
|
||||
|
||||
with feed_lock:
|
||||
parsed = list(urlparse.urlparse(url))
|
||||
parsed[2] = re.sub("/{2,}", "/", parsed[2]) # replace two or more / with one
|
||||
|
||||
if post_data:
|
||||
url += urllib.urlencode(post_data)
|
||||
|
||||
feed = self.fc.fetch(url)
|
||||
if not feed:
|
||||
logger.log(u"RSS Error loading URL: " + url, logger.ERROR)
|
||||
return
|
||||
elif 'error' in feed.feed:
|
||||
logger.log(u"RSS ERROR:[%s] CODE:[%s]" % (feed.feed['error']['description'], feed.feed['error']['code']),
|
||||
logger.DEBUG)
|
||||
return
|
||||
elif not feed.entries:
|
||||
logger.log(u"No RSS items found using URL: " + url, logger.WARNING)
|
||||
return
|
||||
|
||||
return feed
|
|
@ -385,8 +385,13 @@ class QueueItemAdd(ShowQueueItem):
|
|||
|
||||
self.show.flushEpisodes()
|
||||
|
||||
# if there are specific episodes that need to be added by trakt
|
||||
sickbeard.traktWatchListCheckerScheduler.action.manageNewShow(self.show)
|
||||
if sickbeard.USE_TRAKT:
|
||||
# if there are specific episodes that need to be added by trakt
|
||||
sickbeard.traktCheckerScheduler.action.manageNewShow(self.show)
|
||||
|
||||
# add show to trakt.tv library
|
||||
if sickbeard.TRAKT_SYNC:
|
||||
sickbeard.traktCheckerScheduler.action.addShowToTraktLibrary(self.show)
|
||||
|
||||
# Load XEM data to DB for show
|
||||
sickbeard.scene_numbering.xem_refresh(self.show.indexerid, self.show.indexer, force=True)
|
||||
|
|
|
@ -34,6 +34,7 @@ class TraktChecker():
|
|||
self.todoBacklog = []
|
||||
|
||||
def run(self, force=False):
|
||||
# add shows from trakt.tv watchlist
|
||||
if sickbeard.TRAKT_USE_WATCHLIST:
|
||||
self.todoWanted = [] #its about to all get re-added
|
||||
if len(sickbeard.ROOT_DIRS.split('|')) < 2:
|
||||
|
@ -42,6 +43,60 @@ class TraktChecker():
|
|||
self.updateShows()
|
||||
self.updateEpisodes()
|
||||
|
||||
# sync trakt.tv library with sickrage library
|
||||
if sickbeard.TRAKT_SYNC:
|
||||
self.syncLibrary()
|
||||
|
||||
def findShow(self, indexerid):
|
||||
library = TraktCall("user/library/shows/all.json/%API%/" + sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_API,
|
||||
sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD)
|
||||
|
||||
results = filter(lambda x: int(x['tvdb_id']) == int(indexerid), library)
|
||||
if len(results) == 0:
|
||||
return None
|
||||
else:
|
||||
return results[0]
|
||||
|
||||
def syncLibrary(self):
|
||||
logger.log(u"Syncing library to trakt.tv show library", logger.DEBUG)
|
||||
for myShow in sickbeard.showList:
|
||||
self.addShowToTraktLibrary(myShow)
|
||||
|
||||
def removeShowFromTraktLibrary(self, show_obj):
|
||||
if not self.findShow(show_obj.indexerid):
|
||||
return
|
||||
|
||||
# URL parameters
|
||||
data = {
|
||||
'tvdb_id': show_obj.indexerid,
|
||||
'title': show_obj.name,
|
||||
'year': show_obj.startyear,
|
||||
}
|
||||
|
||||
if data is not None:
|
||||
logger.log(u"Removing " + show_obj.name + " from trakt.tv library", logger.DEBUG)
|
||||
TraktCall("show/unlibrary/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD, data)
|
||||
|
||||
def addShowToTraktLibrary(self, show_obj):
|
||||
"""
|
||||
Sends a request to trakt indicating that the given show and all its episodes is part of our library.
|
||||
|
||||
show_obj: The TVShow object to add to trakt
|
||||
"""
|
||||
|
||||
if self.findShow(show_obj.indexerid):
|
||||
return
|
||||
|
||||
# URL parameters
|
||||
data = {
|
||||
'tvdb_id': show_obj.indexerid,
|
||||
'title': show_obj.name,
|
||||
'year': show_obj.startyear,
|
||||
}
|
||||
|
||||
if data is not None:
|
||||
logger.log(u"Adding " + show_obj.name + " to trakt.tv library", logger.DEBUG)
|
||||
TraktCall("show/library/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, sickbeard.TRAKT_PASSWORD, data)
|
||||
|
||||
def updateShows(self):
|
||||
logger.log(u"Starting trakt show watchlist check", logger.DEBUG)
|
|
@ -22,29 +22,21 @@ import os
|
|||
|
||||
import time
|
||||
import datetime
|
||||
import urllib
|
||||
import urlparse
|
||||
import re
|
||||
import threading
|
||||
import sickbeard
|
||||
|
||||
from lib.shove import Shove
|
||||
from lib.feedcache import cache
|
||||
|
||||
from sickbeard import db
|
||||
from sickbeard import logger
|
||||
from sickbeard.common import Quality, cpu_presets
|
||||
from sickbeard.common import Quality
|
||||
|
||||
from sickbeard import helpers, show_name_helpers
|
||||
from sickbeard.exceptions import MultipleShowObjectsException
|
||||
from sickbeard.exceptions import AuthException
|
||||
from sickbeard import encodingKludge as ek
|
||||
|
||||
from name_parser.parser import NameParser, InvalidNameException
|
||||
from sickbeard.rssfeeds import RSSFeeds
|
||||
|
||||
cache_lock = threading.Lock()
|
||||
|
||||
|
||||
class CacheDBConnection(db.DBConnection):
|
||||
def __init__(self, providerName):
|
||||
db.DBConnection.__init__(self, "cache.db")
|
||||
|
@ -55,8 +47,14 @@ class CacheDBConnection(db.DBConnection):
|
|||
self.action(
|
||||
"CREATE TABLE [" + providerName + "] (name TEXT, season NUMERIC, episodes TEXT, indexerid NUMERIC, url TEXT, time NUMERIC, quality TEXT)")
|
||||
else:
|
||||
# remove duplicates
|
||||
self.action("DELETE FROM " + providerName + " WHERE url NOT IN (SELECT url FROM " + providerName + " GROUP BY url)")
|
||||
sqlResults = self.select(
|
||||
"SELECT url, COUNT(url) as count FROM [" + providerName + "] GROUP BY url HAVING count > 1")
|
||||
|
||||
for cur_dupe in sqlResults:
|
||||
self.action("DELETE FROM [" + providerName + "] WHERE url = ?", [cur_dupe["url"]])
|
||||
|
||||
# add unique index to prevent further dupes from happening if one does not exist
|
||||
self.action("CREATE UNIQUE INDEX IF NOT EXISTS idx_url ON " + providerName + " (url)")
|
||||
except Exception, e:
|
||||
if str(e) != "table [" + providerName + "] already exists":
|
||||
raise
|
||||
|
@ -69,13 +67,6 @@ class CacheDBConnection(db.DBConnection):
|
|||
if str(e) != "table lastUpdate already exists":
|
||||
raise
|
||||
|
||||
|
||||
# Create unique index for provider table to prevent duplicate entries
|
||||
try:
|
||||
self.action("CREATE UNIQUE INDEX IF NOT EXISTS idx_url ON " + providerName + " (url)")
|
||||
except Exception, e:
|
||||
raise
|
||||
|
||||
class TVCache():
|
||||
def __init__(self, provider):
|
||||
|
||||
|
@ -88,13 +79,15 @@ class TVCache():
|
|||
return CacheDBConnection(self.providerID)
|
||||
|
||||
def _clearCache(self):
|
||||
if not self.shouldClearCache():
|
||||
return
|
||||
if self.shouldClearCache():
|
||||
curDate = datetime.date.today() - datetime.timedelta(weeks=1)
|
||||
|
||||
curDate = datetime.date.today() - datetime.timedelta(weeks=1)
|
||||
myDB = self._getDB()
|
||||
myDB.action("DELETE FROM [" + self.providerID + "] WHERE time < ?", [int(time.mktime(curDate.timetuple()))])
|
||||
|
||||
myDB = self._getDB()
|
||||
myDB.action("DELETE FROM [" + self.providerID + "] WHERE time < ?", [int(time.mktime(curDate.timetuple()))])
|
||||
# clear RSS Feed cache
|
||||
with RSSFeeds(self.providerID) as feed:
|
||||
feed.clearCache(int(time.mktime(curDate.timetuple())))
|
||||
|
||||
def _getRSSData(self):
|
||||
|
||||
|
@ -127,9 +120,8 @@ class TVCache():
|
|||
return []
|
||||
|
||||
if self._checkAuth(data):
|
||||
items = data.entries
|
||||
cl = []
|
||||
for item in items:
|
||||
for item in data.entries:
|
||||
ci = self._parseItem(item)
|
||||
if ci is not None:
|
||||
cl.append(ci)
|
||||
|
@ -144,34 +136,10 @@ class TVCache():
|
|||
|
||||
return []
|
||||
|
||||
def getRSSFeed(self, url, post_data=None, request_headers=None):
|
||||
# create provider storaqe cache
|
||||
storage = Shove('sqlite:///' + ek.ek(os.path.join, sickbeard.CACHE_DIR, self.provider.name) + '.db')
|
||||
fc = cache.Cache(storage)
|
||||
|
||||
parsed = list(urlparse.urlparse(url))
|
||||
parsed[2] = re.sub("/{2,}", "/", parsed[2]) # replace two or more / with one
|
||||
|
||||
if post_data:
|
||||
url += urllib.urlencode(post_data)
|
||||
|
||||
f = fc.fetch(url, request_headers=request_headers)
|
||||
|
||||
if not f:
|
||||
logger.log(u"Error loading " + self.providerID + " URL: " + url, logger.ERROR)
|
||||
return None
|
||||
elif 'error' in f.feed:
|
||||
logger.log(u"Newznab ERROR:[%s] CODE:[%s]" % (f.feed['error']['description'], f.feed['error']['code']),
|
||||
logger.DEBUG)
|
||||
return None
|
||||
elif not f.entries:
|
||||
logger.log(u"No items found on " + self.providerID + " using URL: " + url, logger.WARNING)
|
||||
return None
|
||||
|
||||
storage.close()
|
||||
|
||||
return f
|
||||
|
||||
def getRSSFeed(self, url, post_data=None):
|
||||
with RSSFeeds(self.providerID) as feed:
|
||||
data = feed.getRSSFeed(url, post_data)
|
||||
return data
|
||||
|
||||
def _translateTitle(self, title):
|
||||
return title.replace(' ', '.')
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -25,5 +25,5 @@ from __future__ import absolute_import, division, print_function, with_statement
|
|||
# is zero for an official release, positive for a development branch,
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
version = "4.0.dev1"
|
||||
version_info = (4, 0, 0, -100)
|
||||
version = "4.0b1"
|
||||
version_info = (4, 0, 0, -99)
|
||||
|
|
|
@ -883,9 +883,10 @@ class FriendFeedMixin(OAuthMixin):
|
|||
class GoogleMixin(OpenIdMixin, OAuthMixin):
|
||||
"""Google Open ID / OAuth authentication.
|
||||
|
||||
*Deprecated:* New applications should use `GoogleOAuth2Mixin`
|
||||
below instead of this class. As of May 19, 2014, Google has stopped
|
||||
supporting registration-free authentication.
|
||||
.. deprecated:: 4.0
|
||||
New applications should use `GoogleOAuth2Mixin`
|
||||
below instead of this class. As of May 19, 2014, Google has stopped
|
||||
supporting registration-free authentication.
|
||||
|
||||
No application registration is necessary to use Google for
|
||||
authentication or to access Google resources on behalf of a user.
|
||||
|
@ -1053,9 +1054,10 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
|
|||
class FacebookMixin(object):
|
||||
"""Facebook Connect authentication.
|
||||
|
||||
*Deprecated:* New applications should use `FacebookGraphMixin`
|
||||
below instead of this class. This class does not support the
|
||||
Future-based interface seen on other classes in this module.
|
||||
.. deprecated:: 1.1
|
||||
New applications should use `FacebookGraphMixin`
|
||||
below instead of this class. This class does not support the
|
||||
Future-based interface seen on other classes in this module.
|
||||
|
||||
To authenticate with Facebook, register your application with
|
||||
Facebook at http://www.facebook.com/developers/apps.php. Then
|
||||
|
|
|
@ -51,18 +51,6 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
|
|||
self._fds = {}
|
||||
self._timeout = None
|
||||
|
||||
try:
|
||||
self._socket_action = self._multi.socket_action
|
||||
except AttributeError:
|
||||
# socket_action is found in pycurl since 7.18.2 (it's been
|
||||
# in libcurl longer than that but wasn't accessible to
|
||||
# python).
|
||||
gen_log.warning("socket_action method missing from pycurl; "
|
||||
"falling back to socket_all. Upgrading "
|
||||
"libcurl and pycurl will improve performance")
|
||||
self._socket_action = \
|
||||
lambda fd, action: self._multi.socket_all()
|
||||
|
||||
# libcurl has bugs that sometimes cause it to not report all
|
||||
# relevant file descriptors and timeouts to TIMERFUNCTION/
|
||||
# SOCKETFUNCTION. Mitigate the effects of such bugs by
|
||||
|
@ -142,7 +130,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
|
|||
action |= pycurl.CSELECT_OUT
|
||||
while True:
|
||||
try:
|
||||
ret, num_handles = self._socket_action(fd, action)
|
||||
ret, num_handles = self._multi.socket_action(fd, action)
|
||||
except pycurl.error as e:
|
||||
ret = e.args[0]
|
||||
if ret != pycurl.E_CALL_MULTI_PERFORM:
|
||||
|
@ -155,7 +143,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
|
|||
self._timeout = None
|
||||
while True:
|
||||
try:
|
||||
ret, num_handles = self._socket_action(
|
||||
ret, num_handles = self._multi.socket_action(
|
||||
pycurl.SOCKET_TIMEOUT, 0)
|
||||
except pycurl.error as e:
|
||||
ret = e.args[0]
|
||||
|
@ -223,11 +211,6 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
|
|||
"callback": callback,
|
||||
"curl_start_time": time.time(),
|
||||
}
|
||||
# Disable IPv6 to mitigate the effects of this bug
|
||||
# on curl versions <= 7.21.0
|
||||
# http://sourceforge.net/tracker/?func=detail&aid=3017819&group_id=976&atid=100976
|
||||
if pycurl.version_info()[2] <= 0x71500: # 7.21.0
|
||||
curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
|
||||
_curl_setup_request(curl, request, curl.info["buffer"],
|
||||
curl.info["headers"])
|
||||
self._multi.add_handle(curl)
|
||||
|
@ -383,7 +366,6 @@ def _curl_setup_request(curl, request, buffer, headers):
|
|||
if request.allow_ipv6 is False:
|
||||
# Curl behaves reasonably when DNS resolution gives an ipv6 address
|
||||
# that we can't reach, so allow ipv6 unless the user asks to disable.
|
||||
# (but see version check in _process_queue above)
|
||||
curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
|
||||
else:
|
||||
curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER)
|
||||
|
|
|
@ -22,14 +22,17 @@ to switch to ``curl_httpclient`` for reasons such as the following:
|
|||
|
||||
* ``curl_httpclient`` was the default prior to Tornado 2.0.
|
||||
|
||||
Note that if you are using ``curl_httpclient``, it is highly recommended that
|
||||
you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum
|
||||
supported version is 7.18.2, and the recommended version is 7.21.1 or newer.
|
||||
It is highly recommended that your ``libcurl`` installation is built with
|
||||
asynchronous DNS resolver (threaded or c-ares), otherwise you may encounter
|
||||
various problems with request timeouts (for more information, see
|
||||
Note that if you are using ``curl_httpclient``, it is highly
|
||||
recommended that you use a recent version of ``libcurl`` and
|
||||
``pycurl``. Currently the minimum supported version of libcurl is
|
||||
7.21.1, and the minimum version of pycurl is 7.18.2. It is highly
|
||||
recommended that your ``libcurl`` installation is built with
|
||||
asynchronous DNS resolver (threaded or c-ares), otherwise you may
|
||||
encounter various problems with request timeouts (for more
|
||||
information, see
|
||||
http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCONNECTTIMEOUTMS
|
||||
and comments in curl_httpclient.py).
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
@ -144,12 +147,21 @@ class AsyncHTTPClient(Configurable):
|
|||
|
||||
def __new__(cls, io_loop=None, force_instance=False, **kwargs):
|
||||
io_loop = io_loop or IOLoop.current()
|
||||
if io_loop in cls._async_clients() and not force_instance:
|
||||
return cls._async_clients()[io_loop]
|
||||
if force_instance:
|
||||
instance_cache = None
|
||||
else:
|
||||
instance_cache = cls._async_clients()
|
||||
if instance_cache is not None and io_loop in instance_cache:
|
||||
return instance_cache[io_loop]
|
||||
instance = super(AsyncHTTPClient, cls).__new__(cls, io_loop=io_loop,
|
||||
**kwargs)
|
||||
if not force_instance:
|
||||
cls._async_clients()[io_loop] = instance
|
||||
# Make sure the instance knows which cache to remove itself from.
|
||||
# It can't simply call _async_clients() because we may be in
|
||||
# __new__(AsyncHTTPClient) but instance.__class__ may be
|
||||
# SimpleAsyncHTTPClient.
|
||||
instance._instance_cache = instance_cache
|
||||
if instance_cache is not None:
|
||||
instance_cache[instance.io_loop] = instance
|
||||
return instance
|
||||
|
||||
def initialize(self, io_loop, defaults=None):
|
||||
|
@ -172,9 +184,13 @@ class AsyncHTTPClient(Configurable):
|
|||
``close()``.
|
||||
|
||||
"""
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if self._async_clients().get(self.io_loop) is self:
|
||||
del self._async_clients()[self.io_loop]
|
||||
if self._instance_cache is not None:
|
||||
if self._instance_cache.get(self.io_loop) is not self:
|
||||
raise RuntimeError("inconsistent AsyncHTTPClient cache")
|
||||
del self._instance_cache[self.io_loop]
|
||||
|
||||
def fetch(self, request, callback=None, **kwargs):
|
||||
"""Executes a request, asynchronously returning an `HTTPResponse`.
|
||||
|
|
|
@ -45,8 +45,7 @@ import traceback
|
|||
from tornado.concurrent import TracebackFuture, is_future
|
||||
from tornado.log import app_log, gen_log
|
||||
from tornado import stack_context
|
||||
from tornado.util import Configurable
|
||||
from tornado.util import errno_from_exception
|
||||
from tornado.util import Configurable, errno_from_exception, timedelta_to_seconds
|
||||
|
||||
try:
|
||||
import signal
|
||||
|
@ -433,7 +432,7 @@ class IOLoop(Configurable):
|
|||
"""
|
||||
return time.time()
|
||||
|
||||
def add_timeout(self, deadline, callback):
|
||||
def add_timeout(self, deadline, callback, *args, **kwargs):
|
||||
"""Runs the ``callback`` at the time ``deadline`` from the I/O loop.
|
||||
|
||||
Returns an opaque handle that may be passed to
|
||||
|
@ -442,13 +441,59 @@ class IOLoop(Configurable):
|
|||
``deadline`` may be a number denoting a time (on the same
|
||||
scale as `IOLoop.time`, normally `time.time`), or a
|
||||
`datetime.timedelta` object for a deadline relative to the
|
||||
current time.
|
||||
current time. Since Tornado 4.0, `call_later` is a more
|
||||
convenient alternative for the relative case since it does not
|
||||
require a timedelta object.
|
||||
|
||||
Note that it is not safe to call `add_timeout` from other threads.
|
||||
Instead, you must use `add_callback` to transfer control to the
|
||||
`IOLoop`'s thread, and then call `add_timeout` from there.
|
||||
|
||||
Subclasses of IOLoop must implement either `add_timeout` or
|
||||
`call_at`; the default implementations of each will call
|
||||
the other. `call_at` is usually easier to implement, but
|
||||
subclasses that wish to maintain compatibility with Tornado
|
||||
versions prior to 4.0 must use `add_timeout` instead.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
Now passes through ``*args`` and ``**kwargs`` to the callback.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
if isinstance(deadline, numbers.Real):
|
||||
return self.call_at(deadline, callback, *args, **kwargs)
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
return self.call_at(self.time() + timedelta_to_seconds(deadline),
|
||||
callback, *args, **kwargs)
|
||||
else:
|
||||
raise TypeError("Unsupported deadline %r" % deadline)
|
||||
|
||||
def call_later(self, delay, callback, *args, **kwargs):
|
||||
"""Runs the ``callback`` after ``delay`` seconds have passed.
|
||||
|
||||
Returns an opaque handle that may be passed to `remove_timeout`
|
||||
to cancel. Note that unlike the `asyncio` method of the same
|
||||
name, the returned object does not have a ``cancel()`` method.
|
||||
|
||||
See `add_timeout` for comments on thread-safety and subclassing.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
"""
|
||||
self.call_at(self.time() + delay, callback, *args, **kwargs)
|
||||
|
||||
def call_at(self, when, callback, *args, **kwargs):
|
||||
"""Runs the ``callback`` at the absolute time designated by ``when``.
|
||||
|
||||
``when`` must be a number using the same reference point as
|
||||
`IOLoop.time`.
|
||||
|
||||
Returns an opaque handle that may be passed to `remove_timeout`
|
||||
to cancel. Note that unlike the `asyncio` method of the same
|
||||
name, the returned object does not have a ``cancel()`` method.
|
||||
|
||||
See `add_timeout` for comments on thread-safety and subclassing.
|
||||
|
||||
.. versionadded:: 4.0
|
||||
"""
|
||||
self.add_timeout(when, callback, *args, **kwargs)
|
||||
|
||||
def remove_timeout(self, timeout):
|
||||
"""Cancels a pending timeout.
|
||||
|
@ -606,7 +651,7 @@ class PollIOLoop(IOLoop):
|
|||
self._thread_ident = None
|
||||
self._blocking_signal_threshold = None
|
||||
self._timeout_counter = itertools.count()
|
||||
|
||||
|
||||
# Create a pipe that we send bogus data to when we want to wake
|
||||
# the I/O loop when it is idle
|
||||
self._waker = Waker()
|
||||
|
@ -813,8 +858,11 @@ class PollIOLoop(IOLoop):
|
|||
def time(self):
|
||||
return self.time_func()
|
||||
|
||||
def add_timeout(self, deadline, callback):
|
||||
timeout = _Timeout(deadline, stack_context.wrap(callback), self)
|
||||
def call_at(self, deadline, callback, *args, **kwargs):
|
||||
timeout = _Timeout(
|
||||
deadline,
|
||||
functools.partial(stack_context.wrap(callback), *args, **kwargs),
|
||||
self)
|
||||
heapq.heappush(self._timeouts, timeout)
|
||||
return timeout
|
||||
|
||||
|
@ -869,24 +917,12 @@ class _Timeout(object):
|
|||
__slots__ = ['deadline', 'callback', 'tiebreaker']
|
||||
|
||||
def __init__(self, deadline, callback, io_loop):
|
||||
if isinstance(deadline, numbers.Real):
|
||||
self.deadline = deadline
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
now = io_loop.time()
|
||||
try:
|
||||
self.deadline = now + deadline.total_seconds()
|
||||
except AttributeError: # py2.6
|
||||
self.deadline = now + _Timeout.timedelta_to_seconds(deadline)
|
||||
else:
|
||||
if not isinstance(deadline, numbers.Real):
|
||||
raise TypeError("Unsupported deadline %r" % deadline)
|
||||
self.deadline = deadline
|
||||
self.callback = callback
|
||||
self.tiebreaker = next(io_loop._timeout_counter)
|
||||
|
||||
@staticmethod
|
||||
def timedelta_to_seconds(td):
|
||||
"""Equivalent to td.total_seconds() (introduced in python 2.7)."""
|
||||
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
|
||||
|
||||
# Comparison methods to sort by deadline, with object id as a tiebreaker
|
||||
# to guarantee a consistent ordering. The heapq module uses __le__
|
||||
# in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
|
||||
|
|
|
@ -57,12 +57,24 @@ except ImportError:
|
|||
# some they differ.
|
||||
_ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN)
|
||||
|
||||
if hasattr(errno, "WSAEWOULDBLOCK"):
|
||||
_ERRNO_WOULDBLOCK += (errno.WSAEWOULDBLOCK,)
|
||||
|
||||
# These errnos indicate that a connection has been abruptly terminated.
|
||||
# They should be caught and handled less noisily than other errors.
|
||||
_ERRNO_CONNRESET = (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE,
|
||||
errno.ETIMEDOUT)
|
||||
|
||||
if hasattr(errno, "WSAECONNRESET"):
|
||||
_ERRNO_CONNRESET += (errno.WSAECONNRESET, errno.WSAECONNABORTED, errno.WSAETIMEDOUT)
|
||||
|
||||
# More non-portable errnos:
|
||||
_ERRNO_INPROGRESS = (errno.EINPROGRESS,)
|
||||
|
||||
if hasattr(errno, "WSAEINPROGRESS"):
|
||||
_ERRNO_INPROGRESS += (errno.WSAEINPROGRESS,)
|
||||
|
||||
#######################################################
|
||||
class StreamClosedError(IOError):
|
||||
"""Exception raised by `IOStream` methods when the stream is closed.
|
||||
|
||||
|
@ -990,7 +1002,7 @@ class IOStream(BaseIOStream):
|
|||
# returned immediately when attempting to connect to
|
||||
# localhost, so handle them the same way as an error
|
||||
# reported later in _handle_connect.
|
||||
if (errno_from_exception(e) != errno.EINPROGRESS and
|
||||
if (errno_from_exception(e) not in _ERRNO_INPROGRESS and
|
||||
errno_from_exception(e) not in _ERRNO_WOULDBLOCK):
|
||||
gen_log.warning("Connect error on fd %s: %s",
|
||||
self.socket.fileno(), e)
|
||||
|
|
|
@ -57,6 +57,9 @@ u('foo').encode('idna')
|
|||
# some they differ.
|
||||
_ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN)
|
||||
|
||||
if hasattr(errno, "WSAEWOULDBLOCK"):
|
||||
_ERRNO_WOULDBLOCK += (errno.WSAEWOULDBLOCK,)
|
||||
|
||||
|
||||
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None):
|
||||
"""Creates listening sockets bound to the given port and address.
|
||||
|
|
|
@ -13,9 +13,9 @@ from __future__ import absolute_import, division, print_function, with_statement
|
|||
import datetime
|
||||
import functools
|
||||
|
||||
# _Timeout is used for its timedelta_to_seconds method for py26 compatibility.
|
||||
from tornado.ioloop import IOLoop, _Timeout
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado import stack_context
|
||||
from tornado.util import timedelta_to_seconds
|
||||
|
||||
try:
|
||||
# Import the real asyncio module for py33+ first. Older versions of the
|
||||
|
@ -109,15 +109,13 @@ class BaseAsyncIOLoop(IOLoop):
|
|||
def stop(self):
|
||||
self.asyncio_loop.stop()
|
||||
|
||||
def add_timeout(self, deadline, callback):
|
||||
if isinstance(deadline, (int, float)):
|
||||
delay = max(deadline - self.time(), 0)
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
delay = _Timeout.timedelta_to_seconds(deadline)
|
||||
else:
|
||||
raise TypeError("Unsupported deadline %r", deadline)
|
||||
return self.asyncio_loop.call_later(delay, self._run_callback,
|
||||
stack_context.wrap(callback))
|
||||
def call_at(self, when, callback, *args, **kwargs):
|
||||
# asyncio.call_at supports *args but not **kwargs, so bind them here.
|
||||
# We do not synchronize self.time and asyncio_loop.time, so
|
||||
# convert from absolute to relative.
|
||||
return self.asyncio_loop.call_later(
|
||||
max(0, when - self.time()), self._run_callback,
|
||||
functools.partial(stack_context.wrap(callback), *args, **kwargs))
|
||||
|
||||
def remove_timeout(self, timeout):
|
||||
timeout.cancel()
|
||||
|
|
|
@ -68,6 +68,7 @@ from __future__ import absolute_import, division, print_function, with_statement
|
|||
|
||||
import datetime
|
||||
import functools
|
||||
import numbers
|
||||
import socket
|
||||
|
||||
import twisted.internet.abstract
|
||||
|
@ -90,11 +91,7 @@ from tornado.log import app_log
|
|||
from tornado.netutil import Resolver
|
||||
from tornado.stack_context import NullContext, wrap
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
try:
|
||||
long # py2
|
||||
except NameError:
|
||||
long = int # py3
|
||||
from tornado.util import timedelta_to_seconds
|
||||
|
||||
|
||||
@implementer(IDelayedCall)
|
||||
|
@ -475,14 +472,19 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
|
|||
def stop(self):
|
||||
self.reactor.crash()
|
||||
|
||||
def add_timeout(self, deadline, callback):
|
||||
if isinstance(deadline, (int, long, float)):
|
||||
def add_timeout(self, deadline, callback, *args, **kwargs):
|
||||
# This method could be simplified (since tornado 4.0) by
|
||||
# overriding call_at instead of add_timeout, but we leave it
|
||||
# for now as a test of backwards-compatibility.
|
||||
if isinstance(deadline, numbers.Real):
|
||||
delay = max(deadline - self.time(), 0)
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
delay = tornado.ioloop._Timeout.timedelta_to_seconds(deadline)
|
||||
delay = timedelta_to_seconds(deadline)
|
||||
else:
|
||||
raise TypeError("Unsupported deadline %r")
|
||||
return self.reactor.callLater(delay, self._run_callback, wrap(callback))
|
||||
return self.reactor.callLater(
|
||||
delay, self._run_callback,
|
||||
functools.partial(wrap(callback), *args, **kwargs))
|
||||
|
||||
def remove_timeout(self, timeout):
|
||||
if timeout.active():
|
||||
|
|
|
@ -155,7 +155,7 @@ class TestIOLoop(AsyncTestCase):
|
|||
|
||||
def test_remove_timeout_after_fire(self):
|
||||
# It is not an error to call remove_timeout after it has run.
|
||||
handle = self.io_loop.add_timeout(self.io_loop.time(), self.stop())
|
||||
handle = self.io_loop.add_timeout(self.io_loop.time(), self.stop)
|
||||
self.wait()
|
||||
self.io_loop.remove_timeout(handle)
|
||||
|
||||
|
@ -173,6 +173,18 @@ class TestIOLoop(AsyncTestCase):
|
|||
self.io_loop.add_callback(lambda: self.io_loop.add_callback(self.stop))
|
||||
self.wait()
|
||||
|
||||
def test_timeout_with_arguments(self):
|
||||
# This tests that all the timeout methods pass through *args correctly.
|
||||
results = []
|
||||
self.io_loop.add_timeout(self.io_loop.time(), results.append, 1)
|
||||
self.io_loop.add_timeout(datetime.timedelta(seconds=0),
|
||||
results.append, 2)
|
||||
self.io_loop.call_at(self.io_loop.time(), results.append, 3)
|
||||
self.io_loop.call_later(0, results.append, 4)
|
||||
self.io_loop.call_later(0, self.stop)
|
||||
self.wait()
|
||||
self.assertEqual(results, [1, 2, 3, 4])
|
||||
|
||||
def test_close_file_object(self):
|
||||
"""When a file object is used instead of a numeric file descriptor,
|
||||
the object should be closed (by IOLoop.close(all_fds=True),
|
||||
|
|
|
@ -232,8 +232,11 @@ class TestIOStreamMixin(object):
|
|||
self.assertFalse(self.connect_called)
|
||||
self.assertTrue(isinstance(stream.error, socket.error), stream.error)
|
||||
if sys.platform != 'cygwin':
|
||||
_ERRNO_CONNREFUSED = (errno.ECONNREFUSED,)
|
||||
if hasattr(errno, "WSAECONNREFUSED"):
|
||||
_ERRNO_CONNREFUSED += (errno.WSAECONNREFUSED,)
|
||||
# cygwin's errnos don't match those used on native windows python
|
||||
self.assertEqual(stream.error.args[0], errno.ECONNREFUSED)
|
||||
self.assertTrue(stream.error.args[0] in _ERRNO_CONNREFUSED)
|
||||
|
||||
def test_gaierror(self):
|
||||
# Test that IOStream sets its exc_info on getaddrinfo error
|
||||
|
|
|
@ -321,8 +321,10 @@ class SimpleHTTPClientTestMixin(object):
|
|||
|
||||
if sys.platform != 'cygwin':
|
||||
# cygwin returns EPERM instead of ECONNREFUSED here
|
||||
self.assertTrue(str(errno.ECONNREFUSED) in str(response.error),
|
||||
response.error)
|
||||
contains_errno = str(errno.ECONNREFUSED) in str(response.error)
|
||||
if not contains_errno and hasattr(errno, "WSAECONNREFUSED"):
|
||||
contains_errno = str(errno.WSAECONNREFUSED) in str(response.error)
|
||||
self.assertTrue(contains_errno, response.error)
|
||||
# This is usually "Connection refused".
|
||||
# On windows, strerror is broken and returns "Unknown error".
|
||||
expected_message = os.strerror(errno.ECONNREFUSED)
|
||||
|
|
|
@ -35,11 +35,11 @@ class TestRequestHandler(RequestHandler):
|
|||
logging.debug('in part3()')
|
||||
raise Exception('test exception')
|
||||
|
||||
def get_error_html(self, status_code, **kwargs):
|
||||
if 'exception' in kwargs and str(kwargs['exception']) == 'test exception':
|
||||
return 'got expected exception'
|
||||
def write_error(self, status_code, **kwargs):
|
||||
if 'exc_info' in kwargs and str(kwargs['exc_info'][1]) == 'test exception':
|
||||
self.write('got expected exception')
|
||||
else:
|
||||
return 'unexpected failure'
|
||||
self.write('unexpected failure')
|
||||
|
||||
|
||||
class HTTPStackContextTest(AsyncHTTPTestCase):
|
||||
|
|
|
@ -10,7 +10,7 @@ from tornado.template import DictLoader
|
|||
from tornado.testing import AsyncHTTPTestCase, ExpectLog, gen_test
|
||||
from tornado.test.util import unittest
|
||||
from tornado.util import u, bytes_type, ObjectDict, unicode_type
|
||||
from tornado.web import RequestHandler, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler, _create_signature_v1, create_signed_value, decode_signed_value, ErrorHandler, UIModule, MissingArgumentError, stream_request_body
|
||||
from tornado.web import RequestHandler, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler, _create_signature_v1, create_signed_value, decode_signed_value, ErrorHandler, UIModule, MissingArgumentError, stream_request_body, Finish
|
||||
|
||||
import binascii
|
||||
import contextlib
|
||||
|
@ -773,20 +773,6 @@ class ErrorResponseTest(WebTestCase):
|
|||
else:
|
||||
self.write("Status: %d" % status_code)
|
||||
|
||||
class GetErrorHtmlHandler(RequestHandler):
|
||||
def get(self):
|
||||
if self.get_argument("status", None):
|
||||
self.send_error(int(self.get_argument("status")))
|
||||
else:
|
||||
1 / 0
|
||||
|
||||
def get_error_html(self, status_code, **kwargs):
|
||||
self.set_header("Content-Type", "text/plain")
|
||||
if "exception" in kwargs:
|
||||
self.write("Exception: %s" % sys.exc_info()[0].__name__)
|
||||
else:
|
||||
self.write("Status: %d" % status_code)
|
||||
|
||||
class FailedWriteErrorHandler(RequestHandler):
|
||||
def get(self):
|
||||
1 / 0
|
||||
|
@ -796,7 +782,6 @@ class ErrorResponseTest(WebTestCase):
|
|||
|
||||
return [url("/default", DefaultHandler),
|
||||
url("/write_error", WriteErrorHandler),
|
||||
url("/get_error_html", GetErrorHtmlHandler),
|
||||
url("/failed_write_error", FailedWriteErrorHandler),
|
||||
]
|
||||
|
||||
|
@ -820,16 +805,6 @@ class ErrorResponseTest(WebTestCase):
|
|||
self.assertEqual(response.code, 503)
|
||||
self.assertEqual(b"Status: 503", response.body)
|
||||
|
||||
def test_get_error_html(self):
|
||||
with ExpectLog(app_log, "Uncaught exception"):
|
||||
response = self.fetch("/get_error_html")
|
||||
self.assertEqual(response.code, 500)
|
||||
self.assertEqual(b"Exception: ZeroDivisionError", response.body)
|
||||
|
||||
response = self.fetch("/get_error_html?status=503")
|
||||
self.assertEqual(response.code, 503)
|
||||
self.assertEqual(b"Status: 503", response.body)
|
||||
|
||||
def test_failed_write_error(self):
|
||||
with ExpectLog(app_log, "Uncaught exception"):
|
||||
response = self.fetch("/failed_write_error")
|
||||
|
@ -2307,3 +2282,20 @@ class XSRFTest(SimpleHandlerTestCase):
|
|||
body=urllib_parse.urlencode(dict(_xsrf=body_token)),
|
||||
headers=self.cookie_headers(cookie_token))
|
||||
self.assertEqual(response.code, 200)
|
||||
|
||||
|
||||
@wsgi_safe
|
||||
class FinishExceptionTest(SimpleHandlerTestCase):
|
||||
class Handler(RequestHandler):
|
||||
def get(self):
|
||||
self.set_status(401)
|
||||
self.set_header('WWW-Authenticate', 'Basic realm="something"')
|
||||
self.write('authentication required')
|
||||
raise Finish()
|
||||
|
||||
def test_finish_exception(self):
|
||||
response = self.fetch('/')
|
||||
self.assertEqual(response.code, 401)
|
||||
self.assertEqual('Basic realm="something"',
|
||||
response.headers.get('WWW-Authenticate'))
|
||||
self.assertEqual(b'authentication required', response.body)
|
||||
|
|
|
@ -70,8 +70,8 @@ def get_unused_port():
|
|||
only that a series of get_unused_port calls in a single process return
|
||||
distinct ports.
|
||||
|
||||
**Deprecated**. Use bind_unused_port instead, which is guaranteed
|
||||
to find an unused port.
|
||||
.. deprecated::
|
||||
Use bind_unused_port instead, which is guaranteed to find an unused port.
|
||||
"""
|
||||
global _next_port
|
||||
port = _next_port
|
||||
|
|
|
@ -311,6 +311,11 @@ class ArgReplacer(object):
|
|||
return old_value, args, kwargs
|
||||
|
||||
|
||||
def timedelta_to_seconds(td):
|
||||
"""Equivalent to td.total_seconds() (introduced in python 2.7)."""
|
||||
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
|
||||
|
||||
|
||||
def _websocket_mask_python(mask, data):
|
||||
"""Websocket masking function.
|
||||
|
||||
|
|
|
@ -630,7 +630,6 @@ class RequestHandler(object):
|
|||
self.set_status(status)
|
||||
self.set_header("Location", urlparse.urljoin(utf8(self.request.uri),
|
||||
utf8(url)))
|
||||
|
||||
self.finish()
|
||||
|
||||
def write(self, chunk):
|
||||
|
@ -944,26 +943,7 @@ class RequestHandler(object):
|
|||
``kwargs["exc_info"]``. Note that this exception may not be
|
||||
the "current" exception for purposes of methods like
|
||||
``sys.exc_info()`` or ``traceback.format_exc``.
|
||||
|
||||
For historical reasons, if a method ``get_error_html`` exists,
|
||||
it will be used instead of the default ``write_error`` implementation.
|
||||
``get_error_html`` returned a string instead of producing output
|
||||
normally, and had different semantics for exception handling.
|
||||
Users of ``get_error_html`` are encouraged to convert their code
|
||||
to override ``write_error`` instead.
|
||||
"""
|
||||
if hasattr(self, 'get_error_html'):
|
||||
if 'exc_info' in kwargs:
|
||||
exc_info = kwargs.pop('exc_info')
|
||||
kwargs['exception'] = exc_info[1]
|
||||
try:
|
||||
# Put the traceback into sys.exc_info()
|
||||
raise_exc_info(exc_info)
|
||||
except Exception:
|
||||
self.finish(self.get_error_html(status_code, **kwargs))
|
||||
else:
|
||||
self.finish(self.get_error_html(status_code, **kwargs))
|
||||
return
|
||||
if self.settings.get("serve_traceback") and "exc_info" in kwargs:
|
||||
# in debug mode, try to send a traceback
|
||||
self.set_header('Content-Type', 'text/plain')
|
||||
|
@ -1385,6 +1365,11 @@ class RequestHandler(object):
|
|||
" (" + self.request.remote_ip + ")"
|
||||
|
||||
def _handle_request_exception(self, e):
|
||||
if isinstance(e, Finish):
|
||||
# Not an error; just finish the request without logging.
|
||||
if not self._finished:
|
||||
self.finish()
|
||||
return
|
||||
self.log_exception(*sys.exc_info())
|
||||
if self._finished:
|
||||
# Extra errors after the request has been finished should
|
||||
|
@ -1939,6 +1924,9 @@ class HTTPError(Exception):
|
|||
`RequestHandler.send_error` since it automatically ends the
|
||||
current function.
|
||||
|
||||
To customize the response sent with an `HTTPError`, override
|
||||
`RequestHandler.write_error`.
|
||||
|
||||
:arg int status_code: HTTP status code. Must be listed in
|
||||
`httplib.responses <http.client.responses>` unless the ``reason``
|
||||
keyword argument is given.
|
||||
|
@ -1967,6 +1955,25 @@ class HTTPError(Exception):
|
|||
return message
|
||||
|
||||
|
||||
class Finish(Exception):
|
||||
"""An exception that ends the request without producing an error response.
|
||||
|
||||
When `Finish` is raised in a `RequestHandler`, the request will end
|
||||
(calling `RequestHandler.finish` if it hasn't already been called),
|
||||
but the outgoing response will not be modified and the error-handling
|
||||
methods (including `RequestHandler.write_error`) will not be called.
|
||||
|
||||
This can be a more convenient way to implement custom error pages
|
||||
than overriding ``write_error`` (especially in library code)::
|
||||
|
||||
if self.current_user is None:
|
||||
self.set_status(401)
|
||||
self.set_header('WWW-Authenticate', 'Basic realm="something"')
|
||||
raise Finish()
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingArgumentError(HTTPError):
|
||||
"""Exception raised by `RequestHandler.get_argument`.
|
||||
|
||||
|
@ -2494,9 +2501,9 @@ class FallbackHandler(RequestHandler):
|
|||
class OutputTransform(object):
|
||||
"""A transform modifies the result of an HTTP request (e.g., GZip encoding)
|
||||
|
||||
A new transform instance is created for every request. See the
|
||||
GZipContentEncoding example below if you want to implement a
|
||||
new Transform.
|
||||
Applications are not expected to create their own OutputTransforms
|
||||
or interact with them directly; the framework chooses which transforms
|
||||
(if any) to apply.
|
||||
"""
|
||||
def __init__(self, request):
|
||||
pass
|
||||
|
|
|
@ -3,18 +3,17 @@
|
|||
`WebSockets <http://dev.w3.org/html5/websockets/>`_ allow for bidirectional
|
||||
communication between the browser and server.
|
||||
|
||||
.. warning::
|
||||
WebSockets are supported in the current versions of all major browsers,
|
||||
although older versions that do not support WebSockets are still in use
|
||||
(refer to http://caniuse.com/websockets for details).
|
||||
|
||||
The WebSocket protocol was recently finalized as `RFC 6455
|
||||
<http://tools.ietf.org/html/rfc6455>`_ and is not yet supported in
|
||||
all browsers. Refer to http://caniuse.com/websockets for details
|
||||
on compatibility. In addition, during development the protocol
|
||||
went through several incompatible versions, and some browsers only
|
||||
support older versions. By default this module only supports the
|
||||
latest version of the protocol, but optional support for an older
|
||||
version (known as "draft 76" or "hixie-76") can be enabled by
|
||||
overriding `WebSocketHandler.allow_draft76` (see that method's
|
||||
documentation for caveats).
|
||||
This module implements the final version of the WebSocket protocol as
|
||||
defined in `RFC 6455 <http://tools.ietf.org/html/rfc6455>`_. Certain
|
||||
browser versions (notably Safari 5.x) implemented an earlier draft of
|
||||
the protocol (known as "draft 76") and are not compatible with this module.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
Removed support for the draft 76 protocol version.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
@ -22,11 +21,9 @@ from __future__ import absolute_import, division, print_function, with_statement
|
|||
|
||||
import base64
|
||||
import collections
|
||||
import functools
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
|
||||
|
@ -38,7 +35,7 @@ from tornado.iostream import StreamClosedError
|
|||
from tornado.log import gen_log, app_log
|
||||
from tornado import simple_httpclient
|
||||
from tornado.tcpclient import TCPClient
|
||||
from tornado.util import bytes_type, unicode_type, _websocket_mask
|
||||
from tornado.util import bytes_type, _websocket_mask
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse # py2
|
||||
|
@ -161,10 +158,6 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
|
||||
self.ws_connection = WebSocketProtocol13(self)
|
||||
self.ws_connection.accept_connection()
|
||||
elif (self.allow_draft76() and
|
||||
"Sec-WebSocket-Version" not in self.request.headers):
|
||||
self.ws_connection = WebSocketProtocol76(self)
|
||||
self.ws_connection.accept_connection()
|
||||
else:
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 426 Upgrade Required\r\n"
|
||||
|
@ -256,9 +249,6 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||
closing. These values are made available to the client, but are
|
||||
not otherwise interpreted by the websocket protocol.
|
||||
|
||||
The ``code`` and ``reason`` arguments are ignored in the "draft76"
|
||||
protocol version.
|
||||
|
||||
.. versionchanged:: 4.0
|
||||
|
||||
Added the ``code`` and ``reason`` arguments.
|
||||
|
@ -296,21 +286,6 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||
# Check to see that origin matches host directly, including ports
|
||||
return origin == host
|
||||
|
||||
def allow_draft76(self):
|
||||
"""Override to enable support for the older "draft76" protocol.
|
||||
|
||||
The draft76 version of the websocket protocol is disabled by
|
||||
default due to security concerns, but it can be enabled by
|
||||
overriding this method to return True.
|
||||
|
||||
Connections using the draft76 protocol do not support the
|
||||
``binary=True`` flag to `write_message`.
|
||||
|
||||
Support for the draft76 protocol is deprecated and will be
|
||||
removed in a future version of Tornado.
|
||||
"""
|
||||
return False
|
||||
|
||||
def set_nodelay(self, value):
|
||||
"""Set the no-delay flag for this stream.
|
||||
|
||||
|
@ -327,18 +302,6 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||
"""
|
||||
self.stream.set_nodelay(value)
|
||||
|
||||
def get_websocket_scheme(self):
|
||||
"""Return the url scheme used for this request, either "ws" or "wss".
|
||||
|
||||
This is normally decided by HTTPServer, but applications
|
||||
may wish to override this if they are using an SSL proxy
|
||||
that does not provide the X-Scheme header as understood
|
||||
by HTTPServer.
|
||||
|
||||
Note that this is only used by the draft76 protocol.
|
||||
"""
|
||||
return "wss" if self.request.protocol == "https" else "ws"
|
||||
|
||||
def on_connection_close(self):
|
||||
if self.ws_connection:
|
||||
self.ws_connection.on_connection_close()
|
||||
|
@ -392,175 +355,6 @@ class WebSocketProtocol(object):
|
|||
self.close() # let the subclass cleanup
|
||||
|
||||
|
||||
class WebSocketProtocol76(WebSocketProtocol):
|
||||
"""Implementation of the WebSockets protocol, version hixie-76.
|
||||
|
||||
This class provides basic functionality to process WebSockets requests as
|
||||
specified in
|
||||
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
"""
|
||||
def __init__(self, handler):
|
||||
WebSocketProtocol.__init__(self, handler)
|
||||
self.challenge = None
|
||||
self._waiting = None
|
||||
|
||||
def accept_connection(self):
|
||||
try:
|
||||
self._handle_websocket_headers()
|
||||
except ValueError:
|
||||
gen_log.debug("Malformed WebSocket request received")
|
||||
self._abort()
|
||||
return
|
||||
|
||||
scheme = self.handler.get_websocket_scheme()
|
||||
|
||||
# draft76 only allows a single subprotocol
|
||||
subprotocol_header = ''
|
||||
subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
|
||||
if subprotocol:
|
||||
selected = self.handler.select_subprotocol([subprotocol])
|
||||
if selected:
|
||||
assert selected == subprotocol
|
||||
subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
|
||||
|
||||
# Write the initial headers before attempting to read the challenge.
|
||||
# This is necessary when using proxies (such as HAProxy), which
|
||||
# need to see the Upgrade headers before passing through the
|
||||
# non-HTTP traffic that follows.
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
||||
"Upgrade: WebSocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Server: TornadoServer/%(version)s\r\n"
|
||||
"Sec-WebSocket-Origin: %(origin)s\r\n"
|
||||
"Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
|
||||
"%(subprotocol)s"
|
||||
"\r\n" % (dict(
|
||||
version=tornado.version,
|
||||
origin=self.request.headers["Origin"],
|
||||
scheme=scheme,
|
||||
host=self.request.host,
|
||||
uri=self.request.uri,
|
||||
subprotocol=subprotocol_header))))
|
||||
self.stream.read_bytes(8, self._handle_challenge)
|
||||
|
||||
def challenge_response(self, challenge):
|
||||
"""Generates the challenge response that's needed in the handshake
|
||||
|
||||
The challenge parameter should be the raw bytes as sent from the
|
||||
client.
|
||||
"""
|
||||
key_1 = self.request.headers.get("Sec-Websocket-Key1")
|
||||
key_2 = self.request.headers.get("Sec-Websocket-Key2")
|
||||
try:
|
||||
part_1 = self._calculate_part(key_1)
|
||||
part_2 = self._calculate_part(key_2)
|
||||
except ValueError:
|
||||
raise ValueError("Invalid Keys/Challenge")
|
||||
return self._generate_challenge_response(part_1, part_2, challenge)
|
||||
|
||||
def _handle_challenge(self, challenge):
|
||||
try:
|
||||
challenge_response = self.challenge_response(challenge)
|
||||
except ValueError:
|
||||
gen_log.debug("Malformed key data in WebSocket request")
|
||||
self._abort()
|
||||
return
|
||||
self._write_response(challenge_response)
|
||||
|
||||
def _write_response(self, challenge):
|
||||
self.stream.write(challenge)
|
||||
self._run_callback(self.handler.open, *self.handler.open_args,
|
||||
**self.handler.open_kwargs)
|
||||
self._receive_message()
|
||||
|
||||
def _handle_websocket_headers(self):
|
||||
"""Verifies all invariant- and required headers
|
||||
|
||||
If a header is missing or have an incorrect value ValueError will be
|
||||
raised
|
||||
"""
|
||||
fields = ("Origin", "Host", "Sec-Websocket-Key1",
|
||||
"Sec-Websocket-Key2")
|
||||
if not all(map(lambda f: self.request.headers.get(f), fields)):
|
||||
raise ValueError("Missing/Invalid WebSocket headers")
|
||||
|
||||
def _calculate_part(self, key):
|
||||
"""Processes the key headers and calculates their key value.
|
||||
|
||||
Raises ValueError when feed invalid key."""
|
||||
# pyflakes complains about variable reuse if both of these lines use 'c'
|
||||
number = int(''.join(c for c in key if c.isdigit()))
|
||||
spaces = len([c2 for c2 in key if c2.isspace()])
|
||||
try:
|
||||
key_number = number // spaces
|
||||
except (ValueError, ZeroDivisionError):
|
||||
raise ValueError
|
||||
return struct.pack(">I", key_number)
|
||||
|
||||
def _generate_challenge_response(self, part_1, part_2, part_3):
|
||||
m = hashlib.md5()
|
||||
m.update(part_1)
|
||||
m.update(part_2)
|
||||
m.update(part_3)
|
||||
return m.digest()
|
||||
|
||||
def _receive_message(self):
|
||||
self.stream.read_bytes(1, self._on_frame_type)
|
||||
|
||||
def _on_frame_type(self, byte):
|
||||
frame_type = ord(byte)
|
||||
if frame_type == 0x00:
|
||||
self.stream.read_until(b"\xff", self._on_end_delimiter)
|
||||
elif frame_type == 0xff:
|
||||
self.stream.read_bytes(1, self._on_length_indicator)
|
||||
else:
|
||||
self._abort()
|
||||
|
||||
def _on_end_delimiter(self, frame):
|
||||
if not self.client_terminated:
|
||||
self._run_callback(self.handler.on_message,
|
||||
frame[:-1].decode("utf-8", "replace"))
|
||||
if not self.client_terminated:
|
||||
self._receive_message()
|
||||
|
||||
def _on_length_indicator(self, byte):
|
||||
if ord(byte) != 0x00:
|
||||
self._abort()
|
||||
return
|
||||
self.client_terminated = True
|
||||
self.close()
|
||||
|
||||
def write_message(self, message, binary=False):
|
||||
"""Sends the given message to the client of this Web Socket."""
|
||||
if binary:
|
||||
raise ValueError(
|
||||
"Binary messages not supported by this version of websockets")
|
||||
if isinstance(message, unicode_type):
|
||||
message = message.encode("utf-8")
|
||||
assert isinstance(message, bytes_type)
|
||||
self.stream.write(b"\x00" + message + b"\xff")
|
||||
|
||||
def write_ping(self, data):
|
||||
"""Send ping frame."""
|
||||
raise ValueError("Ping messages not supported by this version of websockets")
|
||||
|
||||
def close(self, code=None, reason=None):
|
||||
"""Closes the WebSocket connection."""
|
||||
if not self.server_terminated:
|
||||
if not self.stream.closed():
|
||||
self.stream.write("\xff\x00")
|
||||
self.server_terminated = True
|
||||
if self.client_terminated:
|
||||
if self._waiting is not None:
|
||||
self.stream.io_loop.remove_timeout(self._waiting)
|
||||
self._waiting = None
|
||||
self.stream.close()
|
||||
elif self._waiting is None:
|
||||
self._waiting = self.stream.io_loop.add_timeout(
|
||||
time.time() + 5, self._abort)
|
||||
|
||||
|
||||
class WebSocketProtocol13(WebSocketProtocol):
|
||||
"""Implementation of the WebSocket protocol from RFC 6455.
|
||||
|
||||
|
|
Loading…
Reference in a new issue