Compare commits
170 Commits
build/2.6.
...
build/2.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b20a590aab | ||
|
|
f08f2b0339 | ||
|
|
427c77a9ef | ||
|
|
a4dff7a331 | ||
|
|
40cd1b4e08 | ||
|
|
94c3969f10 | ||
|
|
debd1855dd | ||
|
|
9f77597c11 | ||
|
|
afc9039625 | ||
|
|
920d3cb44e | ||
|
|
b1fc8ad862 | ||
|
|
11b9bc39ab | ||
|
|
6dcb3f3bf2 | ||
|
|
ce768f45c5 | ||
|
|
9b91d1d6c0 | ||
|
|
d9c7a97604 | ||
|
|
0fd01aa697 | ||
|
|
58615e6f9b | ||
|
|
2277322e57 | ||
|
|
18020e609e | ||
|
|
6a31b920ac | ||
|
|
c1266a36e4 | ||
|
|
578effc538 | ||
|
|
d881120013 | ||
|
|
da5318033a | ||
|
|
31df5bce01 | ||
|
|
d5622b7cba | ||
|
|
26ad1b354f | ||
|
|
7a616a81f7 | ||
|
|
275aefc3cc | ||
|
|
2b32490f72 | ||
|
|
7b9043c16b | ||
|
|
cf83f99be0 | ||
|
|
fb8a66d207 | ||
|
|
e8a3645bc6 | ||
|
|
592e40993c | ||
|
|
b00e69e222 | ||
|
|
c9b4c8167f | ||
|
|
cdb9cfe756 | ||
|
|
e52f50b204 | ||
|
|
5b1dd68675 | ||
|
|
770c2be14c | ||
|
|
4603b0c3b9 | ||
|
|
ab61961a64 | ||
|
|
6aca799bbb | ||
|
|
89836be1d1 | ||
|
|
20e1283627 | ||
|
|
ee8406e026 | ||
|
|
514941b785 | ||
|
|
1510e37652 | ||
|
|
e1e39cd3f4 | ||
|
|
e1bb8c5419 | ||
|
|
568e42e2f3 | ||
|
|
17fa33a496 | ||
|
|
601f0b54cf | ||
|
|
51d44bfc3e | ||
|
|
12148217a2 | ||
|
|
4929e7bbcc | ||
|
|
38ee0ebe7b | ||
|
|
132fa12ef4 | ||
|
|
1827c2e4cd | ||
|
|
f423bca06b | ||
|
|
e7b089edf5 | ||
|
|
b8b7d94a6a | ||
|
|
9964d9591b | ||
|
|
2c080fec3d | ||
|
|
4c68566c77 | ||
|
|
a3af784c18 | ||
|
|
ac6f295c93 | ||
|
|
2c72cd7d9f | ||
|
|
d012dc5c85 | ||
|
|
038b4c63ee | ||
|
|
2f5526d57e | ||
|
|
17e37996c4 | ||
|
|
9318e19347 | ||
|
|
8f4e03d04b | ||
|
|
0288cc8848 | ||
|
|
229d67c086 | ||
|
|
d84897ff33 | ||
|
|
387a711538 | ||
|
|
7a1b914824 | ||
|
|
5e62801666 | ||
|
|
00d887153f | ||
|
|
739d668261 | ||
|
|
6d5882001a | ||
|
|
4a6b45c65c | ||
|
|
b0d1fe5c33 | ||
|
|
a6e49098c8 | ||
|
|
f450f2d1e3 | ||
|
|
ffcd36cbf4 | ||
|
|
a2be29c3b2 | ||
|
|
3bf2d844a0 | ||
|
|
8770ab6696 | ||
|
|
dd24eb8893 | ||
|
|
aea673ddcd | ||
|
|
538f51dd5b | ||
|
|
f9fa87ce1d | ||
|
|
eea9f40501 | ||
|
|
576bcb9f4b | ||
|
|
62c5365329 | ||
|
|
ddf575a86e | ||
|
|
6b9383ce92 | ||
|
|
cb8d24ef1f | ||
|
|
814ddfb79f | ||
|
|
766f819c0b | ||
|
|
ff43df9ef1 | ||
|
|
2e907e93e7 | ||
|
|
4d329d6a36 | ||
|
|
752191bc23 | ||
|
|
1d73fd9d7e | ||
|
|
79688c412a | ||
|
|
fc1c95fefb | ||
|
|
6a174716af | ||
|
|
defe256f1b | ||
|
|
8a5f154d9e | ||
|
|
fe56a69e8f | ||
|
|
c6d326f973 | ||
|
|
9e5f670feb | ||
|
|
9ebacf8816 | ||
|
|
df2d7ec9c2 | ||
|
|
ddab74582b | ||
|
|
2801079bc8 | ||
|
|
1deb49b524 | ||
|
|
ac65775743 | ||
|
|
49d550f652 | ||
|
|
1a43ce6ecc | ||
|
|
15a0131587 | ||
|
|
0dca34958c | ||
|
|
4b231e36ea | ||
|
|
52478a00db | ||
|
|
e177766270 | ||
|
|
ff8da7c8f8 | ||
|
|
89c8c5a0c7 | ||
|
|
38c6266f9c | ||
|
|
16f8e7e123 | ||
|
|
7110c7a11f | ||
|
|
6d79f316a6 | ||
|
|
c1b6811b8a | ||
|
|
7d7b76b2e9 | ||
|
|
657aa52fa7 | ||
|
|
8e9ef8db39 | ||
|
|
92a0096b54 | ||
|
|
87338760ad | ||
|
|
28019b0a09 | ||
|
|
248b007f4a | ||
|
|
9e31c59de8 | ||
|
|
269e785888 | ||
|
|
3669aef42d | ||
|
|
1087eb3a06 | ||
|
|
43af80a137 | ||
|
|
0766a27a71 | ||
|
|
61f634a21e | ||
|
|
d626fda710 | ||
|
|
b40d1f3463 | ||
|
|
1030d0d748 | ||
|
|
2e52c8124a | ||
|
|
8de5fcdac6 | ||
|
|
4aa9801be4 | ||
|
|
3e58378490 | ||
|
|
2c40db3074 | ||
|
|
fba228fd9d | ||
|
|
ef2b8e88b4 | ||
|
|
c77b270fa8 | ||
|
|
872a4f4650 | ||
|
|
d0f1e7c6a3 | ||
|
|
53e7e383a3 | ||
|
|
c06e1f3135 | ||
|
|
bb73cb8eec | ||
|
|
5acab98025 | ||
|
|
ed6a46e9c0 |
@@ -40,6 +40,8 @@ class WebHandler(BaseHandler):
|
||||
return
|
||||
|
||||
try:
|
||||
if route == 'robots.txt':
|
||||
self.set_header('Content-Type', 'text/plain')
|
||||
self.write(views[route]())
|
||||
except:
|
||||
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
|
||||
@@ -60,6 +62,13 @@ def index():
|
||||
addView('', index)
|
||||
|
||||
|
||||
# Web view
|
||||
def robots():
|
||||
return 'User-agent: * \n' \
|
||||
'Disallow: /'
|
||||
addView('robots.txt', robots)
|
||||
|
||||
|
||||
# API docs
|
||||
def apiDocs():
|
||||
routes = list(api.keys())
|
||||
|
||||
@@ -7,6 +7,7 @@ import urllib
|
||||
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
|
||||
|
||||
@@ -50,24 +51,22 @@ class NonBlockHandler(RequestHandler):
|
||||
start, stop = api_nonblock[route]
|
||||
self.stopper = stop
|
||||
|
||||
start(self.onNewMessage, last_id = self.get_argument('last_id', None))
|
||||
start(self.sendData, last_id = self.get_argument('last_id', None))
|
||||
|
||||
def onNewMessage(self, response):
|
||||
if self.request.connection.stream.closed():
|
||||
self.on_connection_close()
|
||||
return
|
||||
def sendData(self, response):
|
||||
if not self.request.connection.stream.closed():
|
||||
try:
|
||||
self.finish(response)
|
||||
except:
|
||||
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
|
||||
try:
|
||||
self.finish(response)
|
||||
except:
|
||||
log.debug('Failed doing nonblock request, probably already closed: %s', (traceback.format_exc()))
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
|
||||
def on_connection_close(self):
|
||||
self.removeStopper()
|
||||
|
||||
def removeStopper(self):
|
||||
if self.stopper:
|
||||
self.stopper(self.onNewMessage)
|
||||
self.stopper(self.sendData)
|
||||
|
||||
self.stopper = None
|
||||
|
||||
@@ -83,10 +82,11 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
route = None
|
||||
|
||||
@asynchronous
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
self.route = route = route.strip('/')
|
||||
if not api.get(route):
|
||||
self.write('API call doesn\'t seem to exist')
|
||||
self.finish()
|
||||
@@ -123,11 +123,15 @@ class ApiHandler(RequestHandler):
|
||||
except:
|
||||
log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
|
||||
|
||||
api_locks[route].release()
|
||||
self.unlock()
|
||||
|
||||
post = get
|
||||
|
||||
def taskFinished(self, result, route):
|
||||
IOLoop.current().add_callback(self.sendData, result, route)
|
||||
self.unlock()
|
||||
|
||||
def sendData(self, result, route):
|
||||
|
||||
if not self.request.connection.stream.closed():
|
||||
try:
|
||||
@@ -135,14 +139,12 @@ class ApiHandler(RequestHandler):
|
||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||
|
||||
if jsonp_callback:
|
||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
self.set_header("Content-Type", "text/javascript")
|
||||
self.finish()
|
||||
self.set_header('Content-Type', 'text/javascript')
|
||||
self.finish(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||
self.redirect(result[1])
|
||||
else:
|
||||
self.write(result)
|
||||
self.finish()
|
||||
self.finish(result)
|
||||
except UnicodeDecodeError:
|
||||
log.error('Failed proper encode: %s', traceback.format_exc())
|
||||
except:
|
||||
@@ -150,7 +152,9 @@ class ApiHandler(RequestHandler):
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
|
||||
api_locks[route].release()
|
||||
def unlock(self):
|
||||
try: api_locks[self.route].release()
|
||||
except: pass
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
@@ -20,14 +20,31 @@ class Blackhole(DownloaderBase):
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
|
||||
# The folder needs to exist
|
||||
if not directory or not os.path.isdir(directory):
|
||||
log.error('No directory set for blackhole %s download.', data.get('protocol'))
|
||||
else:
|
||||
try:
|
||||
# Filedata can be empty, which probably means it a magnet link
|
||||
if not filedata or len(filedata) < 50:
|
||||
try:
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
@@ -36,13 +53,16 @@ class Blackhole(DownloaderBase):
|
||||
except:
|
||||
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
||||
|
||||
# If it's still empty, don't know what to do!
|
||||
if not filedata or len(filedata) < 50:
|
||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||
return False
|
||||
|
||||
# Create filename with imdb id and other nice stuff
|
||||
file_name = self.createFileName(data, filedata, media)
|
||||
full_path = os.path.join(directory, file_name)
|
||||
|
||||
# People want thinks nice and tidy, create a subdir
|
||||
if self.conf('create_subdir'):
|
||||
try:
|
||||
new_path = os.path.splitext(full_path)[0]
|
||||
@@ -53,6 +73,8 @@ class Blackhole(DownloaderBase):
|
||||
log.error('Couldnt create sub dir, reverting to old one: %s', full_path)
|
||||
|
||||
try:
|
||||
|
||||
# Make sure the file doesn't exist yet, no need in overwriting it
|
||||
if not os.path.isfile(full_path):
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
|
||||
with open(full_path, 'wb') as f:
|
||||
@@ -74,6 +96,10 @@ class Blackhole(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Test and see if the directory is writable
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
@@ -88,6 +114,10 @@ class Blackhole(DownloaderBase):
|
||||
return False
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
""" What protocols is this downloaded used for
|
||||
:return: list with protocols
|
||||
"""
|
||||
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Blackhole, self).getEnabledProtocol()
|
||||
elif self.conf('use_for') == 'torrent':
|
||||
@@ -96,6 +126,12 @@ class Blackhole(DownloaderBase):
|
||||
return ['nzb']
|
||||
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
""" Check if protocol is used (and enabled)
|
||||
:param manual: The user has clicked to download a link through the webUI
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:return: boolean
|
||||
"""
|
||||
if not data: data = {}
|
||||
for_protocol = ['both']
|
||||
if data and 'torrent' in data.get('protocol'):
|
||||
|
||||
@@ -25,6 +25,11 @@ class Deluge(DownloaderBase):
|
||||
drpc = None
|
||||
|
||||
def connect(self, reconnect = False):
|
||||
""" Connect to the delugeRPC, re-use connection when already available
|
||||
:param reconnect: force reconnect
|
||||
:return: DelugeRPC instance
|
||||
"""
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
|
||||
@@ -42,6 +47,20 @@ class Deluge(DownloaderBase):
|
||||
return self.drpc
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -96,11 +115,21 @@ class Deluge(DownloaderBase):
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
if self.connect(True) and self.drpc.test():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking Deluge download status.')
|
||||
|
||||
|
||||
427
couchpotato/core/downloaders/hadouken.py
Normal file
427
couchpotato/core/downloaders/hadouken.py
Normal file
@@ -0,0 +1,427 @@
|
||||
from base64 import b16encode, b32decode, b64encode
|
||||
from distutils.version import LooseVersion
|
||||
from hashlib import sha1
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from bencode import bencode as benc, bdecode
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Hadouken'
|
||||
|
||||
|
||||
class Hadouken(DownloaderBase):
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
hadouken_api = None
|
||||
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if not self.conf('apikey'):
|
||||
log.error('Config properties are not filled in correctly, API key is missing.')
|
||||
return False
|
||||
|
||||
self.hadouken_api = HadoukenAPI(host[0], port = host[1], api_key = self.conf('api_key'))
|
||||
|
||||
return True
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug("Sending '%s' (%s) to Hadouken.", (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent_params = {}
|
||||
|
||||
if self.conf('label'):
|
||||
torrent_params['label'] = self.conf('label')
|
||||
|
||||
torrent_filename = self.createFileName(data, filedata, media)
|
||||
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||||
torrent_params['trackers'] = self.torrent_trackers
|
||||
torrent_params['name'] = torrent_filename
|
||||
else:
|
||||
info = bdecode(filedata)['info']
|
||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to Hadouken
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
self.hadouken_api.add_magnet_link(data.get('url'), torrent_params)
|
||||
else:
|
||||
self.hadouken_api.add_file(filedata, torrent_params)
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def test(self):
|
||||
""" Tests the given host:port and API key """
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
version = self.hadouken_api.get_version()
|
||||
|
||||
if not version:
|
||||
log.error('Could not get Hadouken version.')
|
||||
return False
|
||||
|
||||
# The minimum required version of Hadouken is 4.5.6.
|
||||
if LooseVersion(version) >= LooseVersion('4.5.6'):
|
||||
return True
|
||||
|
||||
log.error('Hadouken v4.5.6 (or newer) required. Found v%s', version)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking Hadouken download status.')
|
||||
|
||||
if not self.connect():
|
||||
return []
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
queue = self.hadouken_api.get_by_hash_list(ids)
|
||||
|
||||
if not queue:
|
||||
return []
|
||||
|
||||
for torrent in queue:
|
||||
if torrent is None:
|
||||
continue
|
||||
|
||||
torrent_filelist = self.hadouken_api.get_files_by_hash(torrent['InfoHash'])
|
||||
torrent_files = []
|
||||
|
||||
save_path = torrent['SavePath']
|
||||
|
||||
# The 'Path' key for each file_item contains
|
||||
# the full path to the single file relative to the
|
||||
# torrents save path.
|
||||
|
||||
# For a single file torrent the result would be,
|
||||
# - Save path: "C:\Downloads"
|
||||
# - file_item['Path'] = "file1.iso"
|
||||
# Resulting path: "C:\Downloads\file1.iso"
|
||||
|
||||
# For a multi file torrent the result would be,
|
||||
# - Save path: "C:\Downloads"
|
||||
# - file_item['Path'] = "dirname/file1.iso"
|
||||
# Resulting path: "C:\Downloads\dirname/file1.iso"
|
||||
|
||||
for file_item in torrent_filelist:
|
||||
torrent_files.append(sp(os.path.join(save_path, file_item['Path'])))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['InfoHash'].upper(),
|
||||
'name': torrent['Name'],
|
||||
'status': self.get_torrent_status(torrent),
|
||||
'seed_ratio': self.get_seed_ratio(torrent),
|
||||
'original_status': torrent['State'],
|
||||
'timeleft': -1,
|
||||
'folder': sp(save_path if len(torrent_files == 1) else os.path.join(save_path, torrent['Name'])),
|
||||
'files': torrent_files
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
def get_seed_ratio(self, torrent):
|
||||
""" Returns the seed ratio for a given torrent.
|
||||
|
||||
Keyword arguments:
|
||||
torrent -- The torrent to calculate seed ratio for.
|
||||
"""
|
||||
|
||||
up = torrent['TotalUploadedBytes']
|
||||
down = torrent['TotalDownloadedBytes']
|
||||
|
||||
if up > 0 and down > 0:
|
||||
return up / down
|
||||
|
||||
return 0
|
||||
|
||||
def get_torrent_status(self, torrent):
|
||||
""" Returns the CouchPotato status for a given torrent.
|
||||
|
||||
Keyword arguments:
|
||||
torrent -- The torrent to translate status for.
|
||||
"""
|
||||
|
||||
if torrent['IsSeeding'] and torrent['IsFinished'] and torrent['Paused']:
|
||||
return 'completed'
|
||||
|
||||
if torrent['IsSeeding']:
|
||||
return 'seeding'
|
||||
|
||||
return 'busy'
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
""" Pauses or resumes the torrent specified by the ID field
|
||||
in release_download.
|
||||
|
||||
Keyword arguments:
|
||||
release_download -- The CouchPotato release_download to pause/resume.
|
||||
pause -- Boolean indicating whether to pause or resume.
|
||||
"""
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
return self.hadouken_api.pause(release_download['id'], pause)
|
||||
|
||||
def removeFailed(self, release_download):
|
||||
""" Removes a failed torrent and also remove the data associated with it.
|
||||
|
||||
Keyword arguments:
|
||||
release_download -- The CouchPotato release_download to remove.
|
||||
"""
|
||||
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
return self.hadouken_api.remove(release_download['id'], remove_data = True)
|
||||
|
||||
def processComplete(self, release_download, delete_files = False):
|
||||
""" Removes the completed torrent from Hadouken and optionally removes the data
|
||||
associated with it.
|
||||
|
||||
Keyword arguments:
|
||||
release_download -- The CouchPotato release_download to remove.
|
||||
delete_files: Boolean indicating whether to remove the associated data.
|
||||
"""
|
||||
|
||||
log.debug('Requesting Hadouken to remove the torrent %s%s.',
|
||||
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
return self.hadouken_api.remove(release_download['id'], remove_data = delete_files)
|
||||
|
||||
|
||||
class HadoukenAPI(object):
|
||||
def __init__(self, host = 'localhost', port = 7890, api_key = None):
|
||||
self.url = 'http://' + str(host) + ':' + str(port)
|
||||
self.api_key = api_key
|
||||
self.requestId = 0;
|
||||
|
||||
self.opener = urllib2.build_opener()
|
||||
self.opener.addheaders = [('User-agent', 'couchpotato-hadouken-client/1.0'), ('Accept', 'application/json')]
|
||||
|
||||
if not api_key:
|
||||
log.error('API key missing.')
|
||||
|
||||
def add_file(self, filedata, torrent_params):
|
||||
""" Add a file to Hadouken with the specified parameters.
|
||||
|
||||
Keyword arguments:
|
||||
filedata -- The binary torrent data.
|
||||
torrent_params -- Additional parameters for the file.
|
||||
"""
|
||||
data = {
|
||||
'method': 'torrents.addFile',
|
||||
'params': [b64encode(filedata), torrent_params]
|
||||
}
|
||||
|
||||
return self._request(data)
|
||||
|
||||
def add_magnet_link(self, magnetLink, torrent_params):
|
||||
""" Add a magnet link to Hadouken with the specified parameters.
|
||||
|
||||
Keyword arguments:
|
||||
magnetLink -- The magnet link to send.
|
||||
torrent_params -- Additional parameters for the magnet link.
|
||||
"""
|
||||
data = {
|
||||
'method': 'torrents.addUrl',
|
||||
'params': [magnetLink, torrent_params]
|
||||
}
|
||||
|
||||
return self._request(data)
|
||||
|
||||
def get_by_hash_list(self, infoHashList):
|
||||
""" Gets a list of torrents filtered by the given info hash list.
|
||||
|
||||
Keyword arguments:
|
||||
infoHashList -- A list of info hashes.
|
||||
"""
|
||||
data = {
|
||||
'method': 'torrents.getByInfoHashList',
|
||||
'params': [infoHashList]
|
||||
}
|
||||
|
||||
return self._request(data)
|
||||
|
||||
def get_files_by_hash(self, infoHash):
|
||||
""" Gets a list of files for the torrent identified by the
|
||||
given info hash.
|
||||
|
||||
Keyword arguments:
|
||||
infoHash -- The info hash of the torrent to return files for.
|
||||
"""
|
||||
data = {
|
||||
'method': 'torrents.getFiles',
|
||||
'params': [infoHash]
|
||||
}
|
||||
|
||||
return self._request(data)
|
||||
|
||||
def get_version(self):
|
||||
""" Gets the version, commitish and build date of Hadouken. """
|
||||
data = {
|
||||
'method': 'core.getVersion',
|
||||
'params': None
|
||||
}
|
||||
|
||||
result = self._request(data)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
return result['Version']
|
||||
|
||||
def pause(self, infoHash, pause):
|
||||
""" Pauses/unpauses the torrent identified by the given info hash.
|
||||
|
||||
Keyword arguments:
|
||||
infoHash -- The info hash of the torrent to operate on.
|
||||
pause -- If true, pauses the torrent. Otherwise resumes.
|
||||
"""
|
||||
data = {
|
||||
'method': 'torrents.pause',
|
||||
'params': [infoHash]
|
||||
}
|
||||
|
||||
if not pause:
|
||||
data['method'] = 'torrents.resume'
|
||||
|
||||
return self._request(data)
|
||||
|
||||
def remove(self, infoHash, remove_data = False):
|
||||
""" Removes the torrent identified by the given info hash and
|
||||
optionally removes the data as well.
|
||||
|
||||
Keyword arguments:
|
||||
infoHash -- The info hash of the torrent to remove.
|
||||
remove_data -- If true, removes the data associated with the torrent.
|
||||
"""
|
||||
data = {
|
||||
'method': 'torrents.remove',
|
||||
'params': [infoHash, remove_data]
|
||||
}
|
||||
|
||||
return self._request(data)
|
||||
|
||||
|
||||
def _request(self, data):
|
||||
self.requestId += 1
|
||||
|
||||
data['jsonrpc'] = '2.0'
|
||||
data['id'] = self.requestId
|
||||
|
||||
request = urllib2.Request(self.url + '/jsonrpc', data = json.dumps(data))
|
||||
request.add_header('Authorization', 'Token ' + self.api_key)
|
||||
request.add_header('Content-Type', 'application/json')
|
||||
|
||||
try:
|
||||
f = self.opener.open(request)
|
||||
response = f.read()
|
||||
f.close()
|
||||
|
||||
obj = json.loads(response)
|
||||
|
||||
if not 'error' in obj.keys():
|
||||
return obj['result']
|
||||
|
||||
log.error('JSONRPC error, %s: %s', obj['error']['code'], obj['error']['message'])
|
||||
except httplib.InvalidURL as err:
|
||||
log.error('Invalid Hadouken host, check your config %s', err)
|
||||
except urllib2.HTTPError as err:
|
||||
if err.code == 401:
|
||||
log.error('Invalid Hadouken API key, check your config')
|
||||
else:
|
||||
log.error('Hadouken HTTPError: %s', err)
|
||||
except urllib2.URLError as err:
|
||||
log.error('Unable to connect to Hadouken %s', err)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'hadouken',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'hadouken',
|
||||
'label': 'Hadouken',
|
||||
'description': 'Use <a href="http://www.hdkn.net">Hadouken</a> (>= v4.5.6) to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent'
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:7890'
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'API key',
|
||||
'type': 'password'
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to add torrent as.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -23,6 +23,20 @@ class NZBGet(DownloaderBase):
|
||||
rpc = 'xmlrpc'
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -71,6 +85,10 @@ class NZBGet(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
rpc = self.getRPC()
|
||||
|
||||
try:
|
||||
@@ -91,6 +109,13 @@ class NZBGet(DownloaderBase):
|
||||
return True
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking NZBGet download status.')
|
||||
|
||||
@@ -163,12 +188,12 @@ class NZBGet(DownloaderBase):
|
||||
nzb_id = nzb['NZBID']
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
log.debug('Found %s in NZBGet history. TotalStatus: %s, ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['Status'], nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'status': 'completed' if 'SUCCESS' in nzb['Status'] else 'failed',
|
||||
'original_status': nzb['Status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
|
||||
@@ -24,6 +24,20 @@ class NZBVortex(DownloaderBase):
|
||||
session_id = None
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -45,6 +59,10 @@ class NZBVortex(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
try:
|
||||
login_result = self.login()
|
||||
except:
|
||||
@@ -53,6 +71,13 @@ class NZBVortex(DownloaderBase):
|
||||
return login_result
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
|
||||
|
||||
@@ -19,6 +19,20 @@ class Pneumatic(DownloaderBase):
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -63,6 +77,10 @@ class Pneumatic(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
|
||||
68
couchpotato/core/downloaders/putio/__init__.py
Normal file
68
couchpotato/core/downloaders/putio/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from .main import PutIO
|
||||
|
||||
|
||||
def autoload():
|
||||
return PutIO()
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'putio',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'putio',
|
||||
'label': 'put.io',
|
||||
'description': 'This will start a torrent download on <a href="http://put.io">Put.io</a>.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'oauth_token',
|
||||
'label': 'oauth_token',
|
||||
'description': 'This is the OAUTH_TOKEN from your putio API',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'folder',
|
||||
'description': ('The folder on putio where you want the upload to go','Will find the first first folder that matches this name'),
|
||||
'default': 0,
|
||||
},
|
||||
{
|
||||
'name': 'callback_host',
|
||||
'description': 'External reachable url to CP so put.io can do it\'s thing',
|
||||
},
|
||||
{
|
||||
'name': 'download',
|
||||
'description': 'Set this to have CouchPotato download the file from Put.io',
|
||||
'type': 'bool',
|
||||
'default': 0,
|
||||
},
|
||||
{
|
||||
'name': 'delete_file',
|
||||
'description': ('Set this to remove the file from putio after sucessful download','Does nothing if you don\'t select download'),
|
||||
'type': 'bool',
|
||||
'default': 0,
|
||||
},
|
||||
{
|
||||
'name': 'download_dir',
|
||||
'type': 'directory',
|
||||
'label': 'Download Directory',
|
||||
'description': 'The Directory to download files to, does nothing if you don\'t select download',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
181
couchpotato/core/downloaders/putio/main.py
Normal file
181
couchpotato/core/downloaders/putio/main.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEventAsync
|
||||
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from pio import api as pio
|
||||
import datetime
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Putiodownload'
|
||||
|
||||
|
||||
class PutIO(DownloaderBase):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
downloading_list = []
|
||||
oauth_authenticate = 'https://api.couchpota.to/authorize/putio/'
|
||||
|
||||
def __init__(self):
|
||||
addApiView('downloader.putio.getfrom', self.getFromPutio, docs = {
|
||||
'desc': 'Allows you to download file from prom Put.io',
|
||||
})
|
||||
addApiView('downloader.putio.auth_url', self.getAuthorizationUrl)
|
||||
addApiView('downloader.putio.credentials', self.getCredentials)
|
||||
addEvent('putio.download', self.putioDownloader)
|
||||
|
||||
return super(PutIO, self).__init__()
|
||||
|
||||
# This is a recusive function to check for the folders
|
||||
def recursionFolder(self, client, folder = 0, tfolder = ''):
|
||||
files = client.File.list(folder)
|
||||
for f in files:
|
||||
if f.content_type == 'application/x-directory':
|
||||
if f.name == tfolder:
|
||||
return f.id
|
||||
else:
|
||||
result = self.recursionFolder(client, f.id, tfolder)
|
||||
if result != 0:
|
||||
return result
|
||||
return 0
|
||||
|
||||
# This will check the root for the folder, and kick of recusively checking sub folder
|
||||
def convertFolder(self, client, folder):
|
||||
if folder == 0:
|
||||
return 0
|
||||
else:
|
||||
return self.recursionFolder(client, 0, folder)
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" to put.io', data.get('name'))
|
||||
url = data.get('url')
|
||||
client = pio.Client(self.conf('oauth_token'))
|
||||
putioFolder = self.convertFolder(client, self.conf('folder'))
|
||||
log.debug('putioFolder ID is %s', putioFolder)
|
||||
# It might be possible to call getFromPutio from the renamer if we can then we don't need to do this.
|
||||
# Note callback_host is NOT our address, it's the internet host that putio can call too
|
||||
callbackurl = None
|
||||
if self.conf('download'):
|
||||
callbackurl = 'http://' + self.conf('callback_host') + '%sdownloader.putio.getfrom/' %Env.get('api_base'.strip('/'))
|
||||
resp = client.Transfer.add_url(url, callback_url = callbackurl, parent_id = putioFolder)
|
||||
log.debug('resp is %s', resp.id);
|
||||
return self.downloadReturnId(resp.id)
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
client = pio.Client(self.conf('oauth_token'))
|
||||
if client.File.list():
|
||||
return True
|
||||
except:
|
||||
log.info('Failed to get file listing, check OAUTH_TOKEN')
|
||||
return False
|
||||
|
||||
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||
|
||||
callback_url = cleanHost(host) + '%sdownloader.putio.credentials/' % (Env.get('api_base').lstrip('/'))
|
||||
log.debug('callback_url is %s', callback_url)
|
||||
|
||||
target_url = self.oauth_authenticate + "?target=" + callback_url
|
||||
log.debug('target_url is %s', target_url)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'url': target_url,
|
||||
}
|
||||
|
||||
def getCredentials(self, **kwargs):
|
||||
try:
|
||||
oauth_token = kwargs.get('oauth')
|
||||
except:
|
||||
return 'redirect', Env.get('web_base') + 'settings/downloaders/'
|
||||
log.debug('oauth_token is: %s', oauth_token)
|
||||
self.conf('oauth_token', value = oauth_token);
|
||||
return 'redirect', Env.get('web_base') + 'settings/downloaders/'
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking putio download status.')
|
||||
client = pio.Client(self.conf('oauth_token'))
|
||||
|
||||
transfers = client.Transfer.list()
|
||||
|
||||
log.debug(transfers);
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
for t in transfers:
|
||||
if t.id in ids:
|
||||
|
||||
log.debug('downloading list is %s', self.downloading_list)
|
||||
if t.status == "COMPLETED" and self.conf('download') == False :
|
||||
status = 'completed'
|
||||
|
||||
# So check if we are trying to download something
|
||||
elif t.status == "COMPLETED" and self.conf('download') == True:
|
||||
# Assume we are done
|
||||
status = 'completed'
|
||||
if not self.downloading_list:
|
||||
now = datetime.datetime.utcnow()
|
||||
date_time = datetime.datetime.strptime(t.finished_at,"%Y-%m-%dT%H:%M:%S")
|
||||
# We need to make sure a race condition didn't happen
|
||||
if (now - date_time) < datetime.timedelta(minutes=5):
|
||||
# 5 minutes haven't passed so we wait
|
||||
status = 'busy'
|
||||
else:
|
||||
# If we have the file_id in the downloading_list mark it as busy
|
||||
if str(t.file_id) in self.downloading_list:
|
||||
status = 'busy'
|
||||
else:
|
||||
status = 'busy'
|
||||
release_downloads.append({
|
||||
'id' : t.id,
|
||||
'name': t.name,
|
||||
'status': status,
|
||||
'timeleft': t.estimated_time,
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
def putioDownloader(self, fid):
|
||||
|
||||
log.info('Put.io Real downloader called with file_id: %s',fid)
|
||||
client = pio.Client(self.conf('oauth_token'))
|
||||
|
||||
log.debug('About to get file List')
|
||||
putioFolder = self.convertFolder(client, self.conf('folder'))
|
||||
log.debug('PutioFolderID is %s', putioFolder)
|
||||
files = client.File.list(parent_id=putioFolder)
|
||||
downloaddir = self.conf('download_dir')
|
||||
|
||||
for f in files:
|
||||
if str(f.id) == str(fid):
|
||||
client.File.download(f, dest = downloaddir, delete_after_download = self.conf('delete_file'))
|
||||
# Once the download is complete we need to remove it from the running list.
|
||||
self.downloading_list.remove(fid)
|
||||
|
||||
return True
|
||||
|
||||
def getFromPutio(self, **kwargs):
|
||||
|
||||
try:
|
||||
file_id = str(kwargs.get('file_id'))
|
||||
except:
|
||||
return {
|
||||
'success' : False,
|
||||
}
|
||||
|
||||
log.info('Put.io Download has been called file_id is %s', file_id)
|
||||
if file_id not in self.downloading_list:
|
||||
self.downloading_list.append(file_id)
|
||||
fireEventAsync('putio.download',fid = file_id)
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
}
|
||||
|
||||
68
couchpotato/core/downloaders/putio/static/putio.js
Normal file
68
couchpotato/core/downloaders/putio/static/putio.js
Normal file
@@ -0,0 +1,68 @@
|
||||
var PutIODownloader = new Class({
|
||||
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('loadSettings', self.addRegisterButton.bind(self));
|
||||
},
|
||||
|
||||
addRegisterButton: function(){
|
||||
var self = this;
|
||||
|
||||
var setting_page = App.getPage('Settings');
|
||||
setting_page.addEvent('create', function(){
|
||||
|
||||
var fieldset = setting_page.tabs.downloaders.groups.putio,
|
||||
l = window.location;
|
||||
|
||||
var putio_set = 0;
|
||||
fieldset.getElements('input[type=text]').each(function(el){
|
||||
putio_set += +(el.get('value') != '');
|
||||
});
|
||||
|
||||
new Element('.ctrlHolder').adopt(
|
||||
|
||||
// Unregister button
|
||||
(putio_set > 0) ?
|
||||
[
|
||||
self.unregister = new Element('a.button.red', {
|
||||
'text': 'Unregister "'+fieldset.getElement('input[name*=oauth_token]').get('value')+'"',
|
||||
'events': {
|
||||
'click': function(){
|
||||
fieldset.getElements('input[name*=oauth_token]').set('value', '').fireEvent('change');
|
||||
|
||||
self.unregister.destroy();
|
||||
self.unregister_or.destroy();
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.unregister_or = new Element('span[text=or]')
|
||||
]
|
||||
: null,
|
||||
|
||||
// Register button
|
||||
new Element('a.button', {
|
||||
'text': putio_set > 0 ? 'Register a different account' : 'Register your put.io account',
|
||||
'events': {
|
||||
'click': function(){
|
||||
Api.request('downloader.putio.auth_url', {
|
||||
'data': {
|
||||
'host': l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
window.location = json.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(fieldset.getElement('.test_button'), 'before');
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
window.addEvent('domready', function(){
|
||||
new PutIODownloader();
|
||||
});
|
||||
@@ -41,12 +41,30 @@ class qBittorrent(DownloaderBase):
|
||||
return self.qb
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -95,6 +113,14 @@ class qBittorrent(DownloaderBase):
|
||||
return 'busy'
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking qBittorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
|
||||
@@ -84,6 +84,10 @@ class rTorrent(DownloaderBase):
|
||||
return self.rt
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect(True):
|
||||
return True
|
||||
|
||||
@@ -94,6 +98,20 @@ class rTorrent(DownloaderBase):
|
||||
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
""" Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -161,6 +179,14 @@ class rTorrent(DownloaderBase):
|
||||
return 'completed'
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
|
||||
@@ -21,6 +21,21 @@ class Sabnzbd(DownloaderBase):
|
||||
protocol = ['nzb']
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
"""
|
||||
Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -69,6 +84,11 @@ class Sabnzbd(DownloaderBase):
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
Return message if an old version of SAB is used
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
try:
|
||||
sab_data = self.call({
|
||||
'mode': 'version',
|
||||
@@ -89,6 +109,13 @@ class Sabnzbd(DownloaderBase):
|
||||
return True
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
|
||||
|
||||
@@ -19,6 +19,21 @@ class Synology(DownloaderBase):
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
"""
|
||||
Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -50,6 +65,10 @@ class Synology(DownloaderBase):
|
||||
return self.downloadReturnId('') if response else False
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
try:
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
@@ -118,7 +137,7 @@ class SynologyRPC(object):
|
||||
def _req(self, url, args, files = None):
|
||||
response = {'success': False}
|
||||
try:
|
||||
req = requests.post(url, data = args, files = files)
|
||||
req = requests.post(url, data = args, files = files, verify = False)
|
||||
req.raise_for_status()
|
||||
response = json.loads(req.text)
|
||||
if response['success']:
|
||||
|
||||
@@ -34,6 +34,21 @@ class Transmission(DownloaderBase):
|
||||
return self.trpc
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
"""
|
||||
Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -88,11 +103,22 @@ class Transmission(DownloaderBase):
|
||||
return self.downloadReturnId(data['hashString'])
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect() and self.trpc.get_session():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
|
||||
@@ -121,6 +147,8 @@ class Transmission(DownloaderBase):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] == 16 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
|
||||
@@ -51,6 +51,21 @@ class uTorrent(DownloaderBase):
|
||||
return self.utorrent_api
|
||||
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
"""
|
||||
Send a torrent/nzb file to the downloader
|
||||
|
||||
:param data: dict returned from provider
|
||||
Contains the release information
|
||||
:param media: media dict with information
|
||||
Used for creating the filename when possible
|
||||
:param filedata: downloaded torrent/nzb filedata
|
||||
The file gets downloaded in the searcher and send to this function
|
||||
This is done to have failed checking before using the downloader, so the downloader
|
||||
doesn't need to worry about that
|
||||
:return: boolean
|
||||
One faile returns false, but the downloaded should log his own errors
|
||||
"""
|
||||
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
@@ -120,6 +135,10 @@ class uTorrent(DownloaderBase):
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def test(self):
|
||||
""" Check if connection works
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if self.connect():
|
||||
build_version = self.utorrent_api.get_build()
|
||||
if not build_version:
|
||||
@@ -131,6 +150,13 @@ class uTorrent(DownloaderBase):
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
""" Get status of all active downloads
|
||||
|
||||
:param ids: list of (mixed) downloader ids
|
||||
Used to match the releases for this downloader as there could be
|
||||
other downloaders active that it should ignore
|
||||
:return: list of releases
|
||||
"""
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
|
||||
@@ -37,15 +37,15 @@ def toUnicode(original, *args):
|
||||
except:
|
||||
try:
|
||||
detected = detect(original)
|
||||
if detected.get('encoding') == 'utf-8':
|
||||
return original.decode('utf-8')
|
||||
return ek(original, *args)
|
||||
try:
|
||||
return original.decode(detected.get('encoding'))
|
||||
except:
|
||||
return ek(original, *args)
|
||||
except:
|
||||
raise
|
||||
except:
|
||||
log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
|
||||
ascii_text = str(original).encode('string_escape')
|
||||
return toUnicode(ascii_text)
|
||||
return 'ERROR DECODING STRING'
|
||||
|
||||
|
||||
def ss(original, *args):
|
||||
@@ -92,7 +92,7 @@ def ek(original, *args):
|
||||
if isinstance(original, (str, unicode)):
|
||||
try:
|
||||
from couchpotato.environment import Env
|
||||
return original.decode(Env.get('encoding'))
|
||||
return original.decode(Env.get('encoding'), 'ignore')
|
||||
except UnicodeDecodeError:
|
||||
raise
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ class Provider(Plugin):
|
||||
try:
|
||||
data = XMLTree.fromstring(ss(data))
|
||||
return self.getElements(data, item_path)
|
||||
except XMLTree.ParseError:
|
||||
log.error('Invalid XML returned, check "%s" manually for issues', url)
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
|
||||
@@ -68,8 +68,12 @@ class Base(NZBProvider, RSS):
|
||||
if not date:
|
||||
date = self.getTextElement(nzb, 'pubDate')
|
||||
|
||||
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
|
||||
name = self.getTextElement(nzb, 'title')
|
||||
detail_url = self.getTextElement(nzb, 'guid')
|
||||
nzb_id = detail_url.split('/')[-1:].pop()
|
||||
|
||||
if '://' not in detail_url:
|
||||
detail_url = (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id)
|
||||
|
||||
if not name:
|
||||
continue
|
||||
@@ -103,7 +107,7 @@ class Base(NZBProvider, RSS):
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
||||
'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id),
|
||||
'detail_url': detail_url,
|
||||
'content': self.getTextElement(nzb, 'description'),
|
||||
'description': description,
|
||||
'score': host['extra_score'],
|
||||
@@ -183,7 +187,7 @@ class Base(NZBProvider, RSS):
|
||||
return 'try_next'
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
data = self.urlopen(url, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
self.limits_reached[host] = False
|
||||
return data
|
||||
except HTTPError as e:
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
from urlparse import urlparse, parse_qs
|
||||
import time
|
||||
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,27 +12,19 @@ log = CPLog(__name__)
|
||||
class Base(NZBProvider, RSS):
|
||||
|
||||
urls = {
|
||||
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
|
||||
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
|
||||
'search': 'https://api.omgwtfnzbs.org/json/?%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
cat_ids = [
|
||||
([15], ['dvdrip']),
|
||||
([15], ['dvdrip', 'scr', 'r5', 'tc', 'ts', 'cam']),
|
||||
([15, 16], ['brrip']),
|
||||
([16], ['720p', '1080p', 'bd50']),
|
||||
([17], ['dvdr']),
|
||||
]
|
||||
cat_backup_id = 'movie'
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
|
||||
return []
|
||||
|
||||
return super(Base, self).search(movie, quality)
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
q = '%s %s' % (title, movie['info']['year'])
|
||||
@@ -47,22 +35,20 @@ class Base(NZBProvider, RSS):
|
||||
'api': self.conf('api_key', default = ''),
|
||||
})
|
||||
|
||||
nzbs = self.getRSSData(self.urls['search'] % params)
|
||||
nzbs = self.getJsonData(self.urls['search'] % params)
|
||||
|
||||
for nzb in nzbs:
|
||||
if isinstance(nzbs, list):
|
||||
for nzb in nzbs:
|
||||
|
||||
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'detail_url': self.urls['detail_url'] % nzb_id,
|
||||
'description': self.getTextElement(nzb, 'description')
|
||||
})
|
||||
results.append({
|
||||
'id': nzb.get('nzbid'),
|
||||
'name': toUnicode(nzb.get('release')),
|
||||
'age': self.calculateAge(tryInt(nzb.get('usenetage'))),
|
||||
'size': tryInt(nzb.get('sizebytes')) / 1024 / 1024,
|
||||
'url': nzb.get('getnzb'),
|
||||
'detail_url': nzb.get('details'),
|
||||
'description': nzb.get('weblink')
|
||||
})
|
||||
|
||||
|
||||
config = [{
|
||||
|
||||
130
couchpotato/core/media/_base/providers/torrent/hdaccess.py
Normal file
130
couchpotato/core/media/_base/providers/torrent/hdaccess.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://hdaccess.net/',
|
||||
'detail': 'https://hdaccess.net/details.php?id=%s',
|
||||
'search': 'https://hdaccess.net/searchapi.php?apikey=%s&username=%s&imdbid=%s&internal=%s',
|
||||
'download': 'https://hdaccess.net/grab.php?torrent=%s&apikey=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
data = self.getJsonData(self.urls['search'] % (self.conf('apikey'), self.conf('username'), getIdentifier(movie), self.conf('internal_only')))
|
||||
|
||||
if data:
|
||||
try:
|
||||
#for result in data[]:
|
||||
for key, result in data.iteritems():
|
||||
if tryInt(result['total_results']) == 0:
|
||||
return
|
||||
torrentscore = self.conf('extra_score')
|
||||
releasegroup = result['releasegroup']
|
||||
resolution = result['resolution']
|
||||
encoding = result['encoding']
|
||||
freeleech = tryInt(result['freeleech'])
|
||||
seeders = tryInt(result['seeders'])
|
||||
torrent_desc = '/ %s / %s / %s / %s seeders' % (releasegroup, resolution, encoding, seeders)
|
||||
|
||||
if freeleech > 0 and self.conf('prefer_internal'):
|
||||
torrent_desc += '/ Internal'
|
||||
torrentscore += 200
|
||||
|
||||
if seeders == 0:
|
||||
torrentscore = 0
|
||||
|
||||
name = result['release_name']
|
||||
year = tryInt(result['year'])
|
||||
|
||||
results.append({
|
||||
'id': tryInt(result['torrentid']),
|
||||
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
|
||||
'url': self.urls['download'] % (result['torrentid'], self.conf('apikey')),
|
||||
'detail_url': self.urls['detail'] % result['torrentid'],
|
||||
'size': tryInt(result['size']),
|
||||
'seeders': tryInt(result['seeders']),
|
||||
'leechers': tryInt(result['leechers']),
|
||||
'age': tryInt(result['age']),
|
||||
'score': torrentscore
|
||||
})
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
config = [{
|
||||
'name': 'hdaccess',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'HDAccess',
|
||||
'wizard': True,
|
||||
'description': '<a href="https://hdaccess.net">HDAccess</a>',
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAADuUlEQVQ4yz3T209bdQAH8O/vnNNzWno5FIpAKZdSLi23gWMDtumWuSXOyzJj9M1kyIOPS1xiYuKe9GUPezZZnGIiMTqTxS1bdIuYkG2MWKBAKYVszOgKFkrbA+259HfO+fli/PwPHzI+Pg5CCEAI2VcUlEsl1tHdU7P5bGOkWChEaaUCwvHpmkD93POn6bwgCMQGAMYYYwyCruuQnE7SPzjIstvb8l+bm5fXkokJSmlQEkUQAIpSRH5vd0tyum7I/sA1Z5VH2ctmiGWZjHw4McE1NAZtQ9fD25kXt1VN7es7dNjuGRjiJFeVpWo6slsZPhF/Ys/PPeIs2056ff7zIOS5rpU5/viJEwwEnu3Mi18dojjw0aWP6amz57h9RSE/35zinq2nuGjvIQwOj7K2SKeZWkk0auXSSZ+/ZopSy+CbW1pQKpWu6Jr2/qVPPqWRjm6HWi6Tm999g3RyGbndLCqGgVBrO3F7fHykK0YX47NNtGLYlBq/c+H2iD+3k704dHQUDcFmQVXLyP6zhfTqCl45fQYjx17FemoJunoAk1bQFGoVhkdPwNC0ix2dMT+3llodM02rKdo7gN3dHAEhuH/vNgDg3Pl3cPaNt2GZJpYX5lBbFwClBukfGobL5WrayW6NccVCISY4HIQxYts2Q3J5CXOPHuLlo6NoCoXQ2hbG0JFRpJYWcVDIQ5ZlyL5qW5b9hNlWjKsYBgzDgKppMCoGHty7A0orOHbyNNweL+obGnDm9TdhWSYS8Vn4a2shOZ0QJRGSKIHjeGGtWNhjqqpyG+k04k8eozPai9ZwByavf4kfpyZxZGwMfYOHsbwQx34hB5dL4syKweRq/xpXHwzNapqWSSYWMDszzYqFPEaOn4KiKJiZfoCZ6d8Am+GtC++iXCpjaf4P9vefT8HzfKarp3eWRKMxCILwuWXSz977YIK2RTodDoGH1+OG1+tDlbsKkuiAJEngeWBjNUUnv7rucIiOLyzTvMKJTgnVtbVXLctK3L31g+NAUajL5bEptaDpOnTdgGkzVHl9drms0ju3fnJIkphoaQtfbQiFwAcCAY5wnCE5Xff3i8XX4o9nGksH+8zl9hAGZlWMCivkc9z0L3fZ999+LTCGZKi55YJTFHfye3sc6e/vB88LpK6+iWlqSS4WcpcNXZtwOp3B6mo/REmCSSkEgd+qq3vpRkt75Fp9Y1BZWZwnhq4zEovF/u/MATAti4U7umvyu9kR27aikihC9vvTnV2xufVUMu/2uIksy/9tZvgX49fLmAMx3bsAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
'description': 'Enter your site username.',
|
||||
},
|
||||
{
|
||||
'name': 'apikey',
|
||||
'default': '',
|
||||
'label': 'API Key',
|
||||
'description': 'Enter your site api key. This can be find on <a href="https://hdaccess.net/usercp.php?action=security">Profile Security</a>',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 0,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met. HDAccess minimum is 1:1.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met. HDAccess minimum is 48 hours.',
|
||||
},
|
||||
{
|
||||
'name': 'prefer_internal',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'default': 1,
|
||||
'description': 'Favors internal releases over non-internal releases.',
|
||||
},
|
||||
{
|
||||
'name': 'internal_only',
|
||||
'advanced': True,
|
||||
'label': 'Internal Only',
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Only download releases marked as HDAccess internal',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -29,6 +29,9 @@ class Base(TorrentProvider):
|
||||
}
|
||||
post_data.update(params)
|
||||
|
||||
if self.conf('internal_only'):
|
||||
post_data.update({'origin': [1]})
|
||||
|
||||
try:
|
||||
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
|
||||
|
||||
@@ -110,6 +113,14 @@ config = [{
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
{
|
||||
'name': 'internal_only',
|
||||
'advanced': True,
|
||||
'label': 'Internal Only',
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Only download releases marked as HDBits internal'
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,11 +14,11 @@ log = CPLog(__name__)
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.iptorrents.com/',
|
||||
'base_url': 'https://www.iptorrents.com',
|
||||
'login': 'https://www.iptorrents.com/torrents/',
|
||||
'login_check': 'https://www.iptorrents.com/inbox.php',
|
||||
'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
|
||||
'test': 'https://iptorrents.eu/',
|
||||
'base_url': 'https://iptorrents.eu',
|
||||
'login': 'https://iptorrents.eu/torrents/',
|
||||
'login_check': 'https://iptorrents.eu/inbox.php',
|
||||
'search': 'https://iptorrents.eu/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
@@ -120,7 +120,7 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'IPTorrents',
|
||||
'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
|
||||
'description': '<a href="https://iptorrents.eu">IPTorrents</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
|
||||
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
|
||||
|
||||
link = result.find('td', attrs = {'class': 'ttr_name'}).find('a')
|
||||
url = result.find('td', attrs = {'class': 'td_dl'}).find('a')
|
||||
seeders = result.find('td', attrs = {'class': 'ttr_seeders'}).find('a')
|
||||
leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a')
|
||||
torrent_id = link['href'].replace('details?id=', '')
|
||||
|
||||
@@ -51,7 +52,7 @@ class Base(TorrentProvider):
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
|
||||
'seeders': tryInt(seeders.string) if seeders else 0,
|
||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
@@ -56,11 +56,12 @@ class Base(TorrentProvider):
|
||||
|
||||
full_id = link['href'].replace('details.php?id=', '')
|
||||
torrent_id = full_id[:6]
|
||||
name = toUnicode(link.get('title', link.contents[0]).encode('ISO-8859-1')).strip()
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link.contents[0],
|
||||
'url': self.urls['download'] % (torrent_id, link.contents[0]),
|
||||
'name': name,
|
||||
'url': self.urls['download'] % (torrent_id, name),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
|
||||
'seeders': tryInt(cells[8].find('span').contents[0]),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
@@ -8,12 +9,12 @@ log = CPLog(__name__)
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.td.af/',
|
||||
'login': 'http://www.td.af/torrents/',
|
||||
'login_check': 'http://www.torrentday.com/userdetails.php',
|
||||
'detail': 'http://www.td.af/details.php?id=%s',
|
||||
'search': 'http://www.td.af/V3/API/API.php',
|
||||
'download': 'http://www.td.af/download.php/%s/%s',
|
||||
'test': 'https://torrentday.eu/',
|
||||
'login': 'https://torrentday.eu/torrents/',
|
||||
'login_check': 'https://torrentday.eu/userdetails.php',
|
||||
'detail': 'https://torrentday.eu/details.php?id=%s',
|
||||
'search': 'https://torrentday.eu/V3/API/API.php',
|
||||
'download': 'https://torrentday.eu/download.php/%s/%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
@@ -55,6 +56,10 @@ class Base(TorrentProvider):
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
often = re.search('You tried too often, please wait .*</div>', output)
|
||||
if often:
|
||||
raise Exception(often.group(0)[:-6].strip())
|
||||
|
||||
return 'Password not correct' not in output
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
@@ -68,7 +73,7 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentDay',
|
||||
'description': '<a href="http://www.td.af/">TorrentDay</a>',
|
||||
'description': '<a href="https://torrentday.eu/">TorrentDay</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
|
||||
'options': [
|
||||
|
||||
126
couchpotato/core/media/_base/providers/torrent/torrentleech.py
Normal file
126
couchpotato/core/media/_base/providers/torrent/torrentleech.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import traceback
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
import six
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.torrentleech.org/',
|
||||
'login': 'https://www.torrentleech.org/user/account/login/',
|
||||
'login_check': 'https://torrentleech.org/user/messages',
|
||||
'detail': 'https://www.torrentleech.org/torrent/%s',
|
||||
'search': 'https://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download': 'https://www.torrentleech.org%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # Seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
url = self.urls['search'] % self.buildUrl(title, media, quality)
|
||||
|
||||
data = self.getHTMLData(url)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'id': 'torrenttable'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
|
||||
link = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
|
||||
details = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||
|
||||
results.append({
|
||||
'id': link['href'].replace('/torrent/', ''),
|
||||
'name': six.text_type(link.string),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return {
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'remember_me': 'on',
|
||||
'login': 'submit',
|
||||
}
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'torrentleech',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentLeech',
|
||||
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
|
||||
'wizard': True,
|
||||
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'seed_ratio',
|
||||
'label': 'Seed ratio',
|
||||
'type': 'float',
|
||||
'default': 1,
|
||||
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||
},
|
||||
{
|
||||
'name': 'seed_time',
|
||||
'label': 'Seed time',
|
||||
'type': 'int',
|
||||
'default': 40,
|
||||
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -42,6 +42,7 @@ class Base(TorrentProvider):
|
||||
|
||||
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
|
||||
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
|
||||
size = result.find('td', attrs = {'class': 'size'}).contents[0].strip('\n ')
|
||||
tds = result.find_all('td')
|
||||
|
||||
results.append({
|
||||
@@ -49,7 +50,7 @@ class Base(TorrentProvider):
|
||||
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % link['href'],
|
||||
'size': self.parseSize(result.find_all('td')[5].string),
|
||||
'size': self.parseSize(size),
|
||||
'seeders': tryInt(tds[len(tds)-2].string),
|
||||
'leechers': tryInt(tds[len(tds)-1].string),
|
||||
})
|
||||
|
||||
@@ -22,12 +22,12 @@ class Base(TorrentMagnetProvider, RSS):
|
||||
|
||||
http_time_between_calls = 0
|
||||
|
||||
def _search(self, media, quality, results):
|
||||
def _searchOnTitle(self, title, media, quality, results):
|
||||
|
||||
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
|
||||
|
||||
# Create search parameters
|
||||
search_params = self.buildUrl(media)
|
||||
search_params = self.buildUrl(title, media, quality)
|
||||
|
||||
smin = quality.get('size_min')
|
||||
smax = quality.get('size_max')
|
||||
|
||||
@@ -2,28 +2,25 @@ import traceback
|
||||
|
||||
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Base(TorrentMagnetProvider):
|
||||
class Base(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': '%s/api',
|
||||
'search': '%s/api/list.json?keywords=%s&quality=%s',
|
||||
'detail': '%s/api/movie.json?id=%s'
|
||||
'test': '%s/api/v2',
|
||||
'search': '%s/api/v2/list_movies.json?limit=50&query_term=%s'
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 # seconds
|
||||
|
||||
proxy_list = [
|
||||
'http://yify.unlocktorrent.com',
|
||||
'http://yify-torrents.com.come.in',
|
||||
'http://yts.re',
|
||||
'http://yts.im'
|
||||
'http://yify-torrents.im',
|
||||
'https://yts.re',
|
||||
'https://yts.wf',
|
||||
'https://yts.im',
|
||||
]
|
||||
|
||||
def search(self, movie, quality):
|
||||
@@ -39,28 +36,31 @@ class Base(TorrentMagnetProvider):
|
||||
if not domain:
|
||||
return
|
||||
|
||||
search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
|
||||
search_url = self.urls['search'] % (domain, getIdentifier(movie))
|
||||
|
||||
data = self.getJsonData(search_url)
|
||||
data = data.get('data')
|
||||
|
||||
if data and data.get('MovieList'):
|
||||
if data and data.get('movies'):
|
||||
try:
|
||||
for result in data.get('MovieList'):
|
||||
for result in data.get('movies'):
|
||||
|
||||
if result['Quality'] and result['Quality'] not in result['MovieTitle']:
|
||||
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
|
||||
else:
|
||||
title = result['MovieTitle'] + ' BrRip'
|
||||
for release in result.get('torrents', []):
|
||||
|
||||
results.append({
|
||||
'id': result['MovieID'],
|
||||
'name': title,
|
||||
'url': result['TorrentMagnetUrl'],
|
||||
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
|
||||
'size': self.parseSize(result['Size']),
|
||||
'seeders': tryInt(result['TorrentSeeds']),
|
||||
'leechers': tryInt(result['TorrentPeers']),
|
||||
})
|
||||
if release['quality'] and release['quality'] not in result['title_long']:
|
||||
title = result['title_long'] + ' BRRip ' + release['quality']
|
||||
else:
|
||||
title = result['title_long'] + ' BRRip'
|
||||
|
||||
results.append({
|
||||
'id': release['hash'],
|
||||
'name': title,
|
||||
'url': release['url'],
|
||||
'detail_url': result['url'],
|
||||
'size': self.parseSize(release['size']),
|
||||
'seeders': tryInt(release['seeds']),
|
||||
'leechers': tryInt(release['peers']),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
@@ -65,7 +65,7 @@ class MovieBase(MovieTypeBase):
|
||||
return False
|
||||
elif not params.get('info'):
|
||||
try:
|
||||
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
|
||||
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), adding = True, single = True)
|
||||
if not is_movie:
|
||||
msg = 'Can\'t add movie, seems to be a TV show.'
|
||||
log.error(msg)
|
||||
|
||||
@@ -696,7 +696,7 @@ MA.Readd = new Class({
|
||||
|
||||
if(movie_done || snatched && snatched > 0)
|
||||
self.el = new Element('a.readd', {
|
||||
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
|
||||
'title': 'Re-add the movie and mark all previous snatched/downloaded as ignored',
|
||||
'events': {
|
||||
'click': self.doReadd.bind(self)
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ var Charts = new Class({
|
||||
|
||||
if( Cookie.read('suggestions_charts_menu_selected') === 'charts'){
|
||||
self.show();
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
}
|
||||
else
|
||||
self.el.hide();
|
||||
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
|
||||
},
|
||||
|
||||
fill: function(json){
|
||||
|
||||
89
couchpotato/core/media/movie/providers/automation/crowdai.py
Normal file
89
couchpotato/core/media/movie/providers/automation/crowdai.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import re
|
||||
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie.providers.automation.base import Automation
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'CrowdAI'
|
||||
|
||||
|
||||
class CrowdAI(Automation, RSS):
|
||||
|
||||
interval = 1800
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
movies = []
|
||||
|
||||
urls = dict(zip(splitString(self.conf('automation_urls')), [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]))
|
||||
|
||||
for url in urls:
|
||||
|
||||
if not urls[url]:
|
||||
continue
|
||||
|
||||
rss_movies = self.getRSSData(url)
|
||||
|
||||
for movie in rss_movies:
|
||||
|
||||
description = self.getTextElement(movie, 'description')
|
||||
grabs = 0
|
||||
|
||||
for item in movie:
|
||||
if item.attrib.get('name') == 'grabs':
|
||||
grabs = item.attrib.get('value')
|
||||
break
|
||||
|
||||
if int(grabs) > tryInt(self.conf('number_grabs')):
|
||||
title = re.match(r'.*Title: .a href.*/">(.*) \(\d{4}\).*', description).group(1)
|
||||
log.info2('%s grabs for movie: %s, enqueue...', (grabs, title))
|
||||
year = re.match(r'.*Year: (\d{4}).*', description).group(1)
|
||||
imdb = self.search(title, year)
|
||||
|
||||
if imdb and self.isMinimalMovie(imdb):
|
||||
movies.append(imdb['imdb'])
|
||||
|
||||
return movies
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'crowdai',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'list': 'automation_providers',
|
||||
'name': 'crowdai_automation',
|
||||
'label': 'CrowdAI',
|
||||
'description': 'Imports from any newznab powered NZB providers RSS feed depending on the number of grabs per movie. Go to your newznab site and find the RSS section. Then copy the copy paste the link under "Movies > x264 feed" here.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'automation_urls_use',
|
||||
'label': 'Use',
|
||||
'default': '1',
|
||||
},
|
||||
{
|
||||
'name': 'automation_urls',
|
||||
'label': 'url',
|
||||
'type': 'combined',
|
||||
'combine': ['automation_urls_use', 'automation_urls'],
|
||||
'default': 'http://YOUR_PROVIDER/rss?t=THE_MOVIE_CATEGORY&i=YOUR_USER_ID&r=YOUR_API_KEY&res=2&rls=2&num=100',
|
||||
},
|
||||
{
|
||||
'name': 'number_grabs',
|
||||
'default': '500',
|
||||
'label': 'Grab threshold',
|
||||
'description': 'Number of grabs required',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -48,11 +48,12 @@ class Letterboxd(Automation):
|
||||
|
||||
soup = BeautifulSoup(self.getHTMLData(self.url % username))
|
||||
|
||||
for movie in soup.find_all('a', attrs = {'class': 'frame'}):
|
||||
match = removeEmpty(self.pattern.split(movie['title']))
|
||||
for movie in soup.find_all('li', attrs = {'class': 'poster-container'}):
|
||||
img = movie.find('img', movie)
|
||||
title = img.get('alt')
|
||||
|
||||
movies.append({
|
||||
'title': match[0],
|
||||
'year': match[1]
|
||||
'title': title
|
||||
})
|
||||
|
||||
return movies
|
||||
|
||||
@@ -39,15 +39,14 @@ class Rottentomatoes(Automation, RSS):
|
||||
|
||||
if result:
|
||||
|
||||
log.info2('Something smells...')
|
||||
rating = tryInt(self.getTextElement(movie, rating_tag))
|
||||
name = result.group(0)
|
||||
|
||||
print rating, tryInt(self.conf('tomatometer_percent'))
|
||||
if rating < tryInt(self.conf('tomatometer_percent')):
|
||||
log.info2('%s seems to be rotten...', name)
|
||||
else:
|
||||
|
||||
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
|
||||
log.info2('Found %s with fresh rating %s', (name, rating))
|
||||
year = datetime.datetime.now().strftime("%Y")
|
||||
imdb = self.search(name, year)
|
||||
|
||||
|
||||
@@ -69,12 +69,15 @@ class CouchPotatoApi(MovieProvider):
|
||||
name_enc = base64.b64encode(ss(name))
|
||||
return self.getJsonData(self.urls['validate'] % name_enc, headers = self.getRequestHeaders())
|
||||
|
||||
def isMovie(self, identifier = None):
|
||||
def isMovie(self, identifier = None, adding = False):
|
||||
|
||||
if not identifier:
|
||||
return
|
||||
|
||||
data = self.getJsonData(self.urls['is_movie'] % identifier, headers = self.getRequestHeaders())
|
||||
url = self.urls['is_movie'] % identifier
|
||||
url += '?adding=1' if adding else ''
|
||||
|
||||
data = self.getJsonData(url, headers = self.getRequestHeaders())
|
||||
if data:
|
||||
return data.get('is_movie', True)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ autoload = 'Bitsoup'
|
||||
class Bitsoup(MovieProvider, Base):
|
||||
cat_ids = [
|
||||
([17], ['3d']),
|
||||
([41], ['720p', '1080p']),
|
||||
([80], ['720p', '1080p']),
|
||||
([20], ['dvdr']),
|
||||
([19], ['brrip', 'dvdrip']),
|
||||
]
|
||||
|
||||
11
couchpotato/core/media/movie/providers/torrent/hdaccess.py
Normal file
11
couchpotato/core/media/movie/providers/torrent/hdaccess.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.hdaccess import Base
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'HDAccess'
|
||||
|
||||
|
||||
class HDAccess(MovieProvider, Base):
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.providers.torrent.torrentleech import Base
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'TorrentLeech'
|
||||
|
||||
|
||||
class TorrentLeech(MovieProvider, Base):
|
||||
|
||||
cat_ids = [
|
||||
([13], ['720p', '1080p', 'bd50']),
|
||||
([8], ['cam']),
|
||||
([9], ['ts', 'tc']),
|
||||
([10], ['r5', 'scr']),
|
||||
([11], ['dvdrip']),
|
||||
([14], ['brrip']),
|
||||
([12], ['dvdr']),
|
||||
]
|
||||
|
||||
def buildUrl(self, title, media, quality):
|
||||
return (
|
||||
tryUrlencode(title.replace(':', '')),
|
||||
self.getCatId(quality)[0]
|
||||
)
|
||||
@@ -22,8 +22,8 @@ class TorrentShack(MovieProvider, Base):
|
||||
# Movies-SD Pack - 983 (not included)
|
||||
|
||||
cat_ids = [
|
||||
([970], ['bd50']),
|
||||
([300], ['720p', '1080p']),
|
||||
([970, 320], ['bd50']),
|
||||
([300, 320], ['720p', '1080p']),
|
||||
([350], ['dvdr']),
|
||||
([400], ['brrip', 'dvdrip']),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.media._base.providers.torrent.torrentz import Base
|
||||
from couchpotato.core.media.movie.providers.base import MovieProvider
|
||||
|
||||
@@ -11,5 +10,5 @@ autoload = 'Torrentz'
|
||||
|
||||
class Torrentz(MovieProvider, Base):
|
||||
|
||||
def buildUrl(self, media):
|
||||
return tryUrlencode('"%s"' % fireEvent('library.query', media, single = True))
|
||||
def buildUrl(self, title, media, quality):
|
||||
return tryUrlencode('"%s %s"' % (title, media['info']['year']))
|
||||
@@ -12,7 +12,7 @@ autoload = 'RottenTomatoes'
|
||||
|
||||
class RottenTomatoes(UserscriptBase):
|
||||
|
||||
includes = ['*://www.rottentomatoes.com/m/*/']
|
||||
includes = ['*://www.rottentomatoes.com/m/*']
|
||||
excludes = ['*://www.rottentomatoes.com/m/*/*/']
|
||||
|
||||
version = 2
|
||||
|
||||
@@ -394,8 +394,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
log.info('Trying next release for: %s', getTitle(media))
|
||||
self.single(media, manual = manual, force_download = force_download)
|
||||
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
except:
|
||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
@@ -51,8 +51,8 @@ var SuggestList = new Class({
|
||||
self.show();
|
||||
else
|
||||
self.hide();
|
||||
|
||||
self.fireEvent('created');
|
||||
|
||||
self.fireEvent.delay(0, self, 'created');
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from .index import NotificationIndex, NotificationUnreadIndex
|
||||
from couchpotato.environment import Env
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -110,11 +111,11 @@ class CoreNotifier(Notification):
|
||||
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset)
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
results = db.get_many('notification', limit = limit, offset = offset, with_doc = True)
|
||||
limit = tryInt(splt[0])
|
||||
offset = tryInt(0 if len(splt) is 1 else splt[1])
|
||||
results = db.all('notification', limit = limit, offset = offset, with_doc = True)
|
||||
else:
|
||||
results = db.get_many('notification', limit = 200, with_doc = True)
|
||||
results = db.all('notification', limit = 200, with_doc = True)
|
||||
|
||||
notifications = []
|
||||
for n in results:
|
||||
@@ -148,16 +149,15 @@ class CoreNotifier(Notification):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
n = {
|
||||
'_t': 'notification',
|
||||
'time': int(time.time()),
|
||||
}
|
||||
|
||||
try:
|
||||
db = get_db()
|
||||
|
||||
data['notification_type'] = listener if listener else 'unknown'
|
||||
|
||||
n = {
|
||||
'_t': 'notification',
|
||||
'time': int(time.time()),
|
||||
'message': toUnicode(message)
|
||||
}
|
||||
n['message'] = toUnicode(message)
|
||||
|
||||
if data.get('sticky'):
|
||||
n['sticky'] = True
|
||||
@@ -170,7 +170,7 @@ class CoreNotifier(Notification):
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('Failed notify: %s', traceback.format_exc())
|
||||
log.error('Failed notify "%s": %s', (n, traceback.format_exc()))
|
||||
|
||||
def frontend(self, type = 'notification', data = None, message = None):
|
||||
if not data: data = {}
|
||||
@@ -190,7 +190,7 @@ class CoreNotifier(Notification):
|
||||
while len(self.listeners) > 0 and not self.shuttingDown():
|
||||
try:
|
||||
listener, last_id = self.listeners.pop()
|
||||
listener({
|
||||
IOLoop.current().add_callback(listener, {
|
||||
'success': True,
|
||||
'result': [notification],
|
||||
})
|
||||
|
||||
@@ -23,6 +23,26 @@ config = [{
|
||||
'default': 'localhost',
|
||||
'description': 'Hostname/IP, default localhost'
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'label': 'Username',
|
||||
'default': '',
|
||||
'description': 'Required for myPlex'
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'label': 'Password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
'description': 'Required for myPlex'
|
||||
},
|
||||
{
|
||||
'name': 'auth_token',
|
||||
'label': 'Auth Token',
|
||||
'default': '',
|
||||
'advanced': True,
|
||||
'description': 'Required for myPlex'
|
||||
},
|
||||
{
|
||||
'name': 'clients',
|
||||
'default': '',
|
||||
|
||||
@@ -35,11 +35,46 @@ class PlexServer(object):
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
|
||||
data = self.plex.urlopen('%s/%s' % (
|
||||
self.createHost(self.plex.conf('media_server'), port = 32400),
|
||||
path
|
||||
))
|
||||
#Maintain support for older Plex installations without myPlex
|
||||
if not self.plex.conf('auth_token') and not self.plex.conf('username') and not self.plex.conf('password'):
|
||||
data = self.plex.urlopen('%s/%s' % (
|
||||
self.createHost(self.plex.conf('media_server'), port = 32400),
|
||||
path
|
||||
))
|
||||
else:
|
||||
#Fetch X-Plex-Token if it doesn't exist but a username/password do
|
||||
if not self.plex.conf('auth_token') and (self.plex.conf('username') and self.plex.conf('password')):
|
||||
import urllib2, base64
|
||||
log.info("Fetching a new X-Plex-Token from plex.tv")
|
||||
username = self.plex.conf('username')
|
||||
password = self.plex.conf('password')
|
||||
req = urllib2.Request("https://plex.tv/users/sign_in.xml", data="")
|
||||
authheader = "Basic %s" % base64.encodestring('%s:%s' % (username, password))[:-1]
|
||||
req.add_header("Authorization", authheader)
|
||||
req.add_header("X-Plex-Product", "Couchpotato Notifier")
|
||||
req.add_header("X-Plex-Client-Identifier", "b3a6b24dcab2224bdb101fc6aa08ea5e2f3147d6")
|
||||
req.add_header("X-Plex-Version", "1.0")
|
||||
|
||||
try:
|
||||
response = urllib2.urlopen(req)
|
||||
except urllib2.URLError, e:
|
||||
log.info("Error fetching token from plex.tv")
|
||||
|
||||
try:
|
||||
auth_tree = etree.parse(response)
|
||||
token = auth_tree.findall(".//authentication-token")[0].text
|
||||
self.plex.conf('auth_token', token)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
log.info("Error parsing plex.tv response: " + ex(e))
|
||||
|
||||
#Add X-Plex-Token header for myPlex support workaround
|
||||
data = self.plex.urlopen('%s/%s?X-Plex-Token=%s' % (
|
||||
self.createHost(self.plex.conf('media_server'), port = 32400),
|
||||
path,
|
||||
self.plex.conf('auth_token')
|
||||
))
|
||||
|
||||
if data_type == 'xml':
|
||||
return etree.fromstring(data)
|
||||
else:
|
||||
|
||||
68
couchpotato/core/notifications/webhook.py
Normal file
68
couchpotato/core/notifications/webhook.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import traceback
|
||||
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getIdentifier
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
autoload = 'Webhook'
|
||||
|
||||
class Webhook(Notification):
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
post_data = {
|
||||
'message': toUnicode(message)
|
||||
}
|
||||
|
||||
if getIdentifier(data):
|
||||
post_data.update({
|
||||
'imdb_id': getIdentifier(data)
|
||||
})
|
||||
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.conf('url'), headers = headers, data = post_data, show_error = False)
|
||||
return True
|
||||
except:
|
||||
log.error('Webhook notification failed: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
|
||||
|
||||
config = [{
|
||||
'name': 'webhook',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'webhook',
|
||||
'label': 'Webhook',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'url',
|
||||
'description': 'The URL to send notification data to when '
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -39,7 +39,7 @@ class Plugin(object):
|
||||
|
||||
_locks = {}
|
||||
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:34.0) Gecko/20100101 Firefox/34.0'
|
||||
http_last_use = {}
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
@@ -206,7 +206,7 @@ class Plugin(object):
|
||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
if not show_error:
|
||||
raise Exception('Disabled calls to %s for 15 minutes because so many failed requests')
|
||||
raise Exception('Disabled calls to %s for 15 minutes because so many failed requests' % host)
|
||||
else:
|
||||
return ''
|
||||
else:
|
||||
|
||||
@@ -87,6 +87,7 @@ class FileBrowser(Plugin):
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
|
||||
except:
|
||||
log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
|
||||
dirs = []
|
||||
|
||||
parent = os.path.dirname(path.rstrip(os.path.sep))
|
||||
|
||||
@@ -35,6 +35,7 @@ class Renamer(Plugin):
|
||||
'desc': 'For the renamer to check for new files to rename in a folder',
|
||||
'params': {
|
||||
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
|
||||
'to_folder': {'desc': 'Optional: The folder to move releases to. Leave empty for default folder.'},
|
||||
'media_folder': {'desc': 'Optional: The folder of the media to scan. Keep empty for default renamer folder.'},
|
||||
'files': {'desc': 'Optional: Provide the release files if more releases are in the same media_folder, delimited with a \'|\'. Note that no dedicated release folder is expected for releases with one file.'},
|
||||
'base_folder': {'desc': 'Optional: The folder to find releases in. Leave empty for default folder.'},
|
||||
@@ -44,6 +45,13 @@ class Renamer(Plugin):
|
||||
},
|
||||
})
|
||||
|
||||
addApiView('renamer.progress', self.getProgress, docs = {
|
||||
'desc': 'Get the progress of current renamer scan',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'progress': False || True,
|
||||
}"""},
|
||||
})
|
||||
|
||||
addEvent('renamer.scan', self.scan)
|
||||
addEvent('renamer.check_snatched', self.checkSnatched)
|
||||
|
||||
@@ -67,11 +75,17 @@ class Renamer(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def getProgress(self, **kwargs):
|
||||
return {
|
||||
'progress': self.renaming_started
|
||||
}
|
||||
|
||||
def scanView(self, **kwargs):
|
||||
|
||||
async = tryInt(kwargs.get('async', 0))
|
||||
base_folder = kwargs.get('base_folder')
|
||||
media_folder = sp(kwargs.get('media_folder'))
|
||||
to_folder = kwargs.get('to_folder')
|
||||
|
||||
# Backwards compatibility, to be removed after a few versions :)
|
||||
if not media_folder:
|
||||
@@ -95,13 +109,13 @@ class Renamer(Plugin):
|
||||
})
|
||||
|
||||
fire_handle = fireEvent if not async else fireEventAsync
|
||||
fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download)
|
||||
fire_handle('renamer.scan', base_folder = base_folder, release_download = release_download, to_folder = to_folder)
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
def scan(self, base_folder = None, release_download = None):
|
||||
def scan(self, base_folder = None, release_download = None, to_folder = None):
|
||||
if not release_download: release_download = {}
|
||||
|
||||
if self.isDisabled():
|
||||
@@ -115,7 +129,9 @@ class Renamer(Plugin):
|
||||
base_folder = sp(self.conf('from'))
|
||||
|
||||
from_folder = sp(self.conf('from'))
|
||||
to_folder = sp(self.conf('to'))
|
||||
|
||||
if not to_folder:
|
||||
to_folder = sp(self.conf('to'))
|
||||
|
||||
# Get media folder to process
|
||||
media_folder = sp(release_download.get('folder'))
|
||||
@@ -869,7 +885,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
#If information is not available, we don't want the tag in the filename
|
||||
replaced = replaced.replace('<' + x + '>', '')
|
||||
|
||||
replaced = self.replaceDoubles(replaced.lstrip('. '))
|
||||
if self.conf('replace_doubles'):
|
||||
replaced = self.replaceDoubles(replaced.lstrip('. '))
|
||||
|
||||
for x, r in replacements.items():
|
||||
if x in ['thename', 'namethe']:
|
||||
replaced = replaced.replace(six.u('<%s>') % toUnicode(x), toUnicode(r))
|
||||
@@ -1326,6 +1344,14 @@ config = [{
|
||||
'type': 'choice',
|
||||
'options': rename_options
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'replace_doubles',
|
||||
'type': 'bool',
|
||||
'label': 'Clean Name',
|
||||
'description': ('Attempt to clean up double separaters due to missing data for fields.','Sometimes this eliminates wanted white space (see <a href="https://github.com/RuudBurger/CouchPotatoServer/issues/2782">#2782</a>).'),
|
||||
'default': True
|
||||
},
|
||||
{
|
||||
'name': 'unrar',
|
||||
'type': 'bool',
|
||||
|
||||
@@ -16,7 +16,7 @@ autoload = 'Subtitle'
|
||||
|
||||
class Subtitle(Plugin):
|
||||
|
||||
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
|
||||
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi', 'subscenter']
|
||||
|
||||
def __init__(self):
|
||||
addEvent('renamer.before', self.searchSingle)
|
||||
|
||||
@@ -157,7 +157,15 @@ class Settings(object):
|
||||
values[section] = {}
|
||||
for option in self.p.items(section):
|
||||
(option_name, option_value) = option
|
||||
|
||||
is_password = False
|
||||
try: is_password = self.types[section][option_name] == 'password'
|
||||
except: pass
|
||||
|
||||
values[section][option_name] = self.get(option_name, section)
|
||||
if is_password and values[section][option_name]:
|
||||
values[section][option_name] = len(values[section][option_name]) * '*'
|
||||
|
||||
return values
|
||||
|
||||
def save(self):
|
||||
|
||||
@@ -244,11 +244,13 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
# Basic config
|
||||
host = Env.setting('host', default = '0.0.0.0')
|
||||
# app.debug = development
|
||||
host6 = Env.setting('host6', default = '::')
|
||||
|
||||
config = {
|
||||
'use_reloader': reloader,
|
||||
'port': tryInt(Env.setting('port', default = 5050)),
|
||||
'host': host if host and len(host) > 0 else '0.0.0.0',
|
||||
'host6': host6 if host6 and len(host6) > 0 else '::',
|
||||
'ssl_cert': Env.setting('ssl_cert', default = None),
|
||||
'ssl_key': Env.setting('ssl_key', default = None),
|
||||
}
|
||||
@@ -331,6 +333,10 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
while try_restart:
|
||||
try:
|
||||
server.listen(config['port'], config['host'])
|
||||
|
||||
try: server.listen(config['port'], config['host6'])
|
||||
except: log.info2('Tried to bind to IPV6 but failed')
|
||||
|
||||
loop.start()
|
||||
server.close_all_connections()
|
||||
server.stop()
|
||||
|
||||
@@ -886,6 +886,9 @@ Option.Directory = new Class({
|
||||
'text': 'Selected folder is empty'
|
||||
}).inject(self.dir_list)
|
||||
|
||||
//fix for webkit type browsers to refresh the dom for the file browser
|
||||
//http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
|
||||
self.dir_list.setStyle('webkitTransform', 'scale(1)');
|
||||
self.caretAtEnd();
|
||||
},
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
||||
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||
|
||||
10
init/ubuntu
10
init/ubuntu
@@ -33,8 +33,8 @@ DESC=CouchPotato
|
||||
##
|
||||
## CP_USER= #$RUN_AS, username to run couchpotato under, the default is couchpotato
|
||||
## CP_HOME= #$APP_PATH, the location of couchpotato.py, the default is /opt/couchpotato
|
||||
## CP_DATA= #$DATA_DIR, the location of couchpotato.db, cache, logs, the default is /var/couchpotato
|
||||
## CP_PIDFILE= #$PID_FILE, the location of couchpotato.pid, the default is /var/run/couchpotato.pid
|
||||
## CP_DATA= #$DATA_DIR, the location of couchpotato.db, cache, logs, the default is /var/opt/couchpotato
|
||||
## CP_PIDFILE= #$PID_FILE, the location of couchpotato.pid, the default is /var/run/couchpotato/couchpotato.pid
|
||||
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
|
||||
## CP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for couchpotato, i.e. " --config_file=/home/couchpotato/couchpotato.ini"
|
||||
## SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
|
||||
@@ -51,10 +51,10 @@ RUN_AS=${CP_USER-couchpotato}
|
||||
APP_PATH=${CP_HOME-/opt/couchpotato/}
|
||||
|
||||
# Data directory where couchpotato.db, cache and logs are stored
|
||||
DATA_DIR=${CP_DATA-/var/couchpotato}
|
||||
DATA_DIR=${CP_DATA-/var/opt/couchpotato}
|
||||
|
||||
# Path to store PID file
|
||||
PID_FILE=${CP_PIDFILE-/var/run/couchpotato.pid}
|
||||
PID_FILE=${CP_PIDFILE-/var/run/couchpotato/couchpotato.pid}
|
||||
|
||||
# path to python bin
|
||||
DAEMON=${PYTHON_BIN-/usr/bin/python}
|
||||
@@ -95,6 +95,8 @@ fi
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
touch $PID_FILE
|
||||
chown $RUN_AS $PID_FILE
|
||||
echo "Starting $DESC"
|
||||
start-stop-daemon -d $APP_PATH -c $RUN_AS $EXTRA_SSD_OPTS --start --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
|
||||
;;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define MyAppName "CouchPotato"
|
||||
#define MyAppVer "2.6.1"
|
||||
#define MyAppVer "2.6.2"
|
||||
#define MyAppBit "win32"
|
||||
//#define MyAppBit "win-amd64"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# 02110-1301 USA
|
||||
######################### END LICENSE BLOCK #########################
|
||||
|
||||
__version__ = "2.2.1"
|
||||
__version__ = "2.3.0"
|
||||
from sys import version_info
|
||||
|
||||
|
||||
|
||||
@@ -12,34 +12,68 @@ Example::
|
||||
If no paths are provided, it takes its input from stdin.
|
||||
|
||||
"""
|
||||
from io import open
|
||||
from sys import argv, stdin
|
||||
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from io import open
|
||||
|
||||
from chardet import __version__
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
|
||||
|
||||
def description_of(file, name='stdin'):
|
||||
"""Return a string describing the probable encoding of a file."""
|
||||
def description_of(lines, name='stdin'):
|
||||
"""
|
||||
Return a string describing the probable encoding of a file or
|
||||
list of strings.
|
||||
|
||||
:param lines: The lines to get the encoding of.
|
||||
:type lines: Iterable of bytes
|
||||
:param name: Name of file or collection of lines
|
||||
:type name: str
|
||||
"""
|
||||
u = UniversalDetector()
|
||||
for line in file:
|
||||
for line in lines:
|
||||
u.feed(line)
|
||||
u.close()
|
||||
result = u.result
|
||||
if result['encoding']:
|
||||
return '%s: %s with confidence %s' % (name,
|
||||
result['encoding'],
|
||||
result['confidence'])
|
||||
return '{0}: {1} with confidence {2}'.format(name, result['encoding'],
|
||||
result['confidence'])
|
||||
else:
|
||||
return '%s: no result' % name
|
||||
return '{0}: no result'.format(name)
|
||||
|
||||
|
||||
def main():
|
||||
if len(argv) <= 1:
|
||||
print(description_of(stdin))
|
||||
else:
|
||||
for path in argv[1:]:
|
||||
with open(path, 'rb') as f:
|
||||
print(description_of(f, path))
|
||||
def main(argv=None):
|
||||
'''
|
||||
Handles command line arguments and gets things started.
|
||||
|
||||
:param argv: List of arguments, as if specified on the command-line.
|
||||
If None, ``sys.argv[1:]`` is used instead.
|
||||
:type argv: list of str
|
||||
'''
|
||||
# Get command line arguments
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Takes one or more file paths and reports their detected \
|
||||
encodings",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
conflict_handler='resolve')
|
||||
parser.add_argument('input',
|
||||
help='File whose encoding we would like to determine.',
|
||||
type=argparse.FileType('rb'), nargs='*',
|
||||
default=[sys.stdin])
|
||||
parser.add_argument('--version', action='version',
|
||||
version='%(prog)s {0}'.format(__version__))
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
for f in args.input:
|
||||
if f.isatty():
|
||||
print("You are running chardetect interactively. Press " +
|
||||
"CTRL-D twice at the start of a blank line to signal the " +
|
||||
"end of your input. If you want help, run chardetect " +
|
||||
"--help\n", file=sys.stderr)
|
||||
print(description_of(f, f.name))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -177,6 +177,12 @@ class JapaneseContextAnalysis:
|
||||
return -1, 1
|
||||
|
||||
class SJISContextAnalysis(JapaneseContextAnalysis):
|
||||
def __init__(self):
|
||||
self.charset_name = "SHIFT_JIS"
|
||||
|
||||
def get_charset_name(self):
|
||||
return self.charset_name
|
||||
|
||||
def get_order(self, aBuf):
|
||||
if not aBuf:
|
||||
return -1, 1
|
||||
@@ -184,6 +190,8 @@ class SJISContextAnalysis(JapaneseContextAnalysis):
|
||||
first_char = wrap_ord(aBuf[0])
|
||||
if ((0x81 <= first_char <= 0x9F) or (0xE0 <= first_char <= 0xFC)):
|
||||
charLen = 2
|
||||
if (first_char == 0x87) or (0xFA <= first_char <= 0xFC):
|
||||
self.charset_name = "CP932"
|
||||
else:
|
||||
charLen = 1
|
||||
|
||||
|
||||
@@ -129,11 +129,11 @@ class Latin1Prober(CharSetProber):
|
||||
if total < 0.01:
|
||||
confidence = 0.0
|
||||
else:
|
||||
confidence = ((self._mFreqCounter[3] / total)
|
||||
- (self._mFreqCounter[1] * 20.0 / total))
|
||||
confidence = ((self._mFreqCounter[3] - self._mFreqCounter[1] * 20.0)
|
||||
/ total)
|
||||
if confidence < 0.0:
|
||||
confidence = 0.0
|
||||
# lower the confidence of latin1 so that other more accurate
|
||||
# detector can take priority.
|
||||
confidence = confidence * 0.5
|
||||
confidence = confidence * 0.73
|
||||
return confidence
|
||||
|
||||
@@ -353,7 +353,7 @@ SJIS_cls = (
|
||||
2,2,2,2,2,2,2,2, # 68 - 6f
|
||||
2,2,2,2,2,2,2,2, # 70 - 77
|
||||
2,2,2,2,2,2,2,1, # 78 - 7f
|
||||
3,3,3,3,3,3,3,3, # 80 - 87
|
||||
3,3,3,3,3,2,2,3, # 80 - 87
|
||||
3,3,3,3,3,3,3,3, # 88 - 8f
|
||||
3,3,3,3,3,3,3,3, # 90 - 97
|
||||
3,3,3,3,3,3,3,3, # 98 - 9f
|
||||
@@ -369,9 +369,8 @@ SJIS_cls = (
|
||||
2,2,2,2,2,2,2,2, # d8 - df
|
||||
3,3,3,3,3,3,3,3, # e0 - e7
|
||||
3,3,3,3,3,4,4,4, # e8 - ef
|
||||
4,4,4,4,4,4,4,4, # f0 - f7
|
||||
4,4,4,4,4,0,0,0 # f8 - ff
|
||||
)
|
||||
3,3,3,3,3,3,3,3, # f0 - f7
|
||||
3,3,3,3,3,0,0,0) # f8 - ff
|
||||
|
||||
|
||||
SJIS_st = (
|
||||
@@ -571,5 +570,3 @@ UTF8SMModel = {'classTable': UTF8_cls,
|
||||
'stateTable': UTF8_st,
|
||||
'charLenTable': UTF8CharLenTable,
|
||||
'name': 'UTF-8'}
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
@@ -47,7 +47,7 @@ class SJISProber(MultiByteCharSetProber):
|
||||
self._mContextAnalyzer.reset()
|
||||
|
||||
def get_charset_name(self):
|
||||
return "SHIFT_JIS"
|
||||
return self._mContextAnalyzer.get_charset_name()
|
||||
|
||||
def feed(self, aBuf):
|
||||
aLen = len(aBuf)
|
||||
|
||||
@@ -71,9 +71,9 @@ class UniversalDetector:
|
||||
|
||||
if not self._mGotData:
|
||||
# If the data starts with BOM, we know it is UTF
|
||||
if aBuf[:3] == codecs.BOM:
|
||||
if aBuf[:3] == codecs.BOM_UTF8:
|
||||
# EF BB BF UTF-8 with BOM
|
||||
self.result = {'encoding': "UTF-8", 'confidence': 1.0}
|
||||
self.result = {'encoding': "UTF-8-SIG", 'confidence': 1.0}
|
||||
elif aBuf[:4] == codecs.BOM_UTF32_LE:
|
||||
# FF FE 00 00 UTF-32, little-endian BOM
|
||||
self.result = {'encoding': "UTF-32LE", 'confidence': 1.0}
|
||||
|
||||
0
libs/pio/__init__.py
Normal file
0
libs/pio/__init__.py
Normal file
272
libs/pio/api.py
Normal file
272
libs/pio/api.py
Normal file
@@ -0,0 +1,272 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Changed
|
||||
# Removed iso8601 library requirement
|
||||
# Added CP logging
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import webbrowser
|
||||
from urllib import urlencode
|
||||
from couchpotato import CPLog
|
||||
from dateutil.parser import parse
|
||||
|
||||
import requests
|
||||
|
||||
BASE_URL = 'https://api.put.io/v2'
|
||||
ACCESS_TOKEN_URL = 'https://api.put.io/v2/oauth2/access_token'
|
||||
AUTHENTICATION_URL = 'https://api.put.io/v2/oauth2/authenticate'
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class AuthHelper(object):
|
||||
|
||||
def __init__(self, client_id, client_secret, redirect_uri, type='code'):
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.callback_url = redirect_uri
|
||||
self.type = type
|
||||
|
||||
@property
|
||||
def authentication_url(self):
|
||||
"""Redirect your users to here to authenticate them."""
|
||||
params = {
|
||||
'client_id': self.client_id,
|
||||
'response_type': self.type,
|
||||
'redirect_uri': self.callback_url
|
||||
}
|
||||
return AUTHENTICATION_URL + "?" + urlencode(params)
|
||||
|
||||
def open_authentication_url(self):
|
||||
webbrowser.open(self.authentication_url)
|
||||
|
||||
def get_access_token(self, code):
|
||||
params = {
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret,
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': self.callback_url,
|
||||
'code': code
|
||||
}
|
||||
response = requests.get(ACCESS_TOKEN_URL, params=params)
|
||||
log.debug(response)
|
||||
assert response.status_code == 200
|
||||
return response.json()['access_token']
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, access_token):
|
||||
self.access_token = access_token
|
||||
self.session = requests.session()
|
||||
|
||||
# Keep resource classes as attributes of client.
|
||||
# Pass client to resource classes so resource object
|
||||
# can use the client.
|
||||
attributes = {'client': self}
|
||||
self.File = type('File', (_File,), attributes)
|
||||
self.Transfer = type('Transfer', (_Transfer,), attributes)
|
||||
self.Account = type('Account', (_Account,), attributes)
|
||||
|
||||
def request(self, path, method='GET', params=None, data=None, files=None,
|
||||
headers=None, raw=False, stream=False):
|
||||
"""
|
||||
Wrapper around requests.request()
|
||||
|
||||
Prepends BASE_URL to path.
|
||||
Inserts oauth_token to query params.
|
||||
Parses response as JSON and returns it.
|
||||
|
||||
"""
|
||||
if not params:
|
||||
params = {}
|
||||
|
||||
if not headers:
|
||||
headers = {}
|
||||
|
||||
# All requests must include oauth_token
|
||||
params['oauth_token'] = self.access_token
|
||||
|
||||
headers['Accept'] = 'application/json'
|
||||
|
||||
url = BASE_URL + path
|
||||
log.debug('url: %s', url)
|
||||
|
||||
response = self.session.request(
|
||||
method, url, params=params, data=data, files=files,
|
||||
headers=headers, allow_redirects=True, stream=stream)
|
||||
log.debug('response: %s', response)
|
||||
if raw:
|
||||
return response
|
||||
|
||||
log.debug('content: %s', response.content)
|
||||
try:
|
||||
response = json.loads(response.content)
|
||||
except ValueError:
|
||||
raise Exception('Server didn\'t send valid JSON:\n%s\n%s' % (
|
||||
response, response.content))
|
||||
|
||||
if response['status'] == 'ERROR':
|
||||
raise Exception(response['error_type'])
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class _BaseResource(object):
|
||||
|
||||
client = None
|
||||
|
||||
def __init__(self, resource_dict):
|
||||
"""Constructs the object from a dict."""
|
||||
# All resources must have id and name attributes
|
||||
self.id = None
|
||||
self.name = None
|
||||
self.__dict__.update(resource_dict)
|
||||
try:
|
||||
self.created_at = parse(self.created_at)
|
||||
except AttributeError:
|
||||
self.created_at = None
|
||||
|
||||
def __str__(self):
|
||||
return self.name.encode('utf-8')
|
||||
|
||||
def __repr__(self):
|
||||
# shorten name for display
|
||||
name = self.name[:17] + '...' if len(self.name) > 20 else self.name
|
||||
return '<%s id=%r, name="%r">' % (
|
||||
self.__class__.__name__, self.id, name)
|
||||
|
||||
|
||||
class _File(_BaseResource):
|
||||
|
||||
@classmethod
|
||||
def get(cls, id):
|
||||
d = cls.client.request('/files/%i' % id, method='GET')
|
||||
t = d['file']
|
||||
return cls(t)
|
||||
|
||||
@classmethod
|
||||
def list(cls, parent_id=0):
|
||||
d = cls.client.request('/files/list', params={'parent_id': parent_id})
|
||||
files = d['files']
|
||||
return [cls(f) for f in files]
|
||||
|
||||
@classmethod
|
||||
def upload(cls, path, name=None, parent_id=0):
|
||||
with open(path) as f:
|
||||
if name:
|
||||
files = {'file': (name, f)}
|
||||
else:
|
||||
files = {'file': f}
|
||||
d = cls.client.request('/files/upload', method='POST',
|
||||
data={'parent_id': parent_id}, files=files)
|
||||
|
||||
f = d['file']
|
||||
return cls(f)
|
||||
|
||||
def dir(self):
|
||||
"""List the files under directory."""
|
||||
return self.list(parent_id=self.id)
|
||||
|
||||
def download(self, dest='.', delete_after_download=False):
|
||||
if self.content_type == 'application/x-directory':
|
||||
self._download_directory(dest, delete_after_download)
|
||||
else:
|
||||
self._download_file(dest, delete_after_download)
|
||||
|
||||
def _download_directory(self, dest='.', delete_after_download=False):
|
||||
name = self.name
|
||||
if isinstance(name, unicode):
|
||||
name = name.encode('utf-8', 'replace')
|
||||
|
||||
dest = os.path.join(dest, name)
|
||||
if not os.path.exists(dest):
|
||||
os.mkdir(dest)
|
||||
|
||||
for sub_file in self.dir():
|
||||
sub_file.download(dest, delete_after_download)
|
||||
|
||||
if delete_after_download:
|
||||
self.delete()
|
||||
|
||||
def _download_file(self, dest='.', delete_after_download=False):
|
||||
response = self.client.request(
|
||||
'/files/%s/download' % self.id, raw=True, stream=True)
|
||||
|
||||
filename = re.match(
|
||||
'attachment; filename=(.*)',
|
||||
response.headers['content-disposition']).groups()[0]
|
||||
# If file name has spaces, it must have quotes around.
|
||||
filename = filename.strip('"')
|
||||
|
||||
with open(os.path.join(dest, filename), 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
|
||||
if delete_after_download:
|
||||
self.delete()
|
||||
|
||||
def delete(self):
|
||||
return self.client.request('/files/delete', method='POST',
|
||||
data={'file_ids': str(self.id)})
|
||||
|
||||
def move(self, parent_id):
|
||||
return self.client.request('/files/move', method='POST',
|
||||
data={'file_ids': str(self.id), 'parent_id': str(parent_id)})
|
||||
|
||||
def rename(self, name):
|
||||
return self.client.request('/files/rename', method='POST',
|
||||
data={'file_id': str(self.id), 'name': str(name)})
|
||||
|
||||
|
||||
class _Transfer(_BaseResource):
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
d = cls.client.request('/transfers/list')
|
||||
transfers = d['transfers']
|
||||
return [cls(t) for t in transfers]
|
||||
|
||||
@classmethod
|
||||
def get(cls, id):
|
||||
d = cls.client.request('/transfers/%i' % id, method='GET')
|
||||
t = d['transfer']
|
||||
return cls(t)
|
||||
|
||||
@classmethod
|
||||
def add_url(cls, url, parent_id=0, extract=False, callback_url=None):
|
||||
d = cls.client.request('/transfers/add', method='POST', data=dict(
|
||||
url=url, save_parent_id=parent_id, extract=extract,
|
||||
callback_url=callback_url))
|
||||
t = d['transfer']
|
||||
return cls(t)
|
||||
|
||||
@classmethod
|
||||
def add_torrent(cls, path, parent_id=0, extract=False, callback_url=None):
|
||||
with open(path) as f:
|
||||
files = {'file': f}
|
||||
d = cls.client.request('/files/upload', method='POST', files=files,
|
||||
data=dict(save_parent_id=parent_id,
|
||||
extract=extract,
|
||||
callback_url=callback_url))
|
||||
t = d['transfer']
|
||||
return cls(t)
|
||||
|
||||
@classmethod
|
||||
def clean(cls):
|
||||
return cls.client.request('/transfers/clean', method='POST')
|
||||
|
||||
|
||||
class _Account(_BaseResource):
|
||||
|
||||
@classmethod
|
||||
def info(cls):
|
||||
return cls.client.request('/account/info', method='GET')
|
||||
|
||||
@classmethod
|
||||
def settings(cls):
|
||||
return cls.client.request('/account/settings', method='GET')
|
||||
@@ -13,7 +13,7 @@ Requests is an HTTP library, written in Python, for human beings. Basic GET
|
||||
usage:
|
||||
|
||||
>>> import requests
|
||||
>>> r = requests.get('http://python.org')
|
||||
>>> r = requests.get('https://www.python.org')
|
||||
>>> r.status_code
|
||||
200
|
||||
>>> 'Python is a programming language' in r.content
|
||||
@@ -22,7 +22,7 @@ usage:
|
||||
... or POST:
|
||||
|
||||
>>> payload = dict(key1='value1', key2='value2')
|
||||
>>> r = requests.post("http://httpbin.org/post", data=payload)
|
||||
>>> r = requests.post('http://httpbin.org/post', data=payload)
|
||||
>>> print(r.text)
|
||||
{
|
||||
...
|
||||
@@ -42,8 +42,8 @@ is at <http://python-requests.org>.
|
||||
"""
|
||||
|
||||
__title__ = 'requests'
|
||||
__version__ = '2.4.0'
|
||||
__build__ = 0x020400
|
||||
__version__ = '2.5.1'
|
||||
__build__ = 0x020501
|
||||
__author__ = 'Kenneth Reitz'
|
||||
__license__ = 'Apache 2.0'
|
||||
__copyright__ = 'Copyright 2014 Kenneth Reitz'
|
||||
|
||||
@@ -15,19 +15,21 @@ from .packages.urllib3 import Retry
|
||||
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from .packages.urllib3.response import HTTPResponse
|
||||
from .packages.urllib3.util import Timeout as TimeoutSauce
|
||||
from .compat import urlparse, basestring, urldefrag
|
||||
from .compat import urlparse, basestring
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
|
||||
prepend_scheme_if_needed, get_auth_from_url)
|
||||
prepend_scheme_if_needed, get_auth_from_url, urldefragauth)
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .packages.urllib3.exceptions import ConnectTimeoutError
|
||||
from .packages.urllib3.exceptions import HTTPError as _HTTPError
|
||||
from .packages.urllib3.exceptions import MaxRetryError
|
||||
from .packages.urllib3.exceptions import ProxyError as _ProxyError
|
||||
from .packages.urllib3.exceptions import ProtocolError
|
||||
from .packages.urllib3.exceptions import ReadTimeoutError
|
||||
from .packages.urllib3.exceptions import SSLError as _SSLError
|
||||
from .packages.urllib3.exceptions import ResponseError
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
|
||||
ProxyError)
|
||||
ProxyError, RetryError)
|
||||
from .auth import _basic_auth_str
|
||||
|
||||
DEFAULT_POOLBLOCK = False
|
||||
@@ -59,8 +61,12 @@ class HTTPAdapter(BaseAdapter):
|
||||
:param pool_connections: The number of urllib3 connection pools to cache.
|
||||
:param pool_maxsize: The maximum number of connections to save in the pool.
|
||||
:param int max_retries: The maximum number of retries each connection
|
||||
should attempt. Note, this applies only to failed connections and
|
||||
timeouts, never to requests where the server returns a response.
|
||||
should attempt. Note, this applies only to failed DNS lookups, socket
|
||||
connections and connection timeouts, never to requests where data has
|
||||
made it to the server. By default, Requests does not retry failed
|
||||
connections. If you need granular control over the conditions under
|
||||
which we retry a request, import urllib3's ``Retry`` class and pass
|
||||
that instead.
|
||||
:param pool_block: Whether the connection pool should block for connections.
|
||||
|
||||
Usage::
|
||||
@@ -76,7 +82,10 @@ class HTTPAdapter(BaseAdapter):
|
||||
def __init__(self, pool_connections=DEFAULT_POOLSIZE,
|
||||
pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,
|
||||
pool_block=DEFAULT_POOLBLOCK):
|
||||
self.max_retries = max_retries
|
||||
if max_retries == DEFAULT_RETRIES:
|
||||
self.max_retries = Retry(0, read=False)
|
||||
else:
|
||||
self.max_retries = Retry.from_int(max_retries)
|
||||
self.config = {}
|
||||
self.proxy_manager = {}
|
||||
|
||||
@@ -122,7 +131,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
self._pool_block = block
|
||||
|
||||
self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
|
||||
block=block, **pool_kwargs)
|
||||
block=block, strict=True, **pool_kwargs)
|
||||
|
||||
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
||||
"""Return urllib3 ProxyManager for the given proxy.
|
||||
@@ -269,7 +278,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
proxy = proxies.get(scheme)
|
||||
|
||||
if proxy and scheme != 'https':
|
||||
url, _ = urldefrag(request.url)
|
||||
url = urldefragauth(request.url)
|
||||
else:
|
||||
url = request.path_url
|
||||
|
||||
@@ -316,8 +325,10 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
|
||||
:param stream: (optional) Whether to stream the request content.
|
||||
:param timeout: (optional) The timeout on the request.
|
||||
:type timeout: float or tuple (connect timeout, read timeout), eg (3.1, 20)
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float, or a (`connect timeout, read
|
||||
timeout <user/advanced.html#timeouts>`_) tuple.
|
||||
:type timeout: float or tuple
|
||||
:param verify: (optional) Whether to verify SSL certificates.
|
||||
:param cert: (optional) Any user-provided SSL certificate to be trusted.
|
||||
:param proxies: (optional) The proxies dictionary to apply to the request.
|
||||
@@ -355,7 +366,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
assert_same_host=False,
|
||||
preload_content=False,
|
||||
decode_content=False,
|
||||
retries=Retry(self.max_retries, read=False),
|
||||
retries=self.max_retries,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
@@ -400,13 +411,16 @@ class HTTPAdapter(BaseAdapter):
|
||||
# All is well, return the connection to the pool.
|
||||
conn._put_conn(low_conn)
|
||||
|
||||
except socket.error as sockerr:
|
||||
raise ConnectionError(sockerr, request=request)
|
||||
except (ProtocolError, socket.error) as err:
|
||||
raise ConnectionError(err, request=request)
|
||||
|
||||
except MaxRetryError as e:
|
||||
if isinstance(e.reason, ConnectTimeoutError):
|
||||
raise ConnectTimeout(e, request=request)
|
||||
|
||||
if isinstance(e.reason, ResponseError):
|
||||
raise RetryError(e, request=request)
|
||||
|
||||
raise ConnectionError(e, request=request)
|
||||
|
||||
except _ProxyError as e:
|
||||
|
||||
@@ -22,12 +22,17 @@ def request(method, url, **kwargs):
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json data to send in the body of the :class:`Request`.
|
||||
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
|
||||
:param files: (optional) Dictionary of 'name': file-like-objects (or {'name': ('filename', fileobj)}) for multipart encoding upload.
|
||||
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': ('filename', fileobj)}``) for multipart encoding upload.
|
||||
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) Float describing the timeout of the request in seconds.
|
||||
:param timeout: (optional) How long to wait for the server to send data
|
||||
before giving up, as a float, or a (`connect timeout, read timeout
|
||||
<user/advanced.html#timeouts>`_) tuple.
|
||||
:type timeout: float or tuple
|
||||
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
|
||||
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
|
||||
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
||||
@@ -41,7 +46,12 @@ def request(method, url, **kwargs):
|
||||
"""
|
||||
|
||||
session = sessions.Session()
|
||||
return session.request(method=method, url=url, **kwargs)
|
||||
response = session.request(method=method, url=url, **kwargs)
|
||||
# By explicitly closing the session, we avoid leaving sockets open which
|
||||
# can trigger a ResourceWarning in some cases, and look like a memory leak
|
||||
# in others.
|
||||
session.close()
|
||||
return response
|
||||
|
||||
|
||||
def get(url, **kwargs):
|
||||
@@ -77,15 +87,16 @@ def head(url, **kwargs):
|
||||
return request('head', url, **kwargs)
|
||||
|
||||
|
||||
def post(url, data=None, **kwargs):
|
||||
def post(url, data=None, json=None, **kwargs):
|
||||
"""Sends a POST request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json data to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
return request('post', url, data=data, **kwargs)
|
||||
return request('post', url, data=data, json=json, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **kwargs):
|
||||
|
||||
@@ -17,6 +17,7 @@ from base64 import b64encode
|
||||
from .compat import urlparse, str
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .utils import parse_dict_header, to_native_string
|
||||
from .status_codes import codes
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
||||
CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
|
||||
@@ -66,6 +67,7 @@ class HTTPDigestAuth(AuthBase):
|
||||
self.nonce_count = 0
|
||||
self.chal = {}
|
||||
self.pos = None
|
||||
self.num_401_calls = 1
|
||||
|
||||
def build_digest_header(self, method, url):
|
||||
|
||||
@@ -150,6 +152,11 @@ class HTTPDigestAuth(AuthBase):
|
||||
|
||||
return 'Digest %s' % (base)
|
||||
|
||||
def handle_redirect(self, r, **kwargs):
|
||||
"""Reset num_401_calls counter on redirects."""
|
||||
if r.is_redirect:
|
||||
self.num_401_calls = 1
|
||||
|
||||
def handle_401(self, r, **kwargs):
|
||||
"""Takes the given response and tries digest-auth, if needed."""
|
||||
|
||||
@@ -162,7 +169,7 @@ class HTTPDigestAuth(AuthBase):
|
||||
|
||||
if 'digest' in s_auth.lower() and num_401_calls < 2:
|
||||
|
||||
setattr(self, 'num_401_calls', num_401_calls + 1)
|
||||
self.num_401_calls += 1
|
||||
pat = re.compile(r'digest ', flags=re.IGNORECASE)
|
||||
self.chal = parse_dict_header(pat.sub('', s_auth, count=1))
|
||||
|
||||
@@ -182,7 +189,7 @@ class HTTPDigestAuth(AuthBase):
|
||||
|
||||
return _r
|
||||
|
||||
setattr(self, 'num_401_calls', 1)
|
||||
self.num_401_calls = 1
|
||||
return r
|
||||
|
||||
def __call__(self, r):
|
||||
@@ -192,6 +199,11 @@ class HTTPDigestAuth(AuthBase):
|
||||
try:
|
||||
self.pos = r.body.tell()
|
||||
except AttributeError:
|
||||
pass
|
||||
# In the case of HTTPDigestAuth being reused and the body of
|
||||
# the previous request was a file-like object, pos has the
|
||||
# file position of the previous body. Ensure it's set to
|
||||
# None.
|
||||
self.pos = None
|
||||
r.register_hook('response', self.handle_401)
|
||||
r.register_hook('response', self.handle_redirect)
|
||||
return r
|
||||
|
||||
@@ -76,7 +76,7 @@ is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess.
|
||||
try:
|
||||
import simplejson as json
|
||||
except (ImportError, SyntaxError):
|
||||
# simplejson does not support Python 3.2, it thows a SyntaxError
|
||||
# simplejson does not support Python 3.2, it throws a SyntaxError
|
||||
# because of u'...' Unicode literals.
|
||||
import json
|
||||
|
||||
|
||||
@@ -46,15 +46,16 @@ class SSLError(ConnectionError):
|
||||
class Timeout(RequestException):
|
||||
"""The request timed out.
|
||||
|
||||
Catching this error will catch both :exc:`ConnectTimeout` and
|
||||
:exc:`ReadTimeout` errors.
|
||||
Catching this error will catch both
|
||||
:exc:`~requests.exceptions.ConnectTimeout` and
|
||||
:exc:`~requests.exceptions.ReadTimeout` errors.
|
||||
"""
|
||||
|
||||
|
||||
class ConnectTimeout(ConnectionError, Timeout):
|
||||
"""The request timed out while trying to connect to the server.
|
||||
"""The request timed out while trying to connect to the remote server.
|
||||
|
||||
Requests that produce this error are safe to retry
|
||||
Requests that produced this error are safe to retry.
|
||||
"""
|
||||
|
||||
|
||||
@@ -88,3 +89,11 @@ class ChunkedEncodingError(RequestException):
|
||||
|
||||
class ContentDecodingError(RequestException, BaseHTTPError):
|
||||
"""Failed to decode response content"""
|
||||
|
||||
|
||||
class StreamConsumedError(RequestException, TypeError):
|
||||
"""The content for this response was already consumed"""
|
||||
|
||||
|
||||
class RetryError(RequestException):
|
||||
"""Custom retries logic failed"""
|
||||
|
||||
@@ -20,10 +20,10 @@ from .packages.urllib3.fields import RequestField
|
||||
from .packages.urllib3.filepost import encode_multipart_formdata
|
||||
from .packages.urllib3.util import parse_url
|
||||
from .packages.urllib3.exceptions import (
|
||||
DecodeError, ReadTimeoutError, ProtocolError)
|
||||
DecodeError, ReadTimeoutError, ProtocolError, LocationParseError)
|
||||
from .exceptions import (
|
||||
HTTPError, RequestException, MissingSchema, InvalidURL,
|
||||
ChunkedEncodingError, ContentDecodingError, ConnectionError)
|
||||
HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError,
|
||||
ContentDecodingError, ConnectionError, StreamConsumedError)
|
||||
from .utils import (
|
||||
guess_filename, get_auth_from_url, requote_uri,
|
||||
stream_decode_response_unicode, to_key_val_list, parse_header_links,
|
||||
@@ -46,6 +46,8 @@ DEFAULT_REDIRECT_LIMIT = 30
|
||||
CONTENT_CHUNK_SIZE = 10 * 1024
|
||||
ITER_CHUNK_SIZE = 512
|
||||
|
||||
json_dumps = json.dumps
|
||||
|
||||
|
||||
class RequestEncodingMixin(object):
|
||||
@property
|
||||
@@ -189,7 +191,8 @@ class Request(RequestHooksMixin):
|
||||
:param url: URL to send.
|
||||
:param headers: dictionary of headers to send.
|
||||
:param files: dictionary of {filename: fileobject} files to multipart upload.
|
||||
:param data: the body to attach the request. If a dictionary is provided, form-encoding will take place.
|
||||
:param data: the body to attach to the request. If a dictionary is provided, form-encoding will take place.
|
||||
:param json: json for the body to attach to the request (if data is not specified).
|
||||
:param params: dictionary of URL parameters to append to the URL.
|
||||
:param auth: Auth handler or (user, pass) tuple.
|
||||
:param cookies: dictionary or CookieJar of cookies to attach to this request.
|
||||
@@ -212,7 +215,8 @@ class Request(RequestHooksMixin):
|
||||
params=None,
|
||||
auth=None,
|
||||
cookies=None,
|
||||
hooks=None):
|
||||
hooks=None,
|
||||
json=None):
|
||||
|
||||
# Default empty dicts for dict params.
|
||||
data = [] if data is None else data
|
||||
@@ -230,6 +234,7 @@ class Request(RequestHooksMixin):
|
||||
self.headers = headers
|
||||
self.files = files
|
||||
self.data = data
|
||||
self.json = json
|
||||
self.params = params
|
||||
self.auth = auth
|
||||
self.cookies = cookies
|
||||
@@ -246,6 +251,7 @@ class Request(RequestHooksMixin):
|
||||
headers=self.headers,
|
||||
files=self.files,
|
||||
data=self.data,
|
||||
json=self.json,
|
||||
params=self.params,
|
||||
auth=self.auth,
|
||||
cookies=self.cookies,
|
||||
@@ -289,14 +295,15 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
self.hooks = default_hooks()
|
||||
|
||||
def prepare(self, method=None, url=None, headers=None, files=None,
|
||||
data=None, params=None, auth=None, cookies=None, hooks=None):
|
||||
data=None, params=None, auth=None, cookies=None, hooks=None,
|
||||
json=None):
|
||||
"""Prepares the entire request with the given parameters."""
|
||||
|
||||
self.prepare_method(method)
|
||||
self.prepare_url(url, params)
|
||||
self.prepare_headers(headers)
|
||||
self.prepare_cookies(cookies)
|
||||
self.prepare_body(data, files)
|
||||
self.prepare_body(data, files, json)
|
||||
self.prepare_auth(auth, url)
|
||||
# Note that prepare_auth must be last to enable authentication schemes
|
||||
# such as OAuth to work on a fully prepared request.
|
||||
@@ -326,21 +333,27 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
def prepare_url(self, url, params):
|
||||
"""Prepares the given HTTP URL."""
|
||||
#: Accept objects that have string representations.
|
||||
try:
|
||||
url = unicode(url)
|
||||
except NameError:
|
||||
# We're on Python 3.
|
||||
url = str(url)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
#: We're unable to blindy call unicode/str functions
|
||||
#: as this will include the bytestring indicator (b'')
|
||||
#: on python 3.x.
|
||||
#: https://github.com/kennethreitz/requests/pull/2238
|
||||
if isinstance(url, bytes):
|
||||
url = url.decode('utf8')
|
||||
else:
|
||||
url = unicode(url) if is_py2 else str(url)
|
||||
|
||||
# Don't do any URL preparation for oddball schemes
|
||||
# Don't do any URL preparation for non-HTTP schemes like `mailto`,
|
||||
# `data` etc to work around exceptions from `url_parse`, which
|
||||
# handles RFC 3986 only.
|
||||
if ':' in url and not url.lower().startswith('http'):
|
||||
self.url = url
|
||||
return
|
||||
|
||||
# Support for unicode domain names and paths.
|
||||
scheme, auth, host, port, path, query, fragment = parse_url(url)
|
||||
try:
|
||||
scheme, auth, host, port, path, query, fragment = parse_url(url)
|
||||
except LocationParseError as e:
|
||||
raise InvalidURL(*e.args)
|
||||
|
||||
if not scheme:
|
||||
raise MissingSchema("Invalid URL {0!r}: No schema supplied. "
|
||||
@@ -397,7 +410,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
else:
|
||||
self.headers = CaseInsensitiveDict()
|
||||
|
||||
def prepare_body(self, data, files):
|
||||
def prepare_body(self, data, files, json=None):
|
||||
"""Prepares the given HTTP body data."""
|
||||
|
||||
# Check if file, fo, generator, iterator.
|
||||
@@ -408,6 +421,10 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
content_type = None
|
||||
length = None
|
||||
|
||||
if json is not None:
|
||||
content_type = 'application/json'
|
||||
body = json_dumps(json)
|
||||
|
||||
is_stream = all([
|
||||
hasattr(data, '__iter__'),
|
||||
not isinstance(data, (basestring, list, tuple, dict))
|
||||
@@ -433,7 +450,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
if files:
|
||||
(body, content_type) = self._encode_files(files, data)
|
||||
else:
|
||||
if data:
|
||||
if data and json is None:
|
||||
body = self._encode_params(data)
|
||||
if isinstance(data, basestring) or hasattr(data, 'read'):
|
||||
content_type = None
|
||||
@@ -443,7 +460,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
self.prepare_content_length(body)
|
||||
|
||||
# Add content-type if it wasn't explicitly provided.
|
||||
if (content_type) and (not 'content-type' in self.headers):
|
||||
if content_type and ('content-type' not in self.headers):
|
||||
self.headers['Content-Type'] = content_type
|
||||
|
||||
self.body = body
|
||||
@@ -457,7 +474,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
l = super_len(body)
|
||||
if l:
|
||||
self.headers['Content-Length'] = builtin_str(l)
|
||||
elif self.method not in ('GET', 'HEAD'):
|
||||
elif (self.method not in ('GET', 'HEAD')) and (self.headers.get('Content-Length') is None):
|
||||
self.headers['Content-Length'] = '0'
|
||||
|
||||
def prepare_auth(self, auth, url=''):
|
||||
@@ -600,7 +617,7 @@ class Response(object):
|
||||
def ok(self):
|
||||
try:
|
||||
self.raise_for_status()
|
||||
except RequestException:
|
||||
except HTTPError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -653,6 +670,8 @@ class Response(object):
|
||||
|
||||
self._content_consumed = True
|
||||
|
||||
if self._content_consumed and isinstance(self._content, bool):
|
||||
raise StreamConsumedError()
|
||||
# simulate reading small chunks of the content
|
||||
reused_chunks = iter_slices(self._content, chunk_size)
|
||||
|
||||
@@ -665,7 +684,7 @@ class Response(object):
|
||||
|
||||
return chunks
|
||||
|
||||
def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None):
|
||||
def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=None, delimiter=None):
|
||||
"""Iterates over the response data, one line at a time. When
|
||||
stream=True is set on the request, this avoids reading the
|
||||
content at once into memory for large responses.
|
||||
@@ -677,7 +696,11 @@ class Response(object):
|
||||
|
||||
if pending is not None:
|
||||
chunk = pending + chunk
|
||||
lines = chunk.splitlines()
|
||||
|
||||
if delimiter:
|
||||
lines = chunk.split(delimiter)
|
||||
else:
|
||||
lines = chunk.splitlines()
|
||||
|
||||
if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:
|
||||
pending = lines.pop()
|
||||
|
||||
@@ -57,7 +57,7 @@ del NullHandler
|
||||
|
||||
# Set security warning to only go off once by default.
|
||||
import warnings
|
||||
warnings.simplefilter('module', exceptions.SecurityWarning)
|
||||
warnings.simplefilter('always', exceptions.SecurityWarning)
|
||||
|
||||
def disable_warnings(category=exceptions.HTTPWarning):
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,7 @@ try: # Python 2.7+
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from .packages.ordered_dict import OrderedDict
|
||||
from .packages.six import itervalues
|
||||
from .packages.six import iterkeys, itervalues
|
||||
|
||||
|
||||
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
|
||||
@@ -85,8 +85,7 @@ class RecentlyUsedContainer(MutableMapping):
|
||||
def clear(self):
|
||||
with self.lock:
|
||||
# Copy pointers to all values, then wipe the mapping
|
||||
# under Python 2, this copies the list of values twice :-|
|
||||
values = list(self._container.values())
|
||||
values = list(itervalues(self._container))
|
||||
self._container.clear()
|
||||
|
||||
if self.dispose_func:
|
||||
@@ -95,7 +94,7 @@ class RecentlyUsedContainer(MutableMapping):
|
||||
|
||||
def keys(self):
|
||||
with self.lock:
|
||||
return self._container.keys()
|
||||
return list(iterkeys(self._container))
|
||||
|
||||
|
||||
class HTTPHeaderDict(MutableMapping):
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import socket
|
||||
from socket import timeout as SocketTimeout
|
||||
import warnings
|
||||
from .packages import six
|
||||
|
||||
try: # Python 3
|
||||
from http.client import HTTPConnection as _HTTPConnection, HTTPException
|
||||
@@ -26,12 +27,20 @@ except (ImportError, AttributeError): # Platform-specific: No SSL.
|
||||
pass
|
||||
|
||||
|
||||
try: # Python 3:
|
||||
# Not a no-op, we're adding this to the namespace so it can be imported.
|
||||
ConnectionError = ConnectionError
|
||||
except NameError: # Python 2:
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
from .exceptions import (
|
||||
ConnectTimeoutError,
|
||||
SystemTimeWarning,
|
||||
SecurityWarning,
|
||||
)
|
||||
from .packages.ssl_match_hostname import match_hostname
|
||||
from .packages import six
|
||||
|
||||
from .util.ssl_ import (
|
||||
resolve_cert_reqs,
|
||||
@@ -40,8 +49,8 @@ from .util.ssl_ import (
|
||||
assert_fingerprint,
|
||||
)
|
||||
|
||||
from .util import connection
|
||||
|
||||
from .util import connection
|
||||
|
||||
port_by_scheme = {
|
||||
'http': 80,
|
||||
@@ -233,8 +242,15 @@ class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
self.assert_fingerprint)
|
||||
elif resolved_cert_reqs != ssl.CERT_NONE \
|
||||
and self.assert_hostname is not False:
|
||||
match_hostname(self.sock.getpeercert(),
|
||||
self.assert_hostname or hostname)
|
||||
cert = self.sock.getpeercert()
|
||||
if not cert.get('subjectAltName', ()):
|
||||
warnings.warn((
|
||||
'Certificate has no `subjectAltName`, falling back to check for a `commonName` for now. '
|
||||
'This feature is being removed by major browsers and deprecated by RFC 2818. '
|
||||
'(See https://github.com/shazow/urllib3/issues/497 for details.)'),
|
||||
SecurityWarning
|
||||
)
|
||||
match_hostname(cert, self.assert_hostname or hostname)
|
||||
|
||||
self.is_verified = (resolved_cert_reqs == ssl.CERT_REQUIRED
|
||||
or self.assert_fingerprint is not None)
|
||||
|
||||
@@ -32,7 +32,7 @@ from .connection import (
|
||||
port_by_scheme,
|
||||
DummyConnection,
|
||||
HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection,
|
||||
HTTPException, BaseSSLError,
|
||||
HTTPException, BaseSSLError, ConnectionError
|
||||
)
|
||||
from .request import RequestMethods
|
||||
from .response import HTTPResponse
|
||||
@@ -278,6 +278,23 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
# can be removed later
|
||||
return Timeout.from_float(timeout)
|
||||
|
||||
def _raise_timeout(self, err, url, timeout_value):
|
||||
"""Is the error actually a timeout? Will raise a ReadTimeout or pass"""
|
||||
|
||||
if isinstance(err, SocketTimeout):
|
||||
raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value)
|
||||
|
||||
# See the above comment about EAGAIN in Python 3. In Python 2 we have
|
||||
# to specifically catch it and throw the timeout error
|
||||
if hasattr(err, 'errno') and err.errno in _blocking_errnos:
|
||||
raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value)
|
||||
|
||||
# Catch possible read timeouts thrown as SSL errors. If not the
|
||||
# case, rethrow the original. We need to do this because of:
|
||||
# http://bugs.python.org/issue10272
|
||||
if 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python 2.6
|
||||
raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value)
|
||||
|
||||
def _make_request(self, conn, method, url, timeout=_Default,
|
||||
**httplib_request_kw):
|
||||
"""
|
||||
@@ -301,7 +318,12 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
conn.timeout = timeout_obj.connect_timeout
|
||||
|
||||
# Trigger any extra validation we need to do.
|
||||
self._validate_conn(conn)
|
||||
try:
|
||||
self._validate_conn(conn)
|
||||
except (SocketTimeout, BaseSSLError) as e:
|
||||
# Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.
|
||||
self._raise_timeout(err=e, url=url, timeout_value=conn.timeout)
|
||||
raise
|
||||
|
||||
# conn.request() calls httplib.*.request, not the method in
|
||||
# urllib3.request. It also calls makefile (recv) on the socket.
|
||||
@@ -331,28 +353,8 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
httplib_response = conn.getresponse(buffering=True)
|
||||
except TypeError: # Python 2.6 and older
|
||||
httplib_response = conn.getresponse()
|
||||
except SocketTimeout:
|
||||
raise ReadTimeoutError(
|
||||
self, url, "Read timed out. (read timeout=%s)" % read_timeout)
|
||||
|
||||
except BaseSSLError as e:
|
||||
# Catch possible read timeouts thrown as SSL errors. If not the
|
||||
# case, rethrow the original. We need to do this because of:
|
||||
# http://bugs.python.org/issue10272
|
||||
if 'timed out' in str(e) or \
|
||||
'did not complete (read)' in str(e): # Python 2.6
|
||||
raise ReadTimeoutError(
|
||||
self, url, "Read timed out. (read timeout=%s)" % read_timeout)
|
||||
|
||||
raise
|
||||
|
||||
except SocketError as e: # Platform-specific: Python 2
|
||||
# See the above comment about EAGAIN in Python 3. In Python 2 we
|
||||
# have to specifically catch it and throw the timeout error
|
||||
if e.errno in _blocking_errnos:
|
||||
raise ReadTimeoutError(
|
||||
self, url, "Read timed out. (read timeout=%s)" % read_timeout)
|
||||
|
||||
except (SocketTimeout, BaseSSLError, SocketError) as e:
|
||||
self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
|
||||
raise
|
||||
|
||||
# AppEngine doesn't have a version attr.
|
||||
@@ -537,12 +539,15 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
raise EmptyPoolError(self, "No pool connections are available.")
|
||||
|
||||
except (BaseSSLError, CertificateError) as e:
|
||||
# Release connection unconditionally because there is no way to
|
||||
# close it externally in case of exception.
|
||||
release_conn = True
|
||||
# Close the connection. If a connection is reused on which there
|
||||
# was a Certificate error, the next request will certainly raise
|
||||
# another Certificate error.
|
||||
if conn:
|
||||
conn.close()
|
||||
conn = None
|
||||
raise SSLError(e)
|
||||
|
||||
except (TimeoutError, HTTPException, SocketError) as e:
|
||||
except (TimeoutError, HTTPException, SocketError, ConnectionError) as e:
|
||||
if conn:
|
||||
# Discard the connection for these exceptions. It will be
|
||||
# be replaced during the next _get_conn() call.
|
||||
@@ -725,8 +730,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
|
||||
warnings.warn((
|
||||
'Unverified HTTPS request is being made. '
|
||||
'Adding certificate verification is strongly advised. See: '
|
||||
'https://urllib3.readthedocs.org/en/latest/security.html '
|
||||
'(This warning will only appear once by default.)'),
|
||||
'https://urllib3.readthedocs.org/en/latest/security.html'),
|
||||
InsecureRequestWarning)
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Now you can use :mod:`urllib3` as you normally would, and it will support SNI
|
||||
when the required modules are installed.
|
||||
|
||||
Activating this module also has the positive side effect of disabling SSL/TLS
|
||||
encryption in Python 2 (see `CRIME attack`_).
|
||||
compression in Python 2 (see `CRIME attack`_).
|
||||
|
||||
If you want to configure the default list of supported cipher suites, you can
|
||||
set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable.
|
||||
@@ -70,9 +70,14 @@ HAS_SNI = SUBJ_ALT_NAME_SUPPORT
|
||||
# Map from urllib3 to PyOpenSSL compatible parameter-values.
|
||||
_openssl_versions = {
|
||||
ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
|
||||
ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD,
|
||||
ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
|
||||
}
|
||||
|
||||
try:
|
||||
_openssl_versions.update({ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD})
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
_openssl_verify = {
|
||||
ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
|
||||
ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
|
||||
@@ -199,8 +204,21 @@ class WrappedSocket(object):
|
||||
def settimeout(self, timeout):
|
||||
return self.socket.settimeout(timeout)
|
||||
|
||||
def _send_until_done(self, data):
|
||||
while True:
|
||||
try:
|
||||
return self.connection.send(data)
|
||||
except OpenSSL.SSL.WantWriteError:
|
||||
_, wlist, _ = select.select([], [self.socket], [],
|
||||
self.socket.gettimeout())
|
||||
if not wlist:
|
||||
raise timeout()
|
||||
continue
|
||||
|
||||
def sendall(self, data):
|
||||
return self.connection.sendall(data)
|
||||
while len(data):
|
||||
sent = self._send_until_done(data)
|
||||
data = data[sent:]
|
||||
|
||||
def close(self):
|
||||
if self._makefile_refs < 1:
|
||||
@@ -248,6 +266,7 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ssl_version=None):
|
||||
ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version])
|
||||
if certfile:
|
||||
keyfile = keyfile or certfile # Match behaviour of the normal python ssl library
|
||||
ctx.use_certificate_file(certfile)
|
||||
if keyfile:
|
||||
ctx.use_privatekey_file(keyfile)
|
||||
|
||||
@@ -72,11 +72,8 @@ class MaxRetryError(RequestError):
|
||||
def __init__(self, pool, url, reason=None):
|
||||
self.reason = reason
|
||||
|
||||
message = "Max retries exceeded with url: %s" % url
|
||||
if reason:
|
||||
message += " (Caused by %r)" % reason
|
||||
else:
|
||||
message += " (Caused by redirect)"
|
||||
message = "Max retries exceeded with url: %s (Caused by %r)" % (
|
||||
url, reason)
|
||||
|
||||
RequestError.__init__(self, pool, url, message)
|
||||
|
||||
@@ -141,6 +138,12 @@ class LocationParseError(LocationValueError):
|
||||
self.location = location
|
||||
|
||||
|
||||
class ResponseError(HTTPError):
|
||||
"Used as a container for an error reason supplied in a MaxRetryError."
|
||||
GENERIC_ERROR = 'too many error responses'
|
||||
SPECIFIC_ERROR = 'too many {status_code} error responses'
|
||||
|
||||
|
||||
class SecurityWarning(HTTPWarning):
|
||||
"Warned when perfoming security reducing actions"
|
||||
pass
|
||||
|
||||
@@ -118,18 +118,24 @@ class RequestMethods(object):
|
||||
which is used to compose the body of the request. The random boundary
|
||||
string can be explicitly set with the ``multipart_boundary`` parameter.
|
||||
"""
|
||||
if encode_multipart:
|
||||
body, content_type = encode_multipart_formdata(
|
||||
fields or {}, boundary=multipart_boundary)
|
||||
else:
|
||||
body, content_type = (urlencode(fields or {}),
|
||||
'application/x-www-form-urlencoded')
|
||||
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
headers_ = {'Content-Type': content_type}
|
||||
headers_.update(headers)
|
||||
extra_kw = {'headers': {}}
|
||||
|
||||
return self.urlopen(method, url, body=body, headers=headers_,
|
||||
**urlopen_kw)
|
||||
if fields:
|
||||
if 'body' in urlopen_kw:
|
||||
raise TypeError('request got values for both \'fields\' and \'body\', can only specify one.')
|
||||
|
||||
if encode_multipart:
|
||||
body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary)
|
||||
else:
|
||||
body, content_type = urlencode(fields), 'application/x-www-form-urlencoded'
|
||||
|
||||
extra_kw['body'] = body
|
||||
extra_kw['headers'] = {'Content-Type': content_type}
|
||||
|
||||
extra_kw['headers'].update(headers)
|
||||
extra_kw.update(urlopen_kw)
|
||||
|
||||
return self.urlopen(method, url, **extra_kw)
|
||||
|
||||
@@ -2,10 +2,11 @@ import time
|
||||
import logging
|
||||
|
||||
from ..exceptions import (
|
||||
ProtocolError,
|
||||
ConnectTimeoutError,
|
||||
ReadTimeoutError,
|
||||
MaxRetryError,
|
||||
ProtocolError,
|
||||
ReadTimeoutError,
|
||||
ResponseError,
|
||||
)
|
||||
from ..packages import six
|
||||
|
||||
@@ -36,7 +37,6 @@ class Retry(object):
|
||||
Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
|
||||
retries are disabled, in which case the causing exception will be raised.
|
||||
|
||||
|
||||
:param int total:
|
||||
Total number of retries to allow. Takes precedence over other counts.
|
||||
|
||||
@@ -184,8 +184,8 @@ class Retry(object):
|
||||
return isinstance(err, ConnectTimeoutError)
|
||||
|
||||
def _is_read_error(self, err):
|
||||
""" Errors that occur after the request has been started, so we can't
|
||||
assume that the server did not process any of it.
|
||||
""" Errors that occur after the request has been started, so we should
|
||||
assume that the server began processing it.
|
||||
"""
|
||||
return isinstance(err, (ReadTimeoutError, ProtocolError))
|
||||
|
||||
@@ -198,8 +198,7 @@ class Retry(object):
|
||||
return self.status_forcelist and status_code in self.status_forcelist
|
||||
|
||||
def is_exhausted(self):
|
||||
""" Are we out of retries?
|
||||
"""
|
||||
""" Are we out of retries? """
|
||||
retry_counts = (self.total, self.connect, self.read, self.redirect)
|
||||
retry_counts = list(filter(None, retry_counts))
|
||||
if not retry_counts:
|
||||
@@ -230,6 +229,7 @@ class Retry(object):
|
||||
connect = self.connect
|
||||
read = self.read
|
||||
redirect = self.redirect
|
||||
cause = 'unknown'
|
||||
|
||||
if error and self._is_connection_error(error):
|
||||
# Connect retry?
|
||||
@@ -251,10 +251,16 @@ class Retry(object):
|
||||
# Redirect retry?
|
||||
if redirect is not None:
|
||||
redirect -= 1
|
||||
cause = 'too many redirects'
|
||||
|
||||
else:
|
||||
# FIXME: Nothing changed, scenario doesn't make sense.
|
||||
# Incrementing because of a server error like a 500 in
|
||||
# status_forcelist and a the given method is in the whitelist
|
||||
_observed_errors += 1
|
||||
cause = ResponseError.GENERIC_ERROR
|
||||
if response and response.status:
|
||||
cause = ResponseError.SPECIFIC_ERROR.format(
|
||||
status_code=response.status)
|
||||
|
||||
new_retry = self.new(
|
||||
total=total,
|
||||
@@ -262,7 +268,7 @@ class Retry(object):
|
||||
_observed_errors=_observed_errors)
|
||||
|
||||
if new_retry.is_exhausted():
|
||||
raise MaxRetryError(_pool, url, error)
|
||||
raise MaxRetryError(_pool, url, error or ResponseError(cause))
|
||||
|
||||
log.debug("Incremented Retry for (url='%s'): %r" % (url, new_retry))
|
||||
|
||||
|
||||
@@ -4,18 +4,84 @@ from hashlib import md5, sha1
|
||||
from ..exceptions import SSLError
|
||||
|
||||
|
||||
try: # Test for SSL features
|
||||
SSLContext = None
|
||||
HAS_SNI = False
|
||||
SSLContext = None
|
||||
HAS_SNI = False
|
||||
create_default_context = None
|
||||
|
||||
import ssl
|
||||
import errno
|
||||
import ssl
|
||||
|
||||
try: # Test for SSL features
|
||||
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
|
||||
from ssl import SSLContext # Modern SSL?
|
||||
from ssl import HAS_SNI # Has SNI?
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION
|
||||
except ImportError:
|
||||
OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000
|
||||
OP_NO_COMPRESSION = 0x20000
|
||||
|
||||
try:
|
||||
from ssl import _DEFAULT_CIPHERS
|
||||
except ImportError:
|
||||
_DEFAULT_CIPHERS = (
|
||||
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
|
||||
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:ECDH+RC4:'
|
||||
'DH+RC4:RSA+RC4:!aNULL:!eNULL:!MD5'
|
||||
)
|
||||
|
||||
try:
|
||||
from ssl import SSLContext # Modern SSL?
|
||||
except ImportError:
|
||||
import sys
|
||||
|
||||
class SSLContext(object): # Platform-specific: Python 2 & 3.1
|
||||
supports_set_ciphers = sys.version_info >= (2, 7)
|
||||
|
||||
def __init__(self, protocol_version):
|
||||
self.protocol = protocol_version
|
||||
# Use default values from a real SSLContext
|
||||
self.check_hostname = False
|
||||
self.verify_mode = ssl.CERT_NONE
|
||||
self.ca_certs = None
|
||||
self.options = 0
|
||||
self.certfile = None
|
||||
self.keyfile = None
|
||||
self.ciphers = None
|
||||
|
||||
def load_cert_chain(self, certfile, keyfile):
|
||||
self.certfile = certfile
|
||||
self.keyfile = keyfile
|
||||
|
||||
def load_verify_locations(self, location):
|
||||
self.ca_certs = location
|
||||
|
||||
def set_ciphers(self, cipher_suite):
|
||||
if not self.supports_set_ciphers:
|
||||
raise TypeError(
|
||||
'Your version of Python does not support setting '
|
||||
'a custom cipher suite. Please upgrade to Python '
|
||||
'2.7, 3.2, or later if you need this functionality.'
|
||||
)
|
||||
self.ciphers = cipher_suite
|
||||
|
||||
def wrap_socket(self, socket, server_hostname=None):
|
||||
kwargs = {
|
||||
'keyfile': self.keyfile,
|
||||
'certfile': self.certfile,
|
||||
'ca_certs': self.ca_certs,
|
||||
'cert_reqs': self.verify_mode,
|
||||
'ssl_version': self.protocol,
|
||||
}
|
||||
if self.supports_set_ciphers: # Platform-specific: Python 2.7+
|
||||
return wrap_socket(socket, ciphers=self.ciphers, **kwargs)
|
||||
else: # Platform-specific: Python 2.6
|
||||
return wrap_socket(socket, **kwargs)
|
||||
|
||||
|
||||
def assert_fingerprint(cert, fingerprint):
|
||||
"""
|
||||
Checks if given fingerprint matches the supplied certificate.
|
||||
@@ -91,42 +157,98 @@ def resolve_ssl_version(candidate):
|
||||
return candidate
|
||||
|
||||
|
||||
if SSLContext is not None: # Python 3.2+
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None):
|
||||
"""
|
||||
All arguments except `server_hostname` have the same meaning as for
|
||||
:func:`ssl.wrap_socket`
|
||||
def create_urllib3_context(ssl_version=None, cert_reqs=ssl.CERT_REQUIRED,
|
||||
options=None, ciphers=None):
|
||||
"""All arguments have the same meaning as ``ssl_wrap_socket``.
|
||||
|
||||
:param server_hostname:
|
||||
Hostname of the expected certificate
|
||||
"""
|
||||
context = SSLContext(ssl_version)
|
||||
context.verify_mode = cert_reqs
|
||||
By default, this function does a lot of the same work that
|
||||
``ssl.create_default_context`` does on Python 3.4+. It:
|
||||
|
||||
# Disable TLS compression to migitate CRIME attack (issue #309)
|
||||
OP_NO_COMPRESSION = 0x20000
|
||||
context.options |= OP_NO_COMPRESSION
|
||||
- Disables SSLv2, SSLv3, and compression
|
||||
- Sets a restricted set of server ciphers
|
||||
|
||||
if ca_certs:
|
||||
try:
|
||||
context.load_verify_locations(ca_certs)
|
||||
# Py32 raises IOError
|
||||
# Py33 raises FileNotFoundError
|
||||
except Exception as e: # Reraise as SSLError
|
||||
If you wish to enable SSLv3, you can do::
|
||||
|
||||
from urllib3.util import ssl_
|
||||
context = ssl_.create_urllib3_context()
|
||||
context.options &= ~ssl_.OP_NO_SSLv3
|
||||
|
||||
You can do the same to enable compression (substituting ``COMPRESSION``
|
||||
for ``SSLv3`` in the last line above).
|
||||
|
||||
:param ssl_version:
|
||||
The desired protocol version to use. This will default to
|
||||
PROTOCOL_SSLv23 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support.
|
||||
:param cert_reqs:
|
||||
Whether to require the certificate verification. This defaults to
|
||||
``ssl.CERT_REQUIRED``.
|
||||
:param options:
|
||||
Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``,
|
||||
``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``.
|
||||
:param ciphers:
|
||||
Which cipher suites to allow the server to select.
|
||||
:returns:
|
||||
Constructed SSLContext object with specified options
|
||||
:rtype: SSLContext
|
||||
"""
|
||||
context = SSLContext(ssl_version or ssl.PROTOCOL_SSLv23)
|
||||
|
||||
if options is None:
|
||||
options = 0
|
||||
# SSLv2 is easily broken and is considered harmful and dangerous
|
||||
options |= OP_NO_SSLv2
|
||||
# SSLv3 has several problems and is now dangerous
|
||||
options |= OP_NO_SSLv3
|
||||
# Disable compression to prevent CRIME attacks for OpenSSL 1.0+
|
||||
# (issue #309)
|
||||
options |= OP_NO_COMPRESSION
|
||||
|
||||
context.options |= options
|
||||
|
||||
if getattr(context, 'supports_set_ciphers', True): # Platform-specific: Python 2.6
|
||||
context.set_ciphers(ciphers or _DEFAULT_CIPHERS)
|
||||
|
||||
context.verify_mode = cert_reqs
|
||||
if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2
|
||||
context.check_hostname = (context.verify_mode == ssl.CERT_REQUIRED)
|
||||
return context
|
||||
|
||||
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None, ciphers=None, ssl_context=None):
|
||||
"""
|
||||
All arguments except for server_hostname and ssl_context have the same
|
||||
meaning as they do when using :func:`ssl.wrap_socket`.
|
||||
|
||||
:param server_hostname:
|
||||
When SNI is supported, the expected hostname of the certificate
|
||||
:param ssl_context:
|
||||
A pre-made :class:`SSLContext` object. If none is provided, one will
|
||||
be created using :func:`create_urllib3_context`.
|
||||
:param ciphers:
|
||||
A string of ciphers we wish the client to support. This is not
|
||||
supported on Python 2.6 as the ssl module does not support it.
|
||||
"""
|
||||
context = ssl_context
|
||||
if context is None:
|
||||
context = create_urllib3_context(ssl_version, cert_reqs,
|
||||
ciphers=ciphers)
|
||||
|
||||
if ca_certs:
|
||||
try:
|
||||
context.load_verify_locations(ca_certs)
|
||||
except IOError as e: # Platform-specific: Python 2.6, 2.7, 3.2
|
||||
raise SSLError(e)
|
||||
# Py33 raises FileNotFoundError which subclasses OSError
|
||||
# These are not equivalent unless we check the errno attribute
|
||||
except OSError as e: # Platform-specific: Python 3.3 and beyond
|
||||
if e.errno == errno.ENOENT:
|
||||
raise SSLError(e)
|
||||
if certfile:
|
||||
# FIXME: This block needs a test.
|
||||
context.load_cert_chain(certfile, keyfile)
|
||||
if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
|
||||
return context.wrap_socket(sock, server_hostname=server_hostname)
|
||||
return context.wrap_socket(sock)
|
||||
|
||||
else: # Python 3.1 and earlier
|
||||
def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
|
||||
ca_certs=None, server_hostname=None,
|
||||
ssl_version=None):
|
||||
return wrap_socket(sock, keyfile=keyfile, certfile=certfile,
|
||||
ca_certs=ca_certs, cert_reqs=cert_reqs,
|
||||
ssl_version=ssl_version)
|
||||
raise
|
||||
if certfile:
|
||||
context.load_cert_chain(certfile, keyfile)
|
||||
if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI
|
||||
return context.wrap_socket(sock, server_hostname=server_hostname)
|
||||
return context.wrap_socket(sock)
|
||||
|
||||
@@ -40,6 +40,48 @@ class Url(namedtuple('Url', url_attrs)):
|
||||
return '%s:%d' % (self.host, self.port)
|
||||
return self.host
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""
|
||||
Convert self into a url
|
||||
|
||||
This function should more or less round-trip with :func:`.parse_url`. The
|
||||
returned url may not be exactly the same as the url inputted to
|
||||
:func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls
|
||||
with a blank port will have : removed).
|
||||
|
||||
Example: ::
|
||||
|
||||
>>> U = parse_url('http://google.com/mail/')
|
||||
>>> U.url
|
||||
'http://google.com/mail/'
|
||||
>>> Url('http', 'username:password', 'host.com', 80,
|
||||
... '/path', 'query', 'fragment').url
|
||||
'http://username:password@host.com:80/path?query#fragment'
|
||||
"""
|
||||
scheme, auth, host, port, path, query, fragment = self
|
||||
url = ''
|
||||
|
||||
# We use "is not None" we want things to happen with empty strings (or 0 port)
|
||||
if scheme is not None:
|
||||
url += scheme + '://'
|
||||
if auth is not None:
|
||||
url += auth + '@'
|
||||
if host is not None:
|
||||
url += host
|
||||
if port is not None:
|
||||
url += ':' + str(port)
|
||||
if path is not None:
|
||||
url += path
|
||||
if query is not None:
|
||||
url += '?' + query
|
||||
if fragment is not None:
|
||||
url += '#' + fragment
|
||||
|
||||
return url
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
def split_first(s, delims):
|
||||
"""
|
||||
@@ -84,7 +126,7 @@ def parse_url(url):
|
||||
Example::
|
||||
|
||||
>>> parse_url('http://google.com/mail/')
|
||||
Url(scheme='http', host='google.com', port=None, path='/', ...)
|
||||
Url(scheme='http', host='google.com', port=None, path='/mail/', ...)
|
||||
>>> parse_url('google.com:80')
|
||||
Url(scheme=None, host='google.com', port=80, path=None, ...)
|
||||
>>> parse_url('/foo?bar')
|
||||
@@ -162,7 +204,6 @@ def parse_url(url):
|
||||
|
||||
return Url(scheme, auth, host, port, path, query, fragment)
|
||||
|
||||
|
||||
def get_host(url):
|
||||
"""
|
||||
Deprecated. Use :func:`.parse_url` instead.
|
||||
|
||||
@@ -13,7 +13,7 @@ from collections import Mapping
|
||||
from datetime import datetime
|
||||
|
||||
from .auth import _basic_auth_str
|
||||
from .compat import cookielib, OrderedDict, urljoin, urlparse, builtin_str
|
||||
from .compat import cookielib, OrderedDict, urljoin, urlparse
|
||||
from .cookies import (
|
||||
cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies)
|
||||
from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT
|
||||
@@ -21,6 +21,7 @@ from .hooks import default_hooks, dispatch_hook
|
||||
from .utils import to_key_val_list, default_headers, to_native_string
|
||||
from .exceptions import (
|
||||
TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError)
|
||||
from .packages.urllib3._collections import RecentlyUsedContainer
|
||||
from .structures import CaseInsensitiveDict
|
||||
|
||||
from .adapters import HTTPAdapter
|
||||
@@ -35,6 +36,8 @@ from .status_codes import codes
|
||||
# formerly defined here, reexposed here for backward compatibility
|
||||
from .models import REDIRECT_STATI
|
||||
|
||||
REDIRECT_CACHE_SIZE = 1000
|
||||
|
||||
|
||||
def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
"""
|
||||
@@ -128,14 +131,14 @@ class SessionRedirectMixin(object):
|
||||
# Facilitate relative 'location' headers, as allowed by RFC 7231.
|
||||
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
|
||||
# Compliant with RFC3986, we percent encode the url.
|
||||
if not urlparse(url).netloc:
|
||||
if not parsed.netloc:
|
||||
url = urljoin(resp.url, requote_uri(url))
|
||||
else:
|
||||
url = requote_uri(url)
|
||||
|
||||
prepared_request.url = to_native_string(url)
|
||||
# cache the url
|
||||
if resp.is_permanent_redirect:
|
||||
# Cache the url, unless it redirects to itself.
|
||||
if resp.is_permanent_redirect and req.url != prepared_request.url:
|
||||
self.redirect_cache[req.url] = prepared_request.url
|
||||
|
||||
# http://tools.ietf.org/html/rfc7231#section-6.4.4
|
||||
@@ -271,9 +274,10 @@ class Session(SessionRedirectMixin):
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
'headers', 'cookies', 'auth', 'timeout', 'proxies', 'hooks',
|
||||
'params', 'verify', 'cert', 'prefetch', 'adapters', 'stream',
|
||||
'trust_env', 'max_redirects', 'redirect_cache']
|
||||
'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
|
||||
'cert', 'prefetch', 'adapters', 'stream', 'trust_env',
|
||||
'max_redirects',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -326,7 +330,8 @@ class Session(SessionRedirectMixin):
|
||||
self.mount('https://', HTTPAdapter())
|
||||
self.mount('http://', HTTPAdapter())
|
||||
|
||||
self.redirect_cache = {}
|
||||
# Only store 1000 redirects to prevent using infinite memory
|
||||
self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -365,6 +370,7 @@ class Session(SessionRedirectMixin):
|
||||
url=request.url,
|
||||
files=request.files,
|
||||
data=request.data,
|
||||
json=request.json,
|
||||
headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
|
||||
params=merge_setting(request.params, self.params),
|
||||
auth=merge_setting(auth, self.auth),
|
||||
@@ -386,7 +392,8 @@ class Session(SessionRedirectMixin):
|
||||
hooks=None,
|
||||
stream=None,
|
||||
verify=None,
|
||||
cert=None):
|
||||
cert=None,
|
||||
json=None):
|
||||
"""Constructs a :class:`Request <Request>`, prepares it and sends it.
|
||||
Returns :class:`Response <Response>` object.
|
||||
|
||||
@@ -396,17 +403,22 @@ class Session(SessionRedirectMixin):
|
||||
string for the :class:`Request`.
|
||||
:param data: (optional) Dictionary or bytes to send in the body of the
|
||||
:class:`Request`.
|
||||
:param json: (optional) json to send in the body of the
|
||||
:class:`Request`.
|
||||
:param headers: (optional) Dictionary of HTTP Headers to send with the
|
||||
:class:`Request`.
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the
|
||||
:class:`Request`.
|
||||
:param files: (optional) Dictionary of 'filename': file-like-objects
|
||||
:param files: (optional) Dictionary of ``'filename': file-like-objects``
|
||||
for multipart encoding upload.
|
||||
:param auth: (optional) Auth tuple or callable to enable
|
||||
Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) Float describing the timeout of the
|
||||
request in seconds.
|
||||
:param allow_redirects: (optional) Boolean. Set to True by default.
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float, or a (`connect timeout, read
|
||||
timeout <user/advanced.html#timeouts>`_) tuple.
|
||||
:type timeout: float or tuple
|
||||
:param allow_redirects: (optional) Set to True by default.
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of
|
||||
the proxy.
|
||||
:param stream: (optional) whether to immediately download the response
|
||||
@@ -417,7 +429,7 @@ class Session(SessionRedirectMixin):
|
||||
If Tuple, ('cert', 'key') pair.
|
||||
"""
|
||||
|
||||
method = builtin_str(method)
|
||||
method = to_native_string(method)
|
||||
|
||||
# Create the Request.
|
||||
req = Request(
|
||||
@@ -426,6 +438,7 @@ class Session(SessionRedirectMixin):
|
||||
headers = headers,
|
||||
files = files,
|
||||
data = data or {},
|
||||
json = json,
|
||||
params = params or {},
|
||||
auth = auth,
|
||||
cookies = cookies,
|
||||
@@ -479,15 +492,16 @@ class Session(SessionRedirectMixin):
|
||||
kwargs.setdefault('allow_redirects', False)
|
||||
return self.request('HEAD', url, **kwargs)
|
||||
|
||||
def post(self, url, data=None, **kwargs):
|
||||
def post(self, url, data=None, json=None, **kwargs):
|
||||
"""Sends a POST request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
"""
|
||||
|
||||
return self.request('POST', url, data=data, **kwargs)
|
||||
return self.request('POST', url, data=data, json=json, **kwargs)
|
||||
|
||||
def put(self, url, data=None, **kwargs):
|
||||
"""Sends a PUT request. Returns :class:`Response` object.
|
||||
@@ -532,12 +546,13 @@ class Session(SessionRedirectMixin):
|
||||
if not isinstance(request, PreparedRequest):
|
||||
raise ValueError('You can only send PreparedRequests.')
|
||||
|
||||
redirect_count = 0
|
||||
checked_urls = set()
|
||||
while request.url in self.redirect_cache:
|
||||
redirect_count += 1
|
||||
if redirect_count > self.max_redirects:
|
||||
raise TooManyRedirects
|
||||
request.url = self.redirect_cache.get(request.url)
|
||||
checked_urls.add(request.url)
|
||||
new_url = self.redirect_cache.get(request.url)
|
||||
if new_url in checked_urls:
|
||||
break
|
||||
request.url = new_url
|
||||
|
||||
# Set up variables needed for resolve_redirects and dispatching of hooks
|
||||
allow_redirects = kwargs.pop('allow_redirects', True)
|
||||
@@ -647,12 +662,19 @@ class Session(SessionRedirectMixin):
|
||||
self.adapters[key] = self.adapters.pop(key)
|
||||
|
||||
def __getstate__(self):
|
||||
return dict((attr, getattr(self, attr, None)) for attr in self.__attrs__)
|
||||
state = dict((attr, getattr(self, attr, None)) for attr in self.__attrs__)
|
||||
state['redirect_cache'] = dict(self.redirect_cache)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
redirect_cache = state.pop('redirect_cache', {})
|
||||
for attr, value in state.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
self.redirect_cache = RecentlyUsedContainer(REDIRECT_CACHE_SIZE)
|
||||
for redirect, to in redirect_cache.items():
|
||||
self.redirect_cache[redirect] = to
|
||||
|
||||
|
||||
def session():
|
||||
"""Returns a :class:`Session` for context-management."""
|
||||
|
||||
@@ -19,6 +19,7 @@ import re
|
||||
import sys
|
||||
import socket
|
||||
import struct
|
||||
import warnings
|
||||
|
||||
from . import __version__
|
||||
from . import certs
|
||||
@@ -114,7 +115,7 @@ def get_netrc_auth(url):
|
||||
def guess_filename(obj):
|
||||
"""Tries to guess the filename of the given object."""
|
||||
name = getattr(obj, 'name', None)
|
||||
if name and name[0] != '<' and name[-1] != '>':
|
||||
if name and isinstance(name, builtin_str) and name[0] != '<' and name[-1] != '>':
|
||||
return os.path.basename(name)
|
||||
|
||||
|
||||
@@ -287,6 +288,11 @@ def get_encodings_from_content(content):
|
||||
|
||||
:param content: bytestring to extract encodings from.
|
||||
"""
|
||||
warnings.warn((
|
||||
'In requests 3.0, get_encodings_from_content will be removed. For '
|
||||
'more information, please see the discussion on issue #2266. (This'
|
||||
' warning should only appear once.)'),
|
||||
DeprecationWarning)
|
||||
|
||||
charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I)
|
||||
pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I)
|
||||
@@ -351,12 +357,14 @@ def get_unicode_from_response(r):
|
||||
Tried:
|
||||
|
||||
1. charset from content-type
|
||||
|
||||
2. every encodings from ``<meta ... charset=XXX>``
|
||||
|
||||
3. fall back and replace all unicode characters
|
||||
2. fall back and replace all unicode characters
|
||||
|
||||
"""
|
||||
warnings.warn((
|
||||
'In requests 3.0, get_unicode_from_response will be removed. For '
|
||||
'more information, please see the discussion on issue #2266. (This'
|
||||
' warning should only appear once.)'),
|
||||
DeprecationWarning)
|
||||
|
||||
tried_encodings = []
|
||||
|
||||
@@ -555,7 +563,7 @@ def default_headers():
|
||||
'User-Agent': default_user_agent(),
|
||||
'Accept-Encoding': ', '.join(('gzip', 'deflate')),
|
||||
'Accept': '*/*',
|
||||
'Connection': 'keep-alive'
|
||||
'Connection': 'keep-alive',
|
||||
})
|
||||
|
||||
|
||||
@@ -570,7 +578,7 @@ def parse_header_links(value):
|
||||
|
||||
replace_chars = " '\""
|
||||
|
||||
for val in value.split(","):
|
||||
for val in re.split(", *<", value):
|
||||
try:
|
||||
url, params = val.split(";", 1)
|
||||
except ValueError:
|
||||
@@ -672,3 +680,18 @@ def to_native_string(string, encoding='ascii'):
|
||||
out = string.decode(encoding)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def urldefragauth(url):
|
||||
"""
|
||||
Given a url remove the fragment and the authentication part
|
||||
"""
|
||||
scheme, netloc, path, params, query, fragment = urlparse(url)
|
||||
|
||||
# see func:`prepend_scheme_if_needed`
|
||||
if not netloc:
|
||||
netloc, path = path, netloc
|
||||
|
||||
netloc = netloc.rsplit('@', 1)[-1]
|
||||
|
||||
return urlunparse((scheme, netloc, path, params, query, ''))
|
||||
|
||||
@@ -32,7 +32,7 @@ __all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE',
|
||||
'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence',
|
||||
'key_subtitles', 'group_by_video']
|
||||
logger = logging.getLogger(__name__)
|
||||
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles']
|
||||
SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles', 'subscenter']
|
||||
LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4)
|
||||
|
||||
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2011-2012 Antoine Bertin <diaoulael@gmail.com>
|
||||
#
|
||||
# This file is part of subliminal.
|
||||
#
|
||||
# subliminal is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# subliminal is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
|
||||
from . import ServiceBase
|
||||
from ..exceptions import ServiceError, DownloadFailedError
|
||||
from ..language import language_set, Language
|
||||
from ..subtitles import get_subtitle_path, ResultSubtitle
|
||||
from ..utils import to_unicode
|
||||
from ..videos import Episode, Movie
|
||||
from hashlib import md5, sha256
|
||||
import logging
|
||||
import xmlrpclib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Podnapisi(ServiceBase):
|
||||
server_url = 'http://ssp.podnapisi.net:8000'
|
||||
api_based = True
|
||||
languages = language_set(['ar', 'be', 'bg', 'bs', 'ca', 'ca', 'cs', 'da', 'de', 'el', 'en',
|
||||
'es', 'et', 'fa', 'fi', 'fr', 'ga', 'he', 'hi', 'hr', 'hu', 'id',
|
||||
'is', 'it', 'ja', 'ko', 'lt', 'lv', 'mk', 'ms', 'nl', 'nn', 'pl',
|
||||
'pt', 'ro', 'ru', 'sk', 'sl', 'sq', 'sr', 'sv', 'th', 'tr', 'uk',
|
||||
'vi', 'zh', 'es-ar', 'pt-br'])
|
||||
language_map = {'jp': Language('jpn'), Language('jpn'): 'jp',
|
||||
'gr': Language('gre'), Language('gre'): 'gr',
|
||||
'pb': Language('por-BR'), Language('por-BR'): 'pb',
|
||||
'ag': Language('spa-AR'), Language('spa-AR'): 'ag',
|
||||
'cyr': Language('srp')}
|
||||
videos = [Episode, Movie]
|
||||
require_video = True
|
||||
|
||||
def __init__(self, config=None):
|
||||
super(Podnapisi, self).__init__(config)
|
||||
self.server = xmlrpclib.ServerProxy(self.server_url)
|
||||
self.token = None
|
||||
|
||||
def init(self):
|
||||
super(Podnapisi, self).init()
|
||||
result = self.server.initiate(self.user_agent)
|
||||
if result['status'] != 200:
|
||||
raise ServiceError('Initiate failed')
|
||||
username = 'python_subliminal'
|
||||
password = sha256(md5('XWFXQ6gE5Oe12rv4qxXX').hexdigest() + result['nonce']).hexdigest()
|
||||
self.token = result['session']
|
||||
result = self.server.authenticate(self.token, username, password)
|
||||
if result['status'] != 200:
|
||||
raise ServiceError('Authenticate failed')
|
||||
|
||||
def terminate(self):
|
||||
super(Podnapisi, self).terminate()
|
||||
|
||||
def query(self, filepath, languages, moviehash):
|
||||
results = self.server.search(self.token, [moviehash])
|
||||
if results['status'] != 200:
|
||||
logger.error('Search failed with error code %d' % results['status'])
|
||||
return []
|
||||
if not results['results'] or not results['results'][moviehash]['subtitles']:
|
||||
logger.debug(u'Could not find subtitles for %r with token %s' % (moviehash, self.token))
|
||||
return []
|
||||
subtitles = []
|
||||
for result in results['results'][moviehash]['subtitles']:
|
||||
language = self.get_language(result['lang'])
|
||||
if language not in languages:
|
||||
continue
|
||||
path = get_subtitle_path(filepath, language, self.config.multi)
|
||||
subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), result['id'],
|
||||
release=to_unicode(result['release']), confidence=result['weight'])
|
||||
subtitles.append(subtitle)
|
||||
if not subtitles:
|
||||
return []
|
||||
# Convert weight to confidence
|
||||
max_weight = float(max([s.confidence for s in subtitles]))
|
||||
min_weight = float(min([s.confidence for s in subtitles]))
|
||||
for subtitle in subtitles:
|
||||
if max_weight == 0 and min_weight == 0:
|
||||
subtitle.confidence = 1.0
|
||||
else:
|
||||
subtitle.confidence = (subtitle.confidence - min_weight) / (max_weight - min_weight)
|
||||
return subtitles
|
||||
|
||||
def list_checked(self, video, languages):
|
||||
results = self.query(video.path, languages, video.hashes['OpenSubtitles'])
|
||||
return results
|
||||
|
||||
def download(self, subtitle):
|
||||
results = self.server.download(self.token, [subtitle.link])
|
||||
if results['status'] != 200:
|
||||
raise DownloadFailedError()
|
||||
subtitle.link = 'http://www.podnapisi.net/static/podnapisi/' + results['names'][0]['filename']
|
||||
self.download_file(subtitle.link, subtitle.path)
|
||||
return subtitle
|
||||
|
||||
|
||||
Service = Podnapisi
|
||||
100
libs/subliminal/services/subscenter.py
Normal file
100
libs/subliminal/services/subscenter.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2012 Ofir Brukner <ofirbrukner@gmail.com>
|
||||
#
|
||||
# This file is part of subliminal.
|
||||
#
|
||||
# subliminal is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# subliminal is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import re
|
||||
import json
|
||||
from . import ServiceBase
|
||||
from ..exceptions import ServiceError
|
||||
from ..language import language_set
|
||||
from ..subtitles import get_subtitle_path, ResultSubtitle
|
||||
from ..videos import Episode, Movie
|
||||
from ..utils import to_unicode, get_keywords
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Subscenter(ServiceBase):
|
||||
server_url = 'http://subscenter.cinemast.com/he/'
|
||||
api_based = False
|
||||
languages = language_set(['he', 'en'])
|
||||
videos = [Episode, Movie]
|
||||
require_video = False
|
||||
required_features = ['permissive']
|
||||
|
||||
@staticmethod
|
||||
def slugify(string):
|
||||
new_string = string.replace(' ', '-').replace("'", '').replace(':', '').lower()
|
||||
# We remove multiple spaces by using this regular expression.
|
||||
return re.sub('-+', '-', new_string)
|
||||
|
||||
def list_checked(self, video, languages):
|
||||
series = None
|
||||
season = None
|
||||
episode = None
|
||||
title = video.title
|
||||
if isinstance(video, Episode):
|
||||
series = video.series
|
||||
season = video.season
|
||||
episode = video.episode
|
||||
return self.query(video.path or video.release, languages, get_keywords(video.guess), series, season,
|
||||
episode, title)
|
||||
|
||||
def query(self, filepath, languages=None, keywords=None, series=None, season=None, episode=None, title=None):
|
||||
logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages))
|
||||
# Converts the title to Subscenter format by replacing whitespaces and removing specific chars.
|
||||
if series and season and episode:
|
||||
# Search for a TV show.
|
||||
kind = 'episode'
|
||||
slugified_series = self.slugify(series)
|
||||
url = self.server_url + 'cinemast/data/series/sb/' + slugified_series + '/' + str(season) + '/' + \
|
||||
str(episode) + '/'
|
||||
elif title:
|
||||
# Search for a movie.
|
||||
kind = 'movie'
|
||||
slugified_title = self.slugify(title)
|
||||
url = self.server_url + 'cinemast/data/movie/sb/' + slugified_title + '/'
|
||||
else:
|
||||
raise ServiceError('One or more parameters are missing')
|
||||
logger.debug('Searching subtitles %r', {'title': title, 'season': season, 'episode': episode})
|
||||
response = self.session.get(url)
|
||||
if response.status_code != 200:
|
||||
raise ServiceError('Request failed with status code %d' % response.status_code)
|
||||
|
||||
subtitles = []
|
||||
response_json = json.loads(response.content)
|
||||
for lang, lang_json in response_json.items():
|
||||
lang_obj = self.get_language(lang)
|
||||
if lang_obj in self.languages and lang_obj in languages:
|
||||
for group_data in lang_json.values():
|
||||
for quality in group_data.values():
|
||||
for sub in quality.values():
|
||||
release = sub.get('subtitle_version')
|
||||
sub_path = get_subtitle_path(filepath, lang_obj, self.config.multi)
|
||||
link = self.server_url + 'subtitle/download/' + lang + '/' + str(sub.get('id')) + \
|
||||
'/?v=' + release + '&key=' + str(sub.get('key'))
|
||||
subtitles.append(ResultSubtitle(sub_path, lang_obj, self.__class__.__name__.lower(),
|
||||
link, release=to_unicode(release)))
|
||||
return subtitles
|
||||
|
||||
def download(self, subtitle):
|
||||
self.download_zip_file(subtitle.link, subtitle.path)
|
||||
return subtitle
|
||||
|
||||
|
||||
Service = Subscenter
|
||||
@@ -19,7 +19,7 @@ class DelugeTransfer(object):
|
||||
self.disconnect()
|
||||
|
||||
self.sock = socket.create_connection(hostport)
|
||||
self.conn = ssl.wrap_socket(self.sock, None, None, False, ssl.CERT_NONE, ssl.PROTOCOL_SSLv3)
|
||||
self.conn = ssl.wrap_socket(self.sock, None, None, False, ssl.CERT_NONE, ssl.PROTOCOL_TLSv1)
|
||||
self.connected = True
|
||||
|
||||
def disconnect(self):
|
||||
|
||||
@@ -95,7 +95,8 @@ class HTTPClient(object):
|
||||
If it is a string, we construct an `HTTPRequest` using any additional
|
||||
kwargs: ``HTTPRequest(request, **kwargs)``
|
||||
|
||||
If an error occurs during the fetch, we raise an `HTTPError`.
|
||||
If an error occurs during the fetch, we raise an `HTTPError` unless
|
||||
the ``raise_error`` keyword argument is set to False.
|
||||
"""
|
||||
response = self._io_loop.run_sync(functools.partial(
|
||||
self._async_client.fetch, request, **kwargs))
|
||||
@@ -200,7 +201,7 @@ class AsyncHTTPClient(Configurable):
|
||||
raise RuntimeError("inconsistent AsyncHTTPClient cache")
|
||||
del self._instance_cache[self.io_loop]
|
||||
|
||||
def fetch(self, request, callback=None, **kwargs):
|
||||
def fetch(self, request, callback=None, raise_error=True, **kwargs):
|
||||
"""Executes a request, asynchronously returning an `HTTPResponse`.
|
||||
|
||||
The request may be either a string URL or an `HTTPRequest` object.
|
||||
@@ -208,8 +209,10 @@ class AsyncHTTPClient(Configurable):
|
||||
kwargs: ``HTTPRequest(request, **kwargs)``
|
||||
|
||||
This method returns a `.Future` whose result is an
|
||||
`HTTPResponse`. The ``Future`` will raise an `HTTPError` if
|
||||
the request returned a non-200 response code.
|
||||
`HTTPResponse`. By default, the ``Future`` will raise an `HTTPError`
|
||||
if the request returned a non-200 response code. Instead, if
|
||||
``raise_error`` is set to False, the response will always be
|
||||
returned regardless of the response code.
|
||||
|
||||
If a ``callback`` is given, it will be invoked with the `HTTPResponse`.
|
||||
In the callback interface, `HTTPError` is not automatically raised.
|
||||
@@ -243,7 +246,7 @@ class AsyncHTTPClient(Configurable):
|
||||
future.add_done_callback(handle_future)
|
||||
|
||||
def handle_response(response):
|
||||
if response.error:
|
||||
if raise_error and response.error:
|
||||
future.set_exception(response.error)
|
||||
else:
|
||||
future.set_result(response)
|
||||
|
||||
@@ -42,30 +42,10 @@ from tornado.tcpserver import TCPServer
|
||||
class HTTPServer(TCPServer, httputil.HTTPServerConnectionDelegate):
|
||||
r"""A non-blocking, single-threaded HTTP server.
|
||||
|
||||
A server is defined by either a request callback that takes a
|
||||
`.HTTPServerRequest` as an argument or a `.HTTPServerConnectionDelegate`
|
||||
instance.
|
||||
|
||||
A simple example server that echoes back the URI you requested::
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.ioloop
|
||||
from tornado import httputil
|
||||
|
||||
def handle_request(request):
|
||||
message = "You requested %s\n" % request.uri
|
||||
request.connection.write_headers(
|
||||
httputil.ResponseStartLine('HTTP/1.1', 200, 'OK'),
|
||||
httputil.HTTPHeaders({"Content-Length": str(len(message))}))
|
||||
request.connection.write(message)
|
||||
request.connection.finish()
|
||||
|
||||
http_server = tornado.httpserver.HTTPServer(handle_request)
|
||||
http_server.listen(8888)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
Applications should use the methods of `.HTTPConnection` to write
|
||||
their response.
|
||||
A server is defined by a subclass of `.HTTPServerConnectionDelegate`,
|
||||
or, for backwards compatibility, a callback that takes an
|
||||
`.HTTPServerRequest` as an argument. The delegate is usually a
|
||||
`tornado.web.Application`.
|
||||
|
||||
`HTTPServer` supports keep-alive connections by default
|
||||
(automatically for HTTP/1.1, or for HTTP/1.0 when the client
|
||||
|
||||
@@ -331,7 +331,7 @@ class HTTPServerRequest(object):
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
self.headers = headers or HTTPHeaders()
|
||||
self.body = body or ""
|
||||
self.body = body or b""
|
||||
|
||||
# set remote IP and protocol
|
||||
context = getattr(connection, 'context', None)
|
||||
@@ -873,3 +873,17 @@ def _encode_header(key, pdict):
|
||||
def doctests():
|
||||
import doctest
|
||||
return doctest.DocTestSuite()
|
||||
|
||||
def split_host_and_port(netloc):
|
||||
"""Returns ``(host, port)`` tuple from ``netloc``.
|
||||
|
||||
Returned ``port`` will be ``None`` if not present.
|
||||
"""
|
||||
match = re.match(r'^(.+):(\d+)$', netloc)
|
||||
if match:
|
||||
host = match.group(1)
|
||||
port = int(match.group(2))
|
||||
else:
|
||||
host = netloc
|
||||
port = None
|
||||
return (host, port)
|
||||
|
||||
@@ -331,7 +331,7 @@ class BaseIOStream(object):
|
||||
if data:
|
||||
if (self.max_write_buffer_size is not None and
|
||||
self._write_buffer_size + len(data) > self.max_write_buffer_size):
|
||||
raise StreamBufferFullError("Reached maximum read buffer size")
|
||||
raise StreamBufferFullError("Reached maximum write buffer size")
|
||||
# Break up large contiguous strings before inserting them in the
|
||||
# write buffer, so we don't have to recopy the entire thing
|
||||
# as we slice off pieces to send to the socket.
|
||||
|
||||
@@ -20,7 +20,7 @@ from __future__ import absolute_import, division, print_function, with_statement
|
||||
|
||||
import errno
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import socket
|
||||
import stat
|
||||
|
||||
@@ -105,7 +105,7 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC,
|
||||
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
|
||||
0, flags)):
|
||||
af, socktype, proto, canonname, sockaddr = res
|
||||
if (platform.system() == 'Darwin' and address == 'localhost' and
|
||||
if (sys.platform == 'darwin' and address == 'localhost' and
|
||||
af == socket.AF_INET6 and sockaddr[3] != 0):
|
||||
# Mac OS X includes a link-local address fe80::1%lo0 in the
|
||||
# getaddrinfo results for 'localhost'. However, the firewall
|
||||
|
||||
@@ -204,6 +204,13 @@ class OptionParser(object):
|
||||
(name, self._options[name].file_name))
|
||||
frame = sys._getframe(0)
|
||||
options_file = frame.f_code.co_filename
|
||||
|
||||
# Can be called directly, or through top level define() fn, in which
|
||||
# case, step up above that frame to look for real caller.
|
||||
if (frame.f_back.f_code.co_filename == options_file and
|
||||
frame.f_back.f_code.co_name == 'define'):
|
||||
frame = frame.f_back
|
||||
|
||||
file_name = frame.f_back.f_code.co_filename
|
||||
if file_name == options_file:
|
||||
file_name = ""
|
||||
|
||||
@@ -54,8 +54,7 @@ class _KQueue(object):
|
||||
if events & IOLoop.WRITE:
|
||||
kevents.append(select.kevent(
|
||||
fd, filter=select.KQ_FILTER_WRITE, flags=flags))
|
||||
if events & IOLoop.READ or not kevents:
|
||||
# Always read when there is not a write
|
||||
if events & IOLoop.READ:
|
||||
kevents.append(select.kevent(
|
||||
fd, filter=select.KQ_FILTER_READ, flags=flags))
|
||||
# Even though control() takes a list, it seems to return EINVAL
|
||||
|
||||
@@ -47,7 +47,7 @@ class _Select(object):
|
||||
# Closed connections are reported as errors by epoll and kqueue,
|
||||
# but as zero-byte reads by select, so when errors are requested
|
||||
# we need to listen for both read and error.
|
||||
self.read_fds.add(fd)
|
||||
#self.read_fds.add(fd)
|
||||
|
||||
def modify(self, fd, events):
|
||||
self.unregister(fd)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user