From 176b2c818aff8cc6e9a3ebb9dabdce3e838a331f Mon Sep 17 00:00:00 2001
From: Panagiotis Koutsias <pkoutsias@users.noreply.github.com>
Date: Sun, 24 Feb 2019 19:41:22 +0200
Subject: [PATCH] Adds GreekSubtitles, Subs4Free, Subs4Series, SubZ and XSubs
 providers (#310)

* Adds GreekSubtitles, Subs4Free, Subs4Series, SubZ and XSubs providers

* Various optimizations in greek providers
---
 bazarr/config.py                              |   4 +
 bazarr/get_providers.py                       |   3 +
 bazarr/main.py                                |   4 +
 .../providers/greeksubtitles.py               | 184 ++++++++++
 libs/subliminal_patch/providers/subs4free.py  | 283 ++++++++++++++++
 .../subliminal_patch/providers/subs4series.py | 272 +++++++++++++++
 libs/subliminal_patch/providers/subz.py       | 318 ++++++++++++++++++
 libs/subliminal_patch/providers/xsubs.py      | 302 +++++++++++++++++
 views/settings.tpl                            |  94 ++++++
 9 files changed, 1464 insertions(+)
 create mode 100644 libs/subliminal_patch/providers/greeksubtitles.py
 create mode 100644 libs/subliminal_patch/providers/subs4free.py
 create mode 100644 libs/subliminal_patch/providers/subs4series.py
 create mode 100644 libs/subliminal_patch/providers/subz.py
 create mode 100644 libs/subliminal_patch/providers/xsubs.py

diff --git a/bazarr/config.py b/bazarr/config.py
index 895488068..96224413b 100644
--- a/bazarr/config.py
+++ b/bazarr/config.py
@@ -83,6 +83,10 @@ defaults = {
         'username': '',
         'password': ''
     },
+    'xsubs': {
+        'username': '',
+        'password': ''
+    },
     'assrt': {
         'token': ''
     }}
diff --git a/bazarr/get_providers.py b/bazarr/get_providers.py
index aa8a5f8b0..a34ca7233 100644
--- a/bazarr/get_providers.py
+++ b/bazarr/get_providers.py
@@ -43,6 +43,9 @@ def get_providers_auth():
         'legendastv': {'username': settings.legendastv.username,
                        'password': settings.legendastv.password,
                        },
+        'xsubs': {'username': settings.xsubs.username,
+                  'password': settings.xsubs.password,
+                       },
         'assrt': {'token': settings.assrt.token, }
     }
     
diff --git a/bazarr/main.py b/bazarr/main.py
index e19fcc7ab..e58fc701a 100644
--- a/bazarr/main.py
+++ b/bazarr/main.py
@@ -368,6 +368,8 @@ def save_wizard():
     settings.opensubtitles.vip = text_type(settings_opensubtitles_vip)
     settings.opensubtitles.ssl = text_type(settings_opensubtitles_ssl)
     settings.opensubtitles.skip_wrong_fps = text_type(settings_opensubtitles_skip_wrong_fps)
+    settings.xsubs.username = request.forms.get('settings_xsubs_username')
+    settings.xsubs.password = request.forms.get('settings_xsubs_password')
     
     settings_subliminal_languages = request.forms.getall('settings_subliminal_languages')
     c.execute("UPDATE table_settings_languages SET enabled = 0")
@@ -1350,6 +1352,8 @@ def save_settings():
     settings.opensubtitles.vip = text_type(settings_opensubtitles_vip)
     settings.opensubtitles.ssl = text_type(settings_opensubtitles_ssl)
     settings.opensubtitles.skip_wrong_fps = text_type(settings_opensubtitles_skip_wrong_fps)
+    settings.xsubs.username = request.forms.get('settings_xsubs_username')
+    settings.xsubs.password = request.forms.get('settings_xsubs_password')
 
     settings_subliminal_languages = request.forms.getall('settings_subliminal_languages')
     c.execute("UPDATE table_settings_languages SET enabled = 0")
