From 93c219d82ee2aef3ac75ac56562ee26ef08ffc4a Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sat, 6 Jun 2015 16:08:59 +0100 Subject: [PATCH] Add Rarbg torrent provider. Note: The merging of all qualities into one category in API v2 mean more results likely to be cropped. The vendor switched to this method from their (imho, better) original of listing category numbers. It was suggested that clients add and maintain all possible search strings that signify as quality. However, this would still trigger misses, introduce other issues, plus add needless non-standard API complexity. Conclusion; as designed, SG will utilise other configured providers when this one fails to hit targets, this provider should not be configured as a sole provider. --- CHANGES.md | 1 + gui/slick/images/providers/rarbg.png | Bin 0 -> 635 bytes sickbeard/__init__.py | 4 +- sickbeard/providers/__init__.py | 1 + sickbeard/providers/generic.py | 5 +- sickbeard/providers/rarbg.py | 238 +++++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 gui/slick/images/providers/rarbg.png create mode 100644 sickbeard/providers/rarbg.py diff --git a/CHANGES.md b/CHANGES.md index 452ae2d5..2b2d233e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ * Change provider SCC remove 1 of 3 requests per search to save 30% time * Change provider Womble's use SSL * Change refactor and code simplification for torrent providers +* Add Rarbg torrent provider * Fix getManualSearchStatus: object has no attribute 'segment' * Change handling of general HTTP error response codes to prevent issues * Add handling for CloudFlare custom HTTP response codes diff --git a/gui/slick/images/providers/rarbg.png b/gui/slick/images/providers/rarbg.png new file mode 100644 index 0000000000000000000000000000000000000000..11048b1982cb3b0cbfb5b14ecd67e07fb5283b1d GIT binary patch literal 635 zcmV->0)+jEP)j(6lC#WO30Xu$f@P-BgA8&U ziQh~%=iTMRM?PlqX5KgNeQ(C9s>%Xzg}6^#BucdH2l0XU)>TMW8nS=OUweW{rg1&~PvTLG##m1d@} zHOvr)?=GcYT=k*ZQsu%Xjo)kAWm`~sygv3rzQn* z38wGDuBv@l*2jMk6y}>riRs?_$fa%A_c0m-db+D+ZFfN_r1Om3P!Pg~M@7?2)=5zu z9n2Q)vxSr6M|O^CWtv%*LCPE}zCbh?vG<<&momfp%PBLrmbvvcalS=R=Sr~(Szij; z&bW`jb4*ro6l49xRJ;4cvoi?TCI%~(Qq+YXI*#*ucV|RhYw|Fh;>{$|YmbQsT7u4B z4WQ*Wjn*Wr_a{Bu-M~9|J|5CnuWf9Gyikb_*&^=g2eC?QOR2hA1I^F zUa|IvNt&7}mAMZNeE;fK`kz>f=X{-5JNBG4LLynbr%mp=#(={`*$2d1;?o}i1_0>! V7@Z4uamfGx002ovPDHLkV1i`dC364( literal 0 HcmV?d00001 diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 999794ff..a77a2a97 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -37,7 +37,7 @@ from sickbeard import providers, metadata, config, webserveInit from sickbeard.providers.generic import GenericProvider from providers import btn, newznab, womble, thepiratebay, torrentleech, kat, iptorrents, \ omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, nextgen, speedcd, nyaatorrents, torrentbytes, \ - freshontv, bitsoup, tokyotoshokan, animenzb, totv + freshontv, bitsoup, tokyotoshokan, animenzb, totv, rarbg from sickbeard.config import CheckSection, check_setting_int, check_setting_str, check_setting_float, ConfigMigrator, \ naming_ep_type, minimax from sickbeard import searchBacklog, showUpdater, versionChecker, autoPostProcesser, \ @@ -1209,7 +1209,7 @@ def initialize(consoleLogging=True): # processors autoPostProcesserScheduler = scheduler.Scheduler(autoPostProcesser.PostProcesser(), cycleTime=datetime.timedelta( - minutes=AUTOPOSTPROCESSER_FREQUENCY), + minutes=AUTOPOSTPROCESSER_FREQUENCY), threadName='POSTPROCESSER', silent=not PROCESS_AUTOMATICALLY) diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py index 727ec2f5..7ce4f316 100755 --- a/sickbeard/providers/__init__.py +++ b/sickbeard/providers/__init__.py @@ -35,6 +35,7 @@ __all__ = ['womble', 'bitsoup', 'tokyotoshokan', 'animenzb', + 'rarbg', 'totv', ] diff --git a/sickbeard/providers/generic.py b/sickbeard/providers/generic.py index 963e03a2..d075a625 100644 --- a/sickbeard/providers/generic.py +++ b/sickbeard/providers/generic.py @@ -493,6 +493,8 @@ class TorrentProvider(GenericProvider): def getQuality(self, item, anime=False): if isinstance(item, tuple): name = item[0] + elif isinstance(item, dict): + name, url = self._get_title_and_url(item) else: name = item.title return Quality.sceneQuality(name, anime) @@ -535,9 +537,10 @@ class TorrentProvider(GenericProvider): for proper_string in method: search_string = self._get_episode_search_strings(cur_ep, add_string=proper_string) + proper_exp = re.sub(r'(?i)[^a-z\|\.]+', '', proper_string) for item in self._doSearch(search_string[0]): title, url = self._get_title_and_url(item) - if not re.search('(?i)(?:%s)' % '|'.join(method), title): + if not re.search('(?i)(?:%s)' % proper_exp, title): continue results.append(classes.Proper(title, url, datetime.datetime.today(), self.show)) diff --git a/sickbeard/providers/rarbg.py b/sickbeard/providers/rarbg.py new file mode 100644 index 00000000..cd59e585 --- /dev/null +++ b/sickbeard/providers/rarbg.py @@ -0,0 +1,238 @@ +# Author: SickGear +# +# This file is part of SickGear. +# +# SickGear is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SickGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SickGear. If not, see . + +import re +import datetime +import time + +import sickbeard +import generic +from sickbeard import logger +from sickbeard import tvcache +from sickbeard import show_name_helpers +from sickbeard import helpers +from sickbeard.indexers.indexer_config import INDEXER_TVDB + + +class RarbgProvider(generic.TorrentProvider): + + def __init__(self): + generic.TorrentProvider.__init__(self, 'Rarbg') + self.session = None + + self.request_throttle = datetime.datetime.now() + self.token = None + self.token_expiry = None + + self.confirmed = False + self.ratio = None + self.minseed = None + self.minleech = None + + # api_spec: https://rarbg.com/pubapi/apidocs_v2.txt + self.url_api = 'https://torrentapi.org/pubapi_v2.php?' + self.urls = {'config_provider_home_uri': 'https://rarbg.com', + 'api_token': self.url_api + 'get_token=get_token', + 'api_list': self.url_api + 'mode=list', + 'api_search': self.url_api + 'mode=search'} + + self.params = {'defaults': '&category=%(cat)s&ranked=%(ranked)s&limit=100&sort=last' + % {'cat': 'tv', 'ranked': int(self.confirmed)} + '&token=%(token)s', + 'param_iid': '&search_imdb=%(sid)s', + 'param_tid': '&search_tvdb=%(sid)s', + 'param_rid': '&search_tvrage=%(sid)s', + 'param_str': '&search_string=%(str)s', + 'param_seed': '&min_seeders=%(min_seeds)s', + 'param_peer': '&min_leechers=%(min_peers)s'} + + self.url = self.urls['config_provider_home_uri'] + self.cache = RarbgCache(self) + + def _doLogin(self, reset=False): + + if not reset and self.token and self.token_expiry and datetime.datetime.now() < self.token_expiry: + return True + + try: + data = helpers.getURL(self.urls['api_token'], headers=self.headers, json=True) + self.token = data['token'] + self.token_expiry = datetime.datetime.now() + datetime.timedelta(minutes=14) + return True + + except Exception: + pass + + logger.log(u'No usable API token returned from: %s' % self.urls['api_token'], logger.ERROR) + return False + + def _get_season_search_strings(self, ep_obj): + + if ep_obj.show.air_by_date or ep_obj.show.sports: + ep_detail = str(ep_obj.airdate).split('-')[0] + elif ep_obj.show.anime: + ep_detail = '%d' % ep_obj.scene_absolute_number + else: + ep_detail = 'S%02d' % int(ep_obj.scene_season) + + search_string = {'Season': []} + for show_name in set(show_name_helpers.allPossibleShowNames(self.show)): + search_string['Season'].append('%s %s' % (show_name, ep_detail)) + + if not self.show.sports and not self.show.anime: + search_string.update({'Season_only': [ep_detail]}) + + return [search_string] + + def _get_episode_search_strings(self, ep_obj, add_string=''): + + if not ep_obj: + return [] + + airdate = str(ep_obj.airdate).replace('-', ' ') + if self.show.air_by_date: + ep_detail = airdate + elif self.show.sports: + ep_detail = '%s|%s' % (airdate, ep_obj.airdate.strftime('%b')) + elif self.show.anime: + ep_detail = ep_obj.scene_absolute_number + else: + ep_detail = sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season, + 'episodenumber': ep_obj.scene_episode} + if add_string and not self.show.anime: + ep_detail += ' ' + add_string + + search_string = {'Episode': []} + for show_name in set(show_name_helpers.allPossibleShowNames(self.show)): + search_string['Episode'].append(re.sub('\s+', ' ', '%s %s' % (show_name, ep_detail))) + + if not self.show.sports and not self.show.anime: + search_string.update({'Episode_only': [ep_detail]}) + + return [search_string] + + def _doSearch(self, search_params, search_mode='eponly', epcount=0, age=0): + + results = [] + items = {'Season': [], 'Episode': [], 'RSS': []} + + if not self._doLogin(reset=True): + return results + + id_search = None + if hasattr(self, 'show') and self.show and self.show.indexer and self.show.indexerid: + if 0 < len(self.show.imdb_info): + sid = self.show.imdb_info['imdb_id'] + search_with = 'param_iid' + else: + sid = self.show.indexerid + if INDEXER_TVDB == self.show.indexer: + search_with = 'param_tid' + else: # INDEXER_TVRAGE == self.show.indexer: + search_with = 'param_rid' + id_search = self.params[search_with] % {'sid': sid} + + dedupe = [] + search_types = sorted([x for x in search_params.items()], key=lambda tup: tup[1], reverse=True) # sort type "_only" as first to process + for mode_params in search_types: + mode_search = mode_params[0] + mode_base = mode_search.replace('_only', '') + for search_string in mode_params[1]: + search_url = '' + if 'RSS' == mode_search: + url = 'api_list' + else: + url = 'api_search' + + if '_only' in mode_search and id_search: + search_url = id_search + + if None is not search_string: + search_url += self.params['param_str'] % {'str': search_string} + + search_url = self.urls[url] + self.params['defaults'] + search_url + + if self.minseed: + search_url += self.params['param_seed'] % {'min_seeds': self.minseed} + + if self.minleech: + search_url += self.params['param_peer'] % {'min_peers': self.minleech} + + logger.log(u'Base search URL: %s' % search_url, logger.DEBUG) + + for r in xrange(0, 3): + time_out = 0 + while(self.request_throttle > datetime.datetime.now()) and 15 >= time_out: + time_out += 1 + time.sleep(1) + + try: + data = self.getURL(search_url % {'token': self.token}, json=True) + except Exception: + pass + + self.request_throttle = datetime.datetime.now() + datetime.timedelta(seconds=3) + + if 'error' in data: + if 5 == data['error_code']: # Too many requests per second. + continue + + elif 2 == data['error_code']: # Invalid token set! + if self._doLogin(reset=True): + continue + return results + break + + if 'error' not in data: + for item in data['torrent_results']: + try: + title = item['filename'] + get = item['download'] + if not (title and get) or get in dedupe: + continue + dedupe += [get] + items[mode_base].append((title, get)) + logger.log(u'Found result: %s' % title, logger.DEBUG) + except Exception: + pass + + if 0 < len(items[mode_base]): + results += items[mode_base] + items[mode_base] = [] + else: + logger.log(u'No results found for: %s' % search_string, logger.DEBUG) + + if '_only' in mode_search and 0 < len(results): + break + + return results + + def findPropers(self, search_date=datetime.datetime.today()): + return self._find_propers(search_date, '{{.proper.|.repack.}}') + + +class RarbgCache(tvcache.TVCache): + def __init__(self, this_provider): + + tvcache.TVCache.__init__(self, this_provider) + + self.minTime = 10 # cache update frequency + + def _getRSSData(self): + return self.provider.get_cache_data() + + +provider = RarbgProvider()