Browse Source

Add config/Search/Search Tasks/"Host running FlareSolverr" to handle CloudFlare providers.

Change the cf_clearance cookie to an undocumented optional config instead of a requirement.
Change where cf_clearance does not exist or expires, config/Search/Search Tasks/"Host running FlareSolverr" is required.
Fix saving magnet from PAs as files under py3.
Fix SkyTorrents provider.
Fix Torlock provider.
Fix TBP provider.
tags/release_0.23.14^2
JackDandy 4 years ago
parent
commit
2149898a13
  1. 13
      CHANGES.md
  2. 18
      gui/slick/interfaces/default/config_search.tmpl
  3. 23
      gui/slick/js/configSearch.js
  4. 230
      lib/cfscrape/__init__.py
  5. 26
      lib/sg_helpers.py
  6. 5
      sickbeard/__init__.py
  7. 5
      sickbeard/helpers.py
  8. 5
      sickbeard/image_cache.py
  9. 5
      sickbeard/metadata/helpers.py
  10. 9
      sickbeard/providers/generic.py
  11. 4
      sickbeard/providers/iptorrents.py
  12. 4
      sickbeard/providers/scenetime.py
  13. 10
      sickbeard/providers/skytorrents.py
  14. 235
      sickbeard/providers/thepiratebay.py
  15. 13
      sickbeard/providers/torlock.py
  16. 4
      sickbeard/providers/torrentday.py
  17. 4
      sickbeard/providers/torrenting.py
  18. 21
      sickbeard/webserve.py

13
CHANGES.md

@ -1,4 +1,15 @@
### 0.23.13 (2021-02-26 19:05:00 UTC)
### 0.23.14 (2021-03-10 01:40:00 UTC)
* Add config/Search/Search Tasks/"Host running FlareSolverr" to handle CloudFlare providers
* Change the cf_clearance cookie to an undocumented optional config instead of a requirement
* Change where cf_clearance does not exist or expires, config/Search/Search Tasks/"Host running FlareSolverr" is required
* Fix saving magnet from PAs as files under py3
* Fix SkyTorrents provider
* Fix Torlock provider
* Fix TBP provider
### 0.23.13 (2021-02-26 19:05:00 UTC)
* Add Newznab providers can use API only or API + RSS cache fallback. Tip added to Newznab config/Media Providers/API key
* Add correct user entry mistakes for nzbs2go api url

18
gui/slick/interfaces/default/config_search.tmpl

