Merge branch 'development'

# Conflicts:
#	bazarr/get_providers.py
#	libs/subliminal_patch/providers/titrari.py
pull/835/head v0.8.4.1
Louis Vézina 5 years ago
commit f81f7ed27c

@ -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

@ -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()
bazarr_runner = lambda: start_bazarr()
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)
start_bazarr()
bazarr_runner()
# Keep the script running forever.
while True:
daemon()
# Keep the script running forever until stop is requested through term or keyboard interrupt
while not should_stop():
daemon(bazarr_runner)
time.sleep(1)

@ -102,6 +102,10 @@ defaults = {
'password': '',
'random_agents': 'True'
},
'legendasdivx': {
'username': '',
'password': ''
},
'legendastv': {
'username': '',
'password': ''

@ -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,
},

@ -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:
audio_language = profile_id_to_language(show['languageProfileId'], audio_profiles)
# Add shows in Sonarr to current shows list
current_shows_sonarr.append(show['id'])
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:
# 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 = []
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 show['alternateTitles'] != None:
alternateTitles = str([item['title'] for item in show['alternateTitles']])
else:
alternateTitles = None
# 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))
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")
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]
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']))
logging.debug('BAZARR All series synced from Sonarr into database.')
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})
# 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,))
# 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)
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'],))
# 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.')
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.")
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()])
return None
return profiles_list
# 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:
for profile in profiles_json.json():
profiles_list.append([profile['id'], profile['name'].capitalize()])
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]

@ -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)

@ -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)

@ -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),
)

@ -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

@ -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)

@ -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 = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" '
'xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="{search_url}">'
'<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
'<ns1:{func_name}>{params}</ns1:{func_name}></SOAP-ENV:Body></SOAP-ENV:Envelope>'
).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=('<username></username>'
'<password></password>'
'<AppID>BSPlayer v2.67</AppID>')
)
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='<handle>{token}</handle>'.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=(
'<handle>{token}</handle>'
'<movieHash>{movie_hash}</movieHash>'
'<movieSize>{movie_size}</movieSize>'
'<languageId>{language_ids}</languageId>'
'<imdbId>{imdbId}</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')

@ -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')

@ -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()

@ -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()

@ -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<title>(?:.+(?= [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)

@ -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)

@ -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')

@ -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)

@ -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']))

@ -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']))

@ -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>

@ -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>

@ -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>

@ -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>
</script>

@ -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');
}

@ -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')}}>

Loading…
Cancel
Save