diff --git a/README.md b/README.md index ef15757bf..418c33ac4 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 c543f930d..d345b79ff 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", "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..26d26f9bf 100644 --- a/bazarr/get_series.py +++ b/bazarr/get_series.py @@ -6,195 +6,193 @@ import os import requests import logging from queueconfig import notifications -from collections import OrderedDict -import datetime -from get_args import args from config import settings, url_sonarr from list_subtitles import list_missing_subtitles from database import database, dict_converter from utils import get_sonarr_version -import six from helper import path_replace def update_series(): notifications.write(msg="Update series list from Sonarr is running...", queue='get_series') apikey_sonarr = settings.sonarr.apikey + if apikey_sonarr is None: + return + sonarr_version = get_sonarr_version() serie_default_enabled = settings.general.getboolean('serie_default_enabled') serie_default_language = settings.general.serie_default_language serie_default_hi = settings.general.serie_default_hi serie_default_forced = settings.general.serie_default_forced - - if apikey_sonarr is None: - pass - else: - audio_profiles = get_profile_list() - - # Get shows data from Sonarr - url_sonarr_api_series = url_sonarr() + "/api/series?apikey=" + apikey_sonarr - try: - r = requests.get(url_sonarr_api_series, timeout=60, verify=False) - r.raise_for_status() - except requests.exceptions.HTTPError as errh: - logging.exception("BAZARR Error trying to get series from Sonarr. Http error.") - return - except requests.exceptions.ConnectionError as errc: - logging.exception("BAZARR Error trying to get series from Sonarr. Connection Error.") - return - except requests.exceptions.Timeout as errt: - logging.exception("BAZARR Error trying to get series from Sonarr. Timeout Error.") - return - except requests.exceptions.RequestException as err: - logging.exception("BAZARR Error trying to get series from Sonarr.") - return + audio_profiles = get_profile_list() + + # Get shows data from Sonarr + url_sonarr_api_series = url_sonarr() + "/api/series?apikey=" + apikey_sonarr + try: + r = requests.get(url_sonarr_api_series, timeout=60, verify=False) + r.raise_for_status() + except requests.exceptions.HTTPError: + logging.exception("BAZARR Error trying to get series from Sonarr. Http error.") + return + except requests.exceptions.ConnectionError: + logging.exception("BAZARR Error trying to get series from Sonarr. Connection Error.") + return + except requests.exceptions.Timeout: + logging.exception("BAZARR Error trying to get series from Sonarr. Timeout Error.") + return + except requests.exceptions.RequestException: + logging.exception("BAZARR Error trying to get series from Sonarr.") + return + + # Get current shows in DB + current_shows_db = database.execute("SELECT sonarrSeriesId FROM table_shows") + + current_shows_db_list = [x['sonarrSeriesId'] for x in current_shows_db] + current_shows_sonarr = [] + series_to_update = [] + series_to_add = [] + + series_list_length = 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=series_list_length) + + overview = show['overview'] if 'overview' in show else '' + 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] + + alternate_titles = None + if show['alternateTitles'] is not None: + alternate_titles = str([item['title'] for item in show['alternateTitles']]) + + if sonarr_version.startswith('2'): + audio_language = profile_id_to_language(show['qualityProfileId'], audio_profiles) else: - # Get current shows in DB - current_shows_db = database.execute("SELECT sonarrSeriesId FROM table_shows") - - current_shows_db_list = [x['sonarrSeriesId'] for x in current_shows_db] - current_shows_sonarr = [] - series_to_update = [] - series_to_add = [] - altered_series = [] + audio_language = profile_id_to_language(show['languageProfileId'], audio_profiles) - 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 = "" + # Add shows in Sonarr to current shows list + current_shows_sonarr.append(show['id']) - if show['alternateTitles'] != None: - alternateTitles = str([item['title'] for item in show['alternateTitles']]) - else: - alternateTitles = None + 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': overview, + 'poster': poster, + 'fanart': fanart, + 'audio_language': audio_language, + 'sortTitle': show['sortTitle'], + 'year': show['year'], + 'alternateTitles': alternate_titles}) + else: + if serie_default_enabled is True: + series_to_add.append({'title': show["title"], + 'path': show["path"], + 'tvdbId': show["tvdbId"], + 'languages': serie_default_language, + 'hearing_impaired': serie_default_hi, + 'sonarrSeriesId': show["id"], + 'overview': overview, + 'poster': poster, + 'fanart': fanart, + 'audio_language': audio_language, + 'sortTitle': show['sortTitle'], + 'year': show['year'], + 'alternateTitles': alternate_titles, + 'forced': serie_default_forced}) + else: + series_to_add.append({'title': show["title"], + 'path': show["path"], + 'tvdbId': show["tvdbId"], + 'sonarrSeriesId': show["id"], + 'overview': overview, + 'poster': poster, + 'fanart': fanart, + 'audio_language': audio_language, + 'sortTitle': show['sortTitle'], + 'year': show['year'], + 'alternateTitles': alternate_titles}) - # 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"]), - '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)}) - else: - if serie_default_enabled is True: - series_to_add.append({'title': show["title"], - 'path': show["path"], - 'tvdbId': show["tvdbId"], - 'languages': serie_default_language, - 'hearing_impaired': serie_default_hi, - 'sonarrSeriesId': show["id"], - 'overview': overview, - 'poster': poster, - 'fanart': fanart, - 'audio_language': profile_id_to_language((show['qualityProfileId'] if sonarr_version.startswith('2') else show['languageProfileId']), audio_profiles), - 'sortTitle': show['sortTitle'], - 'year': show['year'], - 'alternateTitles': alternateTitles, - 'forced': serie_default_forced}) - else: - series_to_add.append({'title': show["title"], - 'path': show["path"], - 'tvdbId': show["tvdbId"], - 'sonarrSeriesId': show["id"], - 'overview': overview, - 'poster': poster, - 'fanart': fanart, - 'audio_language': profile_id_to_language((show['qualityProfileId'] if sonarr_version.startswith('2') else show['languageProfileId']), audio_profiles), - 'sortTitle': show['sortTitle'], - 'year': show['year'], - 'alternateTitles': alternateTitles}) + # Remove old series from DB + removed_series = list(set(current_shows_db_list) - set(current_shows_sonarr)) - # Remove old series from DB - removed_series = list(set(current_shows_db_list) - set(current_shows_sonarr)) + for series in removed_series: + database.execute("DELETE FROM table_shows WHERE sonarrSEriesId=?", (series,)) - for series in removed_series: - database.execute("DELETE FROM table_shows WHERE sonarrSEriesId=?",(series,)) + # Update existing series in DB + series_in_db_list = [] + series_in_db = database.execute("SELECT title, path, tvdbId, sonarrSeriesId, overview, poster, fanart, " + "audio_language, sortTitle, year, alternateTitles FROM table_shows") - # Update existing series in DB - series_in_db_list = [] - series_in_db = database.execute("SELECT title, path, tvdbId, sonarrSeriesId, overview, poster, fanart, " - "audio_language, sortTitle, year, alternateTitles FROM table_shows") + for item in series_in_db: + series_in_db_list.append(item) - for item in series_in_db: - series_in_db_list.append(item) + series_to_update_list = [i for i in series_to_update if i not in series_in_db_list] - series_to_update_list = [i for i in series_to_update if i not in series_in_db_list] + for updated_series in series_to_update_list: + query = dict_converter.convert(updated_series) + database.execute('''UPDATE table_shows SET ''' + query.keys_update + ''' WHERE sonarrSeriesId = ?''', + query.values + (updated_series['sonarrSeriesId'],)) - for updated_series in series_to_update_list: - query = dict_converter.convert(updated_series) - database.execute('''UPDATE table_shows SET ''' + query.keys_update + ''' WHERE sonarrSeriesId = ?''', - query.values + (updated_series['sonarrSeriesId'],)) + # Insert new series in DB + for added_series in series_to_add: + query = dict_converter.convert(added_series) + result = database.execute( + '''INSERT OR IGNORE INTO table_shows(''' + query.keys_insert + ''') VALUES(''' + + query.question_marks + ''')''', query.values) + if result: + list_missing_subtitles(no=added_series['sonarrSeriesId']) + else: + logging.debug('BAZARR unable to insert this series into the database:', + path_replace(added_series['path'])) - # Insert new series in DB - for added_series in series_to_add: - query = dict_converter.convert(added_series) - result = database.execute( - '''INSERT OR IGNORE INTO table_shows(''' + query.keys_insert + ''') VALUES(''' + - query.question_marks + ''')''', query.values) - if result: - list_missing_subtitles(no=added_series['sonarrSeriesId']) - else: - logging.debug('BAZARR unable to insert this series into the database:', - path_replace(added_series['path'])) - - logging.debug('BAZARR All series synced from Sonarr into database.') + logging.debug('BAZARR All series synced from Sonarr into database.') def get_profile_list(): apikey_sonarr = settings.sonarr.apikey sonarr_version = get_sonarr_version() profiles_list = [] - # Get profiles data from Sonarr + # Get profiles data from Sonarr if sonarr_version.startswith('2'): url_sonarr_api_series = url_sonarr() + "/api/profile?apikey=" + apikey_sonarr - elif sonarr_version.startswith('3'): + else: url_sonarr_api_series = url_sonarr() + "/api/v3/languageprofile?apikey=" + apikey_sonarr try: profiles_json = requests.get(url_sonarr_api_series, timeout=60, verify=False) - except requests.exceptions.ConnectionError as errc: + except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.") - except requests.exceptions.Timeout as errt: + return None + except requests.exceptions.Timeout: logging.exception("BAZARR Error trying to get profiles from Sonarr. Timeout Error.") - except requests.exceptions.RequestException as err: + return None + except requests.exceptions.RequestException: logging.exception("BAZARR Error trying to get profiles from Sonarr.") + return None + + # Parsing data returned from Sonarr + if sonarr_version.startswith('2'): + for profile in profiles_json.json(): + profiles_list.append([profile['id'], profile['language'].capitalize()]) else: - # Parsing data returned from Sonarr - if sonarr_version.startswith('2'): - for profile in profiles_json.json(): - profiles_list.append([profile['id'], profile['language'].capitalize()]) - elif sonarr_version.startswith('3'): - for profile in profiles_json.json(): - profiles_list.append([profile['id'], profile['name'].capitalize()]) + for profile in profiles_json.json(): + profiles_list.append([profile['id'], profile['name'].capitalize()]) - return profiles_list - - return None + return profiles_list -def profile_id_to_language(id, profiles): +def profile_id_to_language(id_, profiles): for profile in profiles: - if id == profile[0]: + if id_ == profile[0]: return profile[1] diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index 81d2e8a6e..8b078f439 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) @@ -1063,7 +1010,7 @@ def upgrade_subtitles(): except ValueError: pass else: - if int(upgradable_movie['score']) < 360: + if int(upgradable_movie['score']) < 120: upgradable_movies_not_perfect.append(upgradable_movie) movies_to_upgrade = [] @@ -1078,6 +1025,8 @@ def upgrade_subtitles(): if settings.general.getboolean('use_sonarr'): for i, episode in enumerate(episodes_to_upgrade, 1): + if episode['languages'] in [None, 'None', '[]']: + continue providers = get_providers() if not providers: notifications.write(msg='BAZARR All providers are throttled', queue='get_subtitle', duration='long') @@ -1127,6 +1076,8 @@ def upgrade_subtitles(): if settings.general.getboolean('use_radarr'): for i, movie in enumerate(movies_to_upgrade, 1): + if movie['languages'] in [None, 'None', '[]']: + continue providers = get_providers() if not providers: notifications.write(msg='BAZARR All providers are throttled', queue='get_subtitle', duration='long') @@ -1173,3 +1124,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..cac891428 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" @@ -9,6 +9,7 @@ os.environ["BAZARR_VERSION"] = bazarr_version import gc import sys import libs +import io import six from six.moves import zip @@ -209,11 +210,11 @@ def shutdown(): else: database.close() try: - stop_file = open(os.path.join(args.config_dir, "bazarr.stop"), "w") + stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8') except Exception as e: logging.error('BAZARR Cannot create bazarr.stop file.') else: - stop_file.write('') + stop_file.write(six.text_type('')) stop_file.close() sys.exit(0) @@ -229,12 +230,12 @@ def restart(): else: database.close() try: - restart_file = open(os.path.join(args.config_dir, "bazarr.restart"), "w") + restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8') except Exception as e: logging.error('BAZARR Cannot create bazarr.restart file.') else: logging.info('Bazarr is being restarted...') - restart_file.write('') + restart_file.write(six.text_type('')) restart_file.close() sys.exit(0) @@ -398,6 +399,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') @@ -996,15 +999,18 @@ def historyseries(): else: series_monitored_only_query_string = '' - upgradable_episodes = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score FROM table_history " + upgradable_episodes = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score, table_shows.languages FROM table_history " "INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = " - "table_history.sonarrEpisodeId WHERE action IN (" + + "table_history.sonarrEpisodeId JOIN table_shows on table_shows.sonarrSeriesId = " + "table_history.sonarrSeriesId WHERE action IN (" + ','.join(map(str, query_actions)) + ") AND timestamp > ? AND " "score is not null" + series_monitored_only_query_string + " GROUP BY " "table_history.video_path, table_history.language", (minimum_timestamp,)) for upgradable_episode in upgradable_episodes: + if upgradable_episode['languages'] in [None, 'None', '[]']: + continue if upgradable_episode['timestamp'] > minimum_timestamp: try: int(upgradable_episode['score']) @@ -1073,15 +1079,17 @@ def historymovies(): else: query_actions = [1, 3] - upgradable_movies = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score FROM table_history_movie " - "INNER JOIN table_movies on table_movies.radarrId=" - "table_history_movie.radarrId WHERE action IN (" + + upgradable_movies = database.execute("SELECT video_path, MAX(timestamp) as timestamp, score, " + "table_movies.languages FROM table_history_movie INNER JOIN table_movies " + "on table_movies.radarrId=table_history_movie.radarrId WHERE action IN (" + ','.join(map(str, query_actions)) + ") AND timestamp > ? AND score is not NULL" + movies_monitored_only_query_string + " GROUP BY video_path, language", (minimum_timestamp,)) for upgradable_movie in upgradable_movies: + if upgradable_movie['languages'] in [None, 'None', '[]']: + continue if upgradable_movie['timestamp'] > minimum_timestamp: try: int(upgradable_movie['score']) @@ -1527,6 +1535,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') @@ -1698,7 +1708,7 @@ def system(): throttled_providers = list_throttled_providers() try: - with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r') as f: + with io.open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r', encoding='UTF-8') as f: releases = ast.literal_eval(f.read()) except Exception as e: releases = [] @@ -1724,7 +1734,7 @@ def system(): def get_logs(): authorize() logs = [] - with open(os.path.join(args.config_dir, 'log', 'bazarr.log')) as file: + with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file: for line in file.readlines(): lin = [] lin = line.split('|') @@ -1749,9 +1759,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 +1778,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 +1798,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 +1833,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 +1853,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 +1891,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 +1935,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 +1969,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 +1989,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 +2025,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/bazarr/notifier.py b/bazarr/notifier.py index 57c9e4960..c9761ac98 100644 --- a/bazarr/notifier.py +++ b/bazarr/notifier.py @@ -2,23 +2,21 @@ from __future__ import absolute_import import apprise -import os import logging -from get_args import args from database import database def update_notifier(): # define apprise object a = apprise.Apprise() - + # Retrieve all of the details results = a.details() - + notifiers_new = [] notifiers_old = [] - + notifiers_current_db = database.execute("SELECT name FROM table_settings_notifier") notifiers_current = [] @@ -31,66 +29,68 @@ def update_notifier(): logging.debug('Adding new notifier agent: ' + x['service_name']) else: notifiers_old.append([x['service_name']]) - + notifiers_to_delete = [item for item in notifiers_current if item not in notifiers_old] - database.execute("INSERT INTO table_settings_notifier (name, enabled) VALUES (?, ?)", notifiers_new, execute_many=True) - + database.execute("INSERT INTO table_settings_notifier (name, enabled) VALUES (?, ?)", notifiers_new, + execute_many=True) + database.execute("DELETE FROM table_settings_notifier WHERE name=?", notifiers_to_delete, execute_many=True) def get_notifier_providers(): providers = database.execute("SELECT name, url FROM table_settings_notifier WHERE enabled=1") + return providers -def get_series_name(sonarrSeriesId): - data = database.execute("SELECT title FROM table_shows WHERE sonarrSeriesId=?", (sonarrSeriesId,), only_one=True) - +def get_series_name(sonarr_series_id): + data = database.execute("SELECT title FROM table_shows WHERE sonarrSeriesId=?", (sonarr_series_id,), only_one=True) + return data['title'] or None -def get_episode_name(sonarrEpisodeId): +def get_episode_name(sonarr_episode_id): data = database.execute("SELECT title, season, episode FROM table_episodes WHERE sonarrEpisodeId=?", - (sonarrEpisodeId,), only_one=True) - + (sonarr_episode_id,), only_one=True) + return data['title'], data['season'], data['episode'] -def get_movies_name(radarrId): - data = database.execute("SELECT title FROM table_movies WHERE radarrId=?", (radarrId,), only_one=True) +def get_movies_name(radarr_id): + data = database.execute("SELECT title FROM table_movies WHERE radarrId=?", (radarr_id,), only_one=True) return data['title'] -def send_notifications(sonarrSeriesId, sonarrEpisodeId, message): +def send_notifications(sonarr_series_id, sonarr_episode_id, message): providers = get_notifier_providers() - series = get_series_name(sonarrSeriesId) - episode = get_episode_name(sonarrEpisodeId) - + series = get_series_name(sonarr_series_id) + episode = get_episode_name(sonarr_episode_id) + apobj = apprise.Apprise() - + for provider in providers: if provider['url'] is not None: apobj.add(provider['url']) - + apobj.notify( title='Bazarr notification', - body=(series + ' - S' + str(episode[1]).zfill(2) + 'E' + str(episode[2]).zfill(2) + ' - ' + episode[0] + ' : ' + message), + body="{} - S{:02d}E{:02d} - {} : {}".format(series, episode[1], episode[2], episode[0], message), ) -def send_notifications_movie(radarrId, message): +def send_notifications_movie(radarr_id, message): providers = get_notifier_providers() - movie = get_movies_name(radarrId) - + movie = get_movies_name(radarr_id) + apobj = apprise.Apprise() - + for provider in providers: if provider['url'] is not None: apobj.add(provider['url']) - + apobj.notify( title='Bazarr notification', - body=movie + ' : ' + message, + body="{} : {}".format(movie, message), ) diff --git a/bazarr/utils.py b/bazarr/utils.py index aee63a298..2b436125e 100644 --- a/bazarr/utils.py +++ b/bazarr/utils.py @@ -4,7 +4,6 @@ from __future__ import absolute_import import os import time import platform -import sys import logging import requests @@ -17,22 +16,20 @@ import datetime import glob -def history_log(action, sonarrSeriesId, sonarrEpisodeId, description, video_path=None, language=None, provider=None, - score=None, forced=False): +def history_log(action, sonarr_series_id, sonarr_episode_id, description, video_path=None, language=None, provider=None, + score=None): from database import database database.execute("INSERT INTO table_history (action, sonarrSeriesId, sonarrEpisodeId, timestamp, description," - "video_path, language, provider, score) VALUES (?,?,?,?,?,?,?,?,?)", (action, sonarrSeriesId, - sonarrEpisodeId, time.time(), - description, video_path, - language, provider, score)) + "video_path, language, provider, score) VALUES (?,?,?,?,?,?,?,?,?)", + (action, sonarr_series_id, sonarr_episode_id, time.time(), description, video_path, language, + provider, score)) -def history_log_movie(action, radarrId, description, video_path=None, language=None, provider=None, score=None, - forced=False): +def history_log_movie(action, radarr_id, description, video_path=None, language=None, provider=None, score=None): from database import database database.execute("INSERT INTO table_history_movie (action, radarrId, timestamp, description, video_path, language, " - "provider, score) VALUES (?,?,?,?,?,?,?,?)", (action, radarrId, time.time(), description, - video_path, language, provider, score)) + "provider, score) VALUES (?,?,?,?,?,?,?,?)", + (action, radarr_id, time.time(), description, video_path, language, provider, score)) def get_binary(name): @@ -46,10 +43,8 @@ def get_binary(name): else: if platform.system() == "Windows": # Windows exe = os.path.abspath(os.path.join(binaries_dir, "Windows", "i386", name, "%s.exe" % name)) - elif platform.system() == "Darwin": # MacOSX exe = os.path.abspath(os.path.join(binaries_dir, "MacOSX", "i386", name, name)) - elif platform.system() == "Linux": # Linux exe = os.path.abspath(os.path.join(binaries_dir, "Linux", platform.machine(), name, name)) @@ -82,62 +77,52 @@ def cache_maintenance(): def get_sonarr_version(): - use_sonarr = settings.general.getboolean('use_sonarr') - apikey_sonarr = settings.sonarr.apikey - sv = url_sonarr() + "/api/system/status?apikey=" + apikey_sonarr sonarr_version = '' - if use_sonarr: + if settings.general.getboolean('use_sonarr'): try: + sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey sonarr_version = requests.get(sv, timeout=60, verify=False).json()['version'] - except Exception as e: + except Exception: logging.debug('BAZARR cannot get Sonarr version') - return sonarr_version def get_sonarr_platform(): - use_sonarr = settings.general.getboolean('use_sonarr') - apikey_sonarr = settings.sonarr.apikey - sv = url_sonarr() + "/api/system/status?apikey=" + apikey_sonarr sonarr_platform = '' - if use_sonarr: + if settings.general.getboolean('use_sonarr'): try: - if requests.get(sv, timeout=60, verify=False).json()['isLinux'] or requests.get(sv, timeout=60, verify=False).json()['isOsx']: + sv = url_sonarr() + "/api/system/status?apikey=" + settings.sonarr.apikey + response = requests.get(sv, timeout=60, verify=False).json() + if response['isLinux'] or response['isOsx']: sonarr_platform = 'posix' - elif requests.get(sv, timeout=60, verify=False).json()['isWindows']: + elif response['isWindows']: sonarr_platform = 'nt' - except Exception as e: - logging.DEBUG('BAZARR cannot get Sonarr platform') - + except Exception: + logging.debug('BAZARR cannot get Sonarr platform') return sonarr_platform def get_radarr_version(): - use_radarr = settings.general.getboolean('use_radarr') - apikey_radarr = settings.radarr.apikey - rv = url_radarr() + "/api/system/status?apikey=" + apikey_radarr radarr_version = '' - if use_radarr: + if settings.general.getboolean('use_radarr'): try: + rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey radarr_version = requests.get(rv, timeout=60, verify=False).json()['version'] - except Exception as e: + except Exception: logging.debug('BAZARR cannot get Radarr version') - return radarr_version def get_radarr_platform(): - use_radarr = settings.general.getboolean('use_radarr') - apikey_radarr = settings.radarr.apikey - rv = url_radarr() + "/api/system/status?apikey=" + apikey_radarr radarr_platform = '' - if use_radarr: + if settings.general.getboolean('use_radarr'): try: - if requests.get(rv, timeout=60, verify=False).json()['isLinux'] or requests.get(rv, timeout=60, verify=False).json()['isOsx']: + rv = url_radarr() + "/api/system/status?apikey=" + settings.radarr.apikey + response = requests.get(rv, timeout=60, verify=False).json() + if response['isLinux'] or response['isOsx']: radarr_platform = 'posix' - elif requests.get(rv, timeout=60, verify=False).json()['isWindows']: + elif response['isWindows']: radarr_platform = 'nt' - except Exception as e: - logging.DEBUG('BAZARR cannot get Radarr platform') - + except Exception: + logging.debug('BAZARR cannot get Radarr platform') return radarr_platform 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/providers/subs4free.py b/libs/subliminal_patch/providers/subs4free.py index e25e1a7e2..9c7e92708 100644 --- a/libs/subliminal_patch/providers/subs4free.py +++ b/libs/subliminal_patch/providers/subs4free.py @@ -84,7 +84,7 @@ class Subs4FreeProvider(Provider): def initialize(self): self.session = Session() - self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__) + self.session.headers['User-Agent'] = os.environ.get("SZ_USER_AGENT", "Sub-Zero/2") def terminate(self): self.session.close() diff --git a/libs/subliminal_patch/providers/titrari.py b/libs/subliminal_patch/providers/titrari.py new file mode 100644 index 000000000..0ccf1a622 --- /dev/null +++ b/libs/subliminal_patch/providers/titrari.py @@ -0,0 +1,229 @@ +# coding=utf-8 + +from __future__ import absolute_import +import io +import logging +import re +from subliminal import __short_version__ +import rarfile + +from zipfile import ZipFile, is_zipfile +from rarfile import RarFile, is_rarfile +from subliminal_patch.providers import Provider +from subliminal_patch.providers.mixins import ProviderSubtitleArchiveMixin +from subliminal_patch.subtitle import Subtitle +from subliminal_patch.utils import sanitize, fix_inconsistent_naming as _fix_inconsistent_naming +from subliminal.exceptions import ProviderError +from subliminal.providers import ParserBeautifulSoup +from subliminal.video import Episode, Movie +from subzero.language import Language + +# parsing regex definitions +title_re = re.compile(r'(?P(?:.+(?= [Aa][Kk][Aa] ))|.+)(?:(?:.+)(?P<altitle>(?<= [Aa][Kk][Aa] ).+))?') + + +def fix_inconsistent_naming(title): + """Fix titles with inconsistent naming using dictionary and sanitize them. + + :param str title: original title. + :return: new title. + :rtype: str + + """ + return _fix_inconsistent_naming(title, {"DC's Legends of Tomorrow": "Legends of Tomorrow", + "Marvel's Jessica Jones": "Jessica Jones"}) + + +logger = logging.getLogger(__name__) + +# Configure :mod:`rarfile` to use the same path separator as :mod:`zipfile` +rarfile.PATH_SEP = '/' + +class TitrariSubtitle(Subtitle): + + provider_name = 'titrari' + + def __init__(self, language, download_link, sid, releases, title, imdb_id, year=None, download_count=None, comments=None): + super(TitrariSubtitle, self).__init__(language) + self.sid = sid + self.title = title + self.imdb_id = imdb_id + self.download_link = download_link + self.year = year + self.download_count = download_count + self.releases = self.release_info = releases + self.comments = comments + + @property + def id(self): + return self.sid + + def __str__(self): + return self.title + "(" + str(self.year) + ")" + " -> " + self.download_link + + def __repr__(self): + return self.title + "(" + str(self.year) + ")" + + def get_matches(self, video): + matches = set() + + if isinstance(video, Movie): + # title + if video.title and sanitize(self.title) == fix_inconsistent_naming(video.title): + matches.add('title') + + if video.year and self.year == video.year: + matches.add('year') + + if video.imdb_id and self.imdb_id == video.imdb_id: + matches.add('imdb_id') + + if video.release_group and video.release_group in self.comments: + matches.add('release_group') + + if video.resolution and video.resolution.lower() in self.comments: + matches.add('resolution') + + self.matches = matches + + return matches + + +class TitrariProvider(Provider, ProviderSubtitleArchiveMixin): + subtitle_class = TitrariSubtitle + languages = {Language(l) for l in ['ron', 'eng']} + languages.update(set(Language.rebuild(l, forced=True) for l in languages)) + api_url = 'https://www.titrari.ro/' + query_advanced_search = 'cautareavansata' + + 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, languages=None, title=None, imdb_id=None, video=None): + subtitles = [] + + params = self.getQueryParams(imdb_id, title) + + search_response = self.session.get(self.api_url, params=params, timeout=15) + search_response.raise_for_status() + + if not search_response.content: + logger.debug('[#### Provider: titrari.ro] No data returned from provider') + return [] + + soup = ParserBeautifulSoup(search_response.content.decode('utf-8', 'ignore'), ['lxml', 'html.parser']) + + # loop over subtitle cells + rows = soup.select('td[rowspan=\'5\']') + for index, row in enumerate(rows): + result_anchor_el = row.select_one('a') + + # Download link + href = result_anchor_el.get('href') + download_link = self.api_url + href + + fullTitle = row.parent.find("h1").find("a").text + + #Get title + try: + title = fullTitle.split("(")[0] + except: + logger.error("[#### Provider: titrari.ro] Error parsing title.") + + # Get downloads count + try: + downloads = int(row.parent.parent.select("span")[index].text[12:]) + except: + logger.error("[#### Provider: titrari.ro] Error parsing downloads.") + + # Get year + try: + year = int(fullTitle.split("(")[1].split(")")[0]) + except: + year = None + logger.error("[#### Provider: titrari.ro] Error parsing year.") + + # Get imdbId + sub_imdb_id = self.getImdbIdFromSubtitle(row) + + try: + comments = row.parent.parent.find_all("td", class_=re.compile("comment"))[index*2+1].text + except: + logger.error("Error parsing comments.") + + subtitle = self.subtitle_class(next(iter(languages)), download_link, index, None, title, sub_imdb_id, year, downloads, comments) + logger.debug('[#### Provider: titrari.ro] Found subtitle %r', str(subtitle)) + subtitles.append(subtitle) + + ordered_subs = self.order(subtitles, video) + + return ordered_subs + + def order(self, subtitles, video): + logger.debug("[#### Provider: titrari.ro] Sorting by download count...") + sorted_subs = sorted(subtitles, key=lambda s: s.download_count, reverse=True) + return sorted_subs + + def getImdbIdFromSubtitle(self, row): + try: + imdbId = row.parent.parent.find_all(src=re.compile("imdb"))[0].parent.get('href').split("tt")[-1] + except: + logger.error("[#### Provider: titrari.ro] Error parsing imdbId.") + if imdbId is not None: + return "tt" + imdbId + else: + return None + + + def getQueryParams(self, imdb_id, title): + queryParams = { + 'page': self.query_advanced_search, + 'z8': '1' + } + if imdb_id is not None: + queryParams["z5"] = imdb_id + elif title is not None: + queryParams["z7"] = title + + return queryParams + + def list_subtitles(self, video, languages): + title = fix_inconsistent_naming(video.title) + imdb_id = None + try: + imdb_id = video.imdb_id[2:] + except: + logger.error("[#### Provider: titrari.ro] Error parsing video.imdb_id.") + + return [s for s in + self.query(languages, title, imdb_id, video)] + + def download_subtitle(self, subtitle): + r = self.session.get(subtitle.download_link, headers={'Referer': self.api_url}, timeout=10) + r.raise_for_status() + + # open the archive + archive_stream = io.BytesIO(r.content) + if is_rarfile(archive_stream): + logger.debug('[#### Provider: titrari.ro] Archive identified as rar') + archive = RarFile(archive_stream) + elif is_zipfile(archive_stream): + logger.debug('[#### Provider: titrari.ro] Archive identified as zip') + archive = ZipFile(archive_stream) + else: + subtitle.content = r.content + if subtitle.is_valid(): + return + subtitle.content = None + + raise ProviderError('[#### Provider: titrari.ro] Unidentified archive type') + + subtitle.content = self.get_subtitle_from_archive(subtitle, archive) + diff --git a/libs/subliminal_patch/providers/titulky.py b/libs/subliminal_patch/providers/titulky.py index b5e408d60..b462f923b 100644 --- a/libs/subliminal_patch/providers/titulky.py +++ b/libs/subliminal_patch/providers/titulky.py @@ -244,7 +244,7 @@ class TitulkyProvider(Provider): for sub in subs: page_link = '%s%s' % (self.server_url, sub.a.get('href').encode('utf-8')) title = sub.find_all('td')[0:1] - title = [x.text.encode('utf-8') for x in title] + title = [x.text for x in title] version = sub.find(class_="fixedTip") if version is None: version = "" @@ -316,13 +316,12 @@ class TitulkyProvider(Provider): elif 'Limit vyčerpán' in r.text: raise DownloadLimitExceeded - soup = ParserBeautifulSoup(r.text.decode('utf-8', 'ignore'), ['lxml', 'html.parser']) + soup = ParserBeautifulSoup(r.text, ['lxml', 'html.parser']) # links = soup.find("a", {"id": "downlink"}).find_all('a') link = soup.find(id="downlink") # TODO: add settings for choice - url = link.get('href') - url = self.dn_url + url + url = self.dn_url + link.get('href') time.sleep(0.5) r = self.session.get(url, headers={'Referer': subtitle.download_link}, timeout=30) 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..c62a03418 100644 --- a/libs/tzlocal/unix.py +++ b/libs/tzlocal/unix.py @@ -84,10 +84,11 @@ def _get_localzone(_root='/'): if not etctz: continue tz = pytz.timezone(etctz.replace(' ', '_')) - if _root == '/': + # Disabling this offset valdation due to issue with some timezone: https://github.com/regebro/tzlocal/issues/80 + # if _root == '/': # We are using a file in etc to name the timezone. # Verify that the timezone specified there is actually used: - utils.assert_tz_offset(tz) + # utils.assert_tz_offset(tz) return tz except IOError: @@ -138,7 +139,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/historymovies.tpl b/views/historymovies.tpl index a10411988..5aaec8090 100644 --- a/views/historymovies.tpl +++ b/views/historymovies.tpl @@ -89,7 +89,7 @@ </div> </td> <td> - % upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['video_path']), ('score', row['score'])]) + % upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['video_path']), ('score', row['score']), ('languages', row['languages'])]) % if upgradable_criteria in upgradable_movies: % if row['languages'] != "None": % desired_languages = ast.literal_eval(str(row['languages'])) diff --git a/views/historyseries.tpl b/views/historyseries.tpl index 24a97f6d4..c30b086e5 100644 --- a/views/historyseries.tpl +++ b/views/historyseries.tpl @@ -104,7 +104,7 @@ </div> </td> <td> - % upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['path']), ('score', row['score'])]) + % upgradable_criteria = dict([('timestamp', row['timestamp']), ('video_path', row['path']), ('score', row['score']), ('languages', row['languages'])]) % if upgradable_criteria in upgradable_episodes: % if row['languages'] != "None": % desired_languages = ast.literal_eval(str(row['languages'])) diff --git a/views/menu.tpl b/views/menu.tpl index 60be0aecb..3f5dec777 100644 --- a/views/menu.tpl +++ b/views/menu.tpl @@ -233,14 +233,14 @@ % if PY2: <div class='ui left aligned grid'> <div class='fluid column'> - <div class="ui yellow icon message"> + <div class="ui red icon message"> <i class="python icon"></i> <div class="content"> <div class="header">Python deprecation warning</div> - Bazarr is now compatible with Python 3.6 and newer. You should upgrade Python as we'll drop support for Python 2.7.x by the end of January 2020. + Bazarr is now compatible with Python 3.6 and newer. You must upgrade Python as we don't support Python 2.x anymore. <div class="ui bulleted list"> % if os.name == 'posix': - <div class="item">If you are running under Docker, don't worry, we'll take care of this for you. Just pull the new image that should be available within a couple of days.</div> + <div class="item">If you are running under Docker, don't worry, we'll take care of this for you. Just pull the new image.</div> % end % if os.name == 'nt': <div class="item">If you have installed using the Windows Installer, just download the new installer that will upgrade your current installation (make sure to not change installation directory).</div> diff --git a/views/providers.tpl b/views/providers.tpl index e8c694411..510ee7857 100644 --- a/views/providers.tpl +++ b/views/providers.tpl @@ -144,6 +144,22 @@ </div> </div> + <div class="middle aligned row"> + <div class="right aligned four wide column"> + <label>BSplayer</label> + </div> + <div class="one wide column"> + <div id="bsplayer" class="ui toggle checkbox provider"> + <input type="checkbox"> + <label></label> + </div> + </div> + </div> + + <div id="bsplayer_option" class="ui grid container"> + + </div> + <div class="middle aligned row"> <div class="right aligned four wide column"> <label>GreekSubtitles</label> @@ -210,6 +226,47 @@ </div> + <div class="middle aligned row"> + <div class="right aligned four wide column"> + <label>LegendasDivx</label> + </div> + <div class="one wide column"> + <div id="legendasdivx" class="ui toggle checkbox provider"> + <input type="checkbox"> + <label></label> + </div> + </div> + <div class="collapsed column"> + <div class="collapsed center aligned column"> + <div class="ui basic icon" data-tooltip="Brazilian & Portuguese Subtitles Provider." data-inverted=""> + <i class="help circle large icon"></i> + </div> + </div> + </div> + </div> + <div id="legendasdivx_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_legendasdivx_username" type="text" value="{{settings.legendasdivx.username if settings.legendasdivx.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_legendasdivx_password" type="password" value="{{settings.legendasdivx.password if settings.legendasdivx.password != None else ''}}"> + </div> + </div> + </div> + </div> + <div class="middle aligned row"> <div class="right aligned four wide column"> <label>LegendasTV</label> diff --git a/views/settings.tpl b/views/settings.tpl index 6c9dc2cc0..a9cdc8b3a 100644 --- a/views/settings.tpl +++ b/views/settings.tpl @@ -45,7 +45,7 @@ % include('menu.tpl') <div id="fondblanc" class="ui container"> - <form name="settings_form" id="settings_form" action="{{base_url}}save_settings" method="post" class="ui form" autocomplete="off"> + <form name="settings_form" id="settings_form" action="{{base_url}}save_settings" method="post" class="ui form" autocomplete="off" enctype="multipart/form-data"> <div id="form_validation_error" class="ui error message"> <p>Some fields are in error and you can't save settings until you have corrected them. Be sure to check in every tabs.</p> </div> diff --git a/views/settings_general.tpl b/views/settings_general.tpl index d057b05eb..2e4998eb0 100644 --- a/views/settings_general.tpl +++ b/views/settings_general.tpl @@ -538,7 +538,7 @@ </div> </div> - <div class="ui dividing header">Post-processing</div> + <div class="ui dividing header">Post-Processing</div> <div class="twelve wide column"> <div class="ui orange message"> <p>Be aware that the execution of post-processing command will prevent the user interface from being accessible until completion, when downloading subtitles in interactive mode (meaning you'll see a loader during post-processing).</p> @@ -546,7 +546,7 @@ <div class="ui grid"> <div class="middle aligned row"> <div class="right aligned four wide column"> - <label>Use post-processing</label> + <label>Use Post-Processing</label> </div> <div class="one wide column"> <div id="settings_use_postprocessing" class="ui toggle checkbox" data-postprocessing={{settings.general.getboolean('use_postprocessing')}}> @@ -893,4 +893,4 @@ } $( "#settings_auth_apikey" ).val( result ); } - </script> \ No newline at end of file + </script> diff --git a/views/settings_radarr.tpl b/views/settings_radarr.tpl index f6c0c116e..08a01d5b1 100644 --- a/views/settings_radarr.tpl +++ b/views/settings_radarr.tpl @@ -207,12 +207,12 @@ $.getJSON("{{base_url}}test_url/" + protocol + "/" + encodeURIComponent(radarr_url), function (data) { if (data.status) { $('#radarr_validated').checkbox('check'); - $('#radarr_validation_result').text('Test successful: Radarr v' + data.version).css('color', 'green'); + $('#radarr_validation_result').text('Test Successful: Radarr v' + data.version).css('color', 'green'); $('.form').form('validate form'); $('#loader').removeClass('active'); } else { $('#radarr_validated').checkbox('uncheck'); - $('#radarr_validation_result').text('Test failed').css('color', 'red'); + $('#radarr_validation_result').text('Test Failed').css('color', 'red'); $('.form').form('validate form'); $('#loader').removeClass('active'); } diff --git a/views/settings_subtitles.tpl b/views/settings_subtitles.tpl index c15c5a8eb..d03d7a394 100644 --- a/views/settings_subtitles.tpl +++ b/views/settings_subtitles.tpl @@ -3,7 +3,7 @@ <div class="ui grid"> <div class="middle aligned row"> <div class="right aligned four wide column"> - <label>Search for missing Subtitles frequency (in hours)</label> + <label>Search for Missing Subtitles Frequency (In Hours)</label> </div> <div class="five wide column"> <div class='field'> @@ -18,7 +18,7 @@ <div class="middle aligned row"> <div class="right aligned four wide column"> - <label>Use Scene name when available</label> + <label>Use Scene Name When Available</label> </div> <div class="one wide column"> <div id="settings_scenename" class="ui toggle checkbox" data-scenename={{settings.general.getboolean('use_scenename')}}>