diff --git a/CHANGES.md b/CHANGES.md
index d4b3fe61..ab23f70f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,6 @@
### 0.15.0 (2018-xx-xx xx:xx:xx UTC)
+* Add showRSS torrent provider
* Add choice to delete watched episodes from a list of played media at Kodi, Emby, and/or Plex,
instructions at Shows/History/Layout/"Watched"
* Add installable SickGear Kodi repository containing addon "SickGear Watched State Updater"
diff --git a/gui/slick/images/providers/showrss.png b/gui/slick/images/providers/showrss.png
new file mode 100644
index 00000000..08357543
Binary files /dev/null and b/gui/slick/images/providers/showrss.png differ
diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py
index 6d580dd7..d5ec1a06 100755
--- a/sickbeard/providers/__init__.py
+++ b/sickbeard/providers/__init__.py
@@ -29,7 +29,7 @@ from . import newznab, omgwtfnzbs
from . import alpharatio, beyondhd, bithdtv, bitmetv, blutopia, btn, btscene, dh, ettv, \
fano, filelist, funfile, gftracker, grabtheinfo, hdbits, hdspace, hdtorrents, \
iptorrents, limetorrents, magnetdl, morethan, nebulance, ncore, nyaa, pisexy, potuk, pretome, privatehd, ptf, \
- rarbg, revtt, scenehd, scenetime, shazbat, skytorrents, speedcd, \
+ rarbg, revtt, scenehd, scenetime, shazbat, showrss, skytorrents, speedcd, \
thepiratebay, torlock, torrentbytes, torrentday, torrenting, torrentleech, \
torrentz2, tvchaosuk, wop, zooqle
# anime
@@ -77,6 +77,7 @@ __all__ = ['omgwtfnzbs',
'scenehd',
'scenetime',
'shazbat',
+ 'showrss',
'skytorrents',
'speedcd',
'thepiratebay',
diff --git a/sickbeard/providers/showrss.py b/sickbeard/providers/showrss.py
new file mode 100644
index 00000000..0e0978e3
--- /dev/null
+++ b/sickbeard/providers/showrss.py
@@ -0,0 +1,153 @@
+# coding=utf-8
+#
+# 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 traceback
+
+from . import generic
+from sickbeard import logger
+from sickbeard.bs4_parser import BS4Parser
+from sickbeard.helpers import tryInt, sanitizeSceneName
+from lib.unidecode import unidecode
+from six.moves.html_parser import HTMLParser
+
+
+class ShowRSSProvider(generic.TorrentProvider):
+
+ def __init__(self):
+
+ generic.TorrentProvider.__init__(self, 'showRSS')
+
+ self.url_base = 'https://showrss.info/'
+ self.urls = {'config_provider_home_uri': self.url_base,
+ 'login_action': self.url_base + 'login',
+ 'browse': self.url_base + 'browse/all',
+ 'search': self.url_base + 'browse/%s'}
+
+ self.url = self.urls['config_provider_home_uri']
+
+ self.username, self.password, self.shows = 3 * [None]
+
+ def _authorised(self, **kwargs):
+
+ return super(ShowRSSProvider, self)._authorised(logged_in=(lambda y=None: self.logged_in(y)))
+
+ def logged_in(self, y):
+ if all([None is y or 'logout' in y,
+ bool(filter(lambda c: 'remember_web_' in c, self.session.cookies.keys()))]):
+ if None is not y:
+ self.shows = dict(re.findall('', y))
+ h = HTMLParser()
+ for k, v in self.shows.items():
+ self.shows[k] = sanitizeSceneName(h.unescape(unidecode(v.decode('utf-8'))))
+ return True
+ return False
+
+ def _search_provider(self, search_params, **kwargs):
+
+ results = []
+ if not self._authorised():
+ return results
+
+ items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []}
+
+ rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'get': 'magnet'}.items())
+ urls = []
+ for mode in search_params.keys():
+ for search_string in search_params[mode]:
+ if 'Cache' == mode:
+ search_url = self.urls['browse']
+ else:
+ search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string
+ show_name = filter(lambda x: x.lower() == re.sub('\s.*', '', search_string.lower()),
+ self.shows.values())
+ if not show_name:
+ continue
+ search_url = self.urls['search'] % self.shows.keys()[self.shows.values().index(show_name[0])]
+
+ if search_url in urls:
+ continue
+ urls += [search_url]
+
+ html = self.get_url(search_url)
+ if self.should_skip():
+ return results
+
+ cnt = len(items[mode])
+ try:
+ if not html or self._has_no_results(html):
+ raise generic.HaltParseException
+
+ with BS4Parser(html, features=['html5lib', 'permissive']) as soup:
+ torrent_rows = soup.select('ul.user-timeline > li')
+
+ if not len(torrent_rows):
+ raise generic.HaltParseException
+
+ for tr in torrent_rows:
+ try:
+ anchor = tr.find('a', href=rc['get'])
+ title = self.regulate_title(anchor)
+ download_url = self._link(anchor['href'])
+ except (AttributeError, TypeError, ValueError):
+ continue
+
+ if title and download_url:
+ items[mode].append((title, download_url, None, None))
+
+ except generic.HaltParseException:
+ pass
+ except (StandardError, Exception):
+ logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR)
+ self._log_search(mode, len(items[mode]) - cnt, search_url)
+
+ results = self._sort_seeding(mode, results + items[mode])
+
+ return results
+
+ @staticmethod
+ def regulate_title(anchor):
+ title = ''
+ t1 = anchor.attrs.get('title').strip()
+ t2 = anchor.get_text().strip()
+ diff, x, offset = 0, 0, 0
+ for x, c in enumerate(t2):
+ if c.lower() == t1[x-offset].lower():
+ title += t1[x-offset]
+ diff = 0
+ elif ' ' != c and ' ' == t1[x-offset]:
+ title += c
+ diff = 0
+ if ' ' == t2[x+1]:
+ offset += 1
+ else:
+ diff += 1
+ if 1 < diff:
+ break
+ return '%s%s' % (title, re.sub('(?i)(xvid|divx|[hx].?26[45])\s(\w+)$', r'\1-\2',
+ ''.join(t1[x - (offset + diff)::]).strip()))
+
+ @staticmethod
+ def ui_string(key):
+
+ return ('showrss_tip' == key
+ and 'lists are not needed, the SickGear list is used as usual' or '')
+
+
+provider = ShowRSSProvider()