@ -1,6 +1,6 @@
#import sickbeard
#from sickbeard import clients
#from sickbeard.helpers import starify, generate_word_str
#from sickbeard.helpers import anon_url, starify, generate_word_str
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
@ -237,7 +237,21 @@
</div>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes">
<div class="field-pair">
<label for="flaresolverr">
<span class="component-title">Host running FlareSolverr</span>
<span class="component-desc">
<input type="text" name="flaresolverr_host" id="flaresolverr" value="$sickbeard.FLARESOLVERR_HOST" class="form-control input-sm input250"><p>IP:Port (default: http://localhost:8191)</p>
<p class="clear-left note">proxy software to handle Cloudflare connections (more info <a href="<%= anon_url('https://github.com/SickGear/SickGear/wiki/Install-SickGear-%5B82%5D-CF-Solve') %>" onclick="window.open(this.href, '_blank'); return false;">here</a>)</p>
</span>
</label>
<span class="component-title"></span>
<span class="component-desc">
<input class="btn" type="button" value="Test FlareSolverr" id="test-flaresolverr"><span class="test-notification" id="test-flaresolverr-result">connection result</span>
</span>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes" style="display:block;margin-top:20px">
</fieldset>
</div><!-- /component-group1 //-->

23
gui/slick/js/configSearch.js

@ -9,6 +9,29 @@ $(document).ready(function(){
/^\s*scgi:\/\//.test($('#torrent_host').val()) ? opts$.hide() && $('#torrent_label').select() && ''
: opts$.show() && $('#torrent_username').select() && $(this).prop('defaultValue'));});};
$('#test-flaresolverr').click(function(){
var host$ = $('#flaresolverr'), host = $.trim(host$.val()), result$ = $('#test-flaresolverr-result'),
valid = !!host && -1 !== host.indexOf('http');
if (!valid) {
result$.html('Please correct the field above');
if (!valid) {
host$.addClass('warning');
} else {
host$.removeClass('warning');
}
return;
}
host$.removeClass('warning');
$(this).prop('disabled', !0);
result$.html(loading);
$.get(sbRoot + '/home/test-flaresolverr',
{host: host})
.done(function (data) {
$('#test-flaresolverr-result').html(data);
$('#test-flaresolverr').prop('disabled', !1);
});
});
function toggleTorrentTitle() {
var noTorrent$ = $('#no_torrents');
if ($('#use_torrents').prop('checked'))

230
lib/cfscrape/__init__.py

@ -1,18 +1,13 @@
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
from requests.models import Response
from requests.sessions import Session
from urllib3.util.ssl_ import create_urllib3_context
import logging
import random
import re
import ssl
import time
import js2py
from _23 import b64decodestring, b64encodestring, urlparse, urlunparse
from _23 import b64encodestring, urlparse
DEFAULT_USER_AGENTS = [
@ -46,25 +41,27 @@ class CloudflareScraper(Session):
self.delay = kwargs.pop('delay', self.default_delay)
self.start_time = None
self.cipher_suite = self.load_cipher_suite()
self.mount('https://', CloudflareAdapter(self.cipher_suite))
self.trust_env = False
def request(self, method, url, *args, **kwargs):
resp = super(CloudflareScraper, self).request(method, url, *args, **kwargs)
url_solver = kwargs.pop('url_solver', None)
if not kwargs.pop('proxy_browser', None):
resp = super(CloudflareScraper, self).request(method, url, *args, **kwargs)
else:
resp = self.get_content(method, url, url_solver,
user_agent=self.headers.get('User-Agent'), proxy_browser=True, **kwargs)
# Check if anti-bot is on
if (isinstance(resp, type(Response()))
and resp.status_code in (503, 429, 403)):
self.start_time = time.time()
if (re.search('(?i)cloudflare', resp.headers.get('Server', ''))
and b'jschl_vc' in resp.content
and b'jschl_answer' in resp.content):
resp = self.solve_cf_challenge(resp, **kwargs)
resp = self.solve_cf_challenge(resp, url_solver, **kwargs)
elif b'ddgu' in resp.content:
resp = self.solve_ddg_challenge(resp, **kwargs)
# Otherwise, no anti-bot detected
return resp
def wait(self):
@ -87,142 +84,74 @@ class CloudflareScraper(Session):
pass
return resp
def solve_cf_challenge(self, resp, **original_kwargs):
def test_flaresolverr(self, url_solver):
# test if FlareSolverr software is running
response_test = super(CloudflareScraper, self).request('GET', url_solver)
fs_ver = None
if 200 == response_test.status_code and response_test.ok:
json_data = response_test.json()
if 'ok' == json_data.get('status'):
fs_ver = json_data.get('version')
if None is fs_ver:
raise ValueError('FlareSolverr software not found (is it running?)')
return fs_ver
def get_content(self, method, url, url_solver, user_agent, proxy_browser=False, **kwargs):
url_solver = url_solver and re.sub(r'(/|v1)*$', '', url_solver) or 'http://localhost:8191'
if not self.test_flaresolverr(url_solver):
raise ValueError('No FlareSolverr software running %sat %s' % (('to solve Cloudflare challenge ',
'')[proxy_browser], url_solver))
try:
response = super(CloudflareScraper, self).request('POST', '%s/v1' % url_solver, json=dict(
cmd='request.%s' % method.lower(), userAgent=user_agent, url=url,
cookies=[{'name': cur_ckee.name, 'value': cur_ckee.value,
'domain': cur_ckee.domain, 'path': cur_ckee.path} for cur_ckee in self.cookies]))
except(BaseException, Exception) as e:
raise ValueError('FlareSolverr software unable to %s: %r' % (('solve Cloudflare anti-bot IUAM challenge',
'fetch content')[proxy_browser], e))
if None is not response:
data_json = response.json()
result = ({}, data_json)[isinstance(data_json, (dict, list))]
if response.ok:
if 'ok' == result.get('status'):
self.cookies.clear()
for cur_ckee in result.get('solution', {}).get('cookies', []):
if cur_ckee.get('value') and cur_ckee.get('name') not in ('', None, '_gid', '_ga', '_gat'):
self.cookies.set(
cur_ckee['name'], cur_ckee['value'],
rest={'httpOnly': cur_ckee.get('httpOnly'), 'session': cur_ckee.get('session')},
**dict([(k, cur_ckee.get(k)) for k in ('expires', 'domain', 'path', 'secure')]))
else:
response = None
elif 'error' == result.get('status'):
raise ValueError('Failure with FlareSolverr: %s' % result.get('message', 'See the FlareSolver output'))
return response
def solve_cf_challenge(self, resp, url_solver, **original_kwargs):
body = resp.text
parsed_url = urlparse(resp.url)
domain = parsed_url.netloc
if '/cdn-cgi/l/chk_captcha' in body or 'cf_chl_captcha' in body:
raise CloudflareError(
'Cloudflare captcha presented for %s, please notify SickGear for an update, ua: %s' %
'Cloudflare captcha presented for %s, safe to ignore as this shouldn\'t happen every time, ua: %s' %
(domain, self.cf_ua), response=resp)
try:
action, method = re.findall(r'(?sim)<form.*?id="challenge.*?action="/?([^?"]+).*?method="([^"]+)', body)[0]
except(Exception, BaseException):
action, method = 'cdn-cgi/l/chk_jschl', resp.request.method
submit_url = '%s://%s/%s' % (parsed_url.scheme, domain, action)
cloudflare_kwargs = {k: v for k, v in original_kwargs.items() if k not in ['hooks']}
params = cloudflare_kwargs.setdefault(('data', 'params')['GET' == method.upper()], {})
headers = cloudflare_kwargs.setdefault('headers', {})
headers['Referer'] = resp.url
try:
token = re.findall(r'(?sim)__cf_chl_jschl_tk__=([^"]+)', body)[0]
cloudflare_kwargs['params'] = dict(__cf_chl_jschl_tk__=token)
except(Exception, BaseException):
pass
if None is self.get_content(
'GET', (resp.request.url, '%s://%s/' % (parsed_url.scheme, domain))['POST' == resp.request.method],
url_solver, user_agent=resp.request.headers.get('User-Agent')):
raise ValueError('Failed to validate Cloudflare anti-bot IUAM challenge')
if self.delay == self.default_delay:
try:
# no instantiated delay, therefore check js for hard coded CF delay
self.delay = float(re.search(r'submit\(\);[^0-9]*?([0-9]+)', body).group(1)) / float(1000)
except (BaseException, Exception):
pass
for i in re.findall(r'(<input[^>]+?hidden[^>]+?>)', re.sub(r'(?sim)<!--\s+<input.*?(?=<)', '', body)):
value = re.findall(r'value="([^"\']+?)["\']', i)
name = re.findall(r'name="([^"\']+?)["\']', i)
if all([name, value]):
params[name[0]] = value[0]
js = self.extract_js(body, domain)
atob = (lambda s: b64decodestring('%s' % s))
try:
# Eval the challenge algorithm
params['jschl_answer'] = str(js2py.EvalJs({'atob': atob}).eval(js))
except (BaseException, Exception):
try:
params['jschl_answer'] = str(js2py.EvalJs({'atob': atob}).eval(js))
except (BaseException, Exception) as e:
# Something is wrong with the page. This may indicate Cloudflare has changed their anti-bot technique.
raise ValueError('Unable to parse Cloudflare anti-bot IUAM page: %r' % e)
# Requests transforms any request into a GET after a redirect,
# so the redirect has to be handled manually here to allow for
# performing other types of requests even as the first request.
cloudflare_kwargs['allow_redirects'] = False
self.wait()
response = self.request(method, submit_url, **cloudflare_kwargs)
if response:
if 200 == getattr(response, 'status_code'):
return response
# legacy redirection handler (pre 2019.11.xx)
location = response.headers.get('Location')
try:
r = urlparse(location)
except(Exception, BaseException):
# Something is wrong with the page, perhaps CF changed their anti-bot technique
raise ValueError('Unable to find a new location from Cloudflare anti-bot IUAM page')
if not r.netloc or location.startswith('/'):
location = urlunparse((parsed_url.scheme, domain, r.path, r.params, r.query, r.fragment))
return self.request(resp.request.method, location, **original_kwargs)
@staticmethod
def extract_js(body, domain):
try:
js = re.search(
r'''(?x)
setTimeout\(function\(\){\s*([\w\W]+)[\r\n]*[\w\W]\.action
''', body).group(1)
except (BaseException, Exception):
raise RuntimeError('Error #1 Cloudflare anti-bots changed, please notify SickGear for an update')
if not re.search(r'(?i)(toFixed|t\.length)', js):
raise RuntimeError('Error #2 Cloudflare anti-bots changed, please notify SickGear for an update')
js = re.sub(r'(;\s+);', r'\1', js.strip())
js = re.sub(r'([)\]];)(\w)', r'\1\n\n\2', js)
js = re.sub(r'\s*\';\s*\d+\'\s*$', '', js)
innerHTML = re.search(r'(?sim)<div(?: [^<>]*)? id="([^<>]*?)">([^<>]*?)</div>', body)
innerHTML = '' if not innerHTML else innerHTML.group(2).strip()
# Prefix the challenge with a fake document object.
# Interpolate the domain, div contents, and JS challenge.
# The `a.value` to be returned is tacked onto the end.
return r'''
var document = {
createElement: function () {
return { firstChild: { href: 'https://%s/' } }
},
getElementById: function () {
return { innerHTML: '%s'};
}
};
String.prototype.italics=function() {return '<i>' + this + '</i>';};
setInterval=function(){};
%s;a.value
''' % (domain, innerHTML, js)
@staticmethod
def load_cipher_suite():
suite = []
if hasattr(ssl, 'PROTOCOL_TLS'):
ctx = ssl.SSLContext(getattr(ssl, 'PROTOCOL_TLSv1_3', ssl.PROTOCOL_TLSv1_2))
for cipher in ([
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-ECDSA-AES256-SHA',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-SHA384',
]):
try:
ctx.set_ciphers(cipher)
suite += [cipher]
except ssl.SSLError:
pass
return ':'.join(suite)
time.sleep(1.2)
final_response = super(CloudflareScraper, self).request(resp.request.method, resp.request.url, headers={
'User-Agent': resp.request.headers.get('User-Agent')}, **original_kwargs)
# if final_response and 200 == getattr(final_response, 'status_code'):
# return final_response
return final_response
@classmethod
def create_scraper(cls, sess=None, **kwargs):
@ -279,29 +208,6 @@ class CloudflareScraper(Session):
return '; '.join(['='.join(pair) for pair in tokens.items()]), user_agent
class CloudflareAdapter(HTTPAdapter):
"""
HTTPS adapter that creates a SSL context with custom ciphers
"""
def __init__(self, cipher_suite=None, **kwargs):
self.cipher_suite = cipher_suite
params = dict(ssl_version=ssl.PROTOCOL_TLSv1)
if hasattr(ssl, 'PROTOCOL_TLS'):
params = dict(ssl_version=getattr(ssl, 'PROTOCOL_TLSv1_3', ssl.PROTOCOL_TLSv1_2), ciphers=cipher_suite)
self.ssl_context = create_urllib3_context(**params)
super(CloudflareAdapter, self).__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_context'] = self.ssl_context
return super(CloudflareAdapter, self).init_poolmanager(*args, **kwargs)
def proxy_manager_for(self, *args, **kwargs):
kwargs['ssl_context'] = self.ssl_context
return super(CloudflareAdapter, self).proxy_manager_for(*args, **kwargs)
create_scraper = CloudflareScraper.create_scraper
get_tokens = CloudflareScraper.get_tokens
get_cookie_string = CloudflareScraper.get_cookie_string

26
lib/sg_helpers.py

@ -714,6 +714,7 @@ def get_url(url, # type: AnyStr
timeout=30, # type: int
session=None, # type: Optional[requests.Session]
parse_json=False, # type: bool
memcache_cookies=None, # type: dict
raise_status_code=False, # type: bool
raise_exceptions=False, # type: bool
as_binary=False, # type: bool
@ -730,6 +731,8 @@ def get_url(url, # type: AnyStr
Return data from a URI with a possible check for authentication prior to the data fetch.
Raised errors and no data in responses are tracked for making future logic decisions.
# param url_solver=sickbeard.FLARESOLVERR_HOST must be passed if url is behind CF for use in cf_scrape/__init__.py
Returned data is either:
1) a byte-string retrieved from the URL provider.
2) a boolean if successfully used kwargs 'savefile' set to file pathname.
@ -743,6 +746,7 @@ def get_url(url, # type: AnyStr
:param timeout: timeout
:param session: optional session object
:param parse_json: return JSON Dict
:param memcache_cookies: memory persistant store for cookies
:param raise_status_code: raise exception for status codes
:param raise_exceptions: raise exceptions
:param as_binary: return bytes instead of text
@ -780,6 +784,13 @@ def get_url(url, # type: AnyStr
session = CloudflareScraper.create_scraper()
session.headers.update({'User-Agent': USER_AGENT})
proxy_browser = kwargs.get('proxy_browser')
if isinstance(memcache_cookies, dict):
parsed_url = urlparse(url)
domain = parsed_url.netloc
if domain in memcache_cookies:
session.cookies.update(memcache_cookies[domain])
# download and save file or simply fetch url
savename = kwargs.pop('savename', None)
if savename:
@ -935,15 +946,24 @@ def get_url(url, # type: AnyStr
return
if None is result and None is not response and response.ok:
if parse_json:
if isinstance(memcache_cookies, dict):
parsed_url = urlparse(url)
domain = parsed_url.netloc
memcache_cookies[domain] = session.cookies.copy()
if parse_json or proxy_browser:
try:
data_json = response.json()
result = ({}, data_json)[isinstance(data_json, (dict, list))]
if proxy_browser:
result = ({}, data_json.get('solution', {}).get('response', {}))[isinstance(data_json, dict)]
else:
result = ({}, data_json)[isinstance(data_json, (dict, list))]
if resp_sess:
result = result, session
except (TypeError, Exception) as e:
raised = e
logger.warning(u'JSON data issue from URL %s\r\nDetail... %s' % (url, ex(e)))
logger.warning(u'%s data issue from URL %s\r\nDetail... %s' % (
('Proxy browser', 'JSON')[parse_json], url, ex(e)))
elif savename:
try:

5
sickbeard/__init__.py

@ -268,6 +268,7 @@ MAX_WATCHEDSTATE_INTERVAL = 60
SEARCH_UNAIRED = False
UNAIRED_RECENT_SEARCH_ONLY = True
FLARESOLVERR_HOST = None
ADD_SHOWS_WO_DIR = False
ADD_SHOWS_METALANG = 'en'
@ -672,7 +673,7 @@ def init_stage_1(console_logging):
global DOWNLOAD_PROPERS, PROPERS_WEBDL_ONEGRP, WEBDL_TYPES, RECENTSEARCH_INTERVAL, \
BACKLOG_LIMITED_PERIOD, BACKLOG_NOFULL, BACKLOG_PERIOD, USENET_RETENTION, IGNORE_WORDS, REQUIRE_WORDS, \
IGNORE_WORDS, IGNORE_WORDS_REGEX, REQUIRE_WORDS, REQUIRE_WORDS_REGEX, \
ALLOW_HIGH_PRIORITY, SEARCH_UNAIRED, UNAIRED_RECENT_SEARCH_ONLY
ALLOW_HIGH_PRIORITY, SEARCH_UNAIRED, UNAIRED_RECENT_SEARCH_ONLY, FLARESOLVERR_HOST
# Search Settings/NZB search
global USE_NZBS, NZB_METHOD, NZB_DIR, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \
NZBGET_USE_HTTPS, NZBGET_HOST, NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY, \
@ -971,6 +972,7 @@ def init_stage_1(console_logging):
SEARCH_UNAIRED = bool(check_setting_int(CFG, 'General', 'search_unaired', 0))
UNAIRED_RECENT_SEARCH_ONLY = bool(check_setting_int(CFG, 'General', 'unaired_recent_search_only', 1))
FLARESOLVERR_HOST = check_setting_str(CFG, 'General', 'flaresolverr_host', '')
NZB_DIR = check_setting_str(CFG, 'Blackhole', 'nzb_dir', '')
TORRENT_DIR = check_setting_str(CFG, 'Blackhole', 'torrent_dir', '')
@ -1867,6 +1869,7 @@ def save_config():
new_config['General']['search_unaired'] = int(SEARCH_UNAIRED)
new_config['General']['unaired_recent_search_only'] = int(UNAIRED_RECENT_SEARCH_ONLY)
new_config['General']['flaresolverr_host'] = FLARESOLVERR_HOST
new_config['General']['cache_dir'] = ACTUAL_CACHE_DIR if ACTUAL_CACHE_DIR else 'cache'
sg_helpers.CACHE_DIR = CACHE_DIR

5
sickbeard/helpers.py

@ -1085,7 +1085,10 @@ def download_file(url, filename, session=None, **kwargs):
:return: success of download
:rtype: bool
"""
if None is get_url(url, session=session, savename=filename, **kwargs):
sickbeard.MEMCACHE.setdefault('cookies', {})
if None is get_url(url, session=session, savename=filename,
url_solver=sickbeard.FLARESOLVERR_HOST, memcache_cookies=sickbeard.MEMCACHE['cookies'],
**kwargs):
remove_file_perm(filename)
return False
return True

5
sickbeard/image_cache.py

@ -450,8 +450,11 @@ class ImageCache(object):
success = 0
count_urls = len(image_urls)
sources = []
sickbeard.MEMCACHE.setdefault('cookies', {})
for image_url in image_urls or []:
img_data = sg_helpers.get_url(image_url, nocache=True, as_binary=True)
img_data = sg_helpers.get_url(image_url, nocache=True, as_binary=True,
url_solver=sickbeard.FLARESOLVERR_HOST,
memcache_cookies=sickbeard.MEMCACHE['cookies'])
if None is img_data:
continue
crc = '%05X' % (zlib.crc32(img_data) & 0xFFFFFFFF)

5
sickbeard/metadata/helpers.py

@ -42,7 +42,10 @@ def getShowImage(url, img_num=None, show_name=None, supress_log=False):
logger.log(u'Fetching image from ' + temp_url, logger.DEBUG)
image_data = sg_helpers.get_url(temp_url, as_binary=True)
from sickbeard import FLARESOLVERR_HOST, MEMCACHE
MEMCACHE.setdefault('cookies', {})
image_data = sg_helpers.get_url(temp_url, as_binary=True,
url_solver=FLARESOLVERR_HOST, memcache_cookies=MEMCACHE['cookies'])
if None is image_data:
if supress_log:
return

9
sickbeard/providers/generic.py

@ -564,7 +564,10 @@ class GenericProvider(object):
kwargs['raise_status_code'] = True
kwargs['failure_monitor'] = False
kwargs['exclude_no_data'] = False
for k, v in iteritems(dict(headers=self.headers, hooks=dict(response=self.cb_response))):
sickbeard.MEMCACHE.setdefault('cookies', {})
for k, v in iteritems(dict(
headers=self.headers, hooks=dict(response=self.cb_response),
url_solver=sickbeard.FLARESOLVERR_HOST, memcache_cookies=sickbeard.MEMCACHE['cookies'])):
kwargs.setdefault(k, v)
if 'nzbs.in' not in url: # this provider returns 503's 3 out of 4 requests with the persistent session system
kwargs.setdefault('session', self.session)
@ -802,7 +805,7 @@ class GenericProvider(object):
final_file = ek.ek(os.path.join, final_dir, '%s.%s' % (helpers.sanitize_filename(result.name), link_type))
try:
with open(final_file, 'wb') as fp:
fp.write(result.url)
fp.write(decode_bytes(result.url))
fp.flush()
os.fsync(fp.fileno())
saved = True
@ -908,7 +911,7 @@ class GenericProvider(object):
return title, url
def _link(self, url, url_tmpl=None, url_quote=None):
url = '%s' % url # ensure string type
if url and not re.match('(?i)magnet:', url):
if PY2:
try:

4
sickbeard/providers/iptorrents.py

@ -57,7 +57,7 @@ class IPTorrentsProvider(generic.TorrentProvider):
logged_in=(lambda y='': all(
['IPTorrents' in y, 'type="password"' not in y[0:2048], self.has_all_cookies()] +
[(self.session.cookies.get(c, domain='') or 'sg!no!pw') in self.digest
for c in ('uid', 'pass', 'cf_clearance')])),
for c in ('uid', 'pass')])),
failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod
@ -168,7 +168,7 @@ class IPTorrentsProvider(generic.TorrentProvider):
@staticmethod
def ui_string(key):
return 'iptorrents_digest' == key and 'use... \'uid=xx; pass=yy; cf_clearance=zz\'' or ''
return 'iptorrents_digest' == key and 'use... \'uid=xx; pass=yy\'' or ''
provider = IPTorrentsProvider()

4
sickbeard/providers/scenetime.py

@ -50,7 +50,7 @@ class SceneTimeProvider(generic.TorrentProvider):
logged_in=(lambda y='': all(
['staff-support' in y, self.has_all_cookies()] +
[(self.session.cookies.get(x, domain='') or 'sg!no!pw') in self.digest
for x in ('uid', 'pass', 'cf_clearance')])),
for x in ('uid', 'pass')])),
failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod
@ -179,7 +179,7 @@ class SceneTimeProvider(generic.TorrentProvider):
return name
def ui_string(self, key):
cookies = 'use... \'uid=xx; pass=yy; cf_clearance=zz\''
cookies = 'use... \'uid=xx; pass=yy\''
if 'cookie_str_only' == key:
return cookies
if 'scenetime_digest' == key and self._valid_home():

10
sickbeard/providers/skytorrents.py

@ -33,13 +33,17 @@ class SkytorrentsProvider(generic.TorrentProvider):
generic.TorrentProvider.__init__(self, 'Skytorrents')
self.url_base = 'https://skytorrents.lol/'
self.url_home = ['https://skytorrents.%s/' % tld for tld in ('org', 'to', 'net')]
self.urls = {'config_provider_home_uri': self.url_base,
'search': self.url_base + '?category=show&sort=created&query=%s&page=%s'}
self.url_vars = {'search': '?search=%s&sort=created&page=%s'}
self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'search': '%(home)s%(vars)s'}
self.minseed, self.minleech = 2 * [None]
@staticmethod
def _has_signature(data=None):
return data and re.search(r'Sky\sTorrents', data[23:1024:])
def _search_provider(self, search_params, **kwargs):
results = []
self.session.headers['Cache-Control'] = 'max-age=0'

235
sickbeard/providers/thepiratebay.py

@ -17,18 +17,15 @@
from __future__ import with_statement, division
import os
import re
import traceback
from . import generic
from .. import logger, show_name_helpers
from ..common import mediaExtensions, Quality
from .. import logger
from ..helpers import try_int
from ..name_parser.parser import InvalidNameException, InvalidShowException, NameParser
from bs4_parser import BS4Parser
from _23 import b64decodestring, filter_list, quote, unidecode
from _23 import b64decodestring, unidecode
from six import iteritems
@ -37,7 +34,7 @@ class ThePirateBayProvider(generic.TorrentProvider):
def __init__(self):
generic.TorrentProvider.__init__(self, 'The Pirate Bay')
self.url_home = ['https://thepiratebay.se/'] + \
self.url_home = ['https://thepiratebay.org/'] + \
['https://%s/' % b64decodestring(x) for x in [''.join(x) for x in [
[re.sub(r'[h\sI]+', '', x[::-1]) for x in [
'm IY', '5 F', 'HhIc', 'vI J', 'HIhe', 'uI k', '2 d', 'uh l']],
@ -45,11 +42,11 @@ class ThePirateBayProvider(generic.TorrentProvider):
'lN Gc', 'X Yy', 'c lNR', 'vNJNH', 'kQNHe', 'GQdQu', 'wNN9']],
]]]
self.url_vars = {'search': 'search/%s/0/7/200', 'browse': 'tv/latest/',
'search2': 'search.php?q=%s&video=on&category=0&page=0&orderby=99', 'browse2': '?load=/recent'}
self.url_vars = {'search': '/s/?q=%s&video=on&page=0&orderby=',
'search2': 'search.php?q=%s&video=on&search=Pirate+Search&page=0&orderby='}
self.url_tmpl = {'config_provider_home_uri': '%(home)s',
'search': '%(home)s%(vars)s', 'search2': '%(home)s%(vars)s',
'browse': '%(home)s%(vars)s', 'browse2': '%(home)s%(vars)s'}
'search': '%(home)s%(vars)s', 'search2': '%(home)s%(vars)s'}
self.urls = {'api': 'https://apibay.org/q.php?q=%s'}
self.proper_search_terms = None
@ -60,63 +57,6 @@ class ThePirateBayProvider(generic.TorrentProvider):
def _has_signature(data=None):
return data and re.search(r'Pirate\sBay', data[33:7632:])
def _find_season_quality(self, title, torrent_id, ep_number):
""" Return the modified title of a Season Torrent with the quality found inspecting torrent file list """
if not self.url:
return False
quality = Quality.UNKNOWN
file_name = None
data = self.get_url('%sajax_details_filelist.php?id=%s' % (self.url, torrent_id))
if self.should_skip() or not data:
return None
files_list = re.findall('<td.+>(.*?)</td>', data)
if not files_list:
logger.log(u'Unable to get the torrent file list for ' + title, logger.ERROR)
video_files = filter_list(lambda x: x.rpartition('.')[2].lower() in mediaExtensions, files_list)
# Filtering SingleEpisode/MultiSeason Torrent
if ep_number > len(video_files) or float(ep_number * 1.1) < len(video_files):
logger.log(u'Result %s has episode %s and total episodes retrieved in torrent are %s'
% (title, str(ep_number), str(len(video_files))), logger.DEBUG)
logger.log(u'Result %s seems to be a single episode or multiseason torrent, skipping result...'
% title, logger.DEBUG)
return None
if Quality.UNKNOWN != Quality.sceneQuality(title):
return title
for file_name in video_files:
quality = Quality.sceneQuality(os.path.basename(file_name))
if Quality.UNKNOWN != quality:
break
if None is not file_name and Quality.UNKNOWN == quality:
quality = Quality.assumeQuality(os.path.basename(file_name))
if Quality.UNKNOWN == quality:
logger.log(u'Unable to obtain a Season Quality for ' + title, logger.DEBUG)
return None
try:
my_parser = NameParser(show_obj=self.show_obj, indexer_lookup=False)
parse_result = my_parser.parse(file_name)
except (InvalidNameException, InvalidShowException):
return None
logger.log(u'Season quality for %s is %s' % (title, Quality.qualityStrings[quality]), logger.DEBUG)
if parse_result.series_name and parse_result.season_number:
title = '%s S%02d %s' % (parse_result.series_name,
int(parse_result.season_number),
self._reverse_quality(quality))
return title
def _season_strings(self, ep_obj, **kwargs):
if ep_obj.show_obj.air_by_date or ep_obj.show_obj.sports:
@ -145,75 +85,162 @@ class ThePirateBayProvider(generic.TorrentProvider):
items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []}
rc = dict([(k, re.compile('(?i)' + v)) for (k, v) in iteritems({
'info': 'detail', 'get': 'download[^"]+magnet', 'tid': r'.*/(\d{5,}).*',
'info': 'detail|descript', 'get': 'magnet',
'verify': '(?:helper|moderator|trusted|vip)', 'size': r'size[^\d]+(\d+(?:[.,]\d+)?\W*[bkmgt]\w+)'})])
for mode in search_params:
for search_string in search_params[mode]:
search_string = unidecode(search_string)
s_mode = 'browse' if 'Cache' == mode else 'search'
for i in ('', '2'):
search_url = self.urls['%s%s' % (s_mode, i)]
if 'Cache' != mode:
search_url = search_url % quote(search_string)
if 'Cache' != mode:
search_url = self.urls['api'] % search_string
pages = [self.get_url(search_url, parse_json=True)]
else:
urls = [self.urls['api'] % 'category:%s' % cur_cat for cur_cat in (205, 208)]
search_url = ', '.join(urls)
pages = [self.get_url(cur_url, parse_json=True) for cur_url in urls]
seen_not_found = False
if any(pages):
cnt = len(items[mode])
for cur_page in pages:
for cur_item in cur_page or []:
title, total_found = [cur_item.get(k) for k in ('name', 'total_found')]
if 1 == try_int(total_found):
seen_not_found = True
continue
seeders, leechers, size = [try_int(n, n) for n in [
cur_item.get(k) for k in ('seeders', 'leechers', 'size')]]
if not self._reject_item(seeders, leechers):
status, info_hash = [cur_item.get(k) for k in ('status', 'info_hash')]
if self.confirmed and not rc['verify'].search(status):
logger.log(u'Skipping untrusted non-verified result: ' + title, logger.DEBUG)
continue
download_magnet = info_hash if '&tr=' in info_hash \
else self._dhtless_magnet(info_hash, title)
if title and download_magnet:
items[mode].append((title, download_magnet, seeders, self._bytesizer(size)))
html = self.get_url(search_url)
if self.should_skip():
return results
if len(items[mode]):
self._log_search(mode, len(items[mode]) - cnt, search_url)
continue
if seen_not_found and not len(items[mode]):
continue
html = self.get_url(self.urls['config_provider_home_uri'])
if self.should_skip() or not html:
return results
body = re.sub(r'(?sim).*?(<body.*?)<foot.*', r'\1</body>', html)
with BS4Parser(body) as soup:
if 'Cache' != mode:
search_url = None
if 'action="/s/' in body:
search_url = self.urls['search'] % search_string
elif 'action="/search.php' in body:
search_url = self.urls['search2'] % search_string
if search_url:
try:
pages = [self.get_url(search_url, proxy_browser=True)]
except ValueError:
pass
else:
try:
html = self.get_url(self._link(soup.find('a', title="Browse Torrents")['href']))
if html:
js = re.findall(r'check\sthat\s+(\w+.js)\s', html)
if js:
js_file = re.findall('<script[^"]+?"([^"]*?%s[^"]*?).*?</script>' % js[0], html)
if js_file:
html = self.get_url(self._link(js_file[0]))
if html: # could be none from previous get_url for js
# html or js can be source for parsing cat|browse links
urls = re.findall(
'(?i)<a[^>]+?href="([^>]+?(?:cat|browse)[^>]+?)"[^>]+?>[^>]*?tv shows<', html)
search_url = ', '.join([self._link(cur_url) for cur_url in urls])
pages = [self.get_url(self._link(cur_url), proxy_browser=True) for cur_url in urls]
except ValueError:
pass
if not any(pages):
return results
list_type = None
head = None
rows = ''
if len(pages) and '<thead' in pages[0]:
list_type = 0
headers = 'seed|leech|size'
for cur_html in pages:
try:
with BS4Parser(cur_html, parse_only=dict(table={'id': 'searchResult'})) as tbl:
rows += ''.join([_r.prettify() for _r in tbl.select('tr')[1:]])
if not head:
header = [re.sub(r'(?i).*?(?:order\sy\s)?(%s)(?:ers)?.*?' % headers, r'\1',
'' if not x else x.get('title', '').lower()) for x in
[t.select_one('[title]') for t in
tbl.find('tr', class_='header').find_all('th')]]
head = dict((k, header.index(k) - len(header)) for k in headers.split('|'))
except(BaseException, Exception):
pass
html = ('', '<table><tr data="header-placeholder"></tr>%s</table>' % rows)[all([head, rows])]
elif len(pages) and '<ol' in pages[0]:
list_type = 1
headers = 'seed|leech|size'
for cur_html in pages:
try:
with BS4Parser(cur_html, parse_only=dict(ol={'id': 'torrents'})) as tbl:
rows += ''.join([_r.prettify() for _r in tbl.find_all('li', class_='list-entry')])
if not head:
header = [re.sub(
'(?i).*(?:item-(%s)).*' % headers, r'\1', ''.join(t.get('class', '')))
for t in tbl.find('li', class_='list-header').find_all('span')]
head = dict((k, header.index(k) - len(header)) for k in headers.split('|'))
except(BaseException, Exception):
pass
html = ('', '<ol><li data="header-placeholder"></li>%s</ol>' % rows)[all([head, rows])]
html = '<!DOCTYPE html><html><head></head><body id="tpb_results">%s</body></html>' % html
if html and not self._has_no_results(html):
break
cnt = len(items[mode])
try:
if not html or self._has_no_results(html):
if None is list_type or not html or self._has_no_results(html):
self._url = None
raise generic.HaltParseException
with BS4Parser(html, parse_only=dict(table={'id': 'searchResult'})) as tbl:
tbl_rows = [] if not tbl else tbl.find_all('tr')
with BS4Parser(html, parse_only=dict(body={'id': 'tpb_results'})) as tbl:
row_type = ('li', 'tr')[not list_type]
tbl_rows = [] if not tbl else tbl.find_all(row_type)
if 2 > len(tbl_rows):
raise generic.HaltParseException
head = None
for tr in tbl.find_all('tr')[1:]:
cells = tr.find_all('td')
for tr in tbl.find_all(row_type)[1:]:
cells = tr.find_all(('span', 'td')[not list_type])
if 3 > len(cells):
continue
try:
head = head if None is not head else self._header_row(tr)
seeders, leechers = [try_int(cells[head[x]].get_text().strip())
for x in ('seed', 'leech')]
seeders, leechers, size = [try_int(n, n) for n in [
cells[head[x]].get_text().strip() for x in ('seed', 'leech', 'size')]]
if self._reject_item(seeders, leechers):
continue
info = tr.find('a', title=rc['info'])
info = tr.find('a', title=rc['info']) or tr.find('a', href=rc['info'])
title = info.get_text().strip().replace('_', '.')
tid = rc['tid'].sub(r'\1', str(info['href']))
download_magnet = tr.find('a', title=rc['get'])['href']
download_magnet = (tr.find('a', title=rc['get'])
or tr.find('a', href=rc['get']))['href']
except (AttributeError, TypeError, ValueError):
continue
if self.confirmed and not tr.find('img', title=rc['verify']):
if self.confirmed and not (
tr.find('img', title=rc['verify']) or tr.find('img', alt=rc['verify'])
or tr.find('img', src=rc['verify'])):
logger.log(u'Skipping untrusted non-verified result: ' + title, logger.DEBUG)
continue
# Check number video files = episode in season and
# find the real Quality for full season torrent analyzing files in torrent
if 'Season' == mode and 'sponly' == search_mode:
ep_number = int(epcount // len(set(show_name_helpers.allPossibleShowNames(
self.show_obj))))
title = self._find_season_quality(title, tid, ep_number)
if title and download_magnet:
size = None
try:
size = rc['size'].findall(tr.find_all(class_='detDesc')[0].get_text())[0]
except (BaseException, Exception):
pass
items[mode].append((title, download_magnet, seeders, self._bytesizer(size)))
except generic.HaltParseException:

13
sickbeard/providers/torlock.py

@ -77,14 +77,15 @@ class TorLockProvider(generic.TorrentProvider):
cnt = len(items[mode])
try:
if not html or self._has_no_results(html):
if not html or self._has_no_results(html) or re.search(r'<b>Error:\sNot\sFound</b>', html):
raise generic.HaltParseException
with BS4Parser(html.replace('thead', 'tr')) as soup:
tbl = soup.find(
'div', class_=('panel panel-default', 'table-responsive')['Cache' == mode])
tbl = soup.find_all('div', class_='table-responsive')
if None is tbl:
raise generic.HaltParseException
if len(tbl):
tbl = tbl[-1]
tbl = tbl.find(
'table', class_='table table-striped table-bordered table-hover table-condensed')
tbl_rows = [] if not tbl else tbl.find_all('tr')
@ -95,12 +96,16 @@ class TorLockProvider(generic.TorrentProvider):
head = None
for tr in tbl_rows[1:]:
cells = tr.find_all('td')
if 5 > len(cells):
if 5 > len(cells) or not tr.find('td', class_='tf'):
continue
try:
head = head if None is not head else self._header_row(tr)
seeders, leechers, size = [try_int(n, n) for n in [
cells[head[x]].get_text().strip() for x in ('seed', 'leech', 'size')]]
if not len(tbl_rows[0].select('th a[href *="file"]')):
seeders, leechers, size = [try_int(n, n) for n in [
tr.find_all('td', class_=x)[0].get_text().strip()
for x in ('tul', 'tdl', 'ts')]]
if self._reject_item(seeders, leechers, verified=self.confirmed and not (
tr.find('img', src=rc['versrc']) or tr.find('img', title=rc['verified']))):
continue

4
sickbeard/providers/torrentday.py

@ -56,7 +56,7 @@ class TorrentDayProvider(generic.TorrentProvider):
logged_in=(lambda y='': all(
['RSS URL' in y, self.has_all_cookies()] +
[(self.session.cookies.get(c, domain='') or 'sg!no!pw') in self.digest
for c in ('uid', 'pass', 'cf_clearance')])),
for c in ('uid', 'pass')])),
failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod
@ -165,7 +165,7 @@ class TorrentDayProvider(generic.TorrentProvider):
return super(TorrentDayProvider, self)._episode_strings(ep_obj, sep_date='.', date_or=True, **kwargs)
def ui_string(self, key):
cookies = 'use... \'uid=xx; pass=yy; cf_clearance=zz\''
cookies = 'use... \'uid=xx; pass=yy\''
if 'cookie_str_only' == key:
return cookies
if 'torrentday_digest' == key and self._valid_home():

4
sickbeard/providers/torrenting.py

@ -47,7 +47,7 @@ class TorrentingProvider(generic.TorrentProvider):
return super(TorrentingProvider, self)._authorised(
logged_in=(lambda y='': all(
['RSS link' in y, self.has_all_cookies()] +
[(self.session.cookies.get(x) or 'sg!no!pw') in self.digest for x in ('uid', 'pass', 'cf_clearance')])),
[(self.session.cookies.get(x) or 'sg!no!pw') in self.digest for x in ('uid', 'pass')])),
failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod
@ -120,7 +120,7 @@ class TorrentingProvider(generic.TorrentProvider):
@staticmethod
def ui_string(key):
return 'torrenting_digest' == key and 'use... \'uid=xx; pass=yy; cf_clearance=zz\'' or ''
return 'torrenting_digest' == key and 'use... \'uid=xx; pass=yy\'' or ''
provider = TorrentingProvider()

21
sickbeard/webserve.py

@ -82,6 +82,7 @@ from tornado.concurrent import run_on_executor
from ._legacy import LegacyBaseHandler
from lib import subliminal
from lib.cfscrape import CloudflareScraper
from lib.dateutil import tz
from lib.fuzzywuzzy import fuzz
from lib.libtrakt import TraktAPI
@ -1535,6 +1536,22 @@ class Home(MainHandler):
return acces_msg
def test_flaresolverr(self, host=None):
self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')
hosts = config.clean_hosts(host, default_port=8191)
if not hosts:
return 'Fail: No valid host(s)'
try:
fs_ver = CloudflareScraper().test_flaresolverr(host)
result = 'Successful connection to FlareSolverr %s' % fs_ver
except(BaseException, Exception):
result = 'Failed host connection (is it running?)'
ui.notifications.message('Tested Flaresolverr:', unquote_plus(hosts))
return result
@staticmethod
def discover_emby():
return notifiers.NotifierFactory().get('EMBY').discover_server()
@ -7186,7 +7203,7 @@ class ConfigSearch(Config):
use_nzbs=None, use_torrents=None, nzb_method=None, torrent_method=None,
usenet_retention=None, ignore_words=None, require_words=None,
download_propers=None, propers_webdl_onegrp=None,
search_unaired=None, unaired_recent_search_only=None,
search_unaired=None, unaired_recent_search_only=None, flaresolverr_host=None,
allow_high_priority=None,
sab_username=None, sab_password=None, sab_apikey=None, sab_category=None, sab_host=None,
nzbget_username=None, nzbget_password=None, nzbget_category=None, nzbget_host=None,
@ -7246,6 +7263,8 @@ class ConfigSearch(Config):
sickbeard.UNAIRED_RECENT_SEARCH_ONLY = bool(config.checkbox_to_value(unaired_recent_search_only,
value_off=1, value_on=0))
sickbeard.FLARESOLVERR_HOST = config.clean_url(flaresolverr_host)
sickbeard.ALLOW_HIGH_PRIORITY = config.checkbox_to_value(allow_high_priority)
sickbeard.SAB_USERNAME = sab_username

Loading…
Cancel
Save