class GenericClient(object): def __init__(self, name, host=None, username=None, password=None): self.name = name self.username = sickrage.app.config.torrent_username if not username else username self.password = sickrage.app.config.torrent_password if not password else password self.host = sickrage.app.config.torrent_host if not host else host self.rpcurl = sickrage.app.config.torrent_rpcurl self.url = None self.auth = None self.last_time = time.time() self.session = WebSession(cache=False) self._response = None @property def response(self): return self._response @response.setter def response(self, value): self._response = value def _request(self, method='get', params=None, data=None, *args, **kwargs): if time.time() > self.last_time + 1800 or not self.auth: self.last_time = time.time() self._get_auth() sickrage.app.log.debug( '{name}: Requested a {method} connection to {url} with' ' params: {params} Data: {data}'.format( name=self.name, method=method.upper(), url=self.url, params=params, data=str(data)[0:99] + '...' if len(str(data)) > 102 else str(data))) if not self.auth: sickrage.app.log.warning(self.name + ': Authentication Failed') return False self.response = self.session.request(method.upper(), self.url, params=params, data=data, auth=(self.username, self.password), timeout=120, verify=False, *args, **kwargs) if not self.response or not self.response.text: return False sickrage.app.log.debug( '{name}: Response to {method} request is {response}'.format( name=self.name, method=method.upper(), response=self.response.text)) return True def _get_auth(self): """ This should be overridden and should return the auth_id needed for the client """ return None def test_authentication(self): # verify valid url self.response = self.session.get(self.url or self.host, timeout=120, verify=False) if self.response is None: return False, 'Error: Unable to connect to ' + self.name # verify auth if self._get_auth(): return True, 'Success: Connected and Authenticated' return False, 'Error: Unable to get ' + self.name + ' Authentication, check your config!'
class GenericClient(object): def __init__(self, name, host=None, username=None, password=None): self.name = name self.username = sickrage.app.config.torrent_username if not username else username self.password = sickrage.app.config.torrent_password if not password else password self.host = sickrage.app.config.torrent_host if not host else host self.rpcurl = sickrage.app.config.torrent_rpcurl self.url = None self.auth = None self.last_time = time.time() self.session = WebSession(cache=False) self._response = None @property def response(self): return self._response @response.setter def response(self, value): self._response = value def _request(self, method='get', params=None, data=None, *args, **kwargs): if time.time() > self.last_time + 1800 or not self.auth: self.last_time = time.time() self._get_auth() sickrage.app.log.debug( '{name}: Requested a {method} connection to {url} with' ' params: {params} Data: {data}'.format( name=self.name, method=method.upper(), url=self.url, params=params, data=str(data)[0:99] + '...' if len(str(data)) > 102 else str(data))) if not self.auth: sickrage.app.log.warning(self.name + ': Authentication Failed') return False try: self.response = self.session.request(method.upper(), self.url, params=params, data=data, auth=(self.username, self.password), timeout=120, verify=False, *args, **kwargs) except Exception: return False sickrage.app.log.debug( '{name}: Response to {method} request is {response}'.format( name=self.name, method=method.upper(), response=self.response.text)) return True def _get_auth(self): """ This should be overridden and should return the auth_id needed for the client """ return None def _add_torrent_uri(self, result): """ This should be overridden should return the True/False from the client when a torrent is added via url (magnet or .torrent link) """ return False def _add_torrent_file(self, result): """ This should be overridden should return the True/False from the client when a torrent is added via result.content (only .torrent file) """ return False def _set_torrent_label(self, result): """ This should be overridden should return the True/False from the client when a torrent is set with label """ return True def _set_torrent_ratio(self, result): """ This should be overridden should return the True/False from the client when a torrent is set with ratio """ return True def _set_torrent_seed_time(self, result): """ This should be overridden should return the True/False from the client when a torrent is set with a seed time """ return True def _set_torrent_priority(self, result): """ This should be overriden should return the True/False from the client when a torrent is set with result.priority (-1 = low, 0 = normal, 1 = high) """ return True def _set_torrent_path(self, torrent_path): """ This should be overridden should return the True/False from the client when a torrent is set with path """ return True def _set_torrent_pause(self, result): """ This should be overridden should return the True/False from the client when a torrent is set with pause """ return True def _get_torrent_hash(self, result): if result.url.startswith('magnet'): result.hash = re.findall(r'urn:btih:([\w]{32,40})', result.url)[0] if len(result.hash) == 32: result.hash = b16encode(b32decode(result.hash)).lower() else: if not result.content: sickrage.app.log.error('Torrent without content') raise Exception('Torrent without content') try: torrent_bdecode = bdecode(result.content) except BTFailure: sickrage.app.log.error('Unable to bdecode torrent') sickrage.app.log.debug('Torrent bencoded data: %r' % result.content) raise try: info = torrent_bdecode["info"] except Exception: sickrage.app.log.error('Unable to find info field in torrent') raise result.hash = sha1(bencode(info)).hexdigest() return result def sendTORRENT(self, result): r_code = False sickrage.app.log.debug('Calling ' + self.name + ' Client') if not self._get_auth(): sickrage.app.log.error(self.name + ': Authentication Failed') return r_code try: # Sets per provider seed ratio result.ratio = result.provider.seed_ratio # lazy fix for now, I'm sure we already do this somewhere else too result = self._get_torrent_hash(result) # convert to magnetic url if result has info hash and is not a private provider if sickrage.app.config.torrent_file_to_magnet: if result.hash and not result.provider.private and not result.url.startswith( 'magnet'): result.url = "magnet:?xt=urn:btih:{}".format(result.hash) if result.url.startswith('magnet'): r_code = self._add_torrent_uri(result) else: r_code = self._add_torrent_file(result) if not r_code: sickrage.app.log.warning(self.name + ': Unable to send Torrent') return False if not self._set_torrent_pause(result): sickrage.app.log.error(self.name + ': Unable to set the pause for Torrent') if not self._set_torrent_label(result): sickrage.app.log.error(self.name + ': Unable to set the label for Torrent') if not self._set_torrent_ratio(result): sickrage.app.log.error(self.name + ': Unable to set the ratio for Torrent') if not self._set_torrent_seed_time(result): sickrage.app.log.error( self.name + ': Unable to set the seed time for Torrent') if not self._set_torrent_path(result): sickrage.app.log.error(self.name + ': Unable to set the path for Torrent') if result.priority != 0 and not self._set_torrent_priority(result): sickrage.app.log.error(self.name + ': Unable to set priority for Torrent') except Exception as e: sickrage.app.log.error(self.name + ': Failed Sending Torrent') sickrage.app.log.debug( self.name + ': Exception raised when sending torrent: ' + str(result) + '. Error: ' + str(e)) return r_code return r_code def testAuthentication(self): try: # verify valid url self.response = self.session.get(self.url, timeout=120, verify=False) except: pass try: # get auth self._get_auth() if not self.response: raise Exception # verify auth if self.auth: return True, 'Success: Connected and Authenticated' return False, 'Error: Unable to get ' + self.name + ' Authentication, check your config!' except Exception as e: return False, 'Error: Unable to connect to ' + self.name
class GenericProvider(object): def __init__(self, name, url, private): self.name = name self.urls = {'base_url': url} self.private = private self.supports_backlog = True self.supports_absolute_numbering = False self.anime_only = False self.search_mode = 'eponly' self.search_fallback = False self.enabled = False self.enable_daily = True self.enable_backlog = True self.cache = TVCache(self) self.proper_strings = ['PROPER|REPACK|REAL|RERIP'] self.search_separator = ' ' # cookies self.enable_cookies = False self.cookies = '' # web session self.session = WebSession(cloudflare=True) @property def id(self): return str(re.sub(r"[^\w\d_]", "_", self.name.strip().lower())) @property def isEnabled(self): return self.enabled @property def imageName(self): return "" @property def seed_ratio(self): return '' @property def isAlive(self): return True def _check_auth(self): return True def login(self): return True def getResult(self, season=None, episodes=None): """ Returns a result of the correct type for this provider """ return SearchResult(season, episodes) def get_content(self, url): if self.login(): headers = {} if url.startswith('http'): headers = {'Referer': '/'.join(url.split('/')[:3]) + '/'} if not url.startswith('magnet'): try: return self.session.get(url, verify=False, headers=headers).content except Exception: pass def make_filename(self, name): return "" def get_quality(self, item, anime=False): """ Figures out the quality of the given RSS item node item: An elementtree.ElementTree element representing the <item> tag of the RSS feed Returns a Quality value obtained from the node's data """ (title, url) = self._get_title_and_url(item) quality = Quality.scene_quality(title, anime) return quality def search(self, search_strings, age=0, show_id=None, season=None, episode=None, **kwargs): return [] @MainDB.with_session def _get_season_search_strings(self, show_id, season, episode, session=None): """ Get season search strings. """ search_string = { 'Season': [] } show_object = find_show(show_id, session=session) episode_object = show_object.get_episode(season, episode) for show_name in all_possible_show_names(show_id, episode_object.scene_season): episode_string = "{}{}".format(show_name, self.search_separator) if show_object.air_by_date or show_object.sports: episode_string += str(episode_object.airdate).split('-')[0] elif show_object.anime: episode_string += 'Season' else: episode_string += 'S{season:0>2}'.format(season=episode_object.scene_season) search_string['Season'].append(episode_string.strip()) return [search_string] @MainDB.with_session def _get_episode_search_strings(self, show_id, season, episode, add_string='', session=None): """ Get episode search strings. """ search_string = { 'Episode': [] } show_object = find_show(show_id, session=session) episode_object = show_object.get_episode(season, episode) for show_name in all_possible_show_names(show_id, episode_object.scene_season): episode_string = "{}{}".format(show_name, self.search_separator) episode_string_fallback = None if show_object.air_by_date: episode_string += str(episode_object.airdate).replace('-', ' ') elif show_object.sports: episode_string += str(episode_object.airdate).replace('-', ' ') episode_string += ('|', ' ')[len(self.proper_strings) > 1] episode_string += episode_object.airdate.strftime('%b') elif show_object.anime: # If the show name is a season scene exception, we want to use the indexer episode number. if (episode_object.scene_season > 1 and show_name in get_scene_exceptions(show_object.indexer_id, episode_object.scene_season)): # This is apparently a season exception, let's use the scene_episode instead of absolute ep = episode_object.scene_episode else: ep = episode_object.scene_absolute_number episode_string += '{episode:0>2}'.format(episode=ep) episode_string_fallback = episode_string + '{episode:0>3}'.format(episode=ep) else: episode_string += sickrage.app.naming_ep_type[2] % { 'seasonnumber': episode_object.scene_season, 'episodenumber': episode_object.scene_episode, } if add_string: episode_string += self.search_separator + add_string episode_string_fallback += self.search_separator + add_string search_string['Episode'].append(episode_string.strip()) if episode_string_fallback: search_string['Episode'].append(episode_string_fallback.strip()) return [search_string] def _get_title_and_url(self, item): """ Retrieves the title and URL data from the item XML node item: An elementtree.ElementTree element representing the <item> tag of the RSS feed Returns: A tuple containing two strings representing title and URL respectively """ title = item.get('title', '').replace(' ', '.') url = item.get('link', '').replace('&', '&').replace('%26tr%3D', '&tr=') return title, url def _get_size(self, item): """Gets the size from the item""" sickrage.app.log.debug("Provider type doesn't have ability to provide download size implemented yet") return -1 def _get_result_stats(self, item): # Get seeders/leechers stats seeders = item.get('seeders', -1) leechers = item.get('leechers', -1) return try_int(seeders, -1), try_int(leechers, -1) @MainDB.with_session def find_search_results(self, show_id, season, episode, search_mode, manualSearch=False, downCurQuality=False, cacheOnly=False, session=None): provider_results = {} item_list = [] if not self._check_auth: return provider_results show_object = find_show(show_id, session=session) episode_object = show_object.get_episode(season, episode) # search cache for episode result provider_results = self.cache.search_cache(show_id, season, episode, manualSearch, downCurQuality) # check if this is a cache only search if cacheOnly: return provider_results search_strings = [] if search_mode == 'sponly': # get season search results search_strings = self._get_season_search_strings(show_id, season, episode) elif search_mode == 'eponly': # get single episode search results search_strings = self._get_episode_search_strings(show_id, season, episode) for curString in search_strings: try: item_list += self.search(curString, show_id=show_id, season=season, episode=episode) except SAXParseException: continue # sort list by quality if item_list: # categorize the items into lists by quality items = defaultdict(list) for item in item_list: items[self.get_quality(item, anime=show_object.is_anime)].append(item) # temporarily remove the list of items with unknown quality unknown_items = items.pop(Quality.UNKNOWN, []) # make a generator to sort the remaining items by descending quality items_list = (items[quality] for quality in sorted(items, reverse=True)) # unpack all of the quality lists into a single sorted list items_list = list(itertools.chain(*items_list)) # extend the list with the unknown qualities, now sorted at the bottom of the list items_list.extend(unknown_items) # filter results for item in item_list: provider_result = self.getResult() provider_result.name, provider_result.url = self._get_title_and_url(item) # ignore invalid urls if not validate_url(provider_result.url) and not provider_result.url.startswith('magnet'): continue try: parse_result = NameParser(show_id=show_id).parse(provider_result.name) except (InvalidNameException, InvalidShowException) as e: sickrage.app.log.debug("{}".format(e)) continue provider_result.show_id = parse_result.indexer_id provider_result.quality = parse_result.quality provider_result.release_group = parse_result.release_group provider_result.version = parse_result.version provider_result.size = self._get_size(item) provider_result.seeders, provider_result.leechers = self._get_result_stats(item) sickrage.app.log.debug("Adding item from search to cache: {}".format(provider_result.name)) self.cache.add_cache_entry(provider_result.name, provider_result.url, provider_result.seeders, provider_result.leechers, provider_result.size) if not provider_result.show_id: continue provider_result_show_obj = find_show(provider_result.show_id, session=session) if not provider_result_show_obj: continue if not parse_result.is_air_by_date and (provider_result_show_obj.air_by_date or provider_result_show_obj.sports): sickrage.app.log.debug("This is supposed to be a date search but the result {} didn't parse as one, skipping it".format(provider_result.name)) continue if search_mode == 'sponly': if len(parse_result.episode_numbers): sickrage.app.log.debug("This is supposed to be a season pack search but the result {} is not " "a valid season pack, skipping it".format(provider_result.name)) continue elif parse_result.season_number != (episode_object.season, episode_object.scene_season)[show_object.is_scene]: sickrage.app.log.debug("This season result {} is for a season we are not searching for, skipping it".format(provider_result.name)) continue else: if not all([parse_result.season_number is not None, parse_result.episode_numbers, parse_result.season_number == (episode_object.season, episode_object.scene_season)[show_object.is_scene], (episode_object.episode, episode_object.scene_episode)[show_object.is_scene] in parse_result.episode_numbers]): sickrage.app.log.debug("The result {} doesn't seem to be a valid episode " "that we are trying to snatch, ignoring".format(provider_result.name)) continue provider_result.season = int(parse_result.season_number) provider_result.episodes = list(map(int, parse_result.episode_numbers)) # make sure we want the episode for episode_number in provider_result.episodes.copy(): if not provider_result_show_obj.want_episode(provider_result.season, episode_number, provider_result.quality, manualSearch, downCurQuality): sickrage.app.log.info("RESULT:[{}] QUALITY:[{}] IGNORED!".format(provider_result.name, Quality.qualityStrings[provider_result.quality])) if episode_number in provider_result.episodes: provider_result.episodes.remove(episode_number) # detects if season pack and if not checks if we wanted any of the episodes if len(provider_result.episodes) != len(parse_result.episode_numbers): continue sickrage.app.log.debug( "FOUND RESULT:[{}] QUALITY:[{}] URL:[{}]".format(provider_result.name, Quality.qualityStrings[provider_result.quality], provider_result.url) ) if len(provider_result.episodes) == 1: episode_number = provider_result.episodes[0] sickrage.app.log.debug("Single episode result.") elif len(provider_result.episodes) > 1: episode_number = MULTI_EP_RESULT sickrage.app.log.debug("Separating multi-episode result to check for later - result contains episodes: " + str(parse_result.episode_numbers)) else: episode_number = SEASON_RESULT sickrage.app.log.debug("Separating full season result to check for later") if episode_number not in provider_results: provider_results[int(episode_number)] = [provider_result] else: provider_results[int(episode_number)] += [provider_result] return provider_results def find_propers(self, show_id, season, episode): results = [] for term in self.proper_strings: search_strngs = self._get_episode_search_strings(show_id, season, episode, add_string=term) for item in self.search(search_strngs[0], show_id=show_id, season=season, episode=episode): result = self.getResult(season, [episode]) result.name, result.url = self._get_title_and_url(item) if not validate_url(result.url) and not result.url.startswith('magnet'): continue result.seeders, result.leechers = self._get_result_stats(item) result.size = self._get_size(item) result.date = datetime.datetime.today() results.append(result) return results def add_cookies_from_ui(self): """ Add the cookies configured from UI to the providers requests session. :return: dict """ if isinstance(self, TorrentRssProvider) and not self.cookies: return {'result': True, 'message': 'This is a TorrentRss provider without any cookies provided. ' 'Cookies for this provider are considered optional.'} # This is the generic attribute used to manually add cookies for provider authentication if not self.enable_cookies: return {'result': False, 'message': 'Adding cookies is not supported for provider: {}'.format(self.name)} if not self.cookies: return {'result': False, 'message': 'No Cookies added from ui for provider: {}'.format(self.name)} cookie_validator = re.compile(r'^([\w%]+=[\w%]+)(;[\w%]+=[\w%]+)*$') if not cookie_validator.match(self.cookies): sickrage.app.alerts.message( 'Failed to validate cookie for provider {}'.format(self.name), 'Cookie is not correctly formatted: {}'.format(self.cookies)) return {'result': False, 'message': 'Cookie is not correctly formatted: {}'.format(self.cookies)} if hasattr(self, 'required_cookies') and not all( req_cookie in [x.rsplit('=', 1)[0] for x in self.cookies.split(';')] for req_cookie in self.required_cookies): return {'result': False, 'message': "You haven't configured the required cookies. Please login at {provider_url}, " "and make sure you have copied the following cookies: {required_cookies!r}" .format(provider_url=self.name, required_cookies=self.required_cookies)} # cookie_validator got at least one cookie key/value pair, let's return success add_dict_to_cookiejar(self.session.cookies, dict(x.rsplit('=', 1) for x in self.cookies.split(';'))) return {'result': True, 'message': ''} def check_required_cookies(self): """ Check if we have the required cookies in the requests sessions object. Meaning that we've already successfully authenticated once, and we don't need to go through this again. Note! This doesn't mean the cookies are correct! """ if hasattr(self, 'required_cookies'): return all(dict_from_cookiejar(self.session.cookies).get(cookie) for cookie in self.required_cookies) # A reminder for the developer, implementing cookie based authentication. sickrage.app.log.error( 'You need to configure the required_cookies attribute, for the provider: {}'.format(self.name)) def cookie_login(self, check_login_text, check_url=None): """ Check the response for text that indicates a login prompt. In that case, the cookie authentication was not successful. :param check_login_text: A string that's visible when the authentication failed. :param check_url: The url to use to test the login with cookies. By default the providers home page is used. :return: False when authentication was not successful. True if successful. """ check_url = check_url or self.urls['base_url'] if self.check_required_cookies(): # All required cookies have been found within the current session, we don't need to go through this again. return True if self.cookies: result = self.add_cookies_from_ui() if not result['result']: sickrage.app.alerts.message(result['message']) sickrage.app.log.warning(result['message']) return False else: sickrage.app.log.warning('Failed to login, you will need to add your cookies in the provider ' 'settings') sickrage.app.alerts.error( 'Failed to auth with {provider}'.format(provider=self.name), 'You will need to add your cookies in the provider settings') return False response = self.session.get(check_url) if any([not response, not (response.text and response.status_code == 200), check_login_text.lower() in response.text.lower()]): sickrage.app.log.warning('Please configure the required cookies for this provider. Check your ' 'provider settings') sickrage.app.alerts.error( 'Wrong cookies for {}'.format(self.name), 'Check your provider settings' ) self.session.cookies.clear() return False else: return True @classmethod def getDefaultProviders(cls): pass @classmethod def getProvider(cls, name): providerMatch = [x for x in cls.get_providers() if x.name == name] if len(providerMatch) == 1: return providerMatch[0] @classmethod def getProviderByID(cls, id): providerMatch = [x for x in cls.get_providers() if x.id == id] if len(providerMatch) == 1: return providerMatch[0] @classmethod def get_providers(cls): modules = [TorrentProvider.type, NZBProvider.type] for type in []: modules += cls.load_providers(type) return modules @classmethod def load_providers(cls, provider_type): providers = [] for (__, name, __) in pkgutil.iter_modules([os.path.join(os.path.dirname(__file__), provider_type)]): imported_module = importlib.import_module('.{}.{}'.format(provider_type, name), package='sickrage.providers') for __, klass in inspect.getmembers(imported_module, predicate=lambda o: all([inspect.isclass(o) and issubclass(o, GenericProvider), o is not NZBProvider, o is not TorrentProvider, getattr(o, 'type', None) == provider_type])): providers += [klass()] break return providers