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