diff --git a/README.md b/README.md index 03fe24a62..077eeecbf 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,10 @@ If you need something that is not already part of Bazarr, feel free to create a * Argenteam * Assrt * BetaSeries +* BSPlayer * GreekSubtitles * Hosszupuska +* LegendasDivx * LegendasTV * Napiprojekt * Napisy24 diff --git a/bazarr.py b/bazarr.py index ae5d88f13..f9228e2be 100644 --- a/bazarr.py +++ b/bazarr.py @@ -11,6 +11,7 @@ import os import sys import platform import re +import signal from bazarr.get_args import args @@ -39,15 +40,97 @@ check_python_version() dir_name = os.path.dirname(__file__) -def start_bazarr(): +class ProcessRegistry: + + def register(self, process): + pass + + def unregister(self, process): + pass + + +class DaemonStatus(ProcessRegistry): + + def __init__(self): + self.__should_stop = False + self.__processes = set() + + def register(self, process): + self.__processes.add(process) + + def unregister(self, process): + self.__processes.remove(process) + + ''' + Waits all the provided processes for the specified amount of time in seconds. + ''' + @staticmethod + def __wait_for_processes(processes, timeout): + reference_ts = time.time() + elapsed = 0 + remaining_processes = list(processes) + while elapsed < timeout and len(remaining_processes) > 0: + remaining_time = timeout - elapsed + for ep in list(remaining_processes): + if ep.poll() is not None: + remaining_processes.remove(ep) + else: + if remaining_time > 0: + if PY3: + try: + ep.wait(remaining_time) + remaining_processes.remove(ep) + except sp.TimeoutExpired: + pass + else: + ''' + In python 2 there is no such thing as some mechanism to wait with a timeout. + ''' + time.sleep(1) + elapsed = time.time() - reference_ts + remaining_time = timeout - elapsed + return remaining_processes + + ''' + Sends to every single of the specified processes the given signal and (if live_processes is not None) append to it processes which are still alive. + ''' + @staticmethod + def __send_signal(processes, signal_no, live_processes=None): + for ep in processes: + if ep.poll() is None: + if live_processes is not None: + live_processes.append(ep) + try: + ep.send_signal(signal_no) + except Exception as e: + print('Failed sending signal %s to process %s because of an unexpected error: %s' % (signal_no, ep.pid, e)) + return live_processes + + ''' + Flags this instance as should stop and terminates as smoothly as possible children processes. + ''' + def stop(self): + self.__should_stop = True + live_processes = DaemonStatus.__send_signal(self.__processes, signal.SIGINT, list()) + live_processes = DaemonStatus.__wait_for_processes(live_processes, 120) + DaemonStatus.__send_signal(live_processes, signal.SIGTERM) + + def should_stop(self): + return self.__should_stop + + +def start_bazarr(process_registry=ProcessRegistry()): script = [sys.executable, "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:] ep = sp.Popen(script, stdout=sp.PIPE, stderr=sp.STDOUT, stdin=sp.PIPE) + process_registry.register(ep) print("Bazarr starting...") try: while True: line = ep.stdout.readline() if line == '' or not line: + # Process ended so let's unregister it + process_registry.unregister(ep) break if PY3: sys.stdout.buffer.write(line) @@ -73,7 +156,7 @@ if __name__ == '__main__': pass - def daemon(): + def daemon(bazarr_runner = lambda: start_bazarr()): if os.path.exists(stopfile): try: os.remove(stopfile) @@ -89,12 +172,30 @@ if __name__ == '__main__': except: print('Unable to delete restart file.') else: - start_bazarr() + bazarr_runner() - start_bazarr() + bazarr_runner = lambda: start_bazarr() - # Keep the script running forever. - while True: - daemon() + should_stop = lambda: False + + if PY3: + daemonStatus = DaemonStatus() + + def shutdown(): + # indicates that everything should stop + daemonStatus.stop() + # emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break") + os.kill(os.getpid(), signal.SIGINT) + + signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown()) + + should_stop = lambda: daemonStatus.should_stop() + bazarr_runner = lambda: start_bazarr(daemonStatus) + + bazarr_runner() + + # Keep the script running forever until stop is requested through term or keyboard interrupt + while not should_stop(): + daemon(bazarr_runner) time.sleep(1) diff --git a/bazarr/config.py b/bazarr/config.py index ca61871e0..b64040ebf 100644 --- a/bazarr/config.py +++ b/bazarr/config.py @@ -102,6 +102,10 @@ defaults = { 'password': '', 'random_agents': 'True' }, + 'legendasdivx': { + 'username': '', + 'password': '' + }, 'legendastv': { 'username': '', 'password': '' diff --git a/bazarr/get_providers.py b/bazarr/get_providers.py index 3e1507138..90f28a1f8 100644 --- a/bazarr/get_providers.py +++ b/bazarr/get_providers.py @@ -38,7 +38,7 @@ PROVIDER_THROTTLE_MAP = { } } -PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendastv", "napiprojekt", "shooter", "hosszupuska", +PROVIDERS_FORCED_OFF = ["addic7ed", "tvsubtitles", "legendasdivx", "legendastv", "napiprojekt", "shooter", "hosszupuska", "supersubtitles", "titlovi", "titrari", "argenteam", "assrt", "subscene"] throttle_count = {} @@ -114,6 +114,9 @@ def get_providers_auth(): 'password': settings.subscene.password, 'only_foreign': False, # fixme }, + 'legendasdivx': {'username': settings.legendasdivx.username, + 'password': settings.legendasdivx.password, + }, 'legendastv': {'username': settings.legendastv.username, 'password': settings.legendastv.password, }, diff --git a/bazarr/get_series.py b/bazarr/get_series.py index c909dda09..d34a509b5 100644 --- a/bazarr/get_series.py +++ b/bazarr/get_series.py @@ -62,19 +62,21 @@ def update_series(): seriesListLength = len(r.json()) for i, show in enumerate(r.json(), 1): notifications.write(msg="Getting series data from Sonarr...", queue='get_series', item=i, length=seriesListLength) - try: - overview = six.text_type(show['overview']) - except: - overview = "" - try: - poster_big = show['images'][2]['url'].split('?')[0] - poster = os.path.splitext(poster_big)[0] + '-250' + os.path.splitext(poster_big)[1] - except: - poster = "" - try: - fanart = show['images'][0]['url'].split('?')[0] - except: - fanart = "" + + if 'overview' in show: + overview = show['overview'] + else: + overview = '' + + poster = '' + fanart = '' + for image in show['images']: + if image['coverType'] == 'poster': + poster_big = image['url'].split('?')[0] + poster = os.path.splitext(poster_big)[0] + '-250' + os.path.splitext(poster_big)[1] + + if image['coverType'] == 'fanart': + fanart = image['url'].split('?')[0] if show['alternateTitles'] != None: alternateTitles = str([item['title'] for item in show['alternateTitles']]) @@ -84,18 +86,18 @@ def update_series(): # Add shows in Sonarr to current shows list current_shows_sonarr.append(show['id']) - if show['tvdbId'] in current_shows_db_list: - series_to_update.append({'title': six.text_type(show["title"]), - 'path': six.text_type(show["path"]), + if show['id'] in current_shows_db_list: + series_to_update.append({'title': show["title"], + 'path': show["path"], 'tvdbId': int(show["tvdbId"]), 'sonarrSeriesId': int(show["id"]), - 'overview': six.text_type(overview), - 'poster': six.text_type(poster), - 'fanart': six.text_type(fanart), - 'audio_language': six.text_type(profile_id_to_language((show['qualityProfileId'] if get_sonarr_version().startswith('2') else show['languageProfileId']), audio_profiles)), - 'sortTitle': six.text_type(show['sortTitle']), - 'year': six.text_type(show['year']), - 'alternateTitles': six.text_type(alternateTitles)}) + 'overview': overview, + 'poster': poster, + 'fanart': fanart, + 'audio_language': profile_id_to_language((show['qualityProfileId'] if get_sonarr_version().startswith('2') else show['languageProfileId']), audio_profiles), + 'sortTitle': show['sortTitle'], + 'year': show['year'], + 'alternateTitles': alternateTitles}) else: if serie_default_enabled is True: series_to_add.append({'title': show["title"], diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index 81d2e8a6e..698afd853 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -40,6 +40,7 @@ from analytics import track_event import six from six.moves import range from functools import reduce +from locale import getpreferredencoding def get_video(path, title, sceneName, use_scenename, providers=None, media_type="movie"): @@ -234,34 +235,7 @@ def download_subtitle(path, language, hi, forced, providers, providers_auth, sce command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, downloaded_language_code2, downloaded_language_code3, subtitle.language.forced) - try: - if os.name == 'nt': - codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - # wait for the process to terminate - out_codepage, err_codepage = codepage.communicate() - encoding = out_codepage.split(':')[-1].strip() - - process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - # wait for the process to terminate - out, err = process.communicate() - - if os.name == 'nt': - out = out.decode(encoding) - - except: - if out == "": - logging.error( - 'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution') - else: - logging.error('BAZARR Post-processing result for file ' + path + ' : ' + out) - else: - if out == "": - logging.info( - 'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution') - else: - logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out) + postprocessing(command, path) # fixme: support multiple languages at once if media_type == 'series': @@ -459,34 +433,7 @@ def manual_download_subtitle(path, language, hi, forced, subtitle, provider, pro command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, downloaded_language_code2, downloaded_language_code3, subtitle.language.forced) - try: - if os.name == 'nt': - codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - # wait for the process to terminate - out_codepage, err_codepage = codepage.communicate() - encoding = out_codepage.split(':')[-1].strip() - - process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - # wait for the process to terminate - out, err = process.communicate() - - if os.name == 'nt': - out = out.decode(encoding) - - except: - if out == "": - logging.error( - 'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution') - else: - logging.error('BAZARR Post-processing result for file ' + path + ' : ' + out) - else: - if out == "": - logging.info( - 'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution') - else: - logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out) + postprocessing(command, path) if media_type == 'series': reversed_path = path_replace_reverse(path) @@ -1173,3 +1120,41 @@ def upgrade_subtitles(): store_subtitles_movie(movie['video_path'], path_replace_movie(movie['video_path'])) history_log_movie(3, movie['radarrId'], message, path, language_code, provider, score) send_notifications_movie(movie['radarrId'], message) + + +def postprocessing(command, path): + try: + encoding = getpreferredencoding() + if os.name == 'nt': + if six.PY3: + codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, encoding=getpreferredencoding()) + else: + codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + # wait for the process to terminate + out_codepage, err_codepage = codepage.communicate() + encoding = out_codepage.split(':')[-1].strip() + + if six.PY3: + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, encoding=encoding) + else: + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + # wait for the process to terminate + out, err = process.communicate() + + if six.PY2: + out = out.decode(encoding) + + out = out.replace('\n', ' ').replace('\r', ' ') + + except Exception as e: + logging.error('BAZARR Post-processing failed for file ' + path + ' : ' + repr(e)) + else: + if out == "": + logging.info( + 'BAZARR Post-processing result for file ' + path + ' : Nothing returned from command execution') + else: + logging.info('BAZARR Post-processing result for file ' + path + ' : ' + out) diff --git a/bazarr/main.py b/bazarr/main.py index 696bc6f68..2115e2976 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -1,6 +1,6 @@ # coding=utf-8 -bazarr_version = '0.8.4' +bazarr_version = '0.8.4.1' import os os.environ["SZ_USER_AGENT"] = "Bazarr/1" @@ -398,6 +398,8 @@ def save_wizard(): settings.addic7ed.password = request.forms.get('settings_addic7ed_password') settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents) settings.assrt.token = request.forms.get('settings_assrt_token') + settings.legendasdivx.username = request.forms.get('settings_legendasdivx_username') + settings.legendasdivx.password = request.forms.get('settings_legendasdivx_password') settings.legendastv.username = request.forms.get('settings_legendastv_username') settings.legendastv.password = request.forms.get('settings_legendastv_password') settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username') @@ -1527,6 +1529,8 @@ def save_settings(): settings.addic7ed.password = request.forms.get('settings_addic7ed_password') settings.addic7ed.random_agents = text_type(settings_addic7ed_random_agents) settings.assrt.token = request.forms.get('settings_assrt_token') + settings.legendasdivx.username = request.forms.get('settings_legendasdivx_username') + settings.legendasdivx.password = request.forms.get('settings_legendasdivx_password') settings.legendastv.username = request.forms.get('settings_legendastv_username') settings.legendastv.password = request.forms.get('settings_legendastv_password') settings.opensubtitles.username = request.forms.get('settings_opensubtitles_username') @@ -1749,9 +1753,9 @@ def execute_task(taskid): @custom_auth_basic(check_credentials) def remove_subtitles(): authorize() - episodePath = request.forms.get('episodePath') + episodePath = request.forms.episodePath language = request.forms.get('language') - subtitlesPath = request.forms.get('subtitlesPath') + subtitlesPath = request.forms.subtitlesPath sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId') @@ -1768,9 +1772,9 @@ def remove_subtitles(): @custom_auth_basic(check_credentials) def remove_subtitles_movie(): authorize() - moviePath = request.forms.get('moviePath') + moviePath = request.forms.moviePath language = request.forms.get('language') - subtitlesPath = request.forms.get('subtitlesPath') + subtitlesPath = request.forms.subtitlesPath radarrId = request.forms.get('radarrId') try: @@ -1788,14 +1792,14 @@ def get_subtitle(): authorize() ref = request.environ['HTTP_REFERER'] - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') + episodePath = request.forms.episodePath + sceneName = request.forms.sceneName language = request.forms.get('language') hi = request.forms.get('hi') forced = request.forms.get('forced') sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId') - title = request.forms.get('title') + title = request.forms.title providers_list = get_providers() providers_auth = get_providers_auth() @@ -1823,12 +1827,12 @@ def get_subtitle(): def manual_search_json(): authorize() - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') + episodePath = request.forms.episodePath + sceneName = request.forms.sceneName language = request.forms.get('language') hi = request.forms.get('hi') forced = request.forms.get('forced') - title = request.forms.get('title') + title = request.forms.title providers_list = get_providers() providers_auth = get_providers_auth() @@ -1843,16 +1847,16 @@ def manual_get_subtitle(): authorize() ref = request.environ['HTTP_REFERER'] - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') + episodePath = request.forms.episodePath + sceneName = request.forms.sceneName language = request.forms.get('language') hi = request.forms.get('hi') forced = request.forms.get('forced') selected_provider = request.forms.get('provider') - subtitle = request.forms.get('subtitle') + subtitle = request.forms.subtitle sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId') - title = request.forms.get('title') + title = request.forms.title providers_auth = get_providers_auth() @@ -1881,14 +1885,14 @@ def perform_manual_upload_subtitle(): authorize() ref = request.environ['HTTP_REFERER'] - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') + episodePath = request.forms.episodePath + sceneName = request.forms.sceneName language = request.forms.get('language') forced = True if request.forms.get('forced') == '1' else False upload = request.files.get('upload') sonarrSeriesId = request.forms.get('sonarrSeriesId') sonarrEpisodeId = request.forms.get('sonarrEpisodeId') - title = request.forms.get('title') + title = request.forms.title _, ext = os.path.splitext(upload.filename) @@ -1925,13 +1929,13 @@ def get_subtitle_movie(): authorize() ref = request.environ['HTTP_REFERER'] - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') + moviePath = request.forms.moviePath + sceneName = request.forms.sceneName language = request.forms.get('language') hi = request.forms.get('hi') forced = request.forms.get('forced') radarrId = request.forms.get('radarrId') - title = request.forms.get('title') + title = request.forms.title providers_list = get_providers() providers_auth = get_providers_auth() @@ -1959,12 +1963,12 @@ def get_subtitle_movie(): def manual_search_movie_json(): authorize() - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') + moviePath = request.forms.moviePath + sceneName = request.forms.sceneName language = request.forms.get('language') hi = request.forms.get('hi') forced = request.forms.get('forced') - title = request.forms.get('title') + title = request.forms.title providers_list = get_providers() providers_auth = get_providers_auth() @@ -1979,15 +1983,15 @@ def manual_get_subtitle_movie(): authorize() ref = request.environ['HTTP_REFERER'] - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') + moviePath = request.forms.moviePath + sceneName = request.forms.sceneName language = request.forms.get('language') hi = request.forms.get('hi') forced = request.forms.get('forced') - selected_provider = request.forms.get('provider') - subtitle = request.forms.get('subtitle') + selected_provider = request.forms.provider + subtitle = request.forms.subtitle radarrId = request.forms.get('radarrId') - title = request.forms.get('title') + title = request.forms.title providers_auth = get_providers_auth() @@ -2015,13 +2019,13 @@ def perform_manual_upload_subtitle_movie(): authorize() ref = request.environ['HTTP_REFERER'] - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') + moviePath = request.forms.moviePath + sceneName = request.forms.sceneName language = request.forms.get('language') forced = True if request.forms.get('forced') == '1' else False upload = request.files.get('upload') radarrId = request.forms.get('radarrId') - title = request.forms.get('title') + title = request.forms.title _, ext = os.path.splitext(upload.filename) diff --git a/libs/subliminal_patch/core.py b/libs/subliminal_patch/core.py index d4685c289..38a603eed 100644 --- a/libs/subliminal_patch/core.py +++ b/libs/subliminal_patch/core.py @@ -543,6 +543,10 @@ def scan_video(path, dont_use_actual_file=False, hints=None, providers=None, ski if video.size > 10485760: logger.debug('Size is %d', video.size) osub_hash = None + + if "bsplayer" in providers: + video.hashes['bsplayer'] = osub_hash = hash_opensubtitles(hash_path) + if "opensubtitles" in providers: video.hashes['opensubtitles'] = osub_hash = hash_opensubtitles(hash_path) diff --git a/libs/subliminal_patch/providers/bsplayer.py b/libs/subliminal_patch/providers/bsplayer.py new file mode 100644 index 000000000..9839a0331 --- /dev/null +++ b/libs/subliminal_patch/providers/bsplayer.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +import logging +import io +import os + +from requests import Session +from guessit import guessit +from subliminal_patch.providers import Provider +from subliminal_patch.subtitle import Subtitle +from subliminal.utils import sanitize_release_group +from subliminal.subtitle import guess_matches +from subzero.language import Language + +import gzip +import random +from time import sleep +from xml.etree import ElementTree + +logger = logging.getLogger(__name__) + +class BSPlayerSubtitle(Subtitle): + """BSPlayer Subtitle.""" + provider_name = 'bsplayer' + + def __init__(self, language, filename, subtype, video, link): + super(BSPlayerSubtitle, self).__init__(language) + self.language = language + self.filename = filename + self.page_link = link + self.subtype = subtype + self.video = video + + @property + def id(self): + return self.page_link + + @property + def release_info(self): + return self.filename + + def get_matches(self, video): + matches = set() + + 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) + + subtitle_filename = self.filename + subtitle_filename = os.path.basename(subtitle_filename) + subtitle_filename, _ = os.path.splitext(subtitle_filename) + subtitle_filename = sanitize_release_group(subtitle_filename) + + + matches |= guess_matches(video, guessit(self.filename)) + + matches.add(id(self)) + matches.add('hash') + + return matches + + + +class BSPlayerProvider(Provider): + """BSPlayer Provider.""" + languages = {Language('por', 'BR')} | {Language(l) for l in [ + 'ara', 'bul', 'ces', 'dan', 'deu', 'ell', 'eng', 'fin', 'fra', 'hun', 'ita', 'jpn', 'kor', 'nld', 'pol', 'por', + 'ron', 'rus', 'spa', 'swe', 'tur', 'ukr', 'zho' + ]} + SEARCH_THROTTLE = 8 + + # batantly based on kodi's bsplayer plugin + # also took from BSPlayer-Subtitles-Downloader + def __init__(self): + self.initialize() + + def initialize(self): + self.session = Session() + self.search_url = self.get_sub_domain() + self.token = None + self.login() + + def terminate(self): + self.session.close() + self.logout() + + def api_request(self, func_name='logIn', params='', tries=5): + headers = { + 'User-Agent': 'BSPlayer/2.x (1022.12360)', + 'Content-Type': 'text/xml; charset=utf-8', + 'Connection': 'close', + 'SOAPAction': '"http://api.bsplayer-subtitles.com/v1.php#{func_name}"'.format(func_name=func_name) + } + data = ( + '\n' + '' + '' + '{params}' + ).format(search_url=self.search_url, func_name=func_name, params=params) + logger.info('Sending request: %s.' % func_name) + for i in iter(range(tries)): + try: + self.session.headers.update(headers.items()) + res = self.session.post(self.search_url, data) + return ElementTree.fromstring(res.text) + + ### with requests + # res = requests.post( + # url=self.search_url, + # data=data, + # headers=headers + # ) + # return ElementTree.fromstring(res.text) + + except Exception as ex: + logger.info("ERROR: %s." % ex) + if func_name == 'logIn': + self.search_url = self.get_sub_domain() + sleep(1) + logger.info('ERROR: Too many tries (%d)...' % tries) + raise Exception('Too many tries...') + + def login(self): + # If already logged in + if self.token: + return True + + root = self.api_request( + func_name='logIn', + params=('' + '' + 'BSPlayer v2.67') + ) + res = root.find('.//return') + if res.find('status').text == 'OK': + self.token = res.find('data').text + logger.info("Logged In Successfully.") + return True + return False + + def logout(self): + # If already logged out / not logged in + if not self.token: + return True + + root = self.api_request( + func_name='logOut', + params='{token}'.format(token=self.token) + ) + res = root.find('.//return') + self.token = None + if res.find('status').text == 'OK': + logger.info("Logged Out Successfully.") + return True + return False + + def query(self, video, video_hash, language): + if not self.login(): + return [] + + if isinstance(language, (tuple, list, set)): + # language_ids = ",".join(language) + # language_ids = 'spa' + language_ids = ','.join(sorted(l.opensubtitles for l in language)) + + + if video.imdb_id is None: + imdbId = '*' + else: + imdbId = video.imdb_id + sleep(self.SEARCH_THROTTLE) + root = self.api_request( + func_name='searchSubtitles', + params=( + '{token}' + '{movie_hash}' + '{movie_size}' + '{language_ids}' + '{imdbId}' + ).format(token=self.token, movie_hash=video_hash, + movie_size=video.size, language_ids=language_ids, imdbId=imdbId) + ) + res = root.find('.//return/result') + if res.find('status').text != 'OK': + return [] + + items = root.findall('.//return/data/item') + subtitles = [] + if items: + logger.info("Subtitles Found.") + for item in items: + subID=item.find('subID').text + subDownloadLink=item.find('subDownloadLink').text + subLang= Language.fromopensubtitles(item.find('subLang').text) + subName=item.find('subName').text + subFormat=item.find('subFormat').text + subtitles.append( + BSPlayerSubtitle(subLang,subName, subFormat, video, subDownloadLink) + ) + return subtitles + + def list_subtitles(self, video, languages): + return self.query(video, video.hashes['bsplayer'], languages) + + def get_sub_domain(self): + # s1-9, s101-109 + SUB_DOMAINS = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', + 's101', 's102', 's103', 's104', 's105', 's106', 's107', 's108', 's109'] + API_URL_TEMPLATE = "http://{sub_domain}.api.bsplayer-subtitles.com/v1.php" + sub_domains_end = len(SUB_DOMAINS) - 1 + return API_URL_TEMPLATE.format(sub_domain=SUB_DOMAINS[random.randint(0, sub_domains_end)]) + + def download_subtitle(self, subtitle): + session = Session() + _addheaders = { + 'User-Agent': 'Mozilla/4.0 (compatible; Synapse)' + } + session.headers.update(_addheaders) + res = session.get(subtitle.page_link) + if res: + if res.text == '500': + raise ValueError('Error 500 on server') + + with gzip.GzipFile(fileobj=io.BytesIO(res.content)) as gf: + subtitle.content = gf.read() + subtitle.normalize() + + return subtitle + raise ValueError('Problems conecting to the server') + + diff --git a/libs/subliminal_patch/providers/legendasdivx.py b/libs/subliminal_patch/providers/legendasdivx.py new file mode 100644 index 000000000..5537e5828 --- /dev/null +++ b/libs/subliminal_patch/providers/legendasdivx.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +import logging +import io +import os +import rarfile +import zipfile + +from requests import Session +from guessit import guessit +from subliminal_patch.exceptions import ParseResponseError +from subliminal_patch.providers import Provider +from subliminal.providers import ParserBeautifulSoup +from subliminal_patch.subtitle import Subtitle +from subliminal.video import Episode +from subliminal.subtitle import SUBTITLE_EXTENSIONS, fix_line_ending,guess_matches +from subzero.language import Language + +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'].lower() + self.video = video + self.videoname =data['videoname'] + + @property + def id(self): + return self.page_link + + @property + def release_info(self): + return self.description + + def get_matches(self, video): + matches = set() + + if self.videoname.lower() in self.description: + matches.update(['title']) + matches.update(['season']) + matches.update(['episode']) + + # episode + if video.title and video.title.lower() in self.description: + matches.update(['title']) + if video.year and '{:04d}'.format(video.year) in self.description: + matches.update(['year']) + + if isinstance(video, Episode): + # already matched in search query + if video.season and 's{:02d}'.format(video.season) in self.description: + matches.update(['season']) + if video.episode and 'e{:02d}'.format(video.episode) in self.description: + matches.update(['episode']) + if video.episode and video.season and video.series: + if '{}.s{:02d}e{:02d}'.format(video.series.lower(),video.season,video.episode) in self.description: + matches.update(['series']) + matches.update(['season']) + matches.update(['episode']) + if '{} s{:02d}e{:02d}'.format(video.series.lower(),video.season,video.episode) in self.description: + matches.update(['series']) + matches.update(['season']) + matches.update(['episode']) + + # release_group + if video.release_group and video.release_group.lower() in self.description: + matches.update(['release_group']) + + # resolution + + if video.resolution and video.resolution.lower() in self.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.lower() in self.description: + matches.update(['format']) + break + + # video_codec + if video.video_codec: + video_codecs = [video.video_codec.lower()] + if video_codecs[0] == "h264": + formats.append("x264") + elif video_codecs[0] == "h265": + formats.append("x265") + for vc in formats: + if vc.lower() in self.description: + matches.update(['video_codec']) + break + + 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': 'Mozilla/5.0 (X11; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0', + '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' + searchurl = site + '/modules.php?name=Downloads&file=jz&d_op=search&op=_jz00&query={query}' + language_list = list(languages) + + + def __init__(self, username, password): + self.username = username + self.password = password + + def initialize(self): + self.session = Session() + self.login() + + def terminate(self): + self.logout() + self.session.close() + + def login(self): + logger.info('Logging in') + self.headers['Referer'] = self.site + '/index.php' + self.session.headers.update(self.headers.items()) + res = self.session.get(self.loginpage) + bsoup = ParserBeautifulSoup(res.content, ['lxml']) + + _allinputs = bsoup.findAll('input') + fields = {} + for field in _allinputs: + fields[field.get('name')] = field.get('value') + + fields['username'] = self.username + fields['password'] = self.password + fields['autologin'] = 'on' + fields['viewonline'] = 'on' + + self.headers['Referer'] = self.loginpage + self.session.headers.update(self.headers.items()) + res = self.session.post(self.loginpage, fields) + try: + logger.debug('Got session id %s' % + self.session.cookies.get_dict()['PHPSESSID']) + except KeyError as e: + logger.error(repr(e)) + logger.error("Didn't get session id, check your credentials") + return False + except Exception as e: + logger.error(repr(e)) + logger.error('uncached error #legendasdivx #AA') + return False + + return True + def logout(self): + # need to figure this out + return True + + def query(self, video, language): + try: + logger.debug('Got session id %s' % + self.session.cookies.get_dict()['PHPSESSID']) + except Exception as e: + self.login() + return [] + + language_ids = '0' + if isinstance(language, (tuple, list, set)): + if len(language) == 1: + language_ids = ','.join(sorted(l.opensubtitles for l in language)) + if language_ids == 'por': + language_ids = '&form_cat=28' + else: + language_ids = '&form_cat=29' + + querytext = video.name + querytext = os.path.basename(querytext) + querytext, _ = os.path.splitext(querytext) + videoname = querytext + querytext = querytext.lower() + querytext = querytext.replace( + ".", "+").replace("[", "").replace("]", "") + if language_ids != '0': + querytext = querytext + language_ids + self.headers['Referer'] = self.site + '/index.php' + self.session.headers.update(self.headers.items()) + res = self.session.get(self.searchurl.format(query=querytext)) + # form_cat=28 = br + # form_cat=29 = pt + if "A legenda não foi encontrada" in res.text: + logger.warning('%s not found', querytext) + return [] + + bsoup = ParserBeautifulSoup(res.content, ['html.parser']) + _allsubs = bsoup.findAll("div", {"class": "sub_box"}) + subtitles = [] + lang = Language.fromopensubtitles("pob") + for _subbox in _allsubs: + hits=0 + for th in _subbox.findAll("th", {"class": "color2"}): + if th.string == 'Hits:': + hits = int(th.parent.find("td").string) + if th.string == 'Idioma:': + lang = th.parent.find("td").find ("img").get ('src') + if 'brazil' in lang: + lang = Language.fromopensubtitles('pob') + else: + lang = Language.fromopensubtitles('por') + + + description = _subbox.find("td", {"class": "td_desc brd_up"}) + download = _subbox.find("a", {"class": "sub_download"}) + try: + # sometimes BSoup just doesn't get the link + logger.debug(download.get('href')) + except Exception as e: + logger.warning('skipping subbox on %s' % self.searchurl.format(query=querytext)) + continue + + exact_match = False + if video.name.lower() in description.get_text().lower(): + exact_match = True + data = {'link': self.site + '/modules.php' + download.get('href'), + 'exact_match': exact_match, + 'hits': hits, + 'videoname': videoname, + 'description': description.get_text() } + subtitles.append( + LegendasdivxSubtitle(lang, video, data) + ) + + 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: + if res.text == '500': + raise ValueError('Error 500 on server') + + archive = self._get_archive(res.content) + # extract the subtitle + subtitle_content = self._get_subtitle_from_archive(archive) + subtitle.content = fix_line_ending(subtitle_content) + subtitle.normalize() + + return subtitle + raise ValueError('Problems conecting to the server') + + 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 ParseResponseError('Unsupported compressed format') + raise Exception('Unsupported compressed format') + + return archive + + def _get_subtitle_from_archive(self, archive): + # some files have a non subtitle with .txt extension + _tmp = list(SUBTITLE_EXTENSIONS) + _tmp.remove('.txt') + _subtitle_extensions = tuple(_tmp) + + 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 + + logger.debug("returning from archive: %s" % name) + return archive.read(name) + + raise ParseResponseError('Can not find the subtitle in the compressed file') diff --git a/libs/subliminal_patch/providers/subdivx.py b/libs/subliminal_patch/providers/subdivx.py index ab7db33b6..74b3c4f7b 100644 --- a/libs/subliminal_patch/providers/subdivx.py +++ b/libs/subliminal_patch/providers/subdivx.py @@ -35,6 +35,10 @@ class SubdivxSubtitle(Subtitle): def id(self): return self.page_link + @property + def release_info(self): + return self.description + def get_matches(self, video): matches = set() diff --git a/libs/subliminal_patch/subtitle.py b/libs/subliminal_patch/subtitle.py index 8116697bf..43e9a9716 100644 --- a/libs/subliminal_patch/subtitle.py +++ b/libs/subliminal_patch/subtitle.py @@ -358,6 +358,15 @@ class ModifiedSubtitle(Subtitle): id = None +MERGED_FORMATS = { + "TV": ("HDTV", "SDTV", "AHDTV", "UHDTV"), + "Air": ("SATRip", "DVB", "PPV"), + "Disk": ("DVD", "HD-DVD", "BluRay") +} + +MERGED_FORMATS_REV = dict((v.lower(), k.lower()) for k in MERGED_FORMATS for v in MERGED_FORMATS[k]) + + def guess_matches(video, guess, partial=False): """Get matches between a `video` and a `guess`. @@ -386,12 +395,15 @@ def guess_matches(video, guess, partial=False): for title in titles: if sanitize(title) in (sanitize(name) for name in [video.series] + video.alternative_series): matches.add('series') + # title if video.title and 'episode_title' in guess and sanitize(guess['episode_title']) == sanitize(video.title): matches.add('title') + # season if video.season and 'season' in guess and guess['season'] == video.season: matches.add('season') + # episode # Currently we only have single-ep support (guessit returns a multi-ep as a list with int values) # Most providers only support single-ep, so make sure it contains only 1 episode @@ -401,12 +413,15 @@ def guess_matches(video, guess, partial=False): episode = min(episode_guess) if episode_guess and isinstance(episode_guess, list) else episode_guess if episode == video.episode: matches.add('episode') + # year if video.year and 'year' in guess and guess['year'] == video.year: matches.add('year') + # count "no year" as an information if not partial and video.original_series and 'year' not in guess: matches.add('year') + elif isinstance(video, Movie): # year if video.year and 'year' in guess and guess['year'] == video.year: @@ -440,21 +455,25 @@ def guess_matches(video, guess, partial=False): formats = [formats] if video.format: - video_format = video.format - if video_format in ("HDTV", "SDTV", "TV"): - video_format = "TV" - logger.debug("Treating HDTV/SDTV the same") + video_format = video.format.lower() + _video_gen_format = MERGED_FORMATS_REV.get(video_format) + if _video_gen_format: + logger.debug("Treating %s as %s the same", video_format, _video_gen_format) for frmt in formats: - if frmt in ("HDTV", "SDTV"): - frmt = "TV" + _guess_gen_frmt = MERGED_FORMATS_REV.get(frmt.lower()) - if frmt.lower() == video_format.lower(): + if _guess_gen_frmt == _video_gen_format: matches.add('format') break + if "release_group" in matches and "format" not in matches: + logger.info("Release group matched but format didn't. Remnoving release group match.") + matches.remove("release_group") + # video_codec if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec: matches.add('video_codec') + # audio_codec if video.audio_codec and 'audio_codec' in guess and guess['audio_codec'] == video.audio_codec: matches.add('audio_codec') diff --git a/libs/tzlocal/unix.py b/libs/tzlocal/unix.py index 388273c27..8574965a5 100644 --- a/libs/tzlocal/unix.py +++ b/libs/tzlocal/unix.py @@ -138,7 +138,7 @@ def _get_localzone(_root='/'): if os.path.exists(tzpath) and os.path.islink(tzpath): tzpath = os.path.realpath(tzpath) start = tzpath.find("/")+1 - while start is not 0: + while start != 0: tzpath = tzpath[start:] try: return pytz.timezone(tzpath) diff --git a/views/providers.tpl b/views/providers.tpl index 2eada8f3d..8d677ffda 100644 --- a/views/providers.tpl +++ b/views/providers.tpl @@ -144,6 +144,22 @@ +
+
+ +
+
+
+ + +
+
+
+ +
+ +
+
@@ -210,6 +226,47 @@
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+