aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorPanagiotis Koutsias <[email protected]>2019-02-24 19:41:22 +0200
committermorpheus65535 <[email protected]>2019-02-24 12:41:22 -0500
commit176b2c818aff8cc6e9a3ebb9dabdce3e838a331f (patch)
tree288ef6a36b810dbcfe8c767540f1f3120b8087e0
parente129cafc7c7a5aea3d408efd0549ce5c65c8fa6b (diff)
downloadbazarr-176b2c818aff8cc6e9a3ebb9dabdce3e838a331f.tar.gz
bazarr-176b2c818aff8cc6e9a3ebb9dabdce3e838a331f.zip
Adds GreekSubtitles, Subs4Free, Subs4Series, SubZ and XSubs providers (#310)
* Adds GreekSubtitles, Subs4Free, Subs4Series, SubZ and XSubs providers * Various optimizations in greek providers
-rw-r--r--bazarr/config.py4
-rw-r--r--bazarr/get_providers.py3
-rw-r--r--bazarr/main.py4
-rw-r--r--libs/subliminal_patch/providers/greeksubtitles.py184
-rw-r--r--libs/subliminal_patch/providers/subs4free.py283
-rw-r--r--libs/subliminal_patch/providers/subs4series.py272
-rw-r--r--libs/subliminal_patch/providers/subz.py318
-rw-r--r--libs/subliminal_patch/providers/xsubs.py302
-rw-r--r--views/settings.tpl94
9 files changed, 1464 insertions, 0 deletions
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
@@ -1494,6 +1494,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>
<select name="settings_subliminal_providers" id="settings_providers" multiple="" class="ui fluid search selection dropdown">