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 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 * Add correct user entry mistakes for nzbs2go api url

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

@ -1,6 +1,6 @@
#import sickbeard #import sickbeard
#from sickbeard import clients #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_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp# <% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
## ##
@ -237,7 +237,21 @@
</div> </div>
</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> </fieldset>
</div><!-- /component-group1 //--> </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() && '' /^\s*scgi:\/\//.test($('#torrent_host').val()) ? opts$.hide() && $('#torrent_label').select() && ''
: opts$.show() && $('#torrent_username').select() && $(this).prop('defaultValue'));});}; : 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() { function toggleTorrentTitle() {
var noTorrent$ = $('#no_torrents'); var noTorrent$ = $('#no_torrents');
if ($('#use_torrents').prop('checked')) 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.exceptions import RequestException
from requests.models import Response from requests.models import Response
from requests.sessions import Session from requests.sessions import Session
from urllib3.util.ssl_ import create_urllib3_context
import logging import logging
import random import random
import re import re
import ssl
import time import time
import js2py from _23 import b64encodestring, urlparse
from _23 import b64decodestring, b64encodestring, urlparse, urlunparse
DEFAULT_USER_AGENTS = [ DEFAULT_USER_AGENTS = [
@ -46,25 +41,27 @@ class CloudflareScraper(Session):
self.delay = kwargs.pop('delay', self.default_delay) self.delay = kwargs.pop('delay', self.default_delay)
self.start_time = None self.start_time = None
self.cipher_suite = self.load_cipher_suite()
self.mount('https://', CloudflareAdapter(self.cipher_suite))
self.trust_env = False self.trust_env = False
def request(self, method, url, *args, **kwargs): 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())) if (isinstance(resp, type(Response()))
and resp.status_code in (503, 429, 403)): and resp.status_code in (503, 429, 403)):
self.start_time = time.time() self.start_time = time.time()
if (re.search('(?i)cloudflare', resp.headers.get('Server', '')) if (re.search('(?i)cloudflare', resp.headers.get('Server', ''))
and b'jschl_vc' in resp.content and b'jschl_vc' in resp.content
and b'jschl_answer' 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: elif b'ddgu' in resp.content:
resp = self.solve_ddg_challenge(resp, **kwargs) resp = self.solve_ddg_challenge(resp, **kwargs)
# Otherwise, no anti-bot detected
return resp return resp
def wait(self): def wait(self):
@ -87,142 +84,74 @@ class CloudflareScraper(Session):
pass pass
return resp 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 body = resp.text
parsed_url = urlparse(resp.url) parsed_url = urlparse(resp.url)
domain = parsed_url.netloc domain = parsed_url.netloc
if '/cdn-cgi/l/chk_captcha' in body or 'cf_chl_captcha' in body: if '/cdn-cgi/l/chk_captcha' in body or 'cf_chl_captcha' in body:
raise CloudflareError( 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) (domain, self.cf_ua), response=resp)
try: if None is self.get_content(
action, method = re.findall(r'(?sim)<form.*?id="challenge.*?action="/?([^?"]+).*?method="([^"]+)', body)[0] 'GET', (resp.request.url, '%s://%s/' % (parsed_url.scheme, domain))['POST' == resp.request.method],
except(Exception, BaseException): url_solver, user_agent=resp.request.headers.get('User-Agent')):
action, method = 'cdn-cgi/l/chk_jschl', resp.request.method raise ValueError('Failed to validate Cloudflare anti-bot IUAM challenge')
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 self.delay == self.default_delay: time.sleep(1.2)
try:
# no instantiated delay, therefore check js for hard coded CF delay final_response = super(CloudflareScraper, self).request(resp.request.method, resp.request.url, headers={
self.delay = float(re.search(r'submit\(\);[^0-9]*?([0-9]+)', body).group(1)) / float(1000) 'User-Agent': resp.request.headers.get('User-Agent')}, **original_kwargs)
except (BaseException, Exception):
pass # if final_response and 200 == getattr(final_response, 'status_code'):
# return final_response
for i in re.findall(r'(<input[^>]+?hidden[^>]+?>)', re.sub(r'(?sim)<!--\s+<input.*?(?=<)', '', body)): return final_response
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)
@classmethod @classmethod
def create_scraper(cls, sess=None, **kwargs): 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 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 create_scraper = CloudflareScraper.create_scraper
get_tokens = CloudflareScraper.get_tokens get_tokens = CloudflareScraper.get_tokens
get_cookie_string = CloudflareScraper.get_cookie_string 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 timeout=30, # type: int
session=None, # type: Optional[requests.Session] session=None, # type: Optional[requests.Session]
parse_json=False, # type: bool parse_json=False, # type: bool
memcache_cookies=None, # type: dict
raise_status_code=False, # type: bool raise_status_code=False, # type: bool
raise_exceptions=False, # type: bool raise_exceptions=False, # type: bool
as_binary=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. 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. 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: Returned data is either:
1) a byte-string retrieved from the URL provider. 1) a byte-string retrieved from the URL provider.
2) a boolean if successfully used kwargs 'savefile' set to file pathname. 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 timeout: timeout
:param session: optional session object :param session: optional session object
:param parse_json: return JSON Dict :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_status_code: raise exception for status codes
:param raise_exceptions: raise exceptions :param raise_exceptions: raise exceptions
:param as_binary: return bytes instead of text :param as_binary: return bytes instead of text
@ -780,6 +784,13 @@ def get_url(url, # type: AnyStr
session = CloudflareScraper.create_scraper() session = CloudflareScraper.create_scraper()
session.headers.update({'User-Agent': USER_AGENT}) 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 # download and save file or simply fetch url
savename = kwargs.pop('savename', None) savename = kwargs.pop('savename', None)
if savename: if savename:
@ -935,15 +946,24 @@ def get_url(url, # type: AnyStr
return return
if None is result and None is not response and response.ok: 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: try:
data_json = response.json() 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: if resp_sess:
result = result, session result = result, session
except (TypeError, Exception) as e: except (TypeError, Exception) as e:
raised = 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: elif savename:
try: try:

5
sickbeard/__init__.py

@ -268,6 +268,7 @@ MAX_WATCHEDSTATE_INTERVAL = 60
SEARCH_UNAIRED = False SEARCH_UNAIRED = False
UNAIRED_RECENT_SEARCH_ONLY = True UNAIRED_RECENT_SEARCH_ONLY = True
FLARESOLVERR_HOST = None
ADD_SHOWS_WO_DIR = False ADD_SHOWS_WO_DIR = False
ADD_SHOWS_METALANG = 'en' ADD_SHOWS_METALANG = 'en'
@ -672,7 +673,7 @@ def init_stage_1(console_logging):
global DOWNLOAD_PROPERS, PROPERS_WEBDL_ONEGRP, WEBDL_TYPES, RECENTSEARCH_INTERVAL, \ global DOWNLOAD_PROPERS, PROPERS_WEBDL_ONEGRP, WEBDL_TYPES, RECENTSEARCH_INTERVAL, \
BACKLOG_LIMITED_PERIOD, BACKLOG_NOFULL, BACKLOG_PERIOD, USENET_RETENTION, IGNORE_WORDS, REQUIRE_WORDS, \ BACKLOG_LIMITED_PERIOD, BACKLOG_NOFULL, BACKLOG_PERIOD, USENET_RETENTION, IGNORE_WORDS, REQUIRE_WORDS, \
IGNORE_WORDS, IGNORE_WORDS_REGEX, REQUIRE_WORDS, REQUIRE_WORDS_REGEX, \ 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 # Search Settings/NZB search
global USE_NZBS, NZB_METHOD, NZB_DIR, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \ 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, \ 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)) 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)) 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', '') NZB_DIR = check_setting_str(CFG, 'Blackhole', 'nzb_dir', '')
TORRENT_DIR = check_setting_str(CFG, 'Blackhole', 'torrent_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']['search_unaired'] = int(SEARCH_UNAIRED)
new_config['General']['unaired_recent_search_only'] = int(UNAIRED_RECENT_SEARCH_ONLY) 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' new_config['General']['cache_dir'] = ACTUAL_CACHE_DIR if ACTUAL_CACHE_DIR else 'cache'
sg_helpers.CACHE_DIR = CACHE_DIR 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 :return: success of download
:rtype: bool :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) remove_file_perm(filename)
return False return False
return True return True