diff --git a/libs/subliminal_patch/providers/greeksubtitles.py b/libs/subliminal_patch/providers/greeksubtitles.py
new file mode 100644
index 000000000..98dfc289e
--- /dev/null
+++ b/libs/subliminal_patch/providers/greeksubtitles.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+import io
+import logging
+import os
+import zipfile
+
+import rarfile
+from subzero.language import Language
+from guessit import guessit
+from requests import Session
+from six import text_type
+
+from subliminal import __short_version__
+from subliminal.providers import ParserBeautifulSoup, Provider
+from subliminal.subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending, guess_matches
+from subliminal.video import Episode, Movie
+
+logger = logging.getLogger(__name__)
+
+
+class GreekSubtitlesSubtitle(Subtitle):
+    """GreekSubtitles Subtitle."""
+    provider_name = 'greeksubtitles'
+
+    def __init__(self, language, page_link, version, download_link):
+        super(GreekSubtitlesSubtitle, self).__init__(language, page_link=page_link)
+        self.version = version
+        self.download_link = download_link
+        self.hearing_impaired = None
+        self.encoding = 'windows-1253'
+
+    @property
+    def id(self):
+        return self.download_link
+
+    def get_matches(self, video):
+        matches = set()
+
+        # episode
+        if isinstance(video, Episode):
+            # other properties
+            matches |= guess_matches(video, guessit(self.version, {'type': 'episode'}), partial=True)
+        # movie
+        elif isinstance(video, Movie):
+            # other properties
+            matches |= guess_matches(video, guessit(self.version, {'type': 'movie'}), partial=True)
+
+        return matches
+
+
+class GreekSubtitlesProvider(Provider):
+    """GreekSubtitles Provider."""
+    languages = {Language(l) for l in ['ell', 'eng']}
+    server_url = 'http://gr.greek-subtitles.com/'
+    search_url = 'search.php?name={}'
+    download_url = 'http://www.greeksubtitles.info/getp.php?id={:d}'
+    subtitle_class = GreekSubtitlesSubtitle
+
+    def __init__(self):
+        self.session = None
+
+    def initialize(self):
+        self.session = Session()
+        self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
+
+    def terminate(self):
+        self.session.close()
+
+    def query(self, keyword, season=None, episode=None, year=None):
+        params = keyword
+        if season and episode:
+            params += ' S{season:02d}E{episode:02d}'.format(season=season, episode=episode)
+        elif year:
+            params += ' {:4d}'.format(year)
+
+        logger.debug('Searching subtitles %r', params)
+        subtitles = []
+        search_link = self.server_url + text_type(self.search_url).format(params)
+        while True:
+            r = self.session.get(search_link, timeout=30)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('No data returned from provider')
+                return []
+
+            soup = ParserBeautifulSoup(r.content.decode('utf-8', 'ignore'), ['lxml', 'html.parser'])
+
+            # loop over subtitles cells
+            for cell in soup.select('td.latest_name > a:nth-of-type(1)'):
+                # read the item
+                subtitle_id = int(cell['href'].rsplit('/', 2)[1])
+                page_link = cell['href']
+                language = Language.fromalpha2(cell.parent.find('img')['src'].split('/')[-1].split('.')[0])
+                version = cell.text.strip() or None
+                if version is None:
+                    version = ""
+
+                subtitle = self.subtitle_class(language, page_link, version, self.download_url.format(subtitle_id))
+
+                logger.debug('Found subtitle %r', subtitle)
+                subtitles.append(subtitle)
+
+            anchors = soup.select('td a')
+            next_page_available = False
+            for anchor in anchors:
+                if 'Next' in anchor.text and 'search.php' in anchor['href']:
+                    search_link = self.server_url + anchor['href']
+                    next_page_available = True
+                    break
+            if not next_page_available:
+                break
+
+        return subtitles
+
+    def list_subtitles(self, video, languages):
+        if isinstance(video, Episode):
+            titles = [video.series] + video.alternative_series
+        elif isinstance(video, Movie):
+            titles = [video.title] + video.alternative_titles
+        else:
+            titles = []
+
+        subtitles = []
+        # query for subtitles with the show_id
+        for title in titles:
+            if isinstance(video, Episode):
+                subtitles += [s for s in self.query(title, season=video.season, episode=video.episode,
+                                                    year=video.year)
+                              if s.language in languages]
+            elif isinstance(video, Movie):
+                subtitles += [s for s in self.query(title, year=video.year)
+                              if s.language in languages]
+
+        return subtitles
+
+    def download_subtitle(self, subtitle):
+        if isinstance(subtitle, GreekSubtitlesSubtitle):
+            # download the subtitle
+            logger.info('Downloading subtitle %r', subtitle)
+            r = self.session.get(subtitle.download_link, headers={'Referer': subtitle.page_link},
+                                 timeout=30)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            archive = _get_archive(r.content)
+
+            subtitle_content = _get_subtitle_from_archive(archive)
+            if subtitle_content:
+                subtitle.content = fix_line_ending(subtitle_content)
+            else:
+                logger.debug('Could not extract subtitle from %r', archive)
+
+
+def _get_archive(content):
+    # open the archive
+    archive_stream = io.BytesIO(content)
+    archive = None
+    if rarfile.is_rarfile(archive_stream):
+        logger.debug('Identified rar archive')
+        archive = rarfile.RarFile(archive_stream)
+    elif zipfile.is_zipfile(archive_stream):
+        logger.debug('Identified zip archive')
+        archive = zipfile.ZipFile(archive_stream)
+
+    return archive
+
+
+def _get_subtitle_from_archive(archive):
+    for name in archive.namelist():
+        # discard hidden files
+        if os.path.split(name)[-1].startswith('.'):
+            continue
+
+        # discard non-subtitle files
+        if not name.lower().endswith(SUBTITLE_EXTENSIONS):
+            continue
+
+        return archive.read(name)
+
+    return None
diff --git a/libs/subliminal_patch/providers/subs4free.py b/libs/subliminal_patch/providers/subs4free.py
new file mode 100644
index 000000000..181b99351
--- /dev/null
+++ b/libs/subliminal_patch/providers/subs4free.py
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+# encoding=utf8
+import io
+import logging
+import os
+import random
+
+import rarfile
+import re
+import zipfile
+
+from subzero.language import Language
+from guessit import guessit
+from requests import Session
+from six import text_type
+
+from subliminal.providers import ParserBeautifulSoup, Provider
+from subliminal import __short_version__
+from subliminal.cache import SHOW_EXPIRATION_TIME, region
+from subliminal.score import get_equivalent_release_groups
+from subliminal.subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending, guess_matches
+from subliminal.utils import sanitize, sanitize_release_group
+from subliminal.video import Movie
+
+logger = logging.getLogger(__name__)
+
+year_re = re.compile(r'^\((\d{4})\)$')
+
+
+class Subs4FreeSubtitle(Subtitle):
+    """Subs4Free Subtitle."""
+    provider_name = 'subs4free'
+
+    def __init__(self, language, page_link, title, year, version, download_link):
+        super(Subs4FreeSubtitle, self).__init__(language, page_link=page_link)
+        self.title = title
+        self.year = year
+        self.version = version
+        self.download_link = download_link
+        self.hearing_impaired = None
+        self.encoding = 'utf8'
+
+    @property
+    def id(self):
+        return self.download_link
+
+    def get_matches(self, video):
+        matches = set()
+
+        # movie
+        if isinstance(video, Movie):
+            # title
+            if video.title and (sanitize(self.title) in (
+                    sanitize(name) for name in [video.title] + video.alternative_titles)):
+                matches.add('title')
+            # year
+            if video.year and self.year == video.year:
+                matches.add('year')
+
+        # release_group
+        if (video.release_group and self.version and
+                any(r in sanitize_release_group(self.version)
+                    for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))):
+            matches.add('release_group')
+        # other properties
+        matches |= guess_matches(video, guessit(self.version, {'type': 'movie'}), partial=True)
+
+        return matches
+
+
+class Subs4FreeProvider(Provider):
+    """Subs4Free Provider."""
+    languages = {Language(l) for l in ['ell', 'eng']}
+    video_types = (Movie,)
+    server_url = 'https://www.sf4-industry.com'
+    download_url = '/getSub.html'
+    search_url = '/search_report.php?search={}&searchType=1'
+    subtitle_class = Subs4FreeSubtitle
+
+    def __init__(self):
+        self.session = None
+
+    def initialize(self):
+        self.session = Session()
+        self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
+
+    def terminate(self):
+        self.session.close()
+
+    def get_show_ids(self, title, year=None):
+        """Get the best matching show id for `series` and `year``.
+
+        First search in the result of :meth:`_get_show_suggestions`.
+
+        :param title: show title.
+        :param year: year of the show, if any.
+        :type year: int
+        :return: the show id, if found.
+        :rtype: str
+
+        """
+        title_sanitized = sanitize(title).lower()
+        show_ids = self._get_suggestions(title)
+
+        matched_show_ids = []
+        for show in show_ids:
+            show_id = None
+            show_title = sanitize(show['title'])
+            # attempt with year
+            if not show_id and year:
+                logger.debug('Getting show id with year')
+                show_id = show['link'].split('?p=')[-1] if show_title == '{title} {year:d}'.format(
+                    title=title_sanitized, year=year) else None
+
+            # attempt clean
+            if not show_id:
+                logger.debug('Getting show id')
+                show_id = show['link'].split('?p=')[-1] if show_title == title_sanitized else None
+
+            if show_id:
+                matched_show_ids.append(show_id)
+
+        return matched_show_ids
+
+    @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME, to_str=text_type,
+                               should_cache_fn=lambda value: value)
+    def _get_suggestions(self, title):
+        """Search the show or movie id from the `title` and `year`.
+
+        :param str title: title of the show.
+        :return: the show suggestions found.
+        :rtype: dict
+
+        """
+        # make the search
+        logger.info('Searching show ids with %r', title)
+        r = self.session.get(self.server_url + text_type(self.search_url).format(title),
+                             headers={'Referer': self.server_url}, timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return {}
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+        suggestions = [{'link': l.attrs['value'], 'title': l.text}
+                       for l in soup.select('select[name="Mov_sel"] > option[value]')]
+        logger.debug('Found suggestions: %r', suggestions)
+
+        return suggestions
+
+    def query(self, movie_id, title, year):
+        # get the season list of the show
+        logger.info('Getting the subtitle list of show id %s', movie_id)
+        if movie_id:
+            page_link = self.server_url + '/' + movie_id
+        else:
+            page_link = self.server_url + text_type(self.search_url).format(' '.join([title, str(year)]))
+
+        r = self.session.get(page_link, timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return []
+
+        soup = ParserBeautifulSoup(r.content, ['html.parser'])
+
+        year_num = None
+        year_element = soup.select_one('td#dates_header > table div')
+        matches = False
+        if year_element:
+            matches = year_re.match(str(year_element.contents[2]).strip())
+        if matches:
+            year_num = int(matches.group(1))
+
+        title_element = soup.select_one('td#dates_header > table u')
+        show_title = str(title_element.contents[0]).strip() if title_element else None
+
+        subtitles = []
+        # loop over episode rows
+        for subtitle in soup.select('table.table_border div[align="center"] > div'):
+            # read common info
+            version = subtitle.find('b').text
+            download_link = self.server_url + subtitle.find('a')['href']
+            language = Language.fromalpha2(subtitle.find('img')['src'].split('/')[-1].split('.')[0])
+
+            subtitle = self.subtitle_class(language, page_link, show_title, year_num, version, download_link)
+
+            logger.debug('Found subtitle {!r}'.format(subtitle))
+            subtitles.append(subtitle)
+
+        return subtitles
+
+    def list_subtitles(self, video, languages):
+        # lookup show_id
+        titles = [video.title] + video.alternative_titles if isinstance(video, Movie) else []
+
+        show_ids = None
+        for title in titles:
+            show_ids = self.get_show_ids(title, video.year)
+            if show_ids and len(show_ids) > 0:
+                break
+
+        subtitles = []
+        # query for subtitles with the show_id
+        if show_ids and len(show_ids) > 0:
+            for show_id in show_ids:
+                subtitles += [s for s in self.query(show_id, video.title, video.year) if s.language in languages]
+        else:
+            subtitles += [s for s in self.query(None, video.title, video.year) if s.language in languages]
+
+        return subtitles
+
+    def download_subtitle(self, subtitle):
+        if isinstance(subtitle, Subs4FreeSubtitle):
+            # download the subtitle
+            logger.info('Downloading subtitle %r', subtitle)
+            r = self.session.get(subtitle.download_link, headers={'Referer': subtitle.page_link}, timeout=10)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+            download_element = soup.select_one('input[name="id"]')
+            image_element = soup.select_one('input[type="image"]')
+            subtitle_id = download_element['value'] if download_element else None
+            width = int(str(image_element['width']).strip('px')) if image_element else 0
+            height = int(str(image_element['height']).strip('px')) if image_element else 0
+
+            if not subtitle_id:
+                logger.debug('Unable to download subtitle. No download link found')
+                return
+
+            download_url = self.server_url + self.download_url
+            r = self.session.post(download_url, data={'utf8': 1, 'id': subtitle_id, 'x': random.randint(0, width),
+                                                      'y': random.randint(0, height)},
+                                  headers={'Referer': subtitle.download_link}, timeout=10)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            archive = _get_archive(r.content)
+
+            subtitle_content = _get_subtitle_from_archive(archive) if archive else r.content
+
+            if subtitle_content:
+                subtitle.content = fix_line_ending(subtitle_content)
+            else:
+                logger.debug('Could not extract subtitle from %r', archive)
+
+
+def _get_archive(content):
+    # open the archive
+    archive_stream = io.BytesIO(content)
+    archive = None
+    if rarfile.is_rarfile(archive_stream):
+        logger.debug('Identified rar archive')
+        archive = rarfile.RarFile(archive_stream)
+    elif zipfile.is_zipfile(archive_stream):
+        logger.debug('Identified zip archive')
+        archive = zipfile.ZipFile(archive_stream)
+
+    return archive
+
+
+def _get_subtitle_from_archive(archive):
+    for name in archive.namelist():
+        # discard hidden files
+        if os.path.split(name)[-1].startswith('.'):
+            continue
+
+        # discard non-subtitle files
+        if not name.lower().endswith(SUBTITLE_EXTENSIONS):
+            continue
+
+        return archive.read(name)
+
+    return None
diff --git a/libs/subliminal_patch/providers/subs4series.py b/libs/subliminal_patch/providers/subs4series.py
new file mode 100644
index 000000000..5f381feeb
--- /dev/null
+++ b/libs/subliminal_patch/providers/subs4series.py
@@ -0,0 +1,272 @@
+# -*- coding: utf-8 -*-
+import io
+import logging
+import os
+
+import rarfile
+import re
+import zipfile
+
+from subzero.language import Language
+from guessit import guessit
+from requests import Session
+from six import text_type
+
+from subliminal.providers import ParserBeautifulSoup, Provider
+from subliminal import __short_version__
+from subliminal.cache import SHOW_EXPIRATION_TIME, region
+from subliminal.score import get_equivalent_release_groups
+from subliminal.subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending, guess_matches
+from subliminal.utils import sanitize, sanitize_release_group
+from subliminal.video import Episode
+
+logger = logging.getLogger(__name__)
+
+year_re = re.compile(r'^\((\d{4})\)$')
+
+
+class Subs4SeriesSubtitle(Subtitle):
+    """Subs4Series Subtitle."""
+    provider_name = 'subs4series'
+
+    def __init__(self, language, page_link, series, year, version, download_link):
+        super(Subs4SeriesSubtitle, self).__init__(language, page_link=page_link)
+        self.series = series
+        self.year = year
+        self.version = version
+        self.download_link = download_link
+        self.hearing_impaired = None
+        self.encoding = 'windows-1253'
+
+    @property
+    def id(self):
+        return self.download_link
+
+    def get_matches(self, video):
+        matches = set()
+
+        # episode
+        if isinstance(video, Episode):
+            # series name
+            if video.series and sanitize(self.series) in (
+                    sanitize(name) for name in [video.series] + video.alternative_series):
+                matches.add('series')
+            # year
+            if video.original_series and self.year is None or video.year and video.year == self.year:
+                matches.add('year')
+
+        # release_group
+        if (video.release_group and self.version and
+                any(r in sanitize_release_group(self.version)
+                    for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))):
+            matches.add('release_group')
+        # other properties
+        matches |= guess_matches(video, guessit(self.version, {'type': 'episode'}), partial=True)
+
+        return matches
+
+
+class Subs4SeriesProvider(Provider):
+    """Subs4Series Provider."""
+    languages = {Language(l) for l in ['ell', 'eng']}
+    video_types = (Episode,)
+    server_url = 'https://www.subs4series.com'
+    search_url = '/search_report.php?search={}&searchType=1'
+    episode_link = '/tv-series/{show_id}/season-{season:d}/episode-{episode:d}'
+    subtitle_class = Subs4SeriesSubtitle
+
+    def __init__(self):
+        self.session = None
+
+    def initialize(self):
+        self.session = Session()
+        self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
+
+    def terminate(self):
+        self.session.close()
+
+    def get_show_ids(self, title, year=None):
+        """Get the best matching show id for `series` and `year`.
+
+        First search in the result of :meth:`_get_show_suggestions`.
+
+        :param title: show title.
+        :param year: year of the show, if any.
+        :type year: int
+        :return: the show id, if found.
+        :rtype: str
+
+        """
+        title_sanitized = sanitize(title).lower()
+        show_ids = self._get_suggestions(title)
+
+        matched_show_ids = []
+        for show in show_ids:
+            show_id = None
+            show_title = sanitize(show['title'])
+            # attempt with year
+            if not show_id and year:
+                logger.debug('Getting show id with year')
+                show_id = '/'.join(show['link'].rsplit('/', 2)[1:]) if show_title == '{title} {year:d}'.format(
+                    title=title_sanitized, year=year) else None
+
+            # attempt clean
+            if not show_id:
+                logger.debug('Getting show id')
+                show_id = '/'.join(show['link'].rsplit('/', 2)[1:]) if show_title == title_sanitized else None
+
+            if show_id:
+                matched_show_ids.append(show_id)
+
+        return matched_show_ids
+
+    @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME, to_str=text_type,
+                               should_cache_fn=lambda value: value)
+    def _get_suggestions(self, title):
+        """Search the show or movie id from the `title` and `year`.
+
+        :param str title: title of the show.
+        :return: the show suggestions found.
+        :rtype: dict
+
+        """
+        # make the search
+        logger.info('Searching show ids with %r', title)
+        r = self.session.get(self.server_url + text_type(self.search_url).format(title),
+                             headers={'Referer': self.server_url}, timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return {}
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+        series = [{'link': l.attrs['value'], 'title': l.text}
+                  for l in soup.select('select[name="Mov_sel"] > option[value]')]
+        logger.debug('Found suggestions: %r', series)
+
+        return series
+
+    def query(self, show_id, series, season, episode, title):
+        # get the season list of the show
+        logger.info('Getting the subtitle list of show id %s', show_id)
+        if all((show_id, season, episode)):
+            page_link = self.server_url + self.episode_link.format(show_id=show_id, season=season, episode=episode)
+        else:
+            return []
+
+        r = self.session.get(page_link, timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return []
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+
+        year_num = None
+        matches = year_re.match(str(soup.select_one('#dates_header_br > table div').contents[2]).strip())
+        if matches:
+            year_num = int(matches.group(1))
+        show_title = str(soup.select_one('#dates_header_br > table u').contents[0]).strip()
+
+        subtitles = []
+        # loop over episode rows
+        for subtitle in soup.select('table.table_border div[align="center"] > div'):
+            # read common info
+            version = subtitle.find('b').text
+            download_link = self.server_url + subtitle.find('a')['href']
+            language = Language.fromalpha2(subtitle.find('img')['src'].split('/')[-1].split('.')[0])
+
+            subtitle = self.subtitle_class(language, page_link, show_title, year_num, version, download_link)
+
+            logger.debug('Found subtitle %r', subtitle)
+            subtitles.append(subtitle)
+
+        return subtitles
+
+    def list_subtitles(self, video, languages):
+        # lookup show_id
+        titles = [video.series] + video.alternative_series if isinstance(video, Episode) else []
+
+        show_ids = None
+        for title in titles:
+            show_ids = self.get_show_ids(title, video.year)
+            if show_ids and len(show_ids) > 0:
+                break
+
+        subtitles = []
+        # query for subtitles with the show_id
+        for show_id in show_ids:
+            subtitles += [s for s in self.query(show_id, video.series, video.season, video.episode, video.title)
+                          if s.language in languages]
+
+        return subtitles
+
+    def download_subtitle(self, subtitle):
+        if isinstance(subtitle, Subs4SeriesSubtitle):
+            # download the subtitle
+            logger.info('Downloading subtitle %r', subtitle)
+            r = self.session.get(subtitle.download_link, headers={'Referer': subtitle.page_link}, timeout=10)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+            download_element = soup.select_one('a.style55ws')
+            if not download_element:
+                download_element = soup.select_one('form[method="post"]')
+                target = download_element['action'] if download_element else None
+            else:
+                target = download_element['href']
+
+            if not target:
+                logger.debug('Unable to download subtitle. No download link found')
+                return
+
+            download_url = self.server_url + target
+            r = self.session.get(download_url, headers={'Referer': subtitle.download_link}, timeout=10)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            archive = _get_archive(r.content)
+            subtitle_content = _get_subtitle_from_archive(archive) if archive else r.content
+
+            if subtitle_content:
+                subtitle.content = fix_line_ending(subtitle_content)
+            else:
+                logger.debug('Could not extract subtitle from %r', archive)
+
+
+def _get_archive(content):
+    # open the archive
+    archive_stream = io.BytesIO(content)
+    archive = None
+    if rarfile.is_rarfile(archive_stream):
+        logger.debug('Identified rar archive')
+        archive = rarfile.RarFile(archive_stream)
+    elif zipfile.is_zipfile(archive_stream):
+        logger.debug('Identified zip archive')
+        archive = zipfile.ZipFile(archive_stream)
+
+    return archive
+
+
+def _get_subtitle_from_archive(archive):
+    for name in archive.namelist():
+        # discard hidden files
+        if os.path.split(name)[-1].startswith('.'):
+            continue
+
+        # discard non-subtitle files
+        if not name.lower().endswith(SUBTITLE_EXTENSIONS):
+            continue
+
+        return archive.read(name)
+
+    return None
diff --git a/libs/subliminal_patch/providers/subz.py b/libs/subliminal_patch/providers/subz.py
new file mode 100644
index 000000000..dc95cb8d7
--- /dev/null
+++ b/libs/subliminal_patch/providers/subz.py
@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+import io
+import json
+import logging
+import os
+
+import rarfile
+import re
+import zipfile
+
+from subzero.language import Language
+from guessit import guessit
+from requests import Session
+from six import text_type
+
+from subliminal.providers import ParserBeautifulSoup, Provider
+from subliminal import __short_version__
+from subliminal.cache import SHOW_EXPIRATION_TIME, region
+from subliminal.score import get_equivalent_release_groups
+from subliminal.subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending, guess_matches
+from subliminal.utils import sanitize, sanitize_release_group
+from subliminal.video import Episode, Movie
+
+logger = logging.getLogger(__name__)
+
+episode_re = re.compile(r'^S(\d{2})E(\d{2})$')
+
+
+class SubzSubtitle(Subtitle):
+    """Subz Subtitle."""
+    provider_name = 'subz'
+
+    def __init__(self, language, page_link, series, season, episode, title, year, version, download_link):
+        super(SubzSubtitle, self).__init__(language, page_link=page_link)
+        self.series = series
+        self.season = season
+        self.episode = episode
+        self.title = title
+        self.year = year
+        self.version = version
+        self.download_link = download_link
+        self.hearing_impaired = None
+        self.encoding = 'windows-1253'
+
+    @property
+    def id(self):
+        return self.download_link
+
+    def get_matches(self, video):
+        matches = set()
+        video_type = None
+
+        # episode
+        if isinstance(video, Episode):
+            video_type = 'episode'
+            # series name
+            if video.series and sanitize(self.series) in (
+                    sanitize(name) for name in [video.series] + video.alternative_series):
+                matches.add('series')
+            # season
+            if video.season and self.season == video.season:
+                matches.add('season')
+            # episode
+            if video.episode and self.episode == video.episode:
+                matches.add('episode')
+            # title of the episode
+            if video.title and sanitize(self.title) == sanitize(video.title):
+                matches.add('title')
+            # year
+            if video.original_series and self.year is None or video.year and video.year == self.year:
+                matches.add('year')
+        # movie
+        elif isinstance(video, Movie):
+            video_type = 'movie'
+            # title
+            if video.title and (sanitize(self.title) in (
+                    sanitize(name) for name in [video.title] + video.alternative_titles)):
+                matches.add('title')
+            # year
+            if video.year and self.year == video.year:
+                matches.add('year')
+
+        # release_group
+        if (video.release_group and self.version and
+                any(r in sanitize_release_group(self.version)
+                    for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))):
+            matches.add('release_group')
+        # other properties
+        matches |= guess_matches(video, guessit(self.version, {'type': video_type}), partial=True)
+
+        return matches
+
+
+class SubzProvider(Provider):
+    """Subz Provider."""
+    languages = {Language(l) for l in ['ell']}
+    server_url = 'https://subz.xyz'
+    sign_in_url = '/sessions'
+    sign_out_url = '/logout'
+    search_url = '/typeahead/{}'
+    episode_link = '/series/{show_id}/seasons/{season:d}/episodes/{episode:d}'
+    movie_link = '/movies/{}'
+    subtitle_class = SubzSubtitle
+
+    def __init__(self):
+        self.logged_in = False
+        self.session = None
+
+    def initialize(self):
+        self.session = Session()
+        self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
+
+    def terminate(self):
+        self.session.close()
+
+    def get_show_ids(self, title, year=None, is_episode=True, country_code=None):
+        """Get the best matching show id for `series`, `year` and `country_code`.
+
+        First search in the result of :meth:`_get_show_suggestions`.
+
+        :param title: show title.
+        :param year: year of the show, if any.
+        :type year: int
+        :param is_episode: if the search is for episode.
+        :type is_episode: bool
+        :param country_code: country code of the show, if any.
+        :type country_code: str
+        :return: the show id, if found.
+        :rtype: str
+
+        """
+        title_sanitized = sanitize(title).lower()
+        show_ids = self._get_suggestions(title, is_episode)
+
+        matched_show_ids = []
+        for show in show_ids:
+            show_id = None
+            # attempt with country
+            if not show_id and country_code:
+                logger.debug('Getting show id with country')
+                if sanitize(show['title']) == text_type('{title} {country}').format(title=title_sanitized,
+                                                                                    country=country_code.lower()):
+                    show_id = show['link'].split('/')[-1]
+
+            # attempt with year
+            if not show_id and year:
+                logger.debug('Getting show id with year')
+                if sanitize(show['title']) == text_type('{title} {year}').format(title=title_sanitized, year=year):
+                    show_id = show['link'].split('/')[-1]
+
+            # attempt clean
+            if not show_id:
+                logger.debug('Getting show id')
+                show_id = show['link'].split('/')[-1] if sanitize(show['title']) == title_sanitized else None
+
+            if show_id:
+                matched_show_ids.append(show_id)
+
+        return matched_show_ids
+
+    @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME, to_str=text_type,
+                               should_cache_fn=lambda value: value)
+    def _get_suggestions(self, title, is_episode=True):
+        """Search the show or movie id from the `title` and `year`.
+
+        :param str title: title of the show.
+        :param is_episode: if the search is for episode.
+        :type is_episode: bool
+        :return: the show suggestions found.
+        :rtype: dict
+
+        """
+        # make the search
+        logger.info('Searching show ids with %r', title)
+        r = self.session.get(self.server_url + text_type(self.search_url).format(title), timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return {}
+
+        show_type = 'series' if is_episode else 'movie'
+        parsed_suggestions = [s for s in json.loads(r.text) if 'type' in s and s['type'] == show_type]
+        logger.debug('Found suggestions: %r', parsed_suggestions)
+
+        return parsed_suggestions
+
+    def query(self, show_id, series, season, episode, title):
+        # get the season list of the show
+        logger.info('Getting the subtitle list of show id %s', show_id)
+        is_episode = False
+        if all((show_id, season, episode)):
+            is_episode = True
+            page_link = self.server_url + self.episode_link.format(show_id=show_id, season=season, episode=episode)
+        elif all((show_id, title)):
+            page_link = self.server_url + self.movie_link.format(show_id)
+        else:
+            return []
+
+        r = self.session.get(page_link, timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return []
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+
+        year_num = None
+        if not is_episode:
+            year_num = int(soup.select_one('span.year').text)
+        show_title = str(soup.select_one('#summary-wrapper > div.summary h1').contents[0]).strip()
+
+        subtitles = []
+        # loop over episode rows
+        for subtitle in soup.select('div[id="subtitles"] tr[data-id]'):
+            # read common info
+            version = subtitle.find('td', {'class': 'name'}).text
+            download_link = subtitle.find('a', {'class': 'btn-success'})['href'].strip('\'')
+
+            # read the episode info
+            if is_episode:
+                episode_numbers = soup.select_one('#summary-wrapper > div.container.summary span.main-title-sxe').text
+                season_num = None
+                episode_num = None
+                matches = episode_re.match(episode_numbers.strip())
+                if matches:
+                    season_num = int(matches.group(1))
+                    episode_num = int(matches.group(2))
+
+                episode_title = soup.select_one('#summary-wrapper > div.container.summary span.main-title').text
+
+                subtitle = self.subtitle_class(Language.fromalpha2('el'), page_link, show_title, season_num,
+                                               episode_num, episode_title, year_num, version, download_link)
+            # read the movie info
+            else:
+                subtitle = self.subtitle_class(Language.fromalpha2('el'), page_link, None, None, None, show_title,
+                                               year_num, version, download_link)
+
+            logger.debug('Found subtitle %r', subtitle)
+            subtitles.append(subtitle)
+
+        return subtitles
+
+    def list_subtitles(self, video, languages):
+        # lookup show_id
+        if isinstance(video, Episode):
+            titles = [video.series] + video.alternative_series
+        elif isinstance(video, Movie):
+            titles = [video.title] + video.alternative_titles
+        else:
+            titles = []
+
+        show_ids = None
+        for title in titles:
+            show_ids = self.get_show_ids(title, video.year, isinstance(video, Episode))
+            if show_ids is not None and len(show_ids) > 0:
+                break
+
+        subtitles = []
+        # query for subtitles with the show_id
+        for show_id in show_ids:
+            if isinstance(video, Episode):
+                subtitles += [s for s in self.query(show_id, video.series, video.season, video.episode, video.title)
+                              if s.language in languages and s.season == video.season and s.episode == video.episode]
+            elif isinstance(video, Movie):
+                subtitles += [s for s in self.query(show_id, None, None, None, video.title)
+                              if s.language in languages and s.year == video.year]
+
+        return subtitles
+
+    def download_subtitle(self, subtitle):
+        if isinstance(subtitle, SubzSubtitle):
+            # download the subtitle
+            logger.info('Downloading subtitle %r', subtitle)
+            r = self.session.get(subtitle.download_link, headers={'Referer': subtitle.page_link}, timeout=10)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            archive = _get_archive(r.content)
+
+            subtitle_content = _get_subtitle_from_archive(archive)
+            if subtitle_content:
+                subtitle.content = fix_line_ending(subtitle_content)
+            else:
+                logger.debug('Could not extract subtitle from %r', archive)
+
+
+def _get_archive(content):
+    # open the archive
+    archive_stream = io.BytesIO(content)
+    archive = None
+    if rarfile.is_rarfile(archive_stream):
+        logger.debug('Identified rar archive')
+        archive = rarfile.RarFile(archive_stream)
+    elif zipfile.is_zipfile(archive_stream):
+        logger.debug('Identified zip archive')
+        archive = zipfile.ZipFile(archive_stream)
+
+    return archive
+
+
+def _get_subtitle_from_archive(archive):
+    for name in archive.namelist():
+        # discard hidden files
+        if os.path.split(name)[-1].startswith('.'):
+            continue
+
+        # discard non-subtitle files
+        if not name.lower().endswith(SUBTITLE_EXTENSIONS):
+            continue
+
+        return archive.read(name)
+
+    return None
diff --git a/libs/subliminal_patch/providers/xsubs.py b/libs/subliminal_patch/providers/xsubs.py
new file mode 100644
index 000000000..102571dd9
--- /dev/null
+++ b/libs/subliminal_patch/providers/xsubs.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+import logging
+import re
+
+from subzero.language import Language
+from guessit import guessit
+from requests import Session
+
+from subliminal.providers import ParserBeautifulSoup, Provider
+from subliminal import __short_version__
+from subliminal.cache import SHOW_EXPIRATION_TIME, region
+from subliminal.exceptions import AuthenticationError, ConfigurationError
+from subliminal.score import get_equivalent_release_groups
+from subliminal.subtitle import Subtitle, fix_line_ending, guess_matches
+from subliminal.utils import sanitize, sanitize_release_group
+from subliminal.video import Episode
+
+logger = logging.getLogger(__name__)
+article_re = re.compile(r'^([A-Za-z]{1,3}) (.*)$')
+
+
+class XSubsSubtitle(Subtitle):
+    """XSubs Subtitle."""
+    provider_name = 'xsubs'
+
+    def __init__(self, language, page_link, series, season, episode, year, title, version, download_link):
+        super(XSubsSubtitle, self).__init__(language, page_link=page_link)
+        self.series = series
+        self.season = season
+        self.episode = episode
+        self.year = year
+        self.title = title
+        self.version = version
+        self.download_link = download_link
+        self.hearing_impaired = None
+        self.encoding = 'windows-1253'
+
+    @property
+    def id(self):
+        return self.download_link
+
+    def get_matches(self, video):
+        matches = set()
+
+        if isinstance(video, Episode):
+            # series name
+            if video.series and sanitize(self.series) in (
+                    sanitize(name) for name in [video.series] + video.alternative_series):
+                matches.add('series')
+            # season
+            if video.season and self.season == video.season:
+                matches.add('season')
+            # episode
+            if video.episode and self.episode == video.episode:
+                matches.add('episode')
+            # title of the episode
+            if video.title and sanitize(self.title) == sanitize(video.title):
+                matches.add('title')
+            # year
+            if video.original_series and self.year is None or video.year and video.year == self.year:
+                matches.add('year')
+            # release_group
+            if (video.release_group and self.version and
+                    any(r in sanitize_release_group(self.version)
+                        for r in get_equivalent_release_groups(sanitize_release_group(video.release_group)))):
+                matches.add('release_group')
+            # other properties
+            matches |= guess_matches(video, guessit(self.version, {'type': 'episode'}), partial=True)
+
+        return matches
+
+
+class XSubsProvider(Provider):
+    """XSubs Provider."""
+    languages = {Language(l) for l in ['ell']}
+    video_types = (Episode,)
+    server_url = 'http://xsubs.tv'
+    sign_in_url = '/xforum/account/signin/'
+    sign_out_url = '/xforum/account/signout/'
+    all_series_url = '/series/all.xml'
+    series_url = '/series/{:d}/main.xml'
+    season_url = '/series/{show_id:d}/{season:d}.xml'
+    page_link = '/ice/xsw.xml?srsid={show_id:d}#{season_id:d};{season:d}'
+    download_link = '/xthru/getsub/{:d}'
+    subtitle_class = XSubsSubtitle
+
+    def __init__(self, username=None, password=None):
+        if any((username, password)) and not all((username, password)):
+            raise ConfigurationError('Username and password must be specified')
+
+        self.username = username
+        self.password = password
+        self.logged_in = False
+        self.session = None
+
+    def initialize(self):
+        self.session = Session()
+        self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
+
+        # login
+        if self.username and self.password:
+            logger.info('Logging in')
+            self.session.get(self.server_url + self.sign_in_url)
+            data = {'username': self.username,
+                    'password': self.password,
+                    'csrfmiddlewaretoken': self.session.cookies['csrftoken']}
+            r = self.session.post(self.server_url + self.sign_in_url, data, allow_redirects=False, timeout=10)
+
+            if r.status_code != 302:
+                raise AuthenticationError(self.username)
+
+            logger.debug('Logged in')
+            self.logged_in = True
+
+    def terminate(self):
+        # logout
+        if self.logged_in:
+            logger.info('Logging out')
+            r = self.session.get(self.server_url + self.sign_out_url, timeout=10)
+            r.raise_for_status()
+            logger.debug('Logged out')
+            self.logged_in = False
+
+        self.session.close()
+
+    @region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME, should_cache_fn=lambda value: value)
+    def _get_show_ids(self):
+        # get the shows page
+        logger.info('Getting show ids')
+        r = self.session.get(self.server_url + self.all_series_url, timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return []
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+
+        # populate the show ids
+        show_ids = {}
+        for show_category in soup.findAll('seriesl'):
+            if show_category.attrs['category'] == u'Σειρές':
+                for show in show_category.findAll('series'):
+                    show_ids[sanitize(show.text)] = int(show['srsid'])
+                break
+        logger.debug('Found %d show ids', len(show_ids))
+
+        return show_ids
+
+    def get_show_id(self, series_names, year=None, country_code=None):
+        series_sanitized_names = []
+        for name in series_names:
+            sanitized_name = sanitize(name)
+            series_sanitized_names.append(sanitized_name)
+            alternative_name = _get_alternative_name(sanitized_name)
+            if alternative_name:
+                series_sanitized_names.append(alternative_name)
+
+        show_ids = self._get_show_ids()
+        show_id = None
+
+        for series_sanitized in series_sanitized_names:
+            # attempt with country
+            if not show_id and country_code:
+                logger.debug('Getting show id with country')
+                show_id = show_ids.get('{series} {country}'.format(series=series_sanitized,
+                                                                   country=country_code.lower()))
+
+            # attempt with year
+            if not show_id and year:
+                logger.debug('Getting show id with year')
+                show_id = show_ids.get('{series} {year:d}'.format(series=series_sanitized, year=year))
+
+            # attempt with article at the end
+            if not show_id and year:
+                logger.debug('Getting show id with year in brackets')
+                show_id = show_ids.get('{series} [{year:d}]'.format(series=series_sanitized, year=year))
+
+            # attempt clean
+            if not show_id:
+                logger.debug('Getting show id')
+                show_id = show_ids.get(series_sanitized)
+
+            if show_id:
+                break
+
+        return int(show_id) if show_id else None
+
+    def query(self, show_id, series, season, year=None, country=None):
+        # get the season list of the show
+        logger.info('Getting the season list of show id %d', show_id)
+        r = self.session.get(self.server_url + self.series_url.format(show_id), timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return []
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+
+        series_title = soup.find('name').text
+
+        # loop over season rows
+        seasons = soup.findAll('series_group')
+        season_id = None
+
+        for season_row in seasons:
+            try:
+                parsed_season = int(season_row['ssnnum'])
+                if parsed_season == season:
+                    season_id = int(season_row['ssnid'])
+                    break
+            except (ValueError, TypeError):
+                continue
+
+        if season_id is None:
+            logger.debug('Season not found in provider')
+            return []
+
+        # get the subtitle list of the season
+        logger.info('Getting the subtitle list of season %d', season)
+        r = self.session.get(self.server_url + self.season_url.format(show_id=show_id, season=season_id), timeout=10)
+        r.raise_for_status()
+
+        if not r.content:
+            logger.debug('No data returned from provider')
+            return []
+
+        soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
+
+        subtitles = []
+        # loop over episode rows
+        for episode in soup.findAll('subg'):
+            # read the episode info
+            etitle = episode.find('etitle')
+            if etitle is None:
+                continue
+
+            episode_num = int(etitle['number'].split('-')[0])
+
+            sgt = episode.find('sgt')
+            if sgt is None:
+                continue
+
+            season_num = int(sgt['ssnnum'])
+
+            # filter out unreleased subtitles
+            for subtitle in episode.findAll('sr'):
+                if subtitle['published_on'] == '':
+                    continue
+
+                page_link = self.server_url + self.page_link.format(show_id=show_id, season_id=season_id,
+                                                                    season=season_num)
+                episode_title = etitle['title']
+                version = subtitle.fmt.text + ' ' + subtitle.team.text
+                download_link = self.server_url + self.download_link.format(int(subtitle['rlsid']))
+
+                subtitle = self.subtitle_class(Language.fromalpha2('el'), page_link, series_title, season_num,
+                                               episode_num, year, episode_title, version, download_link)
+                logger.debug('Found subtitle %r', subtitle)
+                subtitles.append(subtitle)
+
+        return subtitles
+
+    def list_subtitles(self, video, languages):
+        if isinstance(video, Episode):
+            # lookup show_id
+            titles = [video.series] + video.alternative_series
+            show_id = self.get_show_id(titles, video.year)
+
+            # query for subtitles with the show_id
+            if show_id:
+                subtitles = [s for s in self.query(show_id, video.series, video.season, video.year)
+                             if s.language in languages and s.season == video.season and s.episode == video.episode]
+                if subtitles:
+                    return subtitles
+            else:
+                logger.error('No show id found for %r (%r)', video.series, {'year': video.year})
+
+        return []
+
+    def download_subtitle(self, subtitle):
+        if isinstance(subtitle, XSubsSubtitle):
+            # download the subtitle
+            logger.info('Downloading subtitle %r', subtitle)
+            r = self.session.get(subtitle.download_link, headers={'Referer': subtitle.page_link},
+                                 timeout=10)
+            r.raise_for_status()
+
+            if not r.content:
+                logger.debug('Unable to download subtitle. No data returned from provider')
+                return
+
+            subtitle.content = fix_line_ending(r.content)
+
+
+def _get_alternative_name(series):
+    article_match = article_re.match(series)
+    if article_match:
+        return '{series} {article}'.format(series=article_match.group(2), article=article_match.group(1))
+
+    return None
diff --git a/views/settings.tpl b/views/settings.tpl
index cd7b3a669..8c71ddbfe 100644
--- a/views/settings.tpl
+++ b/views/settings.tpl
@@ -1493,6 +1493,100 @@
 
                         </div>
 
+                        <div class="middle aligned row">
+                            <div class="right aligned four wide column">
+                                <label>GreekSubtitles</label>
+                            </div>
+                            <div class="one wide column">
+                                <div id="greeksubtitles" class="ui toggle checkbox provider">
+                                    <input type="checkbox">
+                                    <label></label>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="greeksubtitles_option" class="ui grid container">
+
+                        </div>
+
+                        <div class="middle aligned row">
+                            <div class="right aligned four wide column">
+                                <label>Subs4Free</label>
+                            </div>
+                            <div class="one wide column">
+                                <div id="subs4free" class="ui toggle checkbox provider">
+                                    <input type="checkbox">
+                                    <label></label>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="subs4free_option" class="ui grid container">
+
+                        </div>
+
+                        <div class="middle aligned row">
+                            <div class="right aligned four wide column">
+                                <label>Subs4Series</label>
+                            </div>
+                            <div class="one wide column">
+                                <div id="subs4series" class="ui toggle checkbox provider">
+                                    <input type="checkbox">
+                                    <label></label>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="subs4series_option" class="ui grid container">
+
+                        </div>
+
+                        <div class="middle aligned row">
+                            <div class="right aligned four wide column">
+                                <label>SubZ</label>
+                            </div>
+                            <div class="one wide column">
+                                <div id="subz" class="ui toggle checkbox provider">
+                                    <input type="checkbox">
+                                    <label></label>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="subz_option" class="ui grid container">
+
+                        </div>
+
+                        <div class="middle aligned row">
+                            <div class="right aligned four wide column">
+                                <label>XSubs</label>
+                            </div>
+                            <div class="one wide column">
+                                <div id="xsubs" class="ui toggle checkbox provider">
+                                    <input type="checkbox">
+                                    <label></label>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="xsubs_option" class="ui grid container">
+                        	<div class="middle aligned row">
+                                <div class="right aligned six wide column">
+                                    <label>Username</label>
+                                </div>
+                                <div class="six wide column">
+                                    <div class="ui fluid input">
+                                        <input name="settings_xsubs_username" type="text" value="{{settings.xsubs.username if settings.xsubs.username != None else ''}}">
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="middle aligned row">
+                                <div class="right aligned six wide column">
+                                    <label>Password</label>
+                                </div>
+                                <div class="six wide column">
+                                    <div class="ui fluid input">
+                                        <input name="settings_xsubs_password" type="password" value="{{settings.xsubs.password if settings.xsubs.password != None else ''}}">
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
                         <div class="middle aligned row">
                             <div class="eleven wide column">
                                 <div class='field' hidden>