Browse Source

Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_tvdb

pull/2139/head
Jason Mehring 12 years ago
parent
commit
d62b346a74
  1. 34
      couchpotato/api.py
  2. 51
      couchpotato/core/downloaders/deluge/main.py
  3. 2
      couchpotato/core/downloaders/nzbget/main.py
  4. 2
      couchpotato/core/downloaders/nzbvortex/main.py
  5. 6
      couchpotato/core/downloaders/rtorrent/main.py
  6. 2
      couchpotato/core/downloaders/sabnzbd/main.py
  7. 4
      couchpotato/core/downloaders/transmission/main.py
  8. 2
      couchpotato/core/downloaders/utorrent/main.py
  9. 8
      couchpotato/core/loader.py
  10. 4
      couchpotato/core/media/movie/_base/main.py
  11. 1
      couchpotato/core/media/movie/_base/static/movie.actions.js
  12. 6
      couchpotato/core/media/movie/_base/static/movie.css
  13. 12
      couchpotato/core/media/movie/library/movie/main.py
  14. 8
      couchpotato/core/media/movie/searcher/__init__.py
  15. 8
      couchpotato/core/media/movie/searcher/main.py
  16. 11
      couchpotato/core/notifications/core/main.py
  17. 5
      couchpotato/core/plugins/category/static/category.js
  18. 2
      couchpotato/core/plugins/log/main.py
  19. 4
      couchpotato/core/plugins/renamer/main.py
  20. 9
      couchpotato/core/plugins/scanner/main.py
  21. 2
      couchpotato/core/plugins/suggestion/static/suggest.js
  22. 2
      couchpotato/core/plugins/userscript/main.py
  23. 13
      couchpotato/core/providers/torrent/publichd/main.py
  24. 2
      couchpotato/core/providers/torrent/torrentshack/main.py
  25. 12
      couchpotato/runner.py
  26. 4
      libs/tornado/__init__.py
  27. 14
      libs/tornado/auth.py
  28. 7100
      libs/tornado/ca-certificates.crt
  29. 5
      libs/tornado/escape.py
  30. 17
      libs/tornado/httpclient.py
  31. 38
      libs/tornado/ioloop.py
  32. 56
      libs/tornado/iostream.py
  33. 4
      libs/tornado/netutil.py
  34. 13
      libs/tornado/process.py
  35. 4
      libs/tornado/template.py
  36. 33
      libs/tornado/web.py
  37. 19
      libs/tornado/websocket.py
  38. 10
      libs/tornado/wsgi.py

34
couchpotato/api.py

@ -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

51
couchpotato/core/downloaders/deluge/main.py

@ -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()

2
couchpotato/core/downloaders/nzbget/main.py

@ -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

2
couchpotato/core/downloaders/nzbvortex/main.py

@ -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

6
couchpotato/core/downloaders/rtorrent/main.py

@ -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

2
couchpotato/core/downloaders/sabnzbd/main.py

@ -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

4
couchpotato/core/downloaders/transmission/main.py

@ -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

2
couchpotato/core/downloaders/utorrent/main.py

@ -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

8
couchpotato/core/loader.py

@ -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)

4
couchpotato/core/media/movie/_base/main.py

@ -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'),

1
couchpotato/core/media/movie/_base/static/movie.actions.js

@ -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');
}, },

6
couchpotato/core/media/movie/_base/static/movie.css

@ -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;
} }

12
couchpotato/core/media/movie/library/movie/main.py

@ -18,28 +18,28 @@ 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 # movies don't yet contain these, so lets make sure to set defaults
type = attrs.get('type', 'movie') 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(type = type, 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, type = type,
primary_provider = primary_provider, 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, parent = None
) )
title = LibraryTitle( title = LibraryTitle(

8
couchpotato/core/media/movie/searcher/__init__.py

@ -33,6 +33,14 @@ config = [{
'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',
'label': 'Day', 'label': 'Day',

8
couchpotato/core/media/movie/searcher/main.py

@ -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:

11
couchpotato/core/notifications/core/main.py

@ -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

5
couchpotato/core/plugins/category/static/category.js

@ -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>?', '', [{

2
couchpotato/core/plugins/log/main.py

@ -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:

4
couchpotato/core/plugins/renamer/main.py

@ -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)

9
couchpotato/core/plugins/scanner/main.py

@ -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]

2
couchpotato/core/plugins/suggestion/static/suggest.js

@ -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){

2
couchpotato/core/plugins/userscript/main.py

@ -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)

13
couchpotato/core/providers/torrent/publichd/main.py

@ -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

2
couchpotato/core/providers/torrent/torrentshack/main.py

@ -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):

12
couchpotato/runner.py

@ -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

4
libs/tornado/__init__.py

@ -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)

14
libs/tornado/auth.py

@ -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"

7100
libs/tornado/ca-certificates.crt

File diff suppressed because it is too large

5
libs/tornado/escape.py

@ -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 = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'} _XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;',
'\'': '&#39;'}
def xhtml_escape(value): def xhtml_escape(value):

17
libs/tornado/httpclient.py

@ -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)

38
libs/tornado/ioloop.py

@ -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):

56
libs/tornado/iostream.py

@ -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

4
libs/tornado/netutil.py

@ -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,

13
libs/tornado/process.py

@ -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']:

4
libs/tornado/template.py

@ -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.

33
libs/tornado/web.py

@ -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()

19
libs/tornado/websocket.py

@ -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:

10
libs/tornado/wsgi.py

@ -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")

Loading…
Cancel
Save