5
sickbeard/image_cache.py

@ -450,8 +450,11 @@ class ImageCache(object):
success = 0 success = 0
count_urls = len(image_urls) count_urls = len(image_urls)
sources = [] sources = []
sickbeard.MEMCACHE.setdefault('cookies', {})
for image_url in image_urls or []: 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: if None is img_data:
continue continue
crc = '%05X' % (zlib.crc32(img_data) & 0xFFFFFFFF) 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) 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 None is image_data:
if supress_log: if supress_log:
return return

9
sickbeard/providers/generic.py

@ -564,7 +564,10 @@ class GenericProvider(object):
kwargs['raise_status_code'] = True kwargs['raise_status_code'] = True
kwargs['failure_monitor'] = False kwargs['failure_monitor'] = False
kwargs['exclude_no_data'] = 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) 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 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) 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)) final_file = ek.ek(os.path.join, final_dir, '%s.%s' % (helpers.sanitize_filename(result.name), link_type))
try: try:
with open(final_file, 'wb') as fp: with open(final_file, 'wb') as fp:
fp.write(result.url) fp.write(decode_bytes(result.url))
fp.flush() fp.flush()
os.fsync(fp.fileno()) os.fsync(fp.fileno())
saved = True saved = True
@ -908,7 +911,7 @@ class GenericProvider(object):
return title, url return title, url
def _link(self, url, url_tmpl=None, url_quote=None): 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 url and not re.match('(?i)magnet:', url):
if PY2: if PY2:
try: try:

4
sickbeard/providers/iptorrents.py

@ -57,7 +57,7 @@ class IPTorrentsProvider(generic.TorrentProvider):
logged_in=(lambda y='': all( logged_in=(lambda y='': all(
['IPTorrents' in y, 'type="password"' not in y[0:2048], self.has_all_cookies()] + ['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 [(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')) failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod @staticmethod
@ -168,7 +168,7 @@ class IPTorrentsProvider(generic.TorrentProvider):
@staticmethod @staticmethod
def ui_string(key): 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() provider = IPTorrentsProvider()

4
sickbeard/providers/scenetime.py

@ -50,7 +50,7 @@ class SceneTimeProvider(generic.TorrentProvider):
logged_in=(lambda y='': all( logged_in=(lambda y='': all(
['staff-support' in y, self.has_all_cookies()] + ['staff-support' in y, self.has_all_cookies()] +
[(self.session.cookies.get(x, domain='') or 'sg!no!pw') in self.digest [(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')) failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod @staticmethod
@ -179,7 +179,7 @@ class SceneTimeProvider(generic.TorrentProvider):
return name return name
def ui_string(self, key): 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: if 'cookie_str_only' == key:
return cookies return cookies
if 'scenetime_digest' == key and self._valid_home(): 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') 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, self.url_vars = {'search': '?search=%s&sort=created&page=%s'}
'search': self.url_base + '?category=show&sort=created&query=%s&page=%s'} self.url_tmpl = {'config_provider_home_uri': '%(home)s', 'search': '%(home)s%(vars)s'}
self.minseed, self.minleech = 2 * [None] 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): def _search_provider(self, search_params, **kwargs):
results = [] results = []
self.session.headers['Cache-Control'] = 'max-age=0' self.session.headers['Cache-Control'] = 'max-age=0'

235
sickbeard/providers/thepiratebay.py

@ -17,18 +17,15 @@
from __future__ import with_statement, division from __future__ import with_statement, division
import os
import re import re
import traceback import traceback
from . import generic from . import generic
from .. import logger, show_name_helpers from .. import logger
from ..common import mediaExtensions, Quality
from ..helpers import try_int from ..helpers import try_int
from ..name_parser.parser import InvalidNameException, InvalidShowException, NameParser
from bs4_parser import BS4Parser from bs4_parser import BS4Parser
from _23 import b64decodestring, filter_list, quote, unidecode from _23 import b64decodestring, unidecode
from six import iteritems from six import iteritems
@ -37,7 +34,7 @@ class ThePirateBayProvider(generic.TorrentProvider):
def __init__(self): def __init__(self):
generic.TorrentProvider.__init__(self, 'The Pirate Bay') 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 [ ['https://%s/' % b64decodestring(x) for x in [''.join(x) for x in [
[re.sub(r'[h\sI]+', '', x[::-1]) 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']], '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']], 'lN Gc', 'X Yy', 'c lNR', 'vNJNH', 'kQNHe', 'GQdQu', 'wNN9']],
]]] ]]]
self.url_vars = {'search': 'search/%s/0/7/200', 'browse': 'tv/latest/', self.url_vars = {'search': '/s/?q=%s&video=on&page=0&orderby=',
'search2': 'search.php?q=%s&video=on&category=0&page=0&orderby=99', 'browse2': '?load=/recent'} 'search2': 'search.php?q=%s&video=on&search=Pirate+Search&page=0&orderby='}
self.url_tmpl = {'config_provider_home_uri': '%(home)s', self.url_tmpl = {'config_provider_home_uri': '%(home)s',
'search': '%(home)s%(vars)s', 'search2': '%(home)s%(vars)s', 'search': '%(home)s%(vars)s', 'search2': '%(home)s%(vars)s'}
'browse': '%(home)s%(vars)s', 'browse2': '%(home)s%(vars)s'} self.urls = {'api': 'https://apibay.org/q.php?q=%s'}
self.proper_search_terms = None self.proper_search_terms = None
@ -60,63 +57,6 @@ class ThePirateBayProvider(generic.TorrentProvider):
def _has_signature(data=None): def _has_signature(data=None):
return data and re.search(r'Pirate\sBay', data[33:7632:]) 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): def _season_strings(self, ep_obj, **kwargs):
if ep_obj.show_obj.air_by_date or ep_obj.show_obj.sports: 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': []} items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []}
rc = dict([(k, re.compile('(?i)' + v)) for (k, v) in iteritems({ 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+)'})]) 'verify': '(?:helper|moderator|trusted|vip)', 'size': r'size[^\d]+(\d+(?:[.,]\d+)?\W*[bkmgt]\w+)'})])
for mode in search_params: for mode in search_params:
for search_string in search_params[mode]: for search_string in search_params[mode]:
search_string = unidecode(search_string) search_string = unidecode(search_string)
s_mode = 'browse' if 'Cache' == mode else 'search' if 'Cache' != mode:
for i in ('', '2'): search_url = self.urls['api'] % search_string
search_url = self.urls['%s%s' % (s_mode, i)] pages = [self.get_url(search_url, parse_json=True)]
if 'Cache' != mode: else:
search_url = search_url % quote(search_string) 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 len(items[mode]):
if self.should_skip(): self._log_search(mode, len(items[mode]) - cnt, search_url)
return results 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]) cnt = len(items[mode])
try: 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 self._url = None
raise generic.HaltParseException raise generic.HaltParseException
with BS4Parser(html, parse_only=dict(table={'id': 'searchResult'})) as tbl: with BS4Parser(html, parse_only=dict(body={'id': 'tpb_results'})) as tbl:
tbl_rows = [] if not tbl else tbl.find_all('tr') row_type = ('li', 'tr')[not list_type]
tbl_rows = [] if not tbl else tbl.find_all(row_type)
if 2 > len(tbl_rows): if 2 > len(tbl_rows):
raise generic.HaltParseException raise generic.HaltParseException
head = None for tr in tbl.find_all(row_type)[1:]:
for tr in tbl.find_all('tr')[1:]: cells = tr.find_all(('span', 'td')[not list_type])
cells = tr.find_all('td')
if 3 > len(cells): if 3 > len(cells):
continue continue
try: try:
head = head if None is not head else self._header_row(tr) head = head if None is not head else self._header_row(tr)
seeders, leechers = [try_int(cells[head[x]].get_text().strip()) seeders, leechers, size = [try_int(n, n) for n in [
for x in ('seed', 'leech')] cells[head[x]].get_text().strip() for x in ('seed', 'leech', 'size')]]
if self._reject_item(seeders, leechers): if self._reject_item(seeders, leechers):
continue 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('_', '.') title = info.get_text().strip().replace('_', '.')
tid = rc['tid'].sub(r'\1', str(info['href'])) download_magnet = (tr.find('a', title=rc['get'])
download_magnet = tr.find('a', title=rc['get'])['href'] or tr.find('a', href=rc['get']))['href']
except (AttributeError, TypeError, ValueError): except (AttributeError, TypeError, ValueError):
continue 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) logger.log(u'Skipping untrusted non-verified result: ' + title, logger.DEBUG)
continue 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: 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))) items[mode].append((title, download_magnet, seeders, self._bytesizer(size)))
except generic.HaltParseException: except generic.HaltParseException:

13
sickbeard/providers/torlock.py

@ -77,14 +77,15 @@ class TorLockProvider(generic.TorrentProvider):
cnt = len(items[mode]) cnt = len(items[mode])
try: 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 raise generic.HaltParseException
with BS4Parser(html.replace('thead', 'tr')) as soup: with BS4Parser(html.replace('thead', 'tr')) as soup:
tbl = soup.find( tbl = soup.find_all('div', class_='table-responsive')
'div', class_=('panel panel-default', 'table-responsive')['Cache' == mode])
if None is tbl: if None is tbl:
raise generic.HaltParseException raise generic.HaltParseException
if len(tbl):
tbl = tbl[-1]
tbl = tbl.find( tbl = tbl.find(
'table', class_='table table-striped table-bordered table-hover table-condensed') 'table', class_='table table-striped table-bordered table-hover table-condensed')
tbl_rows = [] if not tbl else tbl.find_all('tr') tbl_rows = [] if not tbl else tbl.find_all('tr')
@ -95,12 +96,16 @@ class TorLockProvider(generic.TorrentProvider):
head = None head = None
for tr in tbl_rows[1:]: for tr in tbl_rows[1:]:
cells = tr.find_all('td') cells = tr.find_all('td')
if 5 > len(cells): if 5 > len(cells) or not tr.find('td', class_='tf'):
continue continue
try: try:
head = head if None is not head else self._header_row(tr) head = head if None is not head else self._header_row(tr)
seeders, leechers, size = [try_int(n, n) for n in [ seeders, leechers, size = [try_int(n, n) for n in [
cells[head[x]].get_text().strip() for x in ('seed', 'leech', 'size')]] 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 ( if self._reject_item(seeders, leechers, verified=self.confirmed and not (
tr.find('img', src=rc['versrc']) or tr.find('img', title=rc['verified']))): tr.find('img', src=rc['versrc']) or tr.find('img', title=rc['verified']))):
continue continue

4
sickbeard/providers/torrentday.py

@ -56,7 +56,7 @@ class TorrentDayProvider(generic.TorrentProvider):
logged_in=(lambda y='': all( logged_in=(lambda y='': all(
['RSS URL' in y, self.has_all_cookies()] + ['RSS URL' in y, self.has_all_cookies()] +
[(self.session.cookies.get(c, domain='') or 'sg!no!pw') in self.digest [(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')) failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod @staticmethod
@ -165,7 +165,7 @@ class TorrentDayProvider(generic.TorrentProvider):
return super(TorrentDayProvider, self)._episode_strings(ep_obj, sep_date='.', date_or=True, **kwargs) return super(TorrentDayProvider, self)._episode_strings(ep_obj, sep_date='.', date_or=True, **kwargs)
def ui_string(self, key): 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: if 'cookie_str_only' == key:
return cookies return cookies
if 'torrentday_digest' == key and self._valid_home(): 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( return super(TorrentingProvider, self)._authorised(
logged_in=(lambda y='': all( logged_in=(lambda y='': all(
['RSS link' in y, self.has_all_cookies()] + ['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')) failed_msg=(lambda y=None: u'Invalid cookie details for %s. Check settings'))
@staticmethod @staticmethod
@ -120,7 +120,7 @@ class TorrentingProvider(generic.TorrentProvider):
@staticmethod @staticmethod
def ui_string(key): 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() provider = TorrentingProvider()

21
sickbeard/webserve.py

@ -82,6 +82,7 @@ from tornado.concurrent import run_on_executor
from ._legacy import LegacyBaseHandler from ._legacy import LegacyBaseHandler
from lib import subliminal from lib import subliminal
from lib.cfscrape import CloudflareScraper
from lib.dateutil import tz from lib.dateutil import tz
from lib.fuzzywuzzy import fuzz from lib.fuzzywuzzy import fuzz
from lib.libtrakt import TraktAPI from lib.libtrakt import TraktAPI
@ -1535,6 +1536,22 @@ class Home(MainHandler):
return acces_msg 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 @staticmethod
def discover_emby(): def discover_emby():
return notifiers.NotifierFactory().get('EMBY').discover_server() 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, use_nzbs=None, use_torrents=None, nzb_method=None, torrent_method=None,
usenet_retention=None, ignore_words=None, require_words=None, usenet_retention=None, ignore_words=None, require_words=None,
download_propers=None, propers_webdl_onegrp=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, allow_high_priority=None,
sab_username=None, sab_password=None, sab_apikey=None, sab_category=None, sab_host=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, 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, sickbeard.UNAIRED_RECENT_SEARCH_ONLY = bool(config.checkbox_to_value(unaired_recent_search_only,
value_off=1, value_on=0)) 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.ALLOW_HIGH_PRIORITY = config.checkbox_to_value(allow_high_priority)
sickbeard.SAB_USERNAME = sab_username sickbeard.SAB_USERNAME = sab_username

Loading…
Cancel
Save