# -*- coding: utf-8 -*- from __future__ import absolute_import import logging import io import re import os import rarfile import zipfile from requests import Session from requests.exceptions import HTTPError from guessit import guessit from subliminal.exceptions import ConfigurationError, AuthenticationError, ServiceUnavailable, DownloadLimitExceeded from subliminal_patch.exceptions import TooManyRequests, IPAddressBlocked from subliminal_patch.providers import Provider from subliminal.providers import ParserBeautifulSoup from subliminal_patch.subtitle import Subtitle from subliminal.video import Episode, Movie from subliminal.subtitle import SUBTITLE_EXTENSIONS, fix_line_ending, guess_matches from subzero.language import Language from subliminal_patch.score import get_scores from subliminal.utils import sanitize, sanitize_release_group from dogpile.cache.api import NO_VALUE from subliminal.cache import region from subliminal_patch.http import RetryingCFSession logger = logging.getLogger(__name__) class LegendasdivxSubtitle(Subtitle): """Legendasdivx Subtitle.""" provider_name = 'legendasdivx' def __init__(self, language, video, data): super(LegendasdivxSubtitle, self).__init__(language) self.language = language self.page_link = data['link'] self.hits = data['hits'] self.exact_match = data['exact_match'] self.description = data['description'] self.video = video self.video_filename = data['video_filename'] self.uploader = data['uploader'] @property def id(self): return self.page_link @property def release_info(self): return self.description def get_matches(self, video): matches = set() description = sanitize(self.description) if sanitize(self.video_filename) in description: matches.update(['title']) matches.update(['season']) matches.update(['episode']) # episode if video.title and sanitize(video.title) in description: matches.update(['title']) if video.year and '{:04d}'.format(video.year) in description: matches.update(['year']) if isinstance(video, Episode): # already matched in search query if video.season and 's{:02d}'.format(video.season) in description: matches.update(['season']) if video.episode and 'e{:02d}'.format(video.episode) in description: matches.update(['episode']) if video.episode and video.season and video.series: if '{} s{:02d}e{:02d}'.format(sanitize(video.series), video.season, video.episode) in description: matches.update(['series']) matches.update(['season']) matches.update(['episode']) # release_group if video.release_group and sanitize_release_group(video.release_group) in sanitize_release_group(description): matches.update(['release_group']) # resolution if video.resolution and video.resolution.lower() in description: matches.update(['resolution']) # format formats = [] if video.format: formats = [video.format.lower()] if formats[0] == "web-dl": formats.append("webdl") formats.append("webrip") formats.append("web") for frmt in formats: if frmt in description: matches.update(['format']) break # video_codec if video.video_codec: video_codecs = [video.video_codec.lower()] if video_codecs[0] == "h264": video_codecs.append("x264") elif video_codecs[0] == "h265": video_codecs.append("x265") for vc in video_codecs: if vc in description: matches.update(['video_codec']) break # running guessit on a huge description may break guessit # matches |= guess_matches(video, guessit(self.description)) return matches class LegendasdivxProvider(Provider): """Legendasdivx Provider.""" languages = {Language('por', 'BR')} | {Language('por')} SEARCH_THROTTLE = 8 site = 'https://www.legendasdivx.pt' headers = { 'User-Agent': os.environ.get("SZ_USER_AGENT", "Sub-Zero/2"), 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Origin': 'https://www.legendasdivx.pt', 'Referer': 'https://www.legendasdivx.pt', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache' } loginpage = site + '/forum/ucp.php?mode=login' logoutpage = site + '/sair.php' searchurl = site + '/modules.php?name=Downloads&file=jz&d_op=search&op=_jz00&query={query}' download_link = site + '/modules.php{link}' def __init__(self, username, password): # make sure login credentials are configured. if any((username, password)) and not all((username, password)): raise ConfigurationError('Legendasdivx.pt :: Username and password must be specified') self.username = username self.password = password def initialize(self): logger.info("Legendasdivx.pt :: Creating session for requests") #self.session = Session() self.session = RetryingCFSession() # re-use PHP Session if present prev_cookies = region.get("legendasdivx_cookies2") if prev_cookies != NO_VALUE: logger.debug("Legendasdivx.pt :: Re-using previous legendasdivx cookies: %s", prev_cookies) self.session.cookies.update(prev_cookies) # Login if session has expired else: logger.debug("Legendasdivx.pt :: Session cookies not found!") self.session.headers.update(self.headers) self.login() def terminate(self): # session close self.session.close() def login(self): logger.info('Legendasdivx.pt :: Logging in') try: res = self.session.get(self.loginpage) res.raise_for_status() bsoup = ParserBeautifulSoup(res.content, ['lxml']) _allinputs = bsoup.findAll('input') data = {} # necessary to set 'sid' for POST request for field in _allinputs: data[field.get('name')] = field.get('value') data['username'] = self.username data['password'] = self.password res = self.session.post(self.loginpage, data) res.raise_for_status() #make sure we're logged in logger.debug('Legendasdivx.pt :: Logged in successfully: PHPSESSID: %s' % self.session.cookies.get_dict()['PHPSESSID']) cj = self.session.cookies.copy() store_cks = ("PHPSESSID", "phpbb3_2z8zs_sid", "phpbb3_2z8zs_k", "phpbb3_2z8zs_u", "lang") for cn in iter(self.session.cookies.keys()): if cn not in store_cks: del cj[cn] #store session cookies on cache logger.debug("Legendasdivx.pt :: Storing legendasdivx session cookies: %r", cj) region.set("legendasdivx_cookies2", cj) except KeyError: logger.error("Legendasdivx.pt :: Couldn't get session ID, check your credentials") raise AuthenticationError("Legendasdivx.pt :: Couldn't get session ID, check your credentials") except HTTPError as e: if res.status_code == "403": logger.error("LegendasDivx.pt :: Your IP is blocked on this server.") raise IPAddressBlocked("LegendasDivx.pt :: Your IP is blocked on this server.") else: logger.error("Legendasdivx.pt :: HTTP Error %s" % e) raise TooManyRequests("Legendasdivx.pt :: HTTP Error %s" % e) except Exception as e: logger.error("LegendasDivx.pt :: Uncaught error: %r" % repr(e)) raise ServiceUnavailable("LegendasDivx.pt :: Uncaught error: %r" % repr(e)) def _process_page(self, video, bsoup, video_filename): subtitles = [] _allsubs = bsoup.findAll("div", {"class": "sub_box"}) for _subbox in _allsubs: hits = 0 for th in _subbox.findAll("th", {"class": "color2"}): if th.text == 'Hits:': hits = int(th.parent.find("td").text) if th.text == 'Idioma:': lang = th.parent.find("td").find("img").get('src') if 'brazil' in lang.lower(): lang = Language.fromopensubtitles('pob') elif 'portugal' in lang.lower(): lang = Language.fromopensubtitles('por') else: continue # get description for matches description = _subbox.find("td", {"class": "td_desc brd_up"}).get_text() #get subtitle link download = _subbox.find("a", {"class": "sub_download"}) # sometimes BSoup can't find 'a' tag and returns None. try: download_link = self.download_link.format(link=download.get('href')) logger.debug("Legendasdivx.pt :: Found subtitle link on: %s " % download_link) except: logger.debug("Legendasdivx.pt :: Couldn't find download link. Trying next...") continue # get subtitle uploader sub_header = _subbox.find("div", {"class" :"sub_header"}) uploader = sub_header.find("a").text if sub_header else 'anonymous' exact_match = False if video.name.lower() in description.lower(): exact_match = True data = {'link': download_link, 'exact_match': exact_match, 'hits': hits, 'uploader': uploader, 'video_filename': video_filename, 'description': description } subtitles.append( LegendasdivxSubtitle(lang, video, data) ) return subtitles def query(self, video, languages): video_filename = video.name video_filename = os.path.basename(video_filename) video_filename, _ = os.path.splitext(video_filename) video_filename = sanitize_release_group(video_filename) _searchurl = self.searchurl if video.imdb_id is None: if isinstance(video, Episode): querytext = "{} S{:02d}E{:02d}".format(video.series, video.season, video.episode) elif isinstance(video, Movie): querytext = video.title else: querytext = video.imdb_id # language query filter if isinstance(languages, (tuple, list, set)): language_ids = ','.join(sorted(l.opensubtitles for l in languages)) if 'por' in language_ids: # prioritize portuguese subtitles lang_filter = '&form_cat=28' # pt elif 'pob' in language_ids: lang_filter = '&form_cat=29' # br else: lang_filter = '' querytext = querytext + lang_filter if lang_filter else querytext self.headers['Referer'] = self.site + '/index.php' self.session.headers.update(self.headers.items()) res = self.session.get(_searchurl.format(query=querytext)) if (res and "A legenda não foi encontrada" in res.text): logger.warning('Legendasdivx.pt :: %s not found', querytext) return [] bsoup = ParserBeautifulSoup(res.content, ['html.parser']) # search for more than 10 results (legendasdivx uses pagination) # don't throttle - maximum results = 6 * 10 MAX_PAGES = 6 #get number of pages bases on results found page_header = bsoup.find("div", {"class": "pager_bar"}) results_found = re.search(r'\((.*?) encontradas\)', page_header.text).group(1) if page_header else 0 logger.debug("Legendasdivx.pt :: Found %s subtitles" % str(results_found)) num_pages = (int(results_found) // 10) + 1 num_pages = min(MAX_PAGES, num_pages) # process first page subtitles = self._process_page(video, bsoup, video_filename) # more pages? if num_pages > 1: for num_page in range(2, num_pages+1): _search_next = self.searchurl.format(query=querytext) + "&page={0}".format(str(num_page)) logger.debug("Legendasdivx.pt :: Moving on to next page: %s" % _search_next) res = self.session.get(_search_next) next_page = ParserBeautifulSoup(res.content, ['html.parser']) subs = self._process_page(video, next_page, video_filename) subtitles.extend(subs) return subtitles def list_subtitles(self, video, languages): return self.query(video, languages) def download_subtitle(self, subtitle): res = self.session.get(subtitle.page_link) if res: try: res.raise_for_status() if 'limite' in res.text.lower(): # daily downloads limit reached logger.error("LegendasDivx.pt :: Daily download limit reached!") raise DownloadLimitReached("Legendasdivx.pt :: Daily download limit reached!") except HTTPError as e: if res.status_code == "403": logger.error("LegendasDivx.pt :: Your IP is blocked on this server.") raise IPAddressBlocked("LegendasDivx.pt :: Your IP is blocked on this server.") else: logger.error("Legendasdivx.pt :: HTTP Error %s" % e) raise TooManyRequests("Legendasdivx.pt :: HTTP Error %s" % e) except Exception as e: logger.error("LegendasDivx.pt :: Uncaught error: %r" % e) raise ServiceUnavailable("LegendasDivx.pt :: Uncaught error: %r" % e) archive = self._get_archive(res.content) # extract the subtitle subtitle_content = self._get_subtitle_from_archive(archive, subtitle) subtitle.content = fix_line_ending(subtitle_content) subtitle.normalize() return subtitle logger.error("Legendasdivx.pt :: there was a problem retrieving subtitle (status %s)" % res.status_code) return def _get_archive(self, content): # open the archive # stole^H^H^H^H^H inspired from subvix provider archive_stream = io.BytesIO(content) 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) else: raise Exception('Unsupported compressed format') return archive def _get_subtitle_from_archive(self, archive, subtitle): # some files have a non subtitle with .txt extension _tmp = list(SUBTITLE_EXTENSIONS) _tmp.remove('.txt') _subtitle_extensions = tuple(_tmp) _max_score = 0 _scores = get_scores(subtitle.video) 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 _guess = guessit (name) if isinstance(subtitle.video, Episode): logger.debug ("guessing %s" % name) logger.debug("subtitle S{}E{} video S{}E{}".format(_guess['season'],_guess['episode'],subtitle.video.season,subtitle.video.episode)) if subtitle.video.episode != _guess['episode'] or subtitle.video.season != _guess['season']: logger.debug('subtitle does not match video, skipping') continue matches = set() matches |= guess_matches (subtitle.video, _guess) logger.debug('srt matches: %s' % matches) _score = sum ((_scores.get (match, 0) for match in matches)) if _score > _max_score: _max_name = name _max_score = _score logger.debug("new max: {} {}".format(name, _score)) if _max_score > 0: logger.debug("returning from archive: {} scored {}".format(_max_name, _max_score)) return archive.read(_max_name) raise ValueError("No subtitle found on compressed file. Max score was 0")