Merge branch 'refs/heads/develop' into tv
This commit is contained in:
+31
-3
@@ -1,14 +1,29 @@
|
|||||||
from couchpotato.core.helpers.request import getParams
|
from couchpotato.core.helpers.request import getParams
|
||||||
|
from functools import wraps
|
||||||
|
from threading import Thread
|
||||||
|
from tornado.gen import coroutine
|
||||||
from tornado.web import RequestHandler, asynchronous
|
from tornado.web import RequestHandler, asynchronous
|
||||||
import json
|
import json
|
||||||
|
import threading
|
||||||
|
import tornado
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
api = {}
|
api = {}
|
||||||
|
api_locks = {}
|
||||||
api_nonblock = {}
|
api_nonblock = {}
|
||||||
|
|
||||||
api_docs = {}
|
api_docs = {}
|
||||||
api_docs_missing = []
|
api_docs_missing = []
|
||||||
|
|
||||||
|
def run_async(func):
|
||||||
|
@wraps(func)
|
||||||
|
def async_func(*args, **kwargs):
|
||||||
|
func_hl = Thread(target = func, args = args, kwargs = kwargs)
|
||||||
|
func_hl.start()
|
||||||
|
return func_hl
|
||||||
|
|
||||||
|
return async_func
|
||||||
|
|
||||||
# NonBlock API handler
|
# NonBlock API handler
|
||||||
class NonBlockHandler(RequestHandler):
|
class NonBlockHandler(RequestHandler):
|
||||||
|
|
||||||
@@ -26,7 +41,7 @@ class NonBlockHandler(RequestHandler):
|
|||||||
if self.request.connection.stream.closed():
|
if self.request.connection.stream.closed():
|
||||||
return
|
return
|
||||||
|
|
||||||
self.finish(response)
|
self.write(response)
|
||||||
|
|
||||||
def on_connection_close(self):
|
def on_connection_close(self):
|
||||||
|
|
||||||
@@ -46,12 +61,15 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
|||||||
# Blocking API handler
|
# Blocking API handler
|
||||||
class ApiHandler(RequestHandler):
|
class ApiHandler(RequestHandler):
|
||||||
|
|
||||||
|
@coroutine
|
||||||
def get(self, route, *args, **kwargs):
|
def get(self, route, *args, **kwargs):
|
||||||
route = route.strip('/')
|
route = route.strip('/')
|
||||||
if not api.get(route):
|
if not api.get(route):
|
||||||
self.write('API call doesn\'t seem to exist')
|
self.write('API call doesn\'t seem to exist')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
api_locks[route].acquire()
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for x in self.request.arguments:
|
for x in self.request.arguments:
|
||||||
kwargs[x] = urllib.unquote(self.get_argument(x))
|
kwargs[x] = urllib.unquote(self.get_argument(x))
|
||||||
@@ -63,8 +81,14 @@ class ApiHandler(RequestHandler):
|
|||||||
try: del kwargs['t']
|
try: del kwargs['t']
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
# Add async callback handler
|
||||||
|
@run_async
|
||||||
|
def run_handler(callback):
|
||||||
|
result = api[route](**kwargs)
|
||||||
|
callback(result)
|
||||||
|
result = yield tornado.gen.Task(run_handler)
|
||||||
|
|
||||||
# Check JSONP callback
|
# Check JSONP callback
|
||||||
result = api[route](**kwargs)
|
|
||||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||||
|
|
||||||
if jsonp_callback:
|
if jsonp_callback:
|
||||||
@@ -74,10 +98,14 @@ class ApiHandler(RequestHandler):
|
|||||||
else:
|
else:
|
||||||
self.write(result)
|
self.write(result)
|
||||||
|
|
||||||
|
api_locks[route].release()
|
||||||
|
|
||||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||||
|
|
||||||
if static: func(route)
|
if static: func(route)
|
||||||
else: api[route] = func
|
else:
|
||||||
|
api[route] = func
|
||||||
|
api_locks[route] = threading.Lock()
|
||||||
|
|
||||||
if docs:
|
if docs:
|
||||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt, ss
|
||||||
|
from couchpotato.core.helpers.variable import tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -31,7 +31,6 @@ class Deluge(Downloader):
|
|||||||
return self.drpc
|
return self.drpc
|
||||||
|
|
||||||
def download(self, data, movie, filedata = None):
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||||
|
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
@@ -72,7 +71,8 @@ class Deluge(Downloader):
|
|||||||
if data.get('protocol') == 'torrent_magnet':
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||||
else:
|
else:
|
||||||
remote_torrent = self.drpc.add_torrent_file(movie, b64encode(filedata), options)
|
filename = self.createFileName(data, filedata, movie)
|
||||||
|
remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
|
||||||
|
|
||||||
if not remote_torrent:
|
if not remote_torrent:
|
||||||
log.error('Failed sending torrent to Deluge')
|
log.error('Failed sending torrent to Deluge')
|
||||||
@@ -85,6 +85,10 @@ class Deluge(Downloader):
|
|||||||
|
|
||||||
log.debug('Checking Deluge download status.')
|
log.debug('Checking Deluge download status.')
|
||||||
|
|
||||||
|
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||||
|
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||||
|
return
|
||||||
|
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -92,23 +96,24 @@ class Deluge(Downloader):
|
|||||||
|
|
||||||
queue = self.drpc.get_alltorrents()
|
queue = self.drpc.get_alltorrents()
|
||||||
|
|
||||||
if not (queue and queue.get('torrents')):
|
if not (queue):
|
||||||
log.debug('Nothing in queue or error')
|
log.debug('Nothing in queue or error')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for torrent_id in queue:
|
for torrent_id in queue:
|
||||||
item = queue[torrent_id]
|
item = queue[torrent_id]
|
||||||
log.debug('name=%s / id=%s / save_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / conf_ratio=%s/ is_seed=%s / is_finished=%s', (item['name'], item['hash'], item['save_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], self.conf('ratio'), item['is_seed'], item['is_finished']))
|
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
|
||||||
|
|
||||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
|
||||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||||
|
#status = 'failed'
|
||||||
status = 'busy'
|
status = 'busy'
|
||||||
# Deluge seems to set both is_seed and is_finished once everything has been downloaded.
|
if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
|
||||||
if item['is_seed'] or item['is_finished']:
|
# We have item['seeding_time'] to work out what the seeding time is, but we do not
|
||||||
|
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||||
|
# when the torrent is added. So Deluge will only look at the ratio.
|
||||||
|
# See above comment in download().
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
elif item['is_seed'] and item['is_finished'] and item['paused']:
|
elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
|
|
||||||
download_dir = item['save_path']
|
download_dir = item['save_path']
|
||||||
@@ -122,7 +127,7 @@ class Deluge(Downloader):
|
|||||||
'original_status': item['state'],
|
'original_status': item['state'],
|
||||||
'seed_ratio': item['ratio'],
|
'seed_ratio': item['ratio'],
|
||||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||||
'folder': os.path.join(download_dir, item['name']),
|
'folder': ss(os.path.join(download_dir, item['name'])),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
@@ -169,22 +174,22 @@ class DelugeRPC(object):
|
|||||||
if options['label']:
|
if options['label']:
|
||||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to add torrent magnet: %s %s', err, traceback.format_exc())
|
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||||
finally:
|
finally:
|
||||||
if self.client:
|
if self.client:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
return torrent_id
|
return torrent_id
|
||||||
|
|
||||||
def add_torrent_file(self, movie, torrent, options):
|
def add_torrent_file(self, filename, torrent, options):
|
||||||
torrent_id = False
|
torrent_id = False
|
||||||
try:
|
try:
|
||||||
self.connect()
|
self.connect()
|
||||||
torrent_id = self.client.core.add_torrent_file(movie, torrent, options).get()
|
torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
|
||||||
if options['label']:
|
if options['label']:
|
||||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to add torrent file: %s %s', err, traceback.format_exc())
|
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||||
finally:
|
finally:
|
||||||
if self.client:
|
if self.client:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
@@ -197,7 +202,7 @@ class DelugeRPC(object):
|
|||||||
self.connect()
|
self.connect()
|
||||||
ret = self.client.core.get_torrents_status({}, {}).get()
|
ret = self.client.core.get_torrents_status({}, {}).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to get all torrents: %s %s', err, traceback.format_exc())
|
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
||||||
finally:
|
finally:
|
||||||
if self.client:
|
if self.client:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
@@ -208,7 +213,7 @@ class DelugeRPC(object):
|
|||||||
self.connect()
|
self.connect()
|
||||||
self.client.core.pause_torrent(torrent_ids).get()
|
self.client.core.pause_torrent(torrent_ids).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to pause torrent: %s %s', err, traceback.format_exc())
|
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
|
||||||
finally:
|
finally:
|
||||||
if self.client:
|
if self.client:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
@@ -218,7 +223,7 @@ class DelugeRPC(object):
|
|||||||
self.connect()
|
self.connect()
|
||||||
self.client.core.resume_torrent(torrent_ids).get()
|
self.client.core.resume_torrent(torrent_ids).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to resume torrent: %s %s', err, traceback.format_exc())
|
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
|
||||||
finally:
|
finally:
|
||||||
if self.client:
|
if self.client:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
@@ -229,7 +234,7 @@ class DelugeRPC(object):
|
|||||||
self.connect()
|
self.connect()
|
||||||
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Failed to remove torrent: %s %s', err, traceback.format_exc())
|
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
|
||||||
finally:
|
finally:
|
||||||
if self.client:
|
if self.client:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class NZBGet(Downloader):
|
|||||||
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
|
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
|
||||||
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
'folder': item['DestDir']
|
'folder': ss(item['DestDir'])
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class NZBVortex(Downloader):
|
|||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['state'],
|
'original_status': item['state'],
|
||||||
'timeleft':-1,
|
'timeleft':-1,
|
||||||
'folder': item['destinationPath'],
|
'folder': ss(item['destinationPath']),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from base64 import b16encode, b32decode
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
import shutil
|
import shutil
|
||||||
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from rtorrent.err import MethodError
|
from rtorrent.err import MethodError
|
||||||
|
|
||||||
from bencode import bencode, bdecode
|
from bencode import bencode, bdecode
|
||||||
@@ -157,9 +158,8 @@ class rTorrent(Downloader):
|
|||||||
'status': status,
|
'status': status,
|
||||||
'seed_ratio': item.ratio,
|
'seed_ratio': item.ratio,
|
||||||
'original_status': item.state,
|
'original_status': item.state,
|
||||||
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate))
|
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
|
||||||
if item.down_rate > 0 else -1,
|
'folder': ss(item.directory)
|
||||||
'folder': item.directory
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class Sabnzbd(Downloader):
|
|||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['status'],
|
'original_status': item['status'],
|
||||||
'timeleft': str(timedelta(seconds = 0)),
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
'folder': item['storage'],
|
'folder': ss(item['storage']),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt, ss
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -122,7 +122,7 @@ class Transmission(Downloader):
|
|||||||
'original_status': item['status'],
|
'original_status': item['status'],
|
||||||
'seed_ratio': item['uploadRatio'],
|
'seed_ratio': item['uploadRatio'],
|
||||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||||
'folder': os.path.join(item['downloadDir'], item['name']),
|
'folder': ss(os.path.join(item['downloadDir'], item['name'])),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class uTorrent(Downloader):
|
|||||||
'seed_ratio': float(item[7]) / 1000,
|
'seed_ratio': float(item[7]) / 1000,
|
||||||
'original_status': item[1],
|
'original_status': item[1],
|
||||||
'timeleft': str(timedelta(seconds = item[10])),
|
'timeleft': str(timedelta(seconds = item[10])),
|
||||||
'folder': item[26],
|
'folder': ss(item[26]),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ class Loader(object):
|
|||||||
for filename in os.listdir(os.path.join(root, *base_path)):
|
for filename in os.listdir(os.path.join(root, *base_path)):
|
||||||
path = os.path.join(os.path.join(root, *base_path), filename)
|
path = os.path.join(os.path.join(root, *base_path), filename)
|
||||||
if os.path.isdir(path) and filename[:2] != '__':
|
if os.path.isdir(path) and filename[:2] != '__':
|
||||||
if not u'__init__.py' in os.listdir(path):
|
if u'__init__.py' in os.listdir(path):
|
||||||
return
|
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
|
||||||
if recursive:
|
if recursive:
|
||||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class MovieBase(MovieTypeBase):
|
|||||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||||
'library': {'titles': {}, 'files':{}},
|
'library': {'titles': {}, 'files':{}},
|
||||||
'files': {},
|
'files': {},
|
||||||
'status': {}
|
'status': {},
|
||||||
|
'category': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -377,6 +378,7 @@ class MovieBase(MovieTypeBase):
|
|||||||
m = db.query(Media).filter_by(library_id = library.get('id')).first()
|
m = db.query(Media).filter_by(library_id = library.get('id')).first()
|
||||||
added = True
|
added = True
|
||||||
do_search = False
|
do_search = False
|
||||||
|
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||||
if not m:
|
if not m:
|
||||||
m = Media(
|
m = Media(
|
||||||
library_id = library.get('id'),
|
library_id = library.get('id'),
|
||||||
|
|||||||
@@ -731,6 +731,7 @@ MA.Delete = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
|
|
||||||
|
self.movie.removeView();
|
||||||
self.movie.slide('out');
|
self.movie.slide('out');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -641,6 +641,12 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
@media only screen and (device-width: 768px) {
|
||||||
|
.trailer_container iframe {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.trailer_container.hide {
|
.trailer_container.hide {
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,28 +18,23 @@ class MovieLibraryPlugin(LibraryBase):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('library.add.movie', self.add)
|
addEvent('library.add.movie', self.add)
|
||||||
addEvent('library.update.movie', self.update)
|
addEvent('library.update.movie', self.update)
|
||||||
addEvent('library.update.movie_release_date', self.updateReleaseDate)
|
addEvent('library.update.movie.release_date', self.updateReleaseDate)
|
||||||
|
|
||||||
def add(self, attrs = {}, update_after = True):
|
def add(self, attrs = {}, update_after = True):
|
||||||
# movies don't yet contain these, so lets make sure to set defaults
|
|
||||||
type = attrs.get('type', 'movie')
|
|
||||||
primary_provider = attrs.get('primary_provider', 'imdb')
|
primary_provider = attrs.get('primary_provider', 'imdb')
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
l = db.query(Library).filter_by(type = type, identifier = attrs.get('identifier')).first()
|
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
|
||||||
if not l:
|
if not l:
|
||||||
status = fireEvent('status.get', 'needs_update', single = True)
|
status = fireEvent('status.get', 'needs_update', single = True)
|
||||||
l = Library(
|
l = Library(
|
||||||
type = type,
|
|
||||||
primary_provider = primary_provider,
|
|
||||||
year = attrs.get('year'),
|
year = attrs.get('year'),
|
||||||
identifier = attrs.get('identifier'),
|
identifier = attrs.get('identifier'),
|
||||||
plot = toUnicode(attrs.get('plot')),
|
plot = toUnicode(attrs.get('plot')),
|
||||||
tagline = toUnicode(attrs.get('tagline')),
|
tagline = toUnicode(attrs.get('tagline')),
|
||||||
status_id = status.get('id'),
|
status_id = status.get('id'),
|
||||||
info = {},
|
info = {}
|
||||||
parent = None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
title = LibraryTitle(
|
title = LibraryTitle(
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ config = [{
|
|||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
'description': 'Force run the searcher after (re)start.',
|
'description': 'Force run the searcher after (re)start.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'search_on_add',
|
||||||
|
'label': 'Search after add',
|
||||||
|
'advanced': True,
|
||||||
|
'default': 1,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Disable this to only search for movies on cron.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'cron_day',
|
'name': 'cron_day',
|
||||||
'migrate_from': 'searcher',
|
'migrate_from': 'searcher',
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
'profile': {'types': {'quality': {}}},
|
'profile': {'types': {'quality': {}}},
|
||||||
'releases': {'status': {}, 'quality': {}},
|
'releases': {'status': {}, 'quality': {}},
|
||||||
'library': {'titles': {}, 'files':{}},
|
'library': {'titles': {}, 'files':{}},
|
||||||
'files': {}
|
'files': {},
|
||||||
})
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -133,7 +133,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
pre_releases = fireEvent('quality.pre_releases', single = True)
|
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||||
release_dates = fireEvent('library.update.movie_release_date', identifier = movie['library']['identifier'], merge = True)
|
release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True)
|
||||||
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
|
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
|
||||||
|
|
||||||
found_releases = []
|
found_releases = []
|
||||||
@@ -179,7 +179,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
|
|
||||||
download_preference = self.conf('preferred_method', section = 'searcher')
|
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||||
if download_preference != 'both':
|
if download_preference != 'both':
|
||||||
sorted_results = sorted(sorted_results, key = lambda k: k['type'][:3], reverse = (download_preference == 'torrent'))
|
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||||
|
|
||||||
# Check if movie isn't deleted while searching
|
# Check if movie isn't deleted while searching
|
||||||
if not db.query(Media).filter_by(id = movie.get('id')).first():
|
if not db.query(Media).filter_by(id = movie.get('id')).first():
|
||||||
@@ -376,7 +376,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
# For movies before 1972
|
# For movies before 1972
|
||||||
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if is_pre_release:
|
if is_pre_release:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class CoreNotifier(Notification):
|
class CoreNotifier(Notification):
|
||||||
|
|
||||||
m_lock = threading.Lock()
|
m_lock = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(CoreNotifier, self).__init__()
|
super(CoreNotifier, self).__init__()
|
||||||
@@ -57,6 +57,7 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
self.messages = []
|
self.messages = []
|
||||||
self.listeners = []
|
self.listeners = []
|
||||||
|
self.m_lock = threading.Lock()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ class CoreNotifier(Notification):
|
|||||||
prop_name = 'messages.last_check'
|
prop_name = 'messages.last_check'
|
||||||
last_check = tryInt(Env.prop(prop_name, default = 0))
|
last_check = tryInt(Env.prop(prop_name, default = 0))
|
||||||
|
|
||||||
messages = fireEvent('cp.messages', last_check = last_check, single = True)
|
messages = fireEvent('cp.messages', last_check = last_check, single = True) or []
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message.get('time') > last_check:
|
if message.get('time') > last_check:
|
||||||
@@ -187,11 +188,14 @@ class CoreNotifier(Notification):
|
|||||||
'result': messages,
|
'result': messages,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.m_lock.acquire()
|
||||||
self.listeners.append((callback, last_id))
|
self.listeners.append((callback, last_id))
|
||||||
|
self.m_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def removeListener(self, callback):
|
def removeListener(self, callback):
|
||||||
|
|
||||||
|
self.m_lock.acquire()
|
||||||
for list_tuple in self.listeners:
|
for list_tuple in self.listeners:
|
||||||
try:
|
try:
|
||||||
listener, last_id = list_tuple
|
listener, last_id = list_tuple
|
||||||
@@ -199,6 +203,7 @@ class CoreNotifier(Notification):
|
|||||||
self.listeners.remove(list_tuple)
|
self.listeners.remove(list_tuple)
|
||||||
except:
|
except:
|
||||||
log.debug('Failed removing listener: %s', traceback.format_exc())
|
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||||
|
self.m_lock.release()
|
||||||
|
|
||||||
def cleanMessages(self):
|
def cleanMessages(self):
|
||||||
|
|
||||||
@@ -222,7 +227,7 @@ class CoreNotifier(Notification):
|
|||||||
recent = []
|
recent = []
|
||||||
try:
|
try:
|
||||||
index = map(itemgetter('message_id'), self.messages).index(last_id)
|
index = map(itemgetter('message_id'), self.messages).index(last_id)
|
||||||
recent = self.messages[index+1:]
|
recent = self.messages[index + 1:]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -271,6 +271,11 @@ var Category = new Class({
|
|||||||
|
|
||||||
del: function(){
|
del: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
if(self.data.label == undefined){
|
||||||
|
self.el.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var label = self.el.getElement('.category_label input').get('value');
|
var label = self.el.getElement('.category_label input').get('value');
|
||||||
var qObj = new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', '', [{
|
var qObj = new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', '', [{
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class Logging(Plugin):
|
|||||||
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
||||||
|
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
break
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ class Renamer(Plugin):
|
|||||||
download_info = self.extendDownloadInfo(download_info)
|
download_info = self.extendDownloadInfo(download_info)
|
||||||
|
|
||||||
# Unpack any archives
|
# Unpack any archives
|
||||||
|
extr_files = None
|
||||||
if self.conf('unrar'):
|
if self.conf('unrar'):
|
||||||
folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files, \
|
folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files, \
|
||||||
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
|
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
|
||||||
@@ -187,7 +188,8 @@ class Renamer(Plugin):
|
|||||||
fireEvent('renamer.before', group)
|
fireEvent('renamer.before', group)
|
||||||
|
|
||||||
# Add extracted files to the before_rename list
|
# Add extracted files to the before_rename list
|
||||||
group['before_rename'].extend(extr_files)
|
if extr_files:
|
||||||
|
group['before_rename'].extend(extr_files)
|
||||||
|
|
||||||
# Remove weird chars from moviename
|
# Remove weird chars from moviename
|
||||||
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
||||||
|
|||||||
@@ -120,13 +120,17 @@ class Scanner(Plugin):
|
|||||||
files = []
|
files = []
|
||||||
for root, dirs, walk_files in os.walk(folder):
|
for root, dirs, walk_files in os.walk(folder):
|
||||||
files.extend(os.path.join(root, filename) for filename in walk_files)
|
files.extend(os.path.join(root, filename) for filename in walk_files)
|
||||||
|
|
||||||
|
# Break if CP wants to shut down
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||||
else:
|
else:
|
||||||
check_file_date = False
|
check_file_date = False
|
||||||
files = [ss(x) for x in files]
|
files = [ss(x) for x in files]
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
|
|
||||||
@@ -339,6 +343,7 @@ class Scanner(Plugin):
|
|||||||
download_info = None
|
download_info = None
|
||||||
|
|
||||||
# Determine file types
|
# Determine file types
|
||||||
|
db = get_session()
|
||||||
processed_movies = {}
|
processed_movies = {}
|
||||||
while True and not self.shuttingDown():
|
while True and not self.shuttingDown():
|
||||||
try:
|
try:
|
||||||
@@ -761,7 +766,7 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
# Year
|
# Year
|
||||||
year = self.findYear(identifier)
|
year = self.findYear(identifier)
|
||||||
if year:
|
if year and identifier[:4] != year:
|
||||||
identifier = '%s %s' % (identifier.split(year)[0].strip(), year)
|
identifier = '%s %s' % (identifier.split(year)[0].strip(), year)
|
||||||
else:
|
else:
|
||||||
identifier = identifier.split('::')[0]
|
identifier = identifier.split('::')[0]
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ var SuggestList = new Class({
|
|||||||
fill: function(json){
|
fill: function(json){
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
if(!json) return;
|
||||||
|
|
||||||
Object.each(json.suggestions, function(movie){
|
Object.each(json.suggestions, function(movie){
|
||||||
|
|
||||||
|
|||||||
@@ -81,8 +81,6 @@ class Userscript(Plugin):
|
|||||||
|
|
||||||
def getViaUrl(self, url = None, **kwargs):
|
def getViaUrl(self, url = None, **kwargs):
|
||||||
|
|
||||||
print url
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'url': url,
|
'url': url,
|
||||||
'movie': fireEvent('userscript.get_movie_via_url', url = url, single = True)
|
'movie': fireEvent('userscript.get_movie_via_url', url = url, single = True)
|
||||||
|
|||||||
@@ -67,10 +67,15 @@ class PublicHD(TorrentMagnetProvider):
|
|||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
def getMoreInfo(self, item):
|
def getMoreInfo(self, item):
|
||||||
full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
|
||||||
html = BeautifulSoup(full_description)
|
try:
|
||||||
nfo_pre = html.find('div', attrs = {'id':'torrmain'})
|
full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
html = BeautifulSoup(full_description)
|
||||||
|
nfo_pre = html.find('div', attrs = {'id':'torrmain'})
|
||||||
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
|
except:
|
||||||
|
log.error('Failed getting more info for %s', item['name'])
|
||||||
|
description = ''
|
||||||
|
|
||||||
item['description'] = description
|
item['description'] = description
|
||||||
return item
|
return item
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TorrentShack(TorrentProvider):
|
|||||||
]
|
]
|
||||||
|
|
||||||
http_time_between_calls = 1 #seconds
|
http_time_between_calls = 1 #seconds
|
||||||
cat_backup_id = None
|
cat_backup_id = 400
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
|||||||
@@ -104,10 +104,14 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
for backup in backups:
|
for backup in backups:
|
||||||
if total_backups > 3:
|
if total_backups > 3:
|
||||||
if tryInt(os.path.basename(backup)) < time.time() - 259200:
|
if tryInt(os.path.basename(backup)) < time.time() - 259200:
|
||||||
for src_file in src_files:
|
for the_file in os.listdir(backup):
|
||||||
b_file = toUnicode(os.path.join(backup, os.path.basename(src_file)))
|
file_path = os.path.join(backup, the_file)
|
||||||
if os.path.isfile(b_file):
|
try:
|
||||||
os.remove(b_file)
|
if os.path.isfile(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
except Exception, e:
|
||||||
|
raise
|
||||||
|
|
||||||
os.rmdir(backup)
|
os.rmdir(backup)
|
||||||
total_backups -= 1
|
total_backups -= 1
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
# is zero for an official release, positive for a development branch,
|
||||||
# or negative for a release candidate or beta (after the base version
|
# or negative for a release candidate or beta (after the base version
|
||||||
# number has been incremented)
|
# number has been incremented)
|
||||||
version = "3.1b1"
|
version = "3.2.dev2"
|
||||||
version_info = (3, 1, 0, -98)
|
version_info = (3, 2, 0, -99)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import hmac
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from tornado.concurrent import Future, chain_future, return_future
|
from tornado.concurrent import TracebackFuture, chain_future, return_future
|
||||||
from tornado import gen
|
from tornado import gen
|
||||||
from tornado import httpclient
|
from tornado import httpclient
|
||||||
from tornado import escape
|
from tornado import escape
|
||||||
@@ -99,7 +99,7 @@ def _auth_return_future(f):
|
|||||||
|
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
future = Future()
|
future = TracebackFuture()
|
||||||
callback, args, kwargs = replacer.replace(future, args, kwargs)
|
callback, args, kwargs = replacer.replace(future, args, kwargs)
|
||||||
if callback is not None:
|
if callback is not None:
|
||||||
future.add_done_callback(
|
future.add_done_callback(
|
||||||
@@ -306,10 +306,10 @@ class OAuthMixin(object):
|
|||||||
"""Redirects the user to obtain OAuth authorization for this service.
|
"""Redirects the user to obtain OAuth authorization for this service.
|
||||||
|
|
||||||
The ``callback_uri`` may be omitted if you have previously
|
The ``callback_uri`` may be omitted if you have previously
|
||||||
registered a callback URI with the third-party service. For some
|
registered a callback URI with the third-party service. For
|
||||||
sevices (including Twitter and Friendfeed), you must use a
|
some sevices (including Friendfeed), you must use a
|
||||||
previously-registered callback URI and cannot specify a callback
|
previously-registered callback URI and cannot specify a
|
||||||
via this method.
|
callback via this method.
|
||||||
|
|
||||||
This method sets a cookie called ``_oauth_request_token`` which is
|
This method sets a cookie called ``_oauth_request_token`` which is
|
||||||
subsequently used (and cleared) in `get_authenticated_user` for
|
subsequently used (and cleared) in `get_authenticated_user` for
|
||||||
@@ -1158,7 +1158,7 @@ class FacebookMixin(object):
|
|||||||
class FacebookGraphMixin(OAuth2Mixin):
|
class FacebookGraphMixin(OAuth2Mixin):
|
||||||
"""Facebook authentication using the new Graph API and OAuth2."""
|
"""Facebook authentication using the new Graph API and OAuth2."""
|
||||||
_OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
|
_OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
|
||||||
_OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?"
|
_OAUTH_AUTHORIZE_URL = "https://www.facebook.com/dialog/oauth?"
|
||||||
_OAUTH_NO_CALLBACKS = False
|
_OAUTH_NO_CALLBACKS = False
|
||||||
_FACEBOOK_BASE_URL = "https://graph.facebook.com"
|
_FACEBOOK_BASE_URL = "https://graph.facebook.com"
|
||||||
|
|
||||||
|
|||||||
+3502
-3516
File diff suppressed because it is too large
Load Diff
@@ -49,8 +49,9 @@ try:
|
|||||||
except NameError:
|
except NameError:
|
||||||
unichr = chr
|
unichr = chr
|
||||||
|
|
||||||
_XHTML_ESCAPE_RE = re.compile('[&<>"]')
|
_XHTML_ESCAPE_RE = re.compile('[&<>"\']')
|
||||||
_XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"'}
|
_XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"',
|
||||||
|
'\'': '''}
|
||||||
|
|
||||||
|
|
||||||
def xhtml_escape(value):
|
def xhtml_escape(value):
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import functools
|
|||||||
import time
|
import time
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
from tornado.concurrent import Future
|
from tornado.concurrent import TracebackFuture
|
||||||
from tornado.escape import utf8
|
from tornado.escape import utf8
|
||||||
from tornado import httputil, stack_context
|
from tornado import httputil, stack_context
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
@@ -144,9 +144,16 @@ class AsyncHTTPClient(Configurable):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Destroys this HTTP client, freeing any file descriptors used.
|
"""Destroys this HTTP client, freeing any file descriptors used.
|
||||||
Not needed in normal use, but may be helpful in unittests that
|
|
||||||
create and destroy http clients. No other methods may be called
|
This method is **not needed in normal use** due to the way
|
||||||
on the `AsyncHTTPClient` after ``close()``.
|
that `AsyncHTTPClient` objects are transparently reused.
|
||||||
|
``close()`` is generally only necessary when either the
|
||||||
|
`.IOLoop` is also being closed, or the ``force_instance=True``
|
||||||
|
argument was used when creating the `AsyncHTTPClient`.
|
||||||
|
|
||||||
|
No other methods may be called on the `AsyncHTTPClient` after
|
||||||
|
``close()``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._async_clients().get(self.io_loop) is self:
|
if self._async_clients().get(self.io_loop) is self:
|
||||||
del self._async_clients()[self.io_loop]
|
del self._async_clients()[self.io_loop]
|
||||||
@@ -174,7 +181,7 @@ class AsyncHTTPClient(Configurable):
|
|||||||
# where normal dicts get converted to HTTPHeaders objects.
|
# where normal dicts get converted to HTTPHeaders objects.
|
||||||
request.headers = httputil.HTTPHeaders(request.headers)
|
request.headers = httputil.HTTPHeaders(request.headers)
|
||||||
request = _RequestProxy(request, self.defaults)
|
request = _RequestProxy(request, self.defaults)
|
||||||
future = Future()
|
future = TracebackFuture()
|
||||||
if callback is not None:
|
if callback is not None:
|
||||||
callback = stack_context.wrap(callback)
|
callback = stack_context.wrap(callback)
|
||||||
|
|
||||||
|
|||||||
+22
-16
@@ -59,6 +59,9 @@ except ImportError:
|
|||||||
from tornado.platform.auto import set_close_exec, Waker
|
from tornado.platform.auto import set_close_exec, Waker
|
||||||
|
|
||||||
|
|
||||||
|
_POLL_TIMEOUT = 3600.0
|
||||||
|
|
||||||
|
|
||||||
class TimeoutError(Exception):
|
class TimeoutError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -356,7 +359,7 @@ class IOLoop(Configurable):
|
|||||||
if isinstance(result, Future):
|
if isinstance(result, Future):
|
||||||
future_cell[0] = result
|
future_cell[0] = result
|
||||||
else:
|
else:
|
||||||
future_cell[0] = Future()
|
future_cell[0] = TracebackFuture()
|
||||||
future_cell[0].set_result(result)
|
future_cell[0].set_result(result)
|
||||||
self.add_future(future_cell[0], lambda future: self.stop())
|
self.add_future(future_cell[0], lambda future: self.stop())
|
||||||
self.add_callback(run)
|
self.add_callback(run)
|
||||||
@@ -596,7 +599,7 @@ class PollIOLoop(IOLoop):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
poll_timeout = 3600.0
|
poll_timeout = _POLL_TIMEOUT
|
||||||
|
|
||||||
# Prevent IO event starvation by delaying new callbacks
|
# Prevent IO event starvation by delaying new callbacks
|
||||||
# to the next iteration of the event loop.
|
# to the next iteration of the event loop.
|
||||||
@@ -605,6 +608,9 @@ class PollIOLoop(IOLoop):
|
|||||||
self._callbacks = []
|
self._callbacks = []
|
||||||
for callback in callbacks:
|
for callback in callbacks:
|
||||||
self._run_callback(callback)
|
self._run_callback(callback)
|
||||||
|
# Closures may be holding on to a lot of memory, so allow
|
||||||
|
# them to be freed before we go into our poll wait.
|
||||||
|
callbacks = callback = None
|
||||||
|
|
||||||
if self._timeouts:
|
if self._timeouts:
|
||||||
now = self.time()
|
now = self.time()
|
||||||
@@ -616,6 +622,7 @@ class PollIOLoop(IOLoop):
|
|||||||
elif self._timeouts[0].deadline <= now:
|
elif self._timeouts[0].deadline <= now:
|
||||||
timeout = heapq.heappop(self._timeouts)
|
timeout = heapq.heappop(self._timeouts)
|
||||||
self._run_callback(timeout.callback)
|
self._run_callback(timeout.callback)
|
||||||
|
del timeout
|
||||||
else:
|
else:
|
||||||
seconds = self._timeouts[0].deadline - now
|
seconds = self._timeouts[0].deadline - now
|
||||||
poll_timeout = min(seconds, poll_timeout)
|
poll_timeout = min(seconds, poll_timeout)
|
||||||
@@ -669,17 +676,16 @@ class PollIOLoop(IOLoop):
|
|||||||
while self._events:
|
while self._events:
|
||||||
fd, events = self._events.popitem()
|
fd, events = self._events.popitem()
|
||||||
try:
|
try:
|
||||||
self._handlers[fd](fd, events)
|
if self._handlers.has_key(fd):
|
||||||
|
self._handlers[fd](fd, events)
|
||||||
except (OSError, IOError) as e:
|
except (OSError, IOError) as e:
|
||||||
if e.args[0] == errno.EPIPE:
|
if e.args[0] == errno.EPIPE:
|
||||||
# Happens when the client closes the connection
|
# Happens when the client closes the connection
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
app_log.error("Exception in I/O handler for fd %s",
|
self.handle_callback_exception(self._handlers.get(fd))
|
||||||
fd, exc_info=True)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
app_log.error("Exception in I/O handler for fd %s",
|
self.handle_callback_exception(self._handlers.get(fd))
|
||||||
fd, exc_info=True)
|
|
||||||
# reset the stopped flag so another start/stop pair can be issued
|
# reset the stopped flag so another start/stop pair can be issued
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
if self._blocking_signal_threshold is not None:
|
if self._blocking_signal_threshold is not None:
|
||||||
@@ -717,14 +723,14 @@ class PollIOLoop(IOLoop):
|
|||||||
list_empty = not self._callbacks
|
list_empty = not self._callbacks
|
||||||
self._callbacks.append(functools.partial(
|
self._callbacks.append(functools.partial(
|
||||||
stack_context.wrap(callback), *args, **kwargs))
|
stack_context.wrap(callback), *args, **kwargs))
|
||||||
if list_empty and thread.get_ident() != self._thread_ident:
|
if list_empty and thread.get_ident() != self._thread_ident:
|
||||||
# If we're in the IOLoop's thread, we know it's not currently
|
# If we're in the IOLoop's thread, we know it's not currently
|
||||||
# polling. If we're not, and we added the first callback to an
|
# polling. If we're not, and we added the first callback to an
|
||||||
# empty list, we may need to wake it up (it may wake up on its
|
# empty list, we may need to wake it up (it may wake up on its
|
||||||
# own, but an occasional extra wake is harmless). Waking
|
# own, but an occasional extra wake is harmless). Waking
|
||||||
# up a polling IOLoop is relatively expensive, so we try to
|
# up a polling IOLoop is relatively expensive, so we try to
|
||||||
# avoid it when we can.
|
# avoid it when we can.
|
||||||
self._waker.wake()
|
self._waker.wake()
|
||||||
|
|
||||||
def add_callback_from_signal(self, callback, *args, **kwargs):
|
def add_callback_from_signal(self, callback, *args, **kwargs):
|
||||||
with stack_context.NullContext():
|
with stack_context.NullContext():
|
||||||
@@ -813,7 +819,7 @@ class PeriodicCallback(object):
|
|||||||
try:
|
try:
|
||||||
self.callback()
|
self.callback()
|
||||||
except Exception:
|
except Exception:
|
||||||
app_log.error("Error in periodic callback", exc_info=True)
|
self.io_loop.handle_callback_exception(self.callback)
|
||||||
self._schedule_next()
|
self._schedule_next()
|
||||||
|
|
||||||
def _schedule_next(self):
|
def _schedule_next(self):
|
||||||
|
|||||||
+40
-16
@@ -46,6 +46,14 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
_set_nonblocking = None
|
_set_nonblocking = None
|
||||||
|
|
||||||
|
# These errnos indicate that a non-blocking operation must be retried
|
||||||
|
# at a later time. On most platforms they're the same value, but on
|
||||||
|
# some they differ.
|
||||||
|
_ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
class StreamClosedError(IOError):
|
class StreamClosedError(IOError):
|
||||||
"""Exception raised by `IOStream` methods when the stream is closed.
|
"""Exception raised by `IOStream` methods when the stream is closed.
|
||||||
@@ -257,15 +265,19 @@ class BaseIOStream(object):
|
|||||||
self._maybe_run_close_callback()
|
self._maybe_run_close_callback()
|
||||||
|
|
||||||
def _maybe_run_close_callback(self):
|
def _maybe_run_close_callback(self):
|
||||||
if (self.closed() and self._close_callback and
|
# If there are pending callbacks, don't run the close callback
|
||||||
self._pending_callbacks == 0):
|
# until they're done (see _maybe_add_error_handler)
|
||||||
# if there are pending callbacks, don't run the close callback
|
if self.closed() and self._pending_callbacks == 0:
|
||||||
# until they're done (see _maybe_add_error_handler)
|
if self._close_callback is not None:
|
||||||
cb = self._close_callback
|
cb = self._close_callback
|
||||||
self._close_callback = None
|
self._close_callback = None
|
||||||
self._run_callback(cb)
|
self._run_callback(cb)
|
||||||
# Delete any unfinished callbacks to break up reference cycles.
|
# Delete any unfinished callbacks to break up reference cycles.
|
||||||
self._read_callback = self._write_callback = None
|
self._read_callback = self._write_callback = None
|
||||||
|
# Clear the buffers so they can be cleared immediately even
|
||||||
|
# if the IOStream object is kept alive by a reference cycle.
|
||||||
|
# TODO: Clear the read buffer too; it currently breaks some tests.
|
||||||
|
self._write_buffer = None
|
||||||
|
|
||||||
def reading(self):
|
def reading(self):
|
||||||
"""Returns true if we are currently reading from the stream."""
|
"""Returns true if we are currently reading from the stream."""
|
||||||
@@ -447,7 +459,7 @@ class BaseIOStream(object):
|
|||||||
chunk = self.read_from_fd()
|
chunk = self.read_from_fd()
|
||||||
except (socket.error, IOError, OSError) as e:
|
except (socket.error, IOError, OSError) as e:
|
||||||
# ssl.SSLError is a subclass of socket.error
|
# ssl.SSLError is a subclass of socket.error
|
||||||
if e.args[0] == errno.ECONNRESET:
|
if e.args[0] in _ERRNO_CONNRESET:
|
||||||
# Treat ECONNRESET as a connection close rather than
|
# Treat ECONNRESET as a connection close rather than
|
||||||
# an error to minimize log spam (the exception will
|
# an error to minimize log spam (the exception will
|
||||||
# be available on self.error for apps that care).
|
# be available on self.error for apps that care).
|
||||||
@@ -550,12 +562,12 @@ class BaseIOStream(object):
|
|||||||
self._write_buffer_frozen = False
|
self._write_buffer_frozen = False
|
||||||
_merge_prefix(self._write_buffer, num_bytes)
|
_merge_prefix(self._write_buffer, num_bytes)
|
||||||
self._write_buffer.popleft()
|
self._write_buffer.popleft()
|
||||||
except socket.error as e:
|
except (socket.error, IOError, OSError) as e:
|
||||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.args[0] in _ERRNO_WOULDBLOCK:
|
||||||
self._write_buffer_frozen = True
|
self._write_buffer_frozen = True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if e.args[0] not in (errno.EPIPE, errno.ECONNRESET):
|
if e.args[0] not in _ERRNO_CONNRESET:
|
||||||
# Broken pipe errors are usually caused by connection
|
# Broken pipe errors are usually caused by connection
|
||||||
# reset, and its better to not log EPIPE errors to
|
# reset, and its better to not log EPIPE errors to
|
||||||
# minimize log spam
|
# minimize log spam
|
||||||
@@ -682,7 +694,7 @@ class IOStream(BaseIOStream):
|
|||||||
try:
|
try:
|
||||||
chunk = self.socket.recv(self.read_chunk_size)
|
chunk = self.socket.recv(self.read_chunk_size)
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.args[0] in _ERRNO_WOULDBLOCK:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@@ -725,7 +737,8 @@ class IOStream(BaseIOStream):
|
|||||||
# returned immediately when attempting to connect to
|
# returned immediately when attempting to connect to
|
||||||
# localhost, so handle them the same way as an error
|
# localhost, so handle them the same way as an error
|
||||||
# reported later in _handle_connect.
|
# reported later in _handle_connect.
|
||||||
if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK):
|
if (e.args[0] != errno.EINPROGRESS and
|
||||||
|
e.args[0] not in _ERRNO_WOULDBLOCK):
|
||||||
gen_log.warning("Connect error on fd %d: %s",
|
gen_log.warning("Connect error on fd %d: %s",
|
||||||
self.socket.fileno(), e)
|
self.socket.fileno(), e)
|
||||||
self.close(exc_info=True)
|
self.close(exc_info=True)
|
||||||
@@ -789,6 +802,17 @@ class SSLIOStream(IOStream):
|
|||||||
self._ssl_connect_callback = None
|
self._ssl_connect_callback = None
|
||||||
self._server_hostname = None
|
self._server_hostname = None
|
||||||
|
|
||||||
|
# If the socket is already connected, attempt to start the handshake.
|
||||||
|
try:
|
||||||
|
self.socket.getpeername()
|
||||||
|
except socket.error:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Indirectly start the handshake, which will run on the next
|
||||||
|
# IOLoop iteration and then the real IO state will be set in
|
||||||
|
# _handle_events.
|
||||||
|
self._add_io_state(self.io_loop.WRITE)
|
||||||
|
|
||||||
def reading(self):
|
def reading(self):
|
||||||
return self._handshake_reading or super(SSLIOStream, self).reading()
|
return self._handshake_reading or super(SSLIOStream, self).reading()
|
||||||
|
|
||||||
@@ -821,7 +845,7 @@ class SSLIOStream(IOStream):
|
|||||||
return self.close(exc_info=True)
|
return self.close(exc_info=True)
|
||||||
raise
|
raise
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
if err.args[0] in (errno.ECONNABORTED, errno.ECONNRESET):
|
if err.args[0] in _ERRNO_CONNRESET:
|
||||||
return self.close(exc_info=True)
|
return self.close(exc_info=True)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# On Linux, if the connection was reset before the call to
|
# On Linux, if the connection was reset before the call to
|
||||||
@@ -917,7 +941,7 @@ class SSLIOStream(IOStream):
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.args[0] in _ERRNO_WOULDBLOCK:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@@ -953,7 +977,7 @@ class PipeIOStream(BaseIOStream):
|
|||||||
try:
|
try:
|
||||||
chunk = os.read(self.fd, self.read_chunk_size)
|
chunk = os.read(self.fd, self.read_chunk_size)
|
||||||
except (IOError, OSError) as e:
|
except (IOError, OSError) as e:
|
||||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
if e.args[0] in _ERRNO_WOULDBLOCK:
|
||||||
return None
|
return None
|
||||||
elif e.args[0] == errno.EBADF:
|
elif e.args[0] == errno.EBADF:
|
||||||
# If the writing half of a pipe is closed, select will
|
# If the writing half of a pipe is closed, select will
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ def is_valid_ip(ip):
|
|||||||
|
|
||||||
Supports IPv4 and IPv6.
|
Supports IPv4 and IPv6.
|
||||||
"""
|
"""
|
||||||
|
if not ip or '\x00' in ip:
|
||||||
|
# getaddrinfo resolves empty strings to localhost, and truncates
|
||||||
|
# on zero bytes.
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC,
|
res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC,
|
||||||
socket.SOCK_STREAM,
|
socket.SOCK_STREAM,
|
||||||
|
|||||||
+12
-1
@@ -190,23 +190,34 @@ class Subprocess(object):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.io_loop = kwargs.pop('io_loop', None) or ioloop.IOLoop.current()
|
self.io_loop = kwargs.pop('io_loop', None) or ioloop.IOLoop.current()
|
||||||
|
# All FDs we create should be closed on error; those in to_close
|
||||||
|
# should be closed in the parent process on success.
|
||||||
|
pipe_fds = []
|
||||||
to_close = []
|
to_close = []
|
||||||
if kwargs.get('stdin') is Subprocess.STREAM:
|
if kwargs.get('stdin') is Subprocess.STREAM:
|
||||||
in_r, in_w = _pipe_cloexec()
|
in_r, in_w = _pipe_cloexec()
|
||||||
kwargs['stdin'] = in_r
|
kwargs['stdin'] = in_r
|
||||||
|
pipe_fds.extend((in_r, in_w))
|
||||||
to_close.append(in_r)
|
to_close.append(in_r)
|
||||||
self.stdin = PipeIOStream(in_w, io_loop=self.io_loop)
|
self.stdin = PipeIOStream(in_w, io_loop=self.io_loop)
|
||||||
if kwargs.get('stdout') is Subprocess.STREAM:
|
if kwargs.get('stdout') is Subprocess.STREAM:
|
||||||
out_r, out_w = _pipe_cloexec()
|
out_r, out_w = _pipe_cloexec()
|
||||||
kwargs['stdout'] = out_w
|
kwargs['stdout'] = out_w
|
||||||
|
pipe_fds.extend((out_r, out_w))
|
||||||
to_close.append(out_w)
|
to_close.append(out_w)
|
||||||
self.stdout = PipeIOStream(out_r, io_loop=self.io_loop)
|
self.stdout = PipeIOStream(out_r, io_loop=self.io_loop)
|
||||||
if kwargs.get('stderr') is Subprocess.STREAM:
|
if kwargs.get('stderr') is Subprocess.STREAM:
|
||||||
err_r, err_w = _pipe_cloexec()
|
err_r, err_w = _pipe_cloexec()
|
||||||
kwargs['stderr'] = err_w
|
kwargs['stderr'] = err_w
|
||||||
|
pipe_fds.extend((err_r, err_w))
|
||||||
to_close.append(err_w)
|
to_close.append(err_w)
|
||||||
self.stderr = PipeIOStream(err_r, io_loop=self.io_loop)
|
self.stderr = PipeIOStream(err_r, io_loop=self.io_loop)
|
||||||
self.proc = subprocess.Popen(*args, **kwargs)
|
try:
|
||||||
|
self.proc = subprocess.Popen(*args, **kwargs)
|
||||||
|
except:
|
||||||
|
for fd in pipe_fds:
|
||||||
|
os.close(fd)
|
||||||
|
raise
|
||||||
for fd in to_close:
|
for fd in to_close:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
for attr in ['stdin', 'stdout', 'stderr', 'pid']:
|
for attr in ['stdin', 'stdout', 'stderr', 'pid']:
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ with ``{# ... #}``.
|
|||||||
|
|
||||||
{% module Template("foo.html", arg=42) %}
|
{% module Template("foo.html", arg=42) %}
|
||||||
|
|
||||||
|
``UIModules`` are a feature of the `tornado.web.RequestHandler`
|
||||||
|
class (and specifically its ``render`` method) and will not work
|
||||||
|
when the template system is used on its own in other contexts.
|
||||||
|
|
||||||
``{% raw *expr* %}``
|
``{% raw *expr* %}``
|
||||||
Outputs the result of the given expression without autoescaping.
|
Outputs the result of the given expression without autoescaping.
|
||||||
|
|
||||||
|
|||||||
+25
-8
@@ -437,15 +437,25 @@ class RequestHandler(object):
|
|||||||
morsel[k] = v
|
morsel[k] = v
|
||||||
|
|
||||||
def clear_cookie(self, name, path="/", domain=None):
|
def clear_cookie(self, name, path="/", domain=None):
|
||||||
"""Deletes the cookie with the given name."""
|
"""Deletes the cookie with the given name.
|
||||||
|
|
||||||
|
Due to limitations of the cookie protocol, you must pass the same
|
||||||
|
path and domain to clear a cookie as were used when that cookie
|
||||||
|
was set (but there is no way to find out on the server side
|
||||||
|
which values were used for a given cookie).
|
||||||
|
"""
|
||||||
expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
|
expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
|
||||||
self.set_cookie(name, value="", path=path, expires=expires,
|
self.set_cookie(name, value="", path=path, expires=expires,
|
||||||
domain=domain)
|
domain=domain)
|
||||||
|
|
||||||
def clear_all_cookies(self):
|
def clear_all_cookies(self, path="/", domain=None):
|
||||||
"""Deletes all the cookies the user sent with this request."""
|
"""Deletes all the cookies the user sent with this request.
|
||||||
|
|
||||||
|
See `clear_cookie` for more information on the path and domain
|
||||||
|
parameters.
|
||||||
|
"""
|
||||||
for name in self.request.cookies:
|
for name in self.request.cookies:
|
||||||
self.clear_cookie(name)
|
self.clear_cookie(name, path=path, domain=domain)
|
||||||
|
|
||||||
def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
|
def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
|
||||||
"""Signs and timestamps a cookie so it cannot be forged.
|
"""Signs and timestamps a cookie so it cannot be forged.
|
||||||
@@ -751,10 +761,10 @@ class RequestHandler(object):
|
|||||||
|
|
||||||
if hasattr(self.request, "connection"):
|
if hasattr(self.request, "connection"):
|
||||||
# Now that the request is finished, clear the callback we
|
# Now that the request is finished, clear the callback we
|
||||||
# set on the IOStream (which would otherwise prevent the
|
# set on the HTTPConnection (which would otherwise prevent the
|
||||||
# garbage collection of the RequestHandler when there
|
# garbage collection of the RequestHandler when there
|
||||||
# are keepalive connections)
|
# are keepalive connections)
|
||||||
self.request.connection.stream.set_close_callback(None)
|
self.request.connection.set_close_callback(None)
|
||||||
|
|
||||||
if not self.application._wsgi:
|
if not self.application._wsgi:
|
||||||
self.flush(include_footers=True)
|
self.flush(include_footers=True)
|
||||||
@@ -1142,7 +1152,7 @@ class RequestHandler(object):
|
|||||||
elif isinstance(result, Future):
|
elif isinstance(result, Future):
|
||||||
if result.done():
|
if result.done():
|
||||||
if result.result() is not None:
|
if result.result() is not None:
|
||||||
raise ValueError('Expected None, got %r' % result)
|
raise ValueError('Expected None, got %r' % result.result())
|
||||||
callback()
|
callback()
|
||||||
else:
|
else:
|
||||||
# Delayed import of IOLoop because it's not available
|
# Delayed import of IOLoop because it's not available
|
||||||
@@ -1827,6 +1837,10 @@ class StaticFileHandler(RequestHandler):
|
|||||||
return
|
return
|
||||||
if start is not None and start < 0:
|
if start is not None and start < 0:
|
||||||
start += size
|
start += size
|
||||||
|
if end is not None and end > size:
|
||||||
|
# Clients sometimes blindly use a large range to limit their
|
||||||
|
# download size; cap the endpoint at the actual file size.
|
||||||
|
end = size
|
||||||
# Note: only return HTTP 206 if less than the entire range has been
|
# Note: only return HTTP 206 if less than the entire range has been
|
||||||
# requested. Not only is this semantically correct, but Chrome
|
# requested. Not only is this semantically correct, but Chrome
|
||||||
# refuses to play audio if it gets an HTTP 206 in response to
|
# refuses to play audio if it gets an HTTP 206 in response to
|
||||||
@@ -2305,9 +2319,12 @@ class UIModule(object):
|
|||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.request = handler.request
|
self.request = handler.request
|
||||||
self.ui = handler.ui
|
self.ui = handler.ui
|
||||||
self.current_user = handler.current_user
|
|
||||||
self.locale = handler.locale
|
self.locale = handler.locale
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_user(self):
|
||||||
|
return self.handler.current_user
|
||||||
|
|
||||||
def render(self, *args, **kwargs):
|
def render(self, *args, **kwargs):
|
||||||
"""Overridden in subclasses to return this module's output."""
|
"""Overridden in subclasses to return this module's output."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import time
|
|||||||
import tornado.escape
|
import tornado.escape
|
||||||
import tornado.web
|
import tornado.web
|
||||||
|
|
||||||
from tornado.concurrent import Future
|
from tornado.concurrent import TracebackFuture
|
||||||
from tornado.escape import utf8, native_str
|
from tornado.escape import utf8, native_str
|
||||||
from tornado import httpclient
|
from tornado import httpclient
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
@@ -51,6 +51,10 @@ class WebSocketError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClosedError(WebSocketError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WebSocketHandler(tornado.web.RequestHandler):
|
class WebSocketHandler(tornado.web.RequestHandler):
|
||||||
"""Subclass this class to create a basic WebSocket handler.
|
"""Subclass this class to create a basic WebSocket handler.
|
||||||
|
|
||||||
@@ -160,6 +164,8 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||||||
message will be sent as utf8; in binary mode any byte string
|
message will be sent as utf8; in binary mode any byte string
|
||||||
is allowed.
|
is allowed.
|
||||||
"""
|
"""
|
||||||
|
if self.ws_connection is None:
|
||||||
|
raise WebSocketClosedError()
|
||||||
if isinstance(message, dict):
|
if isinstance(message, dict):
|
||||||
message = tornado.escape.json_encode(message)
|
message = tornado.escape.json_encode(message)
|
||||||
self.ws_connection.write_message(message, binary=binary)
|
self.ws_connection.write_message(message, binary=binary)
|
||||||
@@ -195,6 +201,8 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
def ping(self, data):
|
def ping(self, data):
|
||||||
"""Send ping frame to the remote end."""
|
"""Send ping frame to the remote end."""
|
||||||
|
if self.ws_connection is None:
|
||||||
|
raise WebSocketClosedError()
|
||||||
self.ws_connection.write_ping(data)
|
self.ws_connection.write_ping(data)
|
||||||
|
|
||||||
def on_pong(self, data):
|
def on_pong(self, data):
|
||||||
@@ -210,8 +218,9 @@ class WebSocketHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
Once the close handshake is successful the socket will be closed.
|
Once the close handshake is successful the socket will be closed.
|
||||||
"""
|
"""
|
||||||
self.ws_connection.close()
|
if self.ws_connection:
|
||||||
self.ws_connection = None
|
self.ws_connection.close()
|
||||||
|
self.ws_connection = None
|
||||||
|
|
||||||
def allow_draft76(self):
|
def allow_draft76(self):
|
||||||
"""Override to enable support for the older "draft76" protocol.
|
"""Override to enable support for the older "draft76" protocol.
|
||||||
@@ -764,7 +773,7 @@ class WebSocketProtocol13(WebSocketProtocol):
|
|||||||
class WebSocketClientConnection(simple_httpclient._HTTPConnection):
|
class WebSocketClientConnection(simple_httpclient._HTTPConnection):
|
||||||
"""WebSocket client connection."""
|
"""WebSocket client connection."""
|
||||||
def __init__(self, io_loop, request):
|
def __init__(self, io_loop, request):
|
||||||
self.connect_future = Future()
|
self.connect_future = TracebackFuture()
|
||||||
self.read_future = None
|
self.read_future = None
|
||||||
self.read_queue = collections.deque()
|
self.read_queue = collections.deque()
|
||||||
self.key = base64.b64encode(os.urandom(16))
|
self.key = base64.b64encode(os.urandom(16))
|
||||||
@@ -825,7 +834,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
|
|||||||
ready.
|
ready.
|
||||||
"""
|
"""
|
||||||
assert self.read_future is None
|
assert self.read_future is None
|
||||||
future = Future()
|
future = TracebackFuture()
|
||||||
if self.read_queue:
|
if self.read_queue:
|
||||||
future.set_result(self.read_queue.popleft())
|
future.set_result(self.read_queue.popleft())
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -242,10 +242,12 @@ class WSGIContainer(object):
|
|||||||
return response.append
|
return response.append
|
||||||
app_response = self.wsgi_application(
|
app_response = self.wsgi_application(
|
||||||
WSGIContainer.environ(request), start_response)
|
WSGIContainer.environ(request), start_response)
|
||||||
response.extend(app_response)
|
try:
|
||||||
body = b"".join(response)
|
response.extend(app_response)
|
||||||
if hasattr(app_response, "close"):
|
body = b"".join(response)
|
||||||
app_response.close()
|
finally:
|
||||||
|
if hasattr(app_response, "close"):
|
||||||
|
app_response.close()
|
||||||
if not data:
|
if not data:
|
||||||
raise Exception("WSGI app did not call start_response")
|
raise Exception("WSGI app did not call start_response")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user