diff --git a/data/images/providers/kat.png b/data/images/providers/kat.png new file mode 100644 index 0000000000..efdb420878 Binary files /dev/null and b/data/images/providers/kat.png differ diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index b9b6eab5de..60ddeb6675 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -34,7 +34,7 @@ # apparently py2exe won't build these unless they're imported somewhere from sickbeard import providers, metadata -from providers import ezrss, tvtorrents, torrentleech, btn, newznab, womble, omgwtfnzbs, hdbits +from providers import ezrss, tvtorrents, torrentleech, btn, newznab, womble, omgwtfnzbs, hdbits, kat from sickbeard.config import CheckSection, check_setting_int, check_setting_str, ConfigMigrator from sickbeard import searchCurrent, searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser @@ -160,6 +160,7 @@ DEFAULT_SEARCH_FREQUENCY = 40 EZRSS = False +kat = False HDBITS = False HDBITS_USERNAME = None @@ -345,6 +346,7 @@ def initialize(consoleLogging=True): PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, \ showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, showList, loadingShowList, \ NEWZNAB_DATA, NZBS, NZBS_UID, NZBS_HASH, EZRSS, HDBITS, HDBITS_USERNAME, HDBITS_PASSKEY, TVTORRENTS, TVTORRENTS_DIGEST, TVTORRENTS_HASH, BTN, BTN_API_KEY, TORRENTLEECH, TORRENTLEECH_KEY, \ + kat, \ TORRENT_DIR, USENET_RETENTION, SOCKET_TIMEOUT, \ SEARCH_FREQUENCY, DEFAULT_SEARCH_FREQUENCY, BACKLOG_SEARCH_FREQUENCY, \ QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, STATUS_DEFAULT, \ @@ -483,6 +485,9 @@ def initialize(consoleLogging=True): CheckSection(CFG, 'EZRSS') EZRSS = bool(check_setting_int(CFG, 'EZRSS', 'ezrss', 0)) + CheckSection(CFG, 'kat') + kat = bool(check_setting_int(CFG, 'kat', 'kat', 0)) + GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') IGNORE_WORDS = check_setting_str(CFG, 'General', 'ignore_words', IGNORE_WORDS) EXTRA_SCRIPTS = [x.strip() for x in check_setting_str(CFG, 'General', 'extra_scripts', '').split('|') if x.strip()] @@ -1058,6 +1063,9 @@ def save_config(): new_config['EZRSS'] = {} new_config['EZRSS']['ezrss'] = int(EZRSS) + new_config['kat'] = {} + new_config['kat']['kat'] = int(kat) + new_config['HDBITS'] = {} new_config['HDBITS']['hdbits'] = int(HDBITS) new_config['HDBITS']['hdbits_username'] = HDBITS_USERNAME diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py index b9b7da0041..e32bec8899 100644 --- a/sickbeard/providers/__init__.py +++ b/sickbeard/providers/__init__.py @@ -22,7 +22,8 @@ 'torrentleech', 'womble', 'btn', - 'omgwtfnzbs' + 'omgwtfnzbs', + 'kat' ] import sickbeard diff --git a/sickbeard/providers/kat.py b/sickbeard/providers/kat.py new file mode 100644 index 0000000000..5e1127cfad --- /dev/null +++ b/sickbeard/providers/kat.py @@ -0,0 +1,323 @@ +# This file is part of Sick Beard. +# +# Sick Beard 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. +# +# Sick Beard 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 Sick Beard. If not, see . + +import urllib, urllib2 +import StringIO, zlib, gzip +import re, socket +from xml.dom.minidom import parseString +from httplib import BadStatusLine +import traceback + +import sickbeard +import generic + +from sickbeard.common import Quality, USER_AGENT +from sickbeard import logger +from sickbeard import tvcache +from sickbeard import helpers +from sickbeard.exceptions import ex +from sickbeard import scene_exceptions + +class KATProvider(generic.TorrentProvider): + + def __init__(self): + + generic.TorrentProvider.__init__(self, "KAT") + + self.supportsBacklog = True + + self.url = 'https://kickass.to/' + + def isEnabled(self): + return sickbeard.kat + + def imageName(self): + return 'kat.png' + + def getQuality(self, item): + + #torrent_node = item.getElementsByTagName('torrent')[0] + #filename_node = torrent_node.getElementsByTagName('title')[0] + #filename = get_xml_text(filename_node) + + # I think the only place we can get anything resembing the filename is in + # the title + filename = helpers.get_xml_text(item.getElementsByTagName('title')[0]) + + quality = Quality.nameQuality(filename) + + return quality + + def findSeasonResults(self, show, season): + + results = {} + + if show.air_by_date: + logger.log(u"KAT doesn't support air-by-date backlog because of limitations on their RSS search.", logger.WARNING) + return results + + results = generic.TorrentProvider.findSeasonResults(self, show, season) + + return results + def _get_season_search_strings(self, show, season=None): + + params = {} + + if not show: + return params + global lang + lang = show.audio_lang + params['show_name'] = helpers.sanitizeSceneName(show.name).replace('.',' ').replace('!','').encode('utf-8') + + if season != None: + params['season'] = season + + return [params] + + def _get_episode_search_strings(self, ep_obj,french=None): + + params = {} + + global lang + + if not ep_obj: + return [params] + + params['show_name'] = helpers.sanitizeSceneName(ep_obj.show.name).replace('.',' ').replace('!','').encode('utf-8') + + if ep_obj.show.air_by_date: + params['date'] = str(ep_obj.airdate) + else: + params['season'] = ep_obj.scene_season + params['episode'] = ep_obj.scene_episode + + to_return = [params] + + # add new query strings for exceptions + name_exceptions = scene_exceptions.get_scene_exceptions(ep_obj.show.tvdbid) + for name_exception in name_exceptions: + # don't add duplicates + if name_exception != ep_obj.show.name: + # only change show name + cur_return = params.copy() + cur_return['show_name'] = helpers.sanitizeSceneName(name_exception) + to_return.append(cur_return) + + logger.log(u"KAT _get_episode_search_strings for %s is returning %s" % (repr(ep_obj), repr(params)), logger.DEBUG) + if french: + lang='fr' + else: + lang = ep_obj.show.audio_lang + return to_return + + def getURL(self, url, headers=None): + """ + Overriding here to capture a 404 (which literally means episode-not-found in KAT). + """ + + if not headers: + headers = [] + + opener = urllib2.build_opener() + opener.addheaders = [('User-Agent', USER_AGENT), ('Accept-Encoding', 'gzip,deflate')] + for cur_header in headers: + opener.addheaders.append(cur_header) + + try: + usock = opener.open(url) + url = usock.geturl() + encoding = usock.info().get("Content-Encoding") + + if encoding in ('gzip', 'x-gzip', 'deflate'): + content = usock.read() + if encoding == 'deflate': + data = StringIO.StringIO(zlib.decompress(content)) + else: + data = gzip.GzipFile(fileobj=StringIO.StringIO(content)) + result = data.read() + + else: + result = usock.read() + + usock.close() + + return result + + except urllib2.HTTPError, e: + if e.code == 404: + # for a 404, we fake an empty result + return '' + + logger.log(u"HTTP error " + str(e.code) + " while loading URL " + url, logger.ERROR) + return None + except urllib2.URLError, e: + logger.log(u"URL error " + str(e.reason) + " while loading URL " + url, logger.ERROR) + return None + except BadStatusLine: + logger.log(u"BadStatusLine error while loading URL " + url, logger.ERROR) + return None + except socket.timeout: + logger.log(u"Timed out while loading URL " + url, logger.ERROR) + return None + except ValueError: + logger.log(u"Unknown error while loading URL " + url, logger.ERROR) + return None + except Exception: + logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.ERROR) + return None + + def _doSearch(self, search_params, show=None, season=None, french=None): + # First run a search using the advanced format -- results are probably more reliable, but often not available for several weeks + # http://kat.ph/usearch/%22james%20may%22%20season:1%20episode:1%20verified:1/?rss=1 + def advancedEpisodeParamBuilder(params): + episodeParam = '' + if 'show_name' in params: + episodeParam = episodeParam + urllib.quote('"' + params.pop('show_name') + '"') +"%20" + if 'season' in params: + episodeParam = episodeParam + 'season:' + str(params.pop('season')) +"%20" + if 'episode' in params: + episodeParam = episodeParam + 'episode:' + str(params.pop('episode')) +"%20" + if str(lang)=="fr" or french: + episodeParam = episodeParam + ' french' + return episodeParam + searchURL = self._buildSearchURL(advancedEpisodeParamBuilder, search_params); + logger.log(u"Advanced-style search string: " + searchURL, logger.DEBUG) + data = self.getURL(searchURL) + + # First run a search using the advanced format -- results are probably more reliable, but often not available for several weeks + # http://kat.ph/usearch/%22james%20may%22%20season:1%20episode:1%20verified:1/?rss=1 + if not data or data == '': + def fuzzyEpisodeParamBuilder(params): + episodeParam = '' + if not 'show_name' in params or not 'season' in params: + return '' + episodeParam = episodeParam + urllib.quote('"' + params.pop('show_name') +'"') + "%20" + episodeParam = episodeParam + 'S' + str(params.pop('season')).zfill(2) + if 'episode' in params: + episodeParam += 'E' + str(params.pop('episode')).zfill(2) + if str(lang)=="fr" or french: + episodeParam = episodeParam + ' french' + return episodeParam + searchURL = self._buildSearchURL(fuzzyEpisodeParamBuilder, search_params); + logger.log(u"Fuzzy-style search string: " + searchURL, logger.DEBUG) + data = self.getURL(searchURL) + + if not data: + return [] + + return self._parseKatRSS(data) + + def _buildSearchURL(self, episodeParamBuilder, search_params): + + params = {"rss": "1", "field": "seeders", "sorder": "desc" } + + if search_params: + params.update(search_params) + + searchURL = self.url + 'usearch/' + + # Build the episode search parameter via a delegate + # Many of the 'params' here actually belong in the path as name:value pairs. + # so we remove the ones we know about (adding them to the path as we do so) + # NOTE: episodeParamBuilder is expected to modify the passed 'params' variable by popping params it uses + searchURL = searchURL + episodeParamBuilder(params) + + if 'date' in params: + logger.log(u"Sorry, air by date not supported by kat. Removing: " + params.pop('date'), logger.WARNING) + + # we probably have an extra %20 at the end of the url. Not likely to + # cause problems, but it is uneeded, so trim it + if searchURL.endswith('%20'): + searchURL = searchURL[:-3] + + searchURL = searchURL + '%20verified:1/?' + urllib.urlencode(params) # this will likely only append the rss=1 part + + return searchURL + + def _parseKatRSS(self, data): + + try: + parsedXML = parseString(data) + items = parsedXML.getElementsByTagName('item') + except Exception, e: + logger.log(u"Error trying to load KAT RSS feed: "+ex(e), logger.ERROR) + logger.log(u"RSS data: "+data, logger.DEBUG) + return [] + + results = [] + + for curItem in items: + + (title, url) = self._get_title_and_url(curItem) + if not title or not url: + logger.log(u"The XML returned from the KAT RSS feed is incomplete, this result is unusable: "+data, logger.ERROR) + continue + + if self._get_seeders(curItem) <= 0: + logger.log(u"Discarded result with no seeders: " + title, logger.DEBUG) + continue + curItem.audio_langs=lang + + results.append(curItem) + + return results + + def _get_title_and_url(self, item): + #(title, url) = generic.TorrentProvider._get_title_and_url(self, item) + + title = helpers.get_xml_text(item.getElementsByTagName('title')[0]) + + url = None + # if we have a preference for magnets, go straight for the throat... + try: + url = helpers.get_xml_text(item.getElementsByTagName('magnetURI')[0]) + except Exception: + pass + + if url is None: + url = item.getElementsByTagName('enclosure')[0].getAttribute('url').replace('&','&') + + return (title, url) + + def _get_seeders(self, item): + return int(helpers.get_xml_text(item.getElementsByTagName('torrent:seeds')[0])) + + def _extract_name_from_filename(self, filename): + name_regex = '(.*?)\.?(\[.*]|\d+\.TPB)\.torrent$' + logger.log(u"Comparing "+name_regex+" against "+filename, logger.DEBUG) + match = re.match(name_regex, filename, re.I) + if match: + return match.group(1) + return None + +# +# James Mays Things You Need To Know S02E06 HDTV XviD-AFG +# random text in here +# Tv +# http://kat.ph/james-mays-things-you-need-to-know-s02e06-hdtv-xvid-afg-t6666685.html +# http://kat.ph/james-mays-things-you-need-to-know-s02e06-hdtv-xvid-afg-t6666685.html +# Mon, 17 Sep 2012 22:48:02 +0000 +# http://kat.ph/james-mays-things-you-need-to-know-s02e06-hdtv-xvid-afg-t6666685.html +# 556022412DE29EE0B0AC1ED83EF610AA3081CDA4 +# 0 +# 0 +# 0 +# 255009149 +# 1 +# +# + + +provider = KATProvider() \ No newline at end of file