Merge branch 'development' into morpheus

# Conflicts:
#	views/settings.tpl
#	views/wizard.tpl
pull/479/head
Louis Vézina 5 years ago
commit 036df00f5b

@ -48,8 +48,10 @@ If you need something that is not already part of Bazarr, feel free to create a
* Hosszupuska * Hosszupuska
* LegendasTV * LegendasTV
* Napiprojekt * Napiprojekt
* Napisy24
* OpenSubtitles * OpenSubtitles
* Podnapisi * Podnapisi
* Subdivx
* Subs.sab.bz * Subs.sab.bz
* Subscene * Subscene
* Subscenter * Subscenter

@ -4,9 +4,11 @@ import logging
import sqlite3 import sqlite3
import json import json
import requests import requests
import tarfile
from get_args import args from get_args import args
from config import settings from config import settings, bazarr_url
from queueconfig import notifications
if not args.no_update: if not args.no_update:
import git import git
@ -33,21 +35,100 @@ def gitconfig():
def check_and_apply_update(): def check_and_apply_update():
gitconfig()
check_releases() check_releases()
branch = settings.general.branch if not args.release_update:
g = git.cmd.Git(current_working_directory) gitconfig()
g.fetch('origin') branch = settings.general.branch
result = g.diff('--shortstat', 'origin/' + branch) g = git.cmd.Git(current_working_directory)
if len(result) == 0: g.fetch('origin')
logging.info('BAZARR No new version of Bazarr available.') result = g.diff('--shortstat', 'origin/' + branch)
if len(result) == 0:
notifications.write(msg='No new version of Bazarr available.', queue='check_update')
logging.info('BAZARR No new version of Bazarr available.')
else:
g.reset('--hard', 'HEAD')
g.checkout(branch)
g.reset('--hard', 'origin/' + branch)
g.pull()
logging.info('BAZARR Updated to latest version. Restart required. ' + result)
updated()
else: else:
g.reset('--hard', 'HEAD') url = 'https://api.github.com/repos/morpheus65535/bazarr/releases'
g.checkout(branch) releases = request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
g.reset('--hard', 'origin/' + branch)
g.pull() if releases is None:
logging.info('BAZARR Updated to latest version. Restart required. ' + result) notifications.write(msg='Could not get releases from GitHub.',
updated() queue='check_update', type='warning')
logging.warn('BAZARR Could not get releases from GitHub.')
return
else:
release = releases[0]
latest_release = release['tag_name']
if ('v' + os.environ["BAZARR_VERSION"]) != latest_release and settings.general.branch == 'master':
update_from_source()
elif settings.general.branch != 'master':
notifications.write(msg="Can't update development branch from source", queue='check_update') # fixme
logging.info("BAZARR Can't update development branch from source") # fixme
else:
notifications.write(msg='Bazarr is up to date', queue='check_update')
logging.info('BAZARR is up to date')
def update_from_source():
tar_download_url = 'https://github.com/morpheus65535/bazarr/tarball/{}'.format(settings.general.branch)
update_dir = os.path.join(os.path.dirname(__file__), '..', 'update')
logging.info('BAZARR Downloading update from: ' + tar_download_url)
notifications.write(msg='Downloading update from: ' + tar_download_url, queue='check_update')
data = request_content(tar_download_url)
if not data:
logging.error("BAZARR Unable to retrieve new version from '%s', can't update", tar_download_url)
notifications.write(msg=("Unable to retrieve new version from '%s', can't update", tar_download_url),
type='error', queue='check_update')
return
download_name = settings.general.branch + '-github'
tar_download_path = os.path.join(os.path.dirname(__file__), '..', download_name)
# Save tar to disk
with open(tar_download_path, 'wb') as f:
f.write(data)
# Extract the tar to update folder
logging.info('BAZARR Extracting file: ' + tar_download_path)
notifications.write(msg='Extracting file: ' + tar_download_path, queue='check_update')
tar = tarfile.open(tar_download_path)
tar.extractall(update_dir)
tar.close()
# Delete the tar.gz
logging.info('BAZARR Deleting file: ' + tar_download_path)
notifications.write(msg='Deleting file: ' + tar_download_path, queue='check_update')
os.remove(tar_download_path)
# Find update dir name
update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))]
if len(update_dir_contents) != 1:
logging.error("BAZARR Invalid update data, update failed: " + str(update_dir_contents))
notifications.write(msg="BAZARR Invalid update data, update failed: " + str(update_dir_contents),
type='error', queue='check_update')
return
content_dir = os.path.join(update_dir, update_dir_contents[0])
# walk temp folder and move files to main folder
for dirname, dirnames, filenames in os.walk(content_dir):
dirname = dirname[len(content_dir) + 1:]
for curfile in filenames:
old_path = os.path.join(content_dir, dirname, curfile)
new_path = os.path.join(os.path.dirname(__file__), '..', dirname, curfile)
if os.path.isfile(new_path):
os.remove(new_path)
os.renames(old_path, new_path)
updated()
def check_releases(): def check_releases():
@ -71,9 +152,158 @@ def check_releases():
json.dump(releases, f) json.dump(releases, f)
def updated(): class FakeLock(object):
conn = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) """
c = conn.cursor() If no locking or request throttling is needed, use this
c.execute("UPDATE system SET updated = 1") """
conn.commit()
c.close() def __enter__(self):
"""
Do nothing on enter
"""
pass
def __exit__(self, type, value, traceback):
"""
Do nothing on exit
"""
pass
fake_lock = FakeLock()
def request_content(url, **kwargs):
"""
Wrapper for `request_response', which will return the raw content.
"""
response = request_response(url, **kwargs)
if response is not None:
return response.content
def request_response(url, method="get", auto_raise=True,
whitelist_status_code=None, lock=fake_lock, **kwargs):
"""
Convenient wrapper for `requests.get', which will capture the exceptions
and log them. On success, the Response object is returned. In case of a
exception, None is returned.
Additionally, there is support for rate limiting. To use this feature,
supply a tuple of (lock, request_limit). The lock is used to make sure no
other request with the same lock is executed. The request limit is the
minimal time between two requests (and so 1/request_limit is the number of
requests per seconds).
"""
# Convert whitelist_status_code to a list if needed
if whitelist_status_code and type(whitelist_status_code) != list:
whitelist_status_code = [whitelist_status_code]
# Disable verification of SSL certificates if requested. Note: this could
# pose a security issue!
kwargs["verify"] = True
# Map method to the request.XXX method. This is a simple hack, but it
# allows requests to apply more magic per method. See lib/requests/api.py.
request_method = getattr(requests, method.lower())
try:
# Request URL and wait for response
with lock:
logging.debug(
"BAZARR Requesting URL via %s method: %s", method.upper(), url)
response = request_method(url, **kwargs)
# If status code != OK, then raise exception, except if the status code
# is white listed.
if whitelist_status_code and auto_raise:
if response.status_code not in whitelist_status_code:
try:
response.raise_for_status()
except:
logging.debug(
"BAZARR Response status code %d is not white "
"listed, raised exception", response.status_code)
raise
elif auto_raise:
response.raise_for_status()
return response
except requests.exceptions.SSLError as e:
if kwargs["verify"]:
logging.error(
"BAZARR Unable to connect to remote host because of a SSL error. "
"It is likely that your system cannot verify the validity"
"of the certificate. The remote certificate is either "
"self-signed, or the remote server uses SNI. See the wiki for "
"more information on this topic.")
else:
logging.error(
"BAZARR SSL error raised during connection, with certificate "
"verification turned off: %s", e)
except requests.ConnectionError:
logging.error(
"BAZARR Unable to connect to remote host. Check if the remote "
"host is up and running.")
except requests.Timeout:
logging.error(
"BAZARR Request timed out. The remote host did not respond timely.")
except requests.HTTPError as e:
if e.response is not None:
if e.response.status_code >= 500:
cause = "remote server error"
elif e.response.status_code >= 400:
cause = "local client error"
else:
# I don't think we will end up here, but for completeness
cause = "unknown"
logging.error(
"BAZARR Request raise HTTP error with status code %d (%s).",
e.response.status_code, cause)
else:
logging.error("BAZARR Request raised HTTP error.")
except requests.RequestException as e:
logging.error("BAZARR Request raised exception: %s", e)
def request_json(url, **kwargs):
"""
Wrapper for `request_response', which will decode the response as JSON
object and return the result, if no exceptions are raised.
As an option, a validator callback can be given, which should return True
if the result is valid.
"""
validator = kwargs.pop("validator", None)
response = request_response(url, **kwargs)
if response is not None:
try:
result = response.json()
if validator and not validator(result):
logging.error("BAZARR JSON validation result failed")
else:
return result
except ValueError:
logging.error("BAZARR Response returned invalid JSON data")
def updated(restart=True):
if settings.general.getboolean('update_restart') and restart:
try:
requests.get(bazarr_url + 'restart')
except requests.ConnectionError:
logging.info('BAZARR Restart failed, please restart Bazarr manualy')
updated(restart=False)
else:
conn = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c = conn.cursor()
c.execute("UPDATE system SET updated = 1")
conn.commit()
c.close()

@ -42,6 +42,7 @@ defaults = {
'chmod': '0640', 'chmod': '0640',
'subfolder': 'current', 'subfolder': 'current',
'subfolder_custom': '', 'subfolder_custom': '',
'update_restart': 'True',
'upgrade_subs': 'True', 'upgrade_subs': 'True',
'days_to_upgrade_subs': '7', 'days_to_upgrade_subs': '7',
'upgrade_manual': 'True', 'upgrade_manual': 'True',
@ -109,6 +110,10 @@ defaults = {
'deathbycaptcha': { 'deathbycaptcha': {
'username': '', 'username': '',
'password': '' 'password': ''
},
'napisy24': {
'username': '',
'password': ''
} }
} }
@ -116,6 +121,7 @@ settings = simpleconfigparser(defaults=defaults)
settings.read(os.path.join(args.config_dir, 'config', 'config.ini')) settings.read(os.path.join(args.config_dir, 'config', 'config.ini'))
base_url = settings.general.base_url base_url = settings.general.base_url
bazarr_url = 'http://localhost:' + (str(args.port) if args.port else settings.general.port) + base_url
# sonarr url # sonarr url
if settings.sonarr.getboolean('ssl'): if settings.sonarr.getboolean('ssl'):

@ -19,6 +19,8 @@ def get_args():
help="Disable update functionality (default: False)") help="Disable update functionality (default: False)")
parser.add_argument('--debug', default=False, type=bool, const=True, metavar="BOOL", nargs="?", parser.add_argument('--debug', default=False, type=bool, const=True, metavar="BOOL", nargs="?",
help="Enable console debugging (default: False)") help="Enable console debugging (default: False)")
parser.add_argument('--release-update', default=False, type=bool, const=True, metavar="BOOL", nargs="?",
help="Enable file based updater (default: False)")
return parser.parse_args() return parser.parse_args()

@ -109,7 +109,10 @@ def get_providers_auth():
'xsubs': {'username': settings.xsubs.username, 'xsubs': {'username': settings.xsubs.username,
'password': settings.xsubs.password, 'password': settings.xsubs.password,
}, },
'assrt': {'token': settings.assrt.token, } 'assrt': {'token': settings.assrt.token, },
'napisy24': {'username': settings.napisy24.username,
'password': settings.napisy24.password,
}
} }
return providers_auth return providers_auth

@ -58,8 +58,6 @@ from get_providers import get_providers, get_providers_auth, list_throttled_prov
from get_series import * from get_series import *
from get_episodes import * from get_episodes import *
if not args.no_update:
from check_update import check_and_apply_update
from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_subtitles, movies_scan_subtitles, \ from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_subtitles, movies_scan_subtitles, \
list_missing_subtitles, list_missing_subtitles_movies list_missing_subtitles, list_missing_subtitles_movies
from get_subtitle import download_subtitle, series_download_subtitles, movies_download_subtitles, \ from get_subtitle import download_subtitle, series_download_subtitles, movies_download_subtitles, \
@ -391,6 +389,8 @@ def save_wizard():
settings.opensubtitles.skip_wrong_fps = text_type(settings_opensubtitles_skip_wrong_fps) settings.opensubtitles.skip_wrong_fps = text_type(settings_opensubtitles_skip_wrong_fps)
settings.xsubs.username = request.forms.get('settings_xsubs_username') settings.xsubs.username = request.forms.get('settings_xsubs_username')
settings.xsubs.password = request.forms.get('settings_xsubs_password') settings.xsubs.password = request.forms.get('settings_xsubs_password')
settings.napisy24.username = request.forms.get('settings_napisy24_username')
settings.napisy24.password = request.forms.get('settings_napisy24_password')
settings_subliminal_languages = request.forms.getall('settings_subliminal_languages') settings_subliminal_languages = request.forms.getall('settings_subliminal_languages')
c.execute("UPDATE table_settings_languages SET enabled = 0") c.execute("UPDATE table_settings_languages SET enabled = 0")
@ -1230,6 +1230,11 @@ def save_settings():
settings_general_automatic = 'False' settings_general_automatic = 'False'
else: else:
settings_general_automatic = 'True' settings_general_automatic = 'True'
settings_general_update_restart = request.forms.get('settings_general_update_restart')
if settings_general_update_restart is None:
settings_general_update_restart = 'False'
else:
settings_general_update_restart = 'True'
settings_general_single_language = request.forms.get('settings_general_single_language') settings_general_single_language = request.forms.get('settings_general_single_language')
if settings_general_single_language is None: if settings_general_single_language is None:
settings_general_single_language = 'False' settings_general_single_language = 'False'
@ -1313,6 +1318,7 @@ def save_settings():
settings.general.chmod = text_type(settings_general_chmod) settings.general.chmod = text_type(settings_general_chmod)
settings.general.branch = text_type(settings_general_branch) settings.general.branch = text_type(settings_general_branch)
settings.general.auto_update = text_type(settings_general_automatic) settings.general.auto_update = text_type(settings_general_automatic)
settings.general.update_restart = text_type(settings_general_update_restart)
settings.general.single_language = text_type(settings_general_single_language) settings.general.single_language = text_type(settings_general_single_language)
settings.general.minimum_score = text_type(settings_general_minimum_score) settings.general.minimum_score = text_type(settings_general_minimum_score)
settings.general.use_scenename = text_type(settings_general_scenename) settings.general.use_scenename = text_type(settings_general_scenename)
@ -1505,6 +1511,8 @@ def save_settings():
settings.opensubtitles.skip_wrong_fps = text_type(settings_opensubtitles_skip_wrong_fps) settings.opensubtitles.skip_wrong_fps = text_type(settings_opensubtitles_skip_wrong_fps)
settings.xsubs.username = request.forms.get('settings_xsubs_username') settings.xsubs.username = request.forms.get('settings_xsubs_username')
settings.xsubs.password = request.forms.get('settings_xsubs_password') settings.xsubs.password = request.forms.get('settings_xsubs_password')
settings.napisy24.username = request.forms.get('settings_napisy24_username')
settings.napisy24.password = request.forms.get('settings_napisy24_password')
settings_subliminal_languages = request.forms.getall('settings_subliminal_languages') settings_subliminal_languages = request.forms.getall('settings_subliminal_languages')
c.execute("UPDATE table_settings_languages SET enabled = 0") c.execute("UPDATE table_settings_languages SET enabled = 0")
@ -1574,13 +1582,12 @@ def save_settings():
conn.commit() conn.commit()
c.close() c.close()
schedule_update_job()
sonarr_full_update() sonarr_full_update()
radarr_full_update() radarr_full_update()
logging.info('BAZARR Settings saved succesfully.') logging.info('BAZARR Settings saved succesfully.')
# reschedule full update task according to settings
sonarr_full_update()
if ref.find('saved=true') > 0: if ref.find('saved=true') > 0:
redirect(ref) redirect(ref)
@ -1994,7 +2001,7 @@ def api_wanted():
db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c = db.cursor() c = db.cursor()
data = c.execute( data = c.execute(
"SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles FROM table_episodes prin WHERE table_episodes.missing_subtitles != '[]' ORDER BY table_episodes._rowid_ DESC LIMIT 10").fetchall() "SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]' ORDER BY table_episodes._rowid_ DESC LIMIT 10").fetchall()
c.close() c.close()
return dict(subtitles=data) return dict(subtitles=data)
@ -2004,7 +2011,7 @@ def api_history():
db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c = db.cursor() c = db.cursor()
data = c.execute( data = c.execute(
"SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, strftime('%Y-%m-%d', datetime(table_history.timestamp, 'unixepoch')), table_history.description FROM table_history INNER JOIN table_shows on table_shows.sonarrSeriesId = table_history.sonarrSeriesId INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = table_history.sonarrEpisodeId WHERE table_history.action = '1' ORDER BY id DESC LIMIT 10").fetchall() "SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, strftime('%Y-%m-%d', datetime(table_history.timestamp, 'unixepoch')), table_history.description FROM table_history INNER JOIN table_shows on table_shows.sonarrSeriesId = table_history.sonarrSeriesId INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = table_history.sonarrEpisodeId WHERE table_history.action != '0' ORDER BY id DESC LIMIT 10").fetchall()
c.close() c.close()
return dict(subtitles=data) return dict(subtitles=data)
@ -2024,7 +2031,7 @@ def api_history():
db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c = db.cursor() c = db.cursor()
data = c.execute( data = c.execute(
"SELECT table_movies.title, strftime('%Y-%m-%d', datetime(table_history_movie.timestamp, 'unixepoch')), table_history_movie.description FROM table_history_movie INNER JOIN table_movies on table_movies.radarrId = table_history_movie.radarrId WHERE table_history_movie.action = '1' ORDER BY id DESC LIMIT 10").fetchall() "SELECT table_movies.title, strftime('%Y-%m-%d', datetime(table_history_movie.timestamp, 'unixepoch')), table_history_movie.description FROM table_history_movie INNER JOIN table_movies on table_movies.radarrId = table_history_movie.radarrId WHERE table_history_movie.action != '0' ORDER BY id DESC LIMIT 10").fetchall()
c.close() c.close()
return dict(subtitles=data) return dict(subtitles=data)
@ -2068,7 +2075,6 @@ def notifications():
def running_tasks_list(): def running_tasks_list():
return dict(tasks=running_tasks) return dict(tasks=running_tasks)
# Mute DeprecationWarning # Mute DeprecationWarning
warnings.simplefilter("ignore", DeprecationWarning) warnings.simplefilter("ignore", DeprecationWarning)
server = WSGIServer((str(settings.general.ip), (int(args.port) if args.port else int(settings.general.port))), app, handler_class=WebSocketHandler) server = WSGIServer((str(settings.general.ip), (int(args.port) if args.port else int(settings.general.port))), app, handler_class=WebSocketHandler)

@ -6,7 +6,6 @@ from get_series import update_series
from config import settings from config import settings
from get_subtitle import wanted_search_missing_subtitles, upgrade_subtitles from get_subtitle import wanted_search_missing_subtitles, upgrade_subtitles
from get_args import args from get_args import args
if not args.no_update: if not args.no_update:
from check_update import check_and_apply_update, check_releases from check_update import check_and_apply_update, check_releases
else: else:
@ -82,18 +81,21 @@ def task_listener(event):
scheduler.add_listener(task_listener, EVENT_JOB_SUBMITTED | EVENT_JOB_EXECUTED) scheduler.add_listener(task_listener, EVENT_JOB_SUBMITTED | EVENT_JOB_EXECUTED)
if not args.no_update: def schedule_update_job():
if settings.general.getboolean('auto_update'): if not args.no_update:
scheduler.add_job(check_and_apply_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True, if settings.general.getboolean('auto_update'):
misfire_grace_time=15, id='update_bazarr', name='Update bazarr from source on Github') scheduler.add_job(check_and_apply_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
misfire_grace_time=15, id='update_bazarr', name='Update bazarr from source on Github' if not args.release_update else 'Update bazarr from release on Github', replace_existing=True)
else:
scheduler.add_job(check_and_apply_update, CronTrigger(year='2100'), hour=4, id='update_bazarr',
name='Update bazarr from source on Github' if not args.release_update else 'Update bazarr from release on Github', replace_existing=True)
scheduler.add_job(check_releases, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
misfire_grace_time=15, id='update_release', name='Update release info', replace_existing=True)
else: else:
scheduler.add_job(check_and_apply_update, CronTrigger(year='2100'), hour=4, id='update_bazarr', scheduler.add_job(check_releases, IntervalTrigger(hours=6), max_instances=1, coalesce=True, misfire_grace_time=15,
name='Update bazarr from source on Github') id='update_release', name='Update release info', replace_existing=True)
scheduler.add_job(check_releases, IntervalTrigger(hours=6), max_instances=1, coalesce=True,
misfire_grace_time=15, id='update_release', name='Update release info')
else:
scheduler.add_job(check_releases, IntervalTrigger(hours=6), max_instances=1, coalesce=True, misfire_grace_time=15,
id='update_release', name='Update release info')
if settings.general.getboolean('use_sonarr'): if settings.general.getboolean('use_sonarr'):
scheduler.add_job(update_series, IntervalTrigger(minutes=1), max_instances=1, coalesce=True, misfire_grace_time=15, scheduler.add_job(update_series, IntervalTrigger(minutes=1), max_instances=1, coalesce=True, misfire_grace_time=15,
@ -114,6 +116,7 @@ if settings.general.getboolean('upgrade_subs') and (settings.general.getboolean(
scheduler.add_job(upgrade_subtitles, IntervalTrigger(hours=12), max_instances=1, coalesce=True, scheduler.add_job(upgrade_subtitles, IntervalTrigger(hours=12), max_instances=1, coalesce=True,
misfire_grace_time=15, id='upgrade_subtitles', name='Upgrade previously downloaded subtitles') misfire_grace_time=15, id='upgrade_subtitles', name='Upgrade previously downloaded subtitles')
schedule_update_job()
sonarr_full_update() sonarr_full_update()
radarr_full_update() radarr_full_update()
scheduler.start() scheduler.start()

@ -0,0 +1,311 @@
import logging
import re
import sys
import ssl
from copy import deepcopy
from time import sleep
from collections import OrderedDict
from requests.sessions import Session
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context
from .interpreters import JavaScriptInterpreter
from .user_agent import User_Agent
try:
from requests_toolbelt.utils import dump
except ImportError:
pass
try:
import brotli
except ImportError:
pass
try:
from urlparse import urlparse
from urlparse import urlunparse
except ImportError:
from urllib.parse import urlparse
from urllib.parse import urlunparse
##########################################################################################################################################################
__version__ = '1.1.9'
BUG_REPORT = 'Cloudflare may have changed their technique, or there may be a bug in the script.'
##########################################################################################################################################################
class CipherSuiteAdapter(HTTPAdapter):
def __init__(self, cipherSuite=None, **kwargs):
self.cipherSuite = cipherSuite
if hasattr(ssl, 'PROTOCOL_TLS'):
self.ssl_context = create_urllib3_context(
ssl_version=getattr(ssl, 'PROTOCOL_TLSv1_3', ssl.PROTOCOL_TLSv1_2),
ciphers=self.cipherSuite
)
else:
self.ssl_context = create_urllib3_context(ssl_version=ssl.PROTOCOL_TLSv1)
super(CipherSuiteAdapter, self).__init__(**kwargs)
##########################################################################################################################################################
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_context'] = self.ssl_context
return super(CipherSuiteAdapter, self).init_poolmanager(*args, **kwargs)
##########################################################################################################################################################
def proxy_manager_for(self, *args, **kwargs):
kwargs['ssl_context'] = self.ssl_context
return super(CipherSuiteAdapter, self).proxy_manager_for(*args, **kwargs)
##########################################################################################################################################################
class CloudScraper(Session):
def __init__(self, *args, **kwargs):
self.debug = kwargs.pop('debug', False)
self.delay = kwargs.pop('delay', None)
self.interpreter = kwargs.pop('interpreter', 'js2py')
self.allow_brotli = kwargs.pop('allow_brotli', True if 'brotli' in sys.modules.keys() else False)
self.cipherSuite = None
super(CloudScraper, self).__init__(*args, **kwargs)
if 'requests' in self.headers['User-Agent']:
# Set a random User-Agent if no custom User-Agent has been set
self.headers = User_Agent(allow_brotli=self.allow_brotli).headers
self.mount('https://', CipherSuiteAdapter(self.loadCipherSuite()))
##########################################################################################################################################################
@staticmethod
def debugRequest(req):
try:
print(dump.dump_all(req).decode('utf-8'))
except: # noqa
pass
##########################################################################################################################################################
def loadCipherSuite(self):
if self.cipherSuite:
return self.cipherSuite
self.cipherSuite = ''
if hasattr(ssl, 'PROTOCOL_TLS'):
ciphers = [
'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-CHACHA20-POLY1305-SHA256', 'ECDHE-RSA-CHACHA20-POLY1305-SHA256',
'ECDHE-RSA-AES128-CBC-SHA', 'ECDHE-RSA-AES256-CBC-SHA', 'RSA-AES128-GCM-SHA256', 'RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES128-GCM-SHA256', 'RSA-AES256-SHA', '3DES-EDE-CBC'
]
if hasattr(ssl, 'PROTOCOL_TLSv1_3'):
ciphers.insert(0, ['GREASE_3A', 'GREASE_6A', 'AES128-GCM-SHA256', 'AES256-GCM-SHA256', 'AES256-GCM-SHA384', 'CHACHA20-POLY1305-SHA256'])
ctx = ssl.SSLContext(getattr(ssl, 'PROTOCOL_TLSv1_3', ssl.PROTOCOL_TLSv1_2))
for cipher in ciphers:
try:
ctx.set_ciphers(cipher)
self.cipherSuite = '{}:{}'.format(self.cipherSuite, cipher).rstrip(':')
except ssl.SSLError:
pass
return self.cipherSuite
##########################################################################################################################################################
def request(self, method, url, *args, **kwargs):
ourSuper = super(CloudScraper, self)
resp = ourSuper.request(method, url, *args, **kwargs)
if resp.headers.get('Content-Encoding') == 'br':
if self.allow_brotli and resp._content:
resp._content = brotli.decompress(resp.content)
else:
logging.warning('Brotli content detected, But option is disabled, we will not continue.')
return resp
# Debug request
if self.debug:
self.debugRequest(resp)
# Check if Cloudflare anti-bot is on
if self.isChallengeRequest(resp):
if resp.request.method != 'GET':
# Work around if the initial request is not a GET,
# Supersede with a GET then re-request the original METHOD.
self.request('GET', resp.url)
resp = ourSuper.request(method, url, *args, **kwargs)
else:
# Solve Challenge
resp = self.sendChallengeResponse(resp, **kwargs)
return resp
##########################################################################################################################################################
@staticmethod
def isChallengeRequest(resp):
if resp.headers.get('Server', '').startswith('cloudflare'):
if b'why_captcha' in resp.content or b'/cdn-cgi/l/chk_captcha' in resp.content:
raise ValueError('Captcha')
return (
resp.status_code in [429, 503]
and all(s in resp.content for s in [b'jschl_vc', b'jschl_answer'])
)
return False
##########################################################################################################################################################
def sendChallengeResponse(self, resp, **original_kwargs):
body = resp.text
# Cloudflare requires a delay before solving the challenge
if not self.delay:
try:
delay = float(re.search(r'submit\(\);\r?\n\s*},\s*([0-9]+)', body).group(1)) / float(1000)
if isinstance(delay, (int, float)):
self.delay = delay
except: # noqa
pass
sleep(self.delay)
parsed_url = urlparse(resp.url)
domain = parsed_url.netloc
submit_url = '{}://{}/cdn-cgi/l/chk_jschl'.format(parsed_url.scheme, domain)
cloudflare_kwargs = deepcopy(original_kwargs)
try:
params = OrderedDict()
s = re.search(r'name="s"\svalue="(?P<s_value>[^"]+)', body)
if s:
params['s'] = s.group('s_value')
params.update(
[
('jschl_vc', re.search(r'name="jschl_vc" value="(\w+)"', body).group(1)),
('pass', re.search(r'name="pass" value="(.+?)"', body).group(1))
]
)
params = cloudflare_kwargs.setdefault('params', params)
except Exception as e:
raise ValueError('Unable to parse Cloudflare anti-bots page: {} {}'.format(e.message, BUG_REPORT))
# Solve the Javascript challenge
params['jschl_answer'] = JavaScriptInterpreter.dynamicImport(self.interpreter).solveChallenge(body, domain)
# Requests transforms any request into a GET after a redirect,
# so the redirect has to be handled manually here to allow for
# performing other types of requests even as the first request.
cloudflare_kwargs['allow_redirects'] = False
redirect = self.request(resp.request.method, submit_url, **cloudflare_kwargs)
redirect_location = urlparse(redirect.headers['Location'])
if not redirect_location.netloc:
redirect_url = urlunparse(
(
parsed_url.scheme,
domain,
redirect_location.path,
redirect_location.params,
redirect_location.query,
redirect_location.fragment
)
)
return self.request(resp.request.method, redirect_url, **original_kwargs)
return self.request(resp.request.method, redirect.headers['Location'], **original_kwargs)
##########################################################################################################################################################
@classmethod
def create_scraper(cls, sess=None, **kwargs):
"""
Convenience function for creating a ready-to-go CloudScraper object.
"""
scraper = cls(**kwargs)
if sess:
attrs = ['auth', 'cert', 'cookies', 'headers', 'hooks', 'params', 'proxies', 'data']
for attr in attrs:
val = getattr(sess, attr, None)
if val:
setattr(scraper, attr, val)
return scraper
##########################################################################################################################################################
# Functions for integrating cloudscraper with other applications and scripts
@classmethod
def get_tokens(cls, url, **kwargs):
scraper = cls.create_scraper(
debug=kwargs.pop('debug', False),
delay=kwargs.pop('delay', None),
interpreter=kwargs.pop('interpreter', 'js2py'),
allow_brotli=kwargs.pop('allow_brotli', True),
)
try:
resp = scraper.get(url, **kwargs)
resp.raise_for_status()
except Exception:
logging.error('"{}" returned an error. Could not collect tokens.'.format(url))
raise
domain = urlparse(resp.url).netloc
# noinspection PyUnusedLocal
cookie_domain = None
for d in scraper.cookies.list_domains():
if d.startswith('.') and d in ('.{}'.format(domain)):
cookie_domain = d
break
else:
raise ValueError('Unable to find Cloudflare cookies. Does the site actually have Cloudflare IUAM ("I\'m Under Attack Mode") enabled?')
return (
{
'__cfduid': scraper.cookies.get('__cfduid', '', domain=cookie_domain),
'cf_clearance': scraper.cookies.get('cf_clearance', '', domain=cookie_domain)
},
scraper.headers['User-Agent']
)
##########################################################################################################################################################
@classmethod
def get_cookie_string(cls, url, **kwargs):
"""
Convenience function for building a Cookie HTTP header value.
"""
tokens, user_agent = cls.get_tokens(url, **kwargs)
return '; '.join('='.join(pair) for pair in tokens.items()), user_agent
##########################################################################################################################################################
create_scraper = CloudScraper.create_scraper
get_tokens = CloudScraper.get_tokens
get_cookie_string = CloudScraper.get_cookie_string

@ -0,0 +1,89 @@
import re
import sys
import logging
import abc
if sys.version_info >= (3, 4):
ABC = abc.ABC # noqa
else:
ABC = abc.ABCMeta('ABC', (), {})
##########################################################################################################################################################
BUG_REPORT = 'Cloudflare may have changed their technique, or there may be a bug in the script.'
##########################################################################################################################################################
interpreters = {}
class JavaScriptInterpreter(ABC):
@abc.abstractmethod
def __init__(self, name):
interpreters[name] = self
@classmethod
def dynamicImport(cls, name):
if name not in interpreters:
try:
__import__('{}.{}'.format(cls.__module__, name))
if not isinstance(interpreters.get(name), JavaScriptInterpreter):
raise ImportError('The interpreter was not initialized.')
except ImportError:
logging.error('Unable to load {} interpreter'.format(name))
raise
return interpreters[name]
@abc.abstractmethod
def eval(self, jsEnv, js):
pass
def solveChallenge(self, body, domain):
try:
js = re.search(
r'setTimeout\(function\(\){\s+(var s,t,o,p,b,r,e,a,k,i,n,g,f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n',
body
).group(1)
except Exception:
raise ValueError('Unable to identify Cloudflare IUAM Javascript on website. {}'.format(BUG_REPORT))
js = re.sub(r'\s{2,}', ' ', js, flags=re.MULTILINE | re.DOTALL).replace('\'; 121\'', '')
js += '\na.value;'
jsEnv = '''
String.prototype.italics=function(str) {{return "<i>" + this + "</i>";}};
var document = {{
createElement: function () {{
return {{ firstChild: {{ href: "https://{domain}/" }} }}
}},
getElementById: function () {{
return {{"innerHTML": "{innerHTML}"}};
}}
}};
'''
try:
innerHTML = re.search(
r'<div(?: [^<>]*)? id="([^<>]*?)">([^<>]*?)</div>',
body,
re.MULTILINE | re.DOTALL
)
innerHTML = innerHTML.group(2) if innerHTML else ''
except: # noqa
logging.error('Error extracting Cloudflare IUAM Javascript. {}'.format(BUG_REPORT))
raise
try:
result = self.eval(
re.sub(r'\s{2,}', ' ', jsEnv.format(domain=domain, innerHTML=innerHTML), flags=re.MULTILINE | re.DOTALL),
js
)
float(result)
except Exception:
logging.error('Error executing Cloudflare IUAM Javascript. {}'.format(BUG_REPORT))
raise
return result

@ -0,0 +1,32 @@
from __future__ import absolute_import
import js2py
import logging
import base64
from . import JavaScriptInterpreter
from .jsunfuck import jsunfuck
class ChallengeInterpreter(JavaScriptInterpreter):
def __init__(self):
super(ChallengeInterpreter, self).__init__('js2py')
def eval(self, jsEnv, js):
if js2py.eval_js('(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]') == '1':
logging.warning('WARNING - Please upgrade your js2py https://github.com/PiotrDabkowski/Js2Py, applying work around for the meantime.')
js = jsunfuck(js)
def atob(s):
return base64.b64decode('{}'.format(s)).decode('utf-8')
js2py.disable_pyimport()
context = js2py.EvalJs({'atob': atob})
result = context.eval('{}{}'.format(jsEnv, js))
return result
ChallengeInterpreter()

@ -0,0 +1,97 @@
MAPPING = {
'a': '(false+"")[1]',
'b': '([]["entries"]()+"")[2]',
'c': '([]["fill"]+"")[3]',
'd': '(undefined+"")[2]',
'e': '(true+"")[3]',
'f': '(false+"")[0]',
'g': '(false+[0]+String)[20]',
'h': '(+(101))["to"+String["name"]](21)[1]',
'i': '([false]+undefined)[10]',
'j': '([]["entries"]()+"")[3]',
'k': '(+(20))["to"+String["name"]](21)',
'l': '(false+"")[2]',
'm': '(Number+"")[11]',
'n': '(undefined+"")[1]',
'o': '(true+[]["fill"])[10]',
'p': '(+(211))["to"+String["name"]](31)[1]',
'q': '(+(212))["to"+String["name"]](31)[1]',
'r': '(true+"")[1]',
's': '(false+"")[3]',
't': '(true+"")[0]',
'u': '(undefined+"")[0]',
'v': '(+(31))["to"+String["name"]](32)',
'w': '(+(32))["to"+String["name"]](33)',
'x': '(+(101))["to"+String["name"]](34)[1]',
'y': '(NaN+[Infinity])[10]',
'z': '(+(35))["to"+String["name"]](36)',
'A': '(+[]+Array)[10]',
'B': '(+[]+Boolean)[10]',
'C': 'Function("return escape")()(("")["italics"]())[2]',
'D': 'Function("return escape")()([]["fill"])["slice"]("-1")',
'E': '(RegExp+"")[12]',
'F': '(+[]+Function)[10]',
'G': '(false+Function("return Date")()())[30]',
'I': '(Infinity+"")[0]',
'M': '(true+Function("return Date")()())[30]',
'N': '(NaN+"")[0]',
'O': '(NaN+Function("return{}")())[11]',
'R': '(+[]+RegExp)[10]',
'S': '(+[]+String)[10]',
'T': '(NaN+Function("return Date")()())[30]',
'U': '(NaN+Function("return{}")()["to"+String["name"]]["call"]())[11]',
' ': '(NaN+[]["fill"])[11]',
'"': '("")["fontcolor"]()[12]',
'%': 'Function("return escape")()([]["fill"])[21]',
'&': '("")["link"](0+")[10]',
'(': '(undefined+[]["fill"])[22]',
')': '([0]+false+[]["fill"])[20]',
'+': '(+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]])+[])[2]',
',': '([]["slice"]["call"](false+"")+"")[1]',
'-': '(+(.+[0000000001])+"")[2]',
'.': '(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]',
'/': '(false+[0])["italics"]()[10]',
':': '(RegExp()+"")[3]',
';': '("")["link"](")[14]',
'<': '("")["italics"]()[0]',
'=': '("")["fontcolor"]()[11]',
'>': '("")["italics"]()[2]',
'?': '(RegExp()+"")[2]',
'[': '([]["entries"]()+"")[0]',
']': '([]["entries"]()+"")[22]',
'{': '(true+[]["fill"])[20]',
'}': '([]["fill"]+"")["slice"]("-1")'
}
SIMPLE = {
'false': '![]',
'true': '!![]',
'undefined': '[][[]]',
'NaN': '+[![]]',
'Infinity': '+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])' # +"1e1000"
}
CONSTRUCTORS = {
'Array': '[]',
'Number': '(+[])',
'String': '([]+[])',
'Boolean': '(![])',
'Function': '[]["fill"]',
'RegExp': 'Function("return/"+false+"/")()'
}
def jsunfuck(jsfuckString):
for key in sorted(MAPPING, key=lambda k: len(MAPPING[k]), reverse=True):
if MAPPING.get(key) in jsfuckString:
jsfuckString = jsfuckString.replace(MAPPING.get(key), '"{}"'.format(key))
for key in sorted(SIMPLE, key=lambda k: len(SIMPLE[k]), reverse=True):
if SIMPLE.get(key) in jsfuckString:
jsfuckString = jsfuckString.replace(SIMPLE.get(key), '{}'.format(key))
# for key in sorted(CONSTRUCTORS, key=lambda k: len(CONSTRUCTORS[k]), reverse=True):
# if CONSTRUCTORS.get(key) in jsfuckString:
# jsfuckString = jsfuckString.replace(CONSTRUCTORS.get(key), '{}'.format(key))
return jsfuckString

@ -0,0 +1,46 @@
import base64
import logging
import subprocess
from . import JavaScriptInterpreter
##########################################################################################################################################################
BUG_REPORT = 'Cloudflare may have changed their technique, or there may be a bug in the script.'
##########################################################################################################################################################
class ChallengeInterpreter(JavaScriptInterpreter):
def __init__(self):
super(ChallengeInterpreter, self).__init__('nodejs')
def eval(self, jsEnv, js):
try:
js = 'var atob = function(str) {return Buffer.from(str, "base64").toString("binary");};' \
'var challenge = atob("%s");' \
'var context = {atob: atob};' \
'var options = {filename: "iuam-challenge.js", timeout: 4000};' \
'var answer = require("vm").runInNewContext(challenge, context, options);' \
'process.stdout.write(String(answer));' \
% base64.b64encode('{}{}'.format(jsEnv, js).encode('UTF-8')).decode('ascii')
return subprocess.check_output(['node', '-e', js])
except OSError as e:
if e.errno == 2:
raise EnvironmentError(
'Missing Node.js runtime. Node is required and must be in the PATH (check with `node -v`). Your Node binary may be called `nodejs` rather than `node`, '
'in which case you may need to run `apt-get install nodejs-legacy` on some Debian-based systems. (Please read the cloudscraper'
' README\'s Dependencies section: https://github.com/VeNoMouS/cloudscraper#dependencies.'
)
raise
except Exception:
logging.error('Error executing Cloudflare IUAM Javascript. %s' % BUG_REPORT)
raise
pass
ChallengeInterpreter()

@ -0,0 +1,40 @@
import os
import json
import random
import logging
from collections import OrderedDict
##########################################################################################################################################################
class User_Agent():
##########################################################################################################################################################
def __init__(self, *args, **kwargs):
self.headers = None
self.loadUserAgent(*args, **kwargs)
##########################################################################################################################################################
def loadUserAgent(self, *args, **kwargs):
browser = kwargs.pop('browser', 'chrome')
user_agents = json.load(
open(os.path.join(os.path.dirname(__file__), 'browsers.json'), 'r'),
object_pairs_hook=OrderedDict
)
if not user_agents.get(browser):
logging.error('Sorry "{}" browser User-Agent was not found.'.format(browser))
raise
user_agent = random.choice(user_agents.get(browser))
self.headers = user_agent.get('headers')
self.headers['User-Agent'] = random.choice(user_agent.get('User-Agent'))
if not kwargs.get('allow_brotli', False):
if 'br' in self.headers['Accept-Encoding']:
self.headers['Accept-Encoding'] = ','.join([encoding for encoding in self.headers['Accept-Encoding'].split(',') if encoding.strip() != 'br']).strip()

@ -0,0 +1,336 @@
{
"chrome": [
{
"User-Agent": [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.113 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": null,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.8",
"Accept-Encoding": "gzip, deflate, , br"
}
},
{
"User-Agent": [
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": null,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br"
}
},
{
"User-Agent": [
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": null,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br"
}
},
{
"User-Agent": [
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"User-Agent": null,
"Upgrade-Insecure-Requests": "1",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br"
}
},
{
"User-Agent": [
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.40 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.40 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.28 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.28 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.28 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.28 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.28 Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": null,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br"
}
},
{
"User-Agent": [
"Mozilla/5.0 (Linux; Android 8.1.0; SM-N960F Build/M1AJQ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G965F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 Build/OPD1.170816.010) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; Pixel Build/OPR6.170623.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; SM-A530F Build/NMF26X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; Pixel Build/NDE63H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G955F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G950F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-T825 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G930F Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; Nexus 6 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; XT1092 Build/MPE24.49-18) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-N910C Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.0.2; SM-G920F Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.0; Nexus 6 Build/LRX21O) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 9; Pixel 3 XL Build/PD1A.180720.030) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PD1A.180720.030) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 9; Pixel 2 Build/PPR1.180610.009) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/KRT16M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; SM-T530 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.4; SM-N910C Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; Nexus 9 Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; SM-N950F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": null,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9"
}
},
{
"User-Agent": [
"Mozilla/5.0 (Linux; Android 8.1.0; SM-T835 Build/M1AJQ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.0; XT1092 Build/LXE22.46-19) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.85 Mobile Safari/537.36"
],
"headers": {
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": null,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8"
}
}
]
}

File diff suppressed because it is too large Load Diff

@ -5,6 +5,7 @@ import re
from .translators.friendly_nodes import REGEXP_CONVERTER from .translators.friendly_nodes import REGEXP_CONVERTER
from .utils.injector import fix_js_args from .utils.injector import fix_js_args
from types import FunctionType, ModuleType, GeneratorType, BuiltinFunctionType, MethodType, BuiltinMethodType from types import FunctionType, ModuleType, GeneratorType, BuiltinFunctionType, MethodType, BuiltinMethodType
from math import floor, log10
import traceback import traceback
try: try:
import numpy import numpy
@ -603,15 +604,7 @@ class PyJs(object):
elif typ == 'Boolean': elif typ == 'Boolean':
return Js('true') if self.value else Js('false') return Js('true') if self.value else Js('false')
elif typ == 'Number': #or self.Class=='Number': elif typ == 'Number': #or self.Class=='Number':
if self.is_nan(): return Js(unicode(js_dtoa(self.value)))
return Js('NaN')
elif self.is_infinity():
sign = '-' if self.value < 0 else ''
return Js(sign + 'Infinity')
elif isinstance(self.value,
long) or self.value.is_integer(): # dont print .0
return Js(unicode(int(self.value)))
return Js(unicode(self.value)) # accurate enough
elif typ == 'String': elif typ == 'String':
return self return self
else: #object else: #object
@ -1046,7 +1039,7 @@ def PyJsComma(a, b):
return b return b
from .internals.simplex import JsException as PyJsException from .internals.simplex import JsException as PyJsException, js_dtoa
import pyjsparser import pyjsparser
pyjsparser.parser.ENABLE_JS2PY_ERRORS = lambda msg: MakeError('SyntaxError', msg) pyjsparser.parser.ENABLE_JS2PY_ERRORS = lambda msg: MakeError('SyntaxError', msg)

@ -116,10 +116,12 @@ def eval_js(js):
def eval_js6(js): def eval_js6(js):
"""Just like eval_js but with experimental support for js6 via babel."""
return eval_js(js6_to_js5(js)) return eval_js(js6_to_js5(js))
def translate_js6(js): def translate_js6(js):
"""Just like translate_js but with experimental support for js6 via babel."""
return translate_js(js6_to_js5(js)) return translate_js(js6_to_js5(js))

@ -3,15 +3,19 @@ import re
import datetime import datetime
from desc import * from .desc import *
from simplex import * from .simplex import *
from conversions import * from .conversions import *
import six
from pyjsparser import PyJsParser from pyjsparser import PyJsParser
from itertools import izip
from conversions import * import six
from simplex import * if six.PY2:
from itertools import izip
else:
izip = zip
def Type(obj): def Type(obj):

@ -1,8 +1,8 @@
from code import Code from .code import Code
from simplex import MakeError from .simplex import MakeError
from opcodes import * from .opcodes import *
from operations import * from .operations import *
from trans_utils import * from .trans_utils import *
SPECIAL_IDENTIFIERS = {'true', 'false', 'this'} SPECIAL_IDENTIFIERS = {'true', 'false', 'this'}
@ -465,10 +465,11 @@ class ByteCodeGenerator:
self.emit('LOAD_OBJECT', tuple(data)) self.emit('LOAD_OBJECT', tuple(data))
def Program(self, body, **kwargs): def Program(self, body, **kwargs):
old_tape_len = len(self.exe.tape)
self.emit('LOAD_UNDEFINED') self.emit('LOAD_UNDEFINED')
self.emit(body) self.emit(body)
# add function tape ! # add function tape !
self.exe.tape = self.function_declaration_tape + self.exe.tape self.exe.tape = self.exe.tape[:old_tape_len] + self.function_declaration_tape + self.exe.tape[old_tape_len:]
def Pyimport(self, imp, **kwargs): def Pyimport(self, imp, **kwargs):
raise NotImplementedError( raise NotImplementedError(
@ -735,17 +736,17 @@ def main():
# #
# } # }
a.emit(d) a.emit(d)
print a.declared_vars print(a.declared_vars)
print a.exe.tape print(a.exe.tape)
print len(a.exe.tape) print(len(a.exe.tape))
a.exe.compile() a.exe.compile()
def log(this, args): def log(this, args):
print args[0] print(args[0])
return 999 return 999
print a.exe.run(a.exe.space.GlobalObj) print(a.exe.run(a.exe.space.GlobalObj))
if __name__ == '__main__': if __name__ == '__main__':

@ -1,16 +1,17 @@
from opcodes import * from .opcodes import *
from space import * from .space import *
from base import * from .base import *
class Code: class Code:
'''Can generate, store and run sequence of ops representing js code''' '''Can generate, store and run sequence of ops representing js code'''
def __init__(self, is_strict=False): def __init__(self, is_strict=False, debug_mode=False):
self.tape = [] self.tape = []
self.compiled = False self.compiled = False
self.label_locs = None self.label_locs = None
self.is_strict = is_strict self.is_strict = is_strict
self.debug_mode = debug_mode
self.contexts = [] self.contexts = []
self.current_ctx = None self.current_ctx = None
@ -22,6 +23,10 @@ class Code:
self.GLOBAL_THIS = None self.GLOBAL_THIS = None
self.space = None self.space = None
# dbg
self.ctx_depth = 0
def get_new_label(self): def get_new_label(self):
self._label_count += 1 self._label_count += 1
return self._label_count return self._label_count
@ -74,21 +79,35 @@ class Code:
# 0=normal, 1=return, 2=jump_outside, 3=errors # 0=normal, 1=return, 2=jump_outside, 3=errors
# execute_fragment_under_context returns: # execute_fragment_under_context returns:
# (return_value, typ, return_value/jump_loc/py_error) # (return_value, typ, return_value/jump_loc/py_error)
# ctx.stack must be len 1 and its always empty after the call. # IMPARTANT: It is guaranteed that the length of the ctx.stack is unchanged.
''' '''
old_curr_ctx = self.current_ctx old_curr_ctx = self.current_ctx
self.ctx_depth += 1
old_stack_len = len(ctx.stack)
old_ret_len = len(self.return_locs)
old_ctx_len = len(self.contexts)
try: try:
self.current_ctx = ctx self.current_ctx = ctx
return self._execute_fragment_under_context( return self._execute_fragment_under_context(
ctx, start_label, end_label) ctx, start_label, end_label)
except JsException as err: except JsException as err:
# undo the things that were put on the stack (if any) if self.debug_mode:
# don't worry, I know the recovery is possible through try statement and for this reason try statement self._on_fragment_exit("js errors")
# has its own context and stack so it will not delete the contents of the outer stack # undo the things that were put on the stack (if any) to ensure a proper error recovery
del ctx.stack[:] del ctx.stack[old_stack_len:]
del self.return_locs[old_ret_len:]
del self.contexts[old_ctx_len :]
return undefined, 3, err return undefined, 3, err
finally: finally:
self.ctx_depth -= 1
self.current_ctx = old_curr_ctx self.current_ctx = old_curr_ctx
assert old_stack_len == len(ctx.stack)
def _get_dbg_indent(self):
return self.ctx_depth * ' '
def _on_fragment_exit(self, mode):
print(self._get_dbg_indent() + 'ctx exit (%s)' % mode)
def _execute_fragment_under_context(self, ctx, start_label, end_label): def _execute_fragment_under_context(self, ctx, start_label, end_label):
start, end = self.label_locs[start_label], self.label_locs[end_label] start, end = self.label_locs[start_label], self.label_locs[end_label]
@ -97,16 +116,20 @@ class Code:
entry_level = len(self.contexts) entry_level = len(self.contexts)
# for e in self.tape[start:end]: # for e in self.tape[start:end]:
# print e # print e
if self.debug_mode:
print(self._get_dbg_indent() + 'ctx entry (from:%d, to:%d)' % (start, end))
while loc < len(self.tape): while loc < len(self.tape):
#print loc, self.tape[loc]
if len(self.contexts) == entry_level and loc >= end: if len(self.contexts) == entry_level and loc >= end:
if self.debug_mode:
self._on_fragment_exit('normal')
assert loc == end assert loc == end
assert len(ctx.stack) == ( delta_stack = len(ctx.stack) - initial_len
1 + initial_len), 'Stack change must be equal to +1!' assert delta_stack == +1, 'Stack change must be equal to +1! got %d' % delta_stack
return ctx.stack.pop(), 0, None # means normal return return ctx.stack.pop(), 0, None # means normal return
# execute instruction # execute instruction
if self.debug_mode:
print(self._get_dbg_indent() + str(loc), self.tape[loc])
status = self.tape[loc].eval(ctx) status = self.tape[loc].eval(ctx)
# check status for special actions # check status for special actions
@ -116,9 +139,10 @@ class Code:
if len(self.contexts) == entry_level: if len(self.contexts) == entry_level:
# check if jumped outside of the fragment and break if so # check if jumped outside of the fragment and break if so
if not start <= loc < end: if not start <= loc < end:
assert len(ctx.stack) == ( if self.debug_mode:
1 + initial_len self._on_fragment_exit('jump outside loc:%d label:%d' % (loc, status))
), 'Stack change must be equal to +1!' delta_stack = len(ctx.stack) - initial_len
assert delta_stack == +1, 'Stack change must be equal to +1! got %d' % delta_stack
return ctx.stack.pop(), 2, status # jump outside return ctx.stack.pop(), 2, status # jump outside
continue continue
@ -137,7 +161,10 @@ class Code:
# return: (None, None) # return: (None, None)
else: else:
if len(self.contexts) == entry_level: if len(self.contexts) == entry_level:
assert len(ctx.stack) == 1 + initial_len if self.debug_mode:
self._on_fragment_exit('return')
delta_stack = len(ctx.stack) - initial_len
assert delta_stack == +1, 'Stack change must be equal to +1! got %d' % delta_stack
return undefined, 1, ctx.stack.pop( return undefined, 1, ctx.stack.pop(
) # return signal ) # return signal
return_value = ctx.stack.pop() return_value = ctx.stack.pop()
@ -149,6 +176,8 @@ class Code:
continue continue
# next instruction # next instruction
loc += 1 loc += 1
if self.debug_mode:
self._on_fragment_exit('internal error - unexpected end of tape, will crash')
assert False, 'Remember to add NOP at the end!' assert False, 'Remember to add NOP at the end!'
def run(self, ctx, starting_loc=0): def run(self, ctx, starting_loc=0):
@ -156,7 +185,8 @@ class Code:
self.current_ctx = ctx self.current_ctx = ctx
while loc < len(self.tape): while loc < len(self.tape):
# execute instruction # execute instruction
#print loc, self.tape[loc] if self.debug_mode:
print(loc, self.tape[loc])
status = self.tape[loc].eval(ctx) status = self.tape[loc].eval(ctx)
# check status for special actions # check status for special actions

@ -42,6 +42,7 @@ def executable_code(code_str, space, global_context=True):
space.byte_generator.emit('LABEL', skip) space.byte_generator.emit('LABEL', skip)
space.byte_generator.emit('NOP') space.byte_generator.emit('NOP')
space.byte_generator.restore_state() space.byte_generator.restore_state()
space.byte_generator.exe.compile( space.byte_generator.exe.compile(
start_loc=old_tape_len start_loc=old_tape_len
) # dont read the code from the beginning, dont be stupid! ) # dont read the code from the beginning, dont be stupid!
@ -71,5 +72,5 @@ def _eval(this, args):
def log(this, args): def log(this, args):
print ' '.join(map(to_string, args)) print(' '.join(map(to_string, args)))
return undefined return undefined

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
# Type Conversions. to_type. All must return PyJs subclass instance # Type Conversions. to_type. All must return PyJs subclass instance
from simplex import * from .simplex import *
def to_primitive(self, hint=None): def to_primitive(self, hint=None):
@ -73,14 +73,7 @@ def to_string(self):
elif typ == 'Boolean': elif typ == 'Boolean':
return 'true' if self else 'false' return 'true' if self else 'false'
elif typ == 'Number': # or self.Class=='Number': elif typ == 'Number': # or self.Class=='Number':
if is_nan(self): return js_dtoa(self)
return 'NaN'
elif is_infinity(self):
sign = '-' if self < 0 else ''
return sign + 'Infinity'
elif int(self) == self: # integer value!
return unicode(int(self))
return unicode(self) # todo make it print exactly like node.js
else: # object else: # object
return to_string(to_primitive(self, 'String')) return to_string(to_primitive(self, 'String'))

@ -1,29 +1,22 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from base import Scope from .base import Scope
from func_utils import * from .func_utils import *
from conversions import * from .conversions import *
import six import six
from prototypes.jsboolean import BooleanPrototype from .prototypes.jsboolean import BooleanPrototype
from prototypes.jserror import ErrorPrototype from .prototypes.jserror import ErrorPrototype
from prototypes.jsfunction import FunctionPrototype from .prototypes.jsfunction import FunctionPrototype
from prototypes.jsnumber import NumberPrototype from .prototypes.jsnumber import NumberPrototype
from prototypes.jsobject import ObjectPrototype from .prototypes.jsobject import ObjectPrototype
from prototypes.jsregexp import RegExpPrototype from .prototypes.jsregexp import RegExpPrototype
from prototypes.jsstring import StringPrototype from .prototypes.jsstring import StringPrototype
from prototypes.jsarray import ArrayPrototype from .prototypes.jsarray import ArrayPrototype
import prototypes.jsjson as jsjson from .prototypes import jsjson
import prototypes.jsutils as jsutils from .prototypes import jsutils
from constructors import jsnumber from .constructors import jsnumber, jsstring, jsarray, jsboolean, jsregexp, jsmath, jsobject, jsfunction, jsconsole
from constructors import jsstring
from constructors import jsarray
from constructors import jsboolean
from constructors import jsregexp
from constructors import jsmath
from constructors import jsobject
from constructors import jsfunction
from constructors import jsconsole
def fill_proto(proto, proto_class, space): def fill_proto(proto, proto_class, space):
@ -155,7 +148,10 @@ def fill_space(space, byte_generator):
j = easy_func(creator, space) j = easy_func(creator, space)
j.name = unicode(typ) j.name = unicode(typ)
j.prototype = space.ERROR_TYPES[typ]
set_protected(j, 'prototype', space.ERROR_TYPES[typ])
set_non_enumerable(space.ERROR_TYPES[typ], 'constructor', j)
def new_create(args, space): def new_create(args, space):
message = get_arg(args, 0) message = get_arg(args, 0)
@ -178,6 +174,7 @@ def fill_space(space, byte_generator):
setattr(space, err_type_name + u'Prototype', extra_err) setattr(space, err_type_name + u'Prototype', extra_err)
error_constructors[err_type_name] = construct_constructor( error_constructors[err_type_name] = construct_constructor(
err_type_name) err_type_name)
assert space.TypeErrorPrototype is not None assert space.TypeErrorPrototype is not None
# RegExp # RegExp

@ -1,5 +1,5 @@
from simplex import * from .simplex import *
from conversions import * from .conversions import *
import six import six
if six.PY3: if six.PY3:

@ -1,5 +1,5 @@
from operations import * from .operations import *
from base import get_member, get_member_dot, PyJsFunction, Scope from .base import get_member, get_member_dot, PyJsFunction, Scope
class OP_CODE(object): class OP_CODE(object):

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from simplex import * from .simplex import *
from conversions import * from .conversions import *
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Unary operations # Unary operations

@ -4,7 +4,7 @@ from __future__ import unicode_literals
import re import re
from ..conversions import * from ..conversions import *
from ..func_utils import * from ..func_utils import *
from jsregexp import RegExpExec from .jsregexp import RegExpExec
DIGS = set(u'0123456789') DIGS = set(u'0123456789')
WHITE = u"\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF" WHITE = u"\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF"

@ -1,11 +1,9 @@
import pyjsparser import pyjsparser
from space import Space from .space import Space
import fill_space from . import fill_space
from byte_trans import ByteCodeGenerator from .byte_trans import ByteCodeGenerator
from code import Code from .code import Code
from simplex import MakeError from .simplex import *
import sys
sys.setrecursionlimit(100000)
pyjsparser.parser.ENABLE_JS2PY_ERRORS = lambda msg: MakeError(u'SyntaxError', unicode(msg)) pyjsparser.parser.ENABLE_JS2PY_ERRORS = lambda msg: MakeError(u'SyntaxError', unicode(msg))
@ -16,8 +14,8 @@ def get_js_bytecode(js):
a.emit(d) a.emit(d)
return a.exe.tape return a.exe.tape
def eval_js_vm(js): def eval_js_vm(js, debug=False):
a = ByteCodeGenerator(Code()) a = ByteCodeGenerator(Code(debug_mode=debug))
s = Space() s = Space()
a.exe.space = s a.exe.space = s
s.exe = a.exe s.exe = a.exe
@ -26,7 +24,10 @@ def eval_js_vm(js):
a.emit(d) a.emit(d)
fill_space.fill_space(s, a) fill_space.fill_space(s, a)
# print a.exe.tape if debug:
from pprint import pprint
pprint(a.exe.tape)
print()
a.exe.compile() a.exe.compile()
return a.exe.run(a.exe.space.GlobalObj) return a.exe.run(a.exe.space.GlobalObj)

@ -1,6 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import six import six
if six.PY3:
basestring = str
long = int
xrange = range
unicode = str
#Undefined #Undefined
class PyJsUndefined(object): class PyJsUndefined(object):
@ -75,7 +79,7 @@ def is_callable(self):
def is_infinity(self): def is_infinity(self):
return self == float('inf') or self == -float('inf') return self == Infinity or self == -Infinity
def is_nan(self): def is_nan(self):
@ -114,7 +118,7 @@ class JsException(Exception):
return self.mes.to_string().value return self.mes.to_string().value
else: else:
if self.throw is not None: if self.throw is not None:
from conversions import to_string from .conversions import to_string
return to_string(self.throw) return to_string(self.throw)
else: else:
return self.typ + ': ' + self.message return self.typ + ': ' + self.message
@ -131,3 +135,26 @@ def value_from_js_exception(js_exception, space):
return js_exception.throw return js_exception.throw
else: else:
return space.NewError(js_exception.typ, js_exception.message) return space.NewError(js_exception.typ, js_exception.message)
def js_dtoa(number):
if is_nan(number):
return u'NaN'
elif is_infinity(number):
if number > 0:
return u'Infinity'
return u'-Infinity'
elif number == 0.:
return u'0'
elif abs(number) < 1e-6 or abs(number) >= 1e21:
frac, exponent = unicode(repr(float(number))).split('e')
# Remove leading zeros from the exponent.
exponent = int(exponent)
return frac + ('e' if exponent < 0 else 'e+') + unicode(exponent)
elif abs(number) < 1e-4: # python starts to return exp notation while we still want the prec
frac, exponent = unicode(repr(float(number))).split('e-')
base = u'0.' + u'0' * (int(exponent) - 1) + frac.lstrip('-').replace('.', '')
return base if number > 0. else u'-' + base
elif isinstance(number, long) or number.is_integer(): # dont print .0
return unicode(int(number))
return unicode(repr(number)) # python representation should be equivalent.

@ -1,5 +1,5 @@
from base import * from .base import *
from simplex import * from .simplex import *
class Space(object): class Space(object):

@ -1,3 +1,10 @@
import six
if six.PY3:
basestring = str
long = int
xrange = range
unicode = str
def to_key(literal_or_identifier): def to_key(literal_or_identifier):
''' returns string representation of this object''' ''' returns string representation of this object'''
if literal_or_identifier['type'] == 'Identifier': if literal_or_identifier['type'] == 'Identifier':

@ -6,8 +6,6 @@ if six.PY3:
xrange = range xrange = range
unicode = str unicode = str
# todo fix apply and bind
class FunctionPrototype: class FunctionPrototype:
def toString(): def toString():
@ -41,6 +39,7 @@ class FunctionPrototype:
return this.call(obj, args) return this.call(obj, args)
def bind(thisArg): def bind(thisArg):
arguments_ = arguments
target = this target = this
if not target.is_callable(): if not target.is_callable():
raise this.MakeError( raise this.MakeError(
@ -48,5 +47,5 @@ class FunctionPrototype:
if len(arguments) <= 1: if len(arguments) <= 1:
args = () args = ()
else: else:
args = tuple([arguments[e] for e in xrange(1, len(arguments))]) args = tuple([arguments_[e] for e in xrange(1, len(arguments_))])
return this.PyJsBoundFunction(target, thisArg, args) return this.PyJsBoundFunction(target, thisArg, args)

@ -345,7 +345,7 @@ def BlockStatement(type, body):
body) # never returns empty string! In the worst case returns pass\n body) # never returns empty string! In the worst case returns pass\n
def ExpressionStatement(type, expression, **ommit): def ExpressionStatement(type, expression):
return trans(expression) + '\n' # end expression space with new line return trans(expression) + '\n' # end expression space with new line

@ -163,3 +163,13 @@ class Pysubs2CLI(object):
elif args.transform_framerate is not None: elif args.transform_framerate is not None:
in_fps, out_fps = args.transform_framerate in_fps, out_fps = args.transform_framerate
subs.transform_framerate(in_fps, out_fps) subs.transform_framerate(in_fps, out_fps)
def __main__():
cli = Pysubs2CLI()
rv = cli(sys.argv[1:])
sys.exit(rv)
if __name__ == "__main__":
__main__()

@ -17,12 +17,14 @@ class Color(_Color):
return _Color.__new__(cls, r, g, b, a) return _Color.__new__(cls, r, g, b, a)
#: Version of the pysubs2 library. #: Version of the pysubs2 library.
VERSION = "0.2.1" VERSION = "0.2.3"
PY3 = sys.version_info.major == 3 PY3 = sys.version_info.major == 3
if PY3: if PY3:
text_type = str text_type = str
binary_string_type = bytes
else: else:
text_type = unicode text_type = unicode
binary_string_type = str

@ -3,7 +3,7 @@ from .microdvd import MicroDVDFormat
from .subrip import SubripFormat from .subrip import SubripFormat
from .jsonformat import JSONFormat from .jsonformat import JSONFormat
from .substation import SubstationFormat from .substation import SubstationFormat
from .txt_generic import TXTGenericFormat, MPL2Format from .mpl2 import MPL2Format
from .exceptions import * from .exceptions import *
#: Dict mapping file extensions to format identifiers. #: Dict mapping file extensions to format identifiers.
@ -13,7 +13,6 @@ FILE_EXTENSION_TO_FORMAT_IDENTIFIER = {
".ssa": "ssa", ".ssa": "ssa",
".sub": "microdvd", ".sub": "microdvd",
".json": "json", ".json": "json",
".txt": "txt_generic",
} }
#: Dict mapping format identifiers to implementations (FormatBase subclasses). #: Dict mapping format identifiers to implementations (FormatBase subclasses).
@ -23,7 +22,6 @@ FORMAT_IDENTIFIER_TO_FORMAT_CLASS = {
"ssa": SubstationFormat, "ssa": SubstationFormat,
"microdvd": MicroDVDFormat, "microdvd": MicroDVDFormat,
"json": JSONFormat, "json": JSONFormat,
"txt_generic": TXTGenericFormat,
"mpl2": MPL2Format, "mpl2": MPL2Format,
} }

@ -0,0 +1,49 @@
# coding=utf-8
from __future__ import print_function, division, unicode_literals
import re
from .time import times_to_ms
from .formatbase import FormatBase
from .ssaevent import SSAEvent
# thanks to http://otsaloma.io/gaupol/doc/api/aeidon.files.mpl2_source.html
MPL2_FORMAT = re.compile(r"^(?um)\[(-?\d+)\]\[(-?\d+)\](.*)")
class MPL2Format(FormatBase):
@classmethod
def guess_format(cls, text):
if MPL2_FORMAT.search(text):
return "mpl2"
@classmethod
def from_file(cls, subs, fp, format_, **kwargs):
def prepare_text(lines):
out = []
for s in lines.split("|"):
s = s.strip()
if s.startswith("/"):
# line beginning with '/' is in italics
s = r"{\i1}%s{\i0}" % s[1:].strip()
out.append(s)
return "\\N".join(out)
subs.events = [SSAEvent(start=times_to_ms(s=float(start) / 10), end=times_to_ms(s=float(end) / 10),
text=prepare_text(text)) for start, end, text in MPL2_FORMAT.findall(fp.getvalue())]
@classmethod
def to_file(cls, subs, fp, format_, **kwargs):
# TODO handle italics
for line in subs:
if line.is_comment:
continue
print("[{start}][{end}] {text}".format(start=int(line.start // 100),
end=int(line.end // 100),
text=line.plaintext.replace("\n", "|")),
file=fp)

@ -78,7 +78,7 @@ class SSAStyle(object):
s += "%rpx " % self.fontsize s += "%rpx " % self.fontsize
if self.bold: s += "bold " if self.bold: s += "bold "
if self.italic: s += "italic " if self.italic: s += "italic "
s += "'%s'>" % self.fontname s += "{!r}>".format(self.fontname)
if not PY3: s = s.encode("utf-8") if not PY3: s = s.encode("utf-8")
return s return s

@ -46,8 +46,16 @@ class SubripFormat(FormatBase):
following_lines[-1].append(line) following_lines[-1].append(line)
def prepare_text(lines): def prepare_text(lines):
# Handle the "happy" empty subtitle case, which is timestamp line followed by blank line(s)
# followed by number line and timestamp line of the next subtitle. Fixes issue #11.
if (len(lines) >= 2
and all(re.match("\s*$", line) for line in lines[:-1])
and re.match("\s*\d+\s*$", lines[-1])):
return ""
# Handle the general case.
s = "".join(lines).strip() s = "".join(lines).strip()
s = re.sub(r"\n* *\d+ *$", "", s) # strip number of next subtitle s = re.sub(r"\n+ *\d+ *$", "", s) # strip number of next subtitle
s = re.sub(r"< *i *>", r"{\i1}", s) s = re.sub(r"< *i *>", r"{\i1}", s)
s = re.sub(r"< */ *i *>", r"{\i0}", s) s = re.sub(r"< */ *i *>", r"{\i0}", s)
s = re.sub(r"< *s *>", r"{\s1}", s) s = re.sub(r"< *s *>", r"{\s1}", s)

@ -4,7 +4,7 @@ from numbers import Number
from .formatbase import FormatBase from .formatbase import FormatBase
from .ssaevent import SSAEvent from .ssaevent import SSAEvent
from .ssastyle import SSAStyle from .ssastyle import SSAStyle
from .common import text_type, Color from .common import text_type, Color, PY3, binary_string_type
from .time import make_time, ms_to_times, timestamp_to_ms, TIMESTAMP from .time import make_time, ms_to_times, timestamp_to_ms, TIMESTAMP
SSA_ALIGNMENT = (1, 2, 3, 9, 10, 11, 5, 6, 7) SSA_ALIGNMENT = (1, 2, 3, 9, 10, 11, 5, 6, 7)
@ -150,14 +150,7 @@ class SubstationFormat(FormatBase):
if format_ == "ass": if format_ == "ass":
return ass_rgba_to_color(v) return ass_rgba_to_color(v)
else: else:
try: return ssa_rgb_to_color(v)
return ssa_rgb_to_color(v)
except ValueError:
try:
return ass_rgba_to_color(v)
except:
return Color(255, 255, 255, 0)
elif f in {"bold", "underline", "italic", "strikeout"}: elif f in {"bold", "underline", "italic", "strikeout"}:
return v == "-1" return v == "-1"
elif f in {"borderstyle", "encoding", "marginl", "marginr", "marginv", "layer", "alphalevel"}: elif f in {"borderstyle", "encoding", "marginl", "marginr", "marginv", "layer", "alphalevel"}:
@ -229,7 +222,7 @@ class SubstationFormat(FormatBase):
for k, v in subs.aegisub_project.items(): for k, v in subs.aegisub_project.items():
print(k, v, sep=": ", file=fp) print(k, v, sep=": ", file=fp)
def field_to_string(f, v): def field_to_string(f, v, line):
if f in {"start", "end"}: if f in {"start", "end"}:
return ms_to_timestamp(v) return ms_to_timestamp(v)
elif f == "marked": elif f == "marked":
@ -240,23 +233,31 @@ class SubstationFormat(FormatBase):
return "-1" if v else "0" return "-1" if v else "0"
elif isinstance(v, (text_type, Number)): elif isinstance(v, (text_type, Number)):
return text_type(v) return text_type(v)
elif not PY3 and isinstance(v, binary_string_type):
# A convenience feature, see issue #12 - accept non-unicode strings
# when they are ASCII; this is useful in Python 2, especially for non-text
# fields like style names, where requiring Unicode type seems too stringent
if all(ord(c) < 128 for c in v):
return text_type(v)
else:
raise TypeError("Encountered binary string with non-ASCII codepoint in SubStation field {!r} for line {!r} - please use unicode string instead of str".format(f, line))
elif isinstance(v, Color): elif isinstance(v, Color):
if format_ == "ass": if format_ == "ass":
return color_to_ass_rgba(v) return color_to_ass_rgba(v)
else: else:
return color_to_ssa_rgb(v) return color_to_ssa_rgb(v)
else: else:
raise TypeError("Unexpected type when writing a SubStation field") raise TypeError("Unexpected type when writing a SubStation field {!r} for line {!r}".format(f, line))
print("\n[V4+ Styles]" if format_ == "ass" else "\n[V4 Styles]", file=fp) print("\n[V4+ Styles]" if format_ == "ass" else "\n[V4 Styles]", file=fp)
print(STYLE_FORMAT_LINE[format_], file=fp) print(STYLE_FORMAT_LINE[format_], file=fp)
for name, sty in subs.styles.items(): for name, sty in subs.styles.items():
fields = [field_to_string(f, getattr(sty, f)) for f in STYLE_FIELDS[format_]] fields = [field_to_string(f, getattr(sty, f), sty) for f in STYLE_FIELDS[format_]]
print("Style: %s" % name, *fields, sep=",", file=fp) print("Style: %s" % name, *fields, sep=",", file=fp)
print("\n[Events]", file=fp) print("\n[Events]", file=fp)
print(EVENT_FORMAT_LINE[format_], file=fp) print(EVENT_FORMAT_LINE[format_], file=fp)
for ev in subs.events: for ev in subs.events:
fields = [field_to_string(f, getattr(ev, f)) for f in EVENT_FIELDS[format_]] fields = [field_to_string(f, getattr(ev, f), ev) for f in EVENT_FIELDS[format_]]
print(ev.type, end=": ", file=fp) print(ev.type, end=": ", file=fp)
print(*fields, sep=",", file=fp) print(*fields, sep=",", file=fp)

@ -75,7 +75,7 @@ class SubsCenterSubtitle(Subtitle):
class SubsCenterProvider(Provider): class SubsCenterProvider(Provider):
"""SubsCenter Provider.""" """SubsCenter Provider."""
languages = {Language.fromalpha2(l) for l in ['he']} languages = {Language.fromalpha2(l) for l in ['he']}
server_url = 'http://www.subscenter.biz/he/' server_url = 'http://www.subscenter.org/he/'
subtitle_class = SubsCenterSubtitle subtitle_class = SubsCenterSubtitle
def __init__(self, username=None, password=None): def __init__(self, username=None, password=None):

@ -258,4 +258,4 @@ def fix_line_ending(content):
:rtype: bytes :rtype: bytes
""" """
return content.replace(b'\r\n', b'\n').replace(b'\r', b'\n') return content.replace(b'\r\n', b'\n')

@ -309,7 +309,8 @@ class SZProviderPool(ProviderPool):
logger.error('Invalid subtitle') logger.error('Invalid subtitle')
return False return False
subtitle.normalize() if not os.environ.get("SZ_KEEP_ENCODING", False):
subtitle.normalize()
return True return True

@ -10,6 +10,8 @@ import logging
import requests import requests
import xmlrpclib import xmlrpclib
import dns.resolver import dns.resolver
import ipaddress
import re
from requests import exceptions from requests import exceptions
from urllib3.util import connection from urllib3.util import connection
@ -17,7 +19,13 @@ from retry.api import retry_call
from exceptions import APIThrottled from exceptions import APIThrottled
from dogpile.cache.api import NO_VALUE from dogpile.cache.api import NO_VALUE
from subliminal.cache import region from subliminal.cache import region
from cfscrape import CloudflareScraper from subliminal_patch.pitcher import pitchers
from cloudscraper import CloudScraper
try:
import brotli
except:
pass
try: try:
from urlparse import urlparse from urlparse import urlparse
@ -55,43 +63,111 @@ class CertifiSession(TimeoutSession):
self.verify = pem_file self.verify = pem_file
class CFSession(CloudflareScraper): class NeedsCaptchaException(Exception):
def __init__(self): pass
super(CFSession, self).__init__()
class CFSession(CloudScraper):
def __init__(self, *args, **kwargs):
super(CFSession, self).__init__(*args, **kwargs)
self.debug = os.environ.get("CF_DEBUG", False) self.debug = os.environ.get("CF_DEBUG", False)
def _request(self, method, url, *args, **kwargs):
ourSuper = super(CloudScraper, self)
resp = ourSuper.request(method, url, *args, **kwargs)
if resp.headers.get('Content-Encoding') == 'br':
if self.allow_brotli and resp._content:
resp._content = brotli.decompress(resp.content)
else:
logging.warning('Brotli content detected, But option is disabled, we will not continue.')
return resp
# Debug request
if self.debug:
self.debugRequest(resp)
# Check if Cloudflare anti-bot is on
try:
if self.isChallengeRequest(resp):
if resp.request.method != 'GET':
# Work around if the initial request is not a GET,
# Supersede with a GET then re-request the original METHOD.
CloudScraper.request(self, 'GET', resp.url)
resp = ourSuper.request(method, url, *args, **kwargs)
else:
# Solve Challenge
resp = self.sendChallengeResponse(resp, **kwargs)
except ValueError, e:
if e.message == "Captcha":
parsed_url = urlparse(url)
domain = parsed_url.netloc
# solve the captcha
site_key = re.search(r'data-sitekey="(.+?)"', resp.content).group(1)
challenge_s = re.search(r'type="hidden" name="s" value="(.+?)"', resp.content).group(1)
challenge_ray = re.search(r'data-ray="(.+?)"', resp.content).group(1)
if not all([site_key, challenge_s, challenge_ray]):
raise Exception("cf: Captcha site-key not found!")
pitcher = pitchers.get_pitcher()("cf: %s" % domain, resp.request.url, site_key,
user_agent=self.headers["User-Agent"],
cookies=self.cookies.get_dict(),
is_invisible=True)
parsed_url = urlparse(resp.url)
logger.info("cf: %s: Solving captcha", domain)
result = pitcher.throw()
if not result:
raise Exception("cf: Couldn't solve captcha!")
submit_url = '{}://{}/cdn-cgi/l/chk_captcha'.format(parsed_url.scheme, domain)
method = resp.request.method
cloudflare_kwargs = {
'allow_redirects': False,
'headers': {'Referer': resp.url},
'params': OrderedDict(
[
('s', challenge_s),
('g-recaptcha-response', result)
]
)
}
return CloudScraper.request(self, method, submit_url, **cloudflare_kwargs)
return resp
def request(self, method, url, *args, **kwargs): def request(self, method, url, *args, **kwargs):
parsed_url = urlparse(url) parsed_url = urlparse(url)
domain = parsed_url.netloc domain = parsed_url.netloc
cache_key = "cf_data2_%s" % domain cache_key = "cf_data3_%s" % domain
if not self.cookies.get("cf_clearance", "", domain=domain): if not self.cookies.get("cf_clearance", "", domain=domain):
cf_data = region.get(cache_key) cf_data = region.get(cache_key)
if cf_data is not NO_VALUE: if cf_data is not NO_VALUE:
cf_cookies, user_agent, hdrs = cf_data cf_cookies, hdrs = cf_data
logger.debug("Trying to use old cf data for %s: %s", domain, cf_data) logger.debug("Trying to use old cf data for %s: %s", domain, cf_data)
for cookie, value in cf_cookies.iteritems(): for cookie, value in cf_cookies.iteritems():
self.cookies.set(cookie, value, domain=domain) self.cookies.set(cookie, value, domain=domain)
self._hdrs = hdrs self.headers = hdrs
self._ua = user_agent
self.headers['User-Agent'] = self._ua
ret = super(CFSession, self).request(method, url, *args, **kwargs) ret = self._request(method, url, *args, **kwargs)
if self._was_cf: try:
self._was_cf = False cf_data = self.get_cf_live_tokens(domain)
logger.debug("We've hit CF, trying to store previous data") except:
try: pass
cf_data = self.get_cf_live_tokens(domain) else:
except: if cf_data and "cf_clearance" in cf_data[0] and cf_data[0]["cf_clearance"]:
logger.debug("Couldn't get CF live tokens for re-use. Cookies: %r", self.cookies) if cf_data != region.get(cache_key):
pass
else:
if cf_data != region.get(cache_key) and cf_data[0]["cf_clearance"]:
logger.debug("Storing cf data for %s: %s", domain, cf_data) logger.debug("Storing cf data for %s: %s", domain, cf_data)
region.set(cache_key, cf_data) region.set(cache_key, cf_data)
elif cf_data[0]["cf_clearance"]:
logger.debug("CF Live tokens not updated")
return ret return ret
@ -109,7 +185,7 @@ class CFSession(CloudflareScraper):
("__cfduid", self.cookies.get("__cfduid", "", domain=cookie_domain)), ("__cfduid", self.cookies.get("__cfduid", "", domain=cookie_domain)),
("cf_clearance", self.cookies.get("cf_clearance", "", domain=cookie_domain)) ("cf_clearance", self.cookies.get("cf_clearance", "", domain=cookie_domain))
])), ])),
self._ua, self._hdrs self.headers
) )
@ -240,42 +316,47 @@ def patch_create_connection():
global _custom_resolver, _custom_resolver_ips, dns_cache global _custom_resolver, _custom_resolver_ips, dns_cache
host, port = address host, port = address
__custom_resolver_ips = os.environ.get("dns_resolvers", None) try:
ipaddress.ip_address(unicode(host))
# resolver ips changed in the meantime? except (ipaddress.AddressValueError, ValueError):
if __custom_resolver_ips != _custom_resolver_ips: __custom_resolver_ips = os.environ.get("dns_resolvers", None)
_custom_resolver = None
_custom_resolver_ips = __custom_resolver_ips # resolver ips changed in the meantime?
dns_cache = {} if __custom_resolver_ips != _custom_resolver_ips:
_custom_resolver = None
custom_resolver = _custom_resolver _custom_resolver_ips = __custom_resolver_ips
dns_cache = {}
if not custom_resolver:
if _custom_resolver_ips: custom_resolver = _custom_resolver
logger.debug("DNS: Trying to use custom DNS resolvers: %s", _custom_resolver_ips)
custom_resolver = dns.resolver.Resolver(configure=False) if not custom_resolver:
custom_resolver.lifetime = 8.0 if _custom_resolver_ips:
try: logger.debug("DNS: Trying to use custom DNS resolvers: %s", _custom_resolver_ips)
custom_resolver.nameservers = json.loads(_custom_resolver_ips) custom_resolver = dns.resolver.Resolver(configure=False)
except: custom_resolver.lifetime = os.environ.get("dns_resolvers_timeout", 8.0)
logger.debug("DNS: Couldn't load custom DNS resolvers: %s", _custom_resolver_ips) try:
custom_resolver.nameservers = json.loads(_custom_resolver_ips)
except:
logger.debug("DNS: Couldn't load custom DNS resolvers: %s", _custom_resolver_ips)
else:
_custom_resolver = custom_resolver
if custom_resolver:
if host in dns_cache:
ip = dns_cache[host]
logger.debug("DNS: Using %s=%s from cache", host, ip)
return _orig_create_connection((ip, port), *args, **kwargs)
else: else:
_custom_resolver = custom_resolver try:
ip = custom_resolver.query(host)[0].address
if custom_resolver: logger.debug("DNS: Resolved %s to %s using %s", host, ip, custom_resolver.nameservers)
if host in dns_cache: dns_cache[host] = ip
ip = dns_cache[host] return _orig_create_connection((ip, port), *args, **kwargs)
logger.debug("DNS: Using %s=%s from cache", host, ip) except dns.exception.DNSException:
return _orig_create_connection((ip, port), *args, **kwargs) logger.warning("DNS: Couldn't resolve %s with DNS: %s", host, custom_resolver.nameservers)
else: raise
try:
ip = custom_resolver.query(host)[0].address logger.debug("DNS: Falling back to default DNS or IP on %s", host)
logger.debug("DNS: Resolved %s to %s using %s", host, ip, custom_resolver.nameservers)
dns_cache[host] = ip
except dns.exception.DNSException:
logger.warning("DNS: Couldn't resolve %s with DNS: %s", host, custom_resolver.nameservers)
raise
return _orig_create_connection((host, port), *args, **kwargs) return _orig_create_connection((host, port), *args, **kwargs)
patch_create_connection._sz_patched = True patch_create_connection._sz_patched = True

@ -23,9 +23,10 @@ class ArgenteamSubtitle(Subtitle):
hearing_impaired_verifiable = False hearing_impaired_verifiable = False
_release_info = None _release_info = None
def __init__(self, language, download_link, movie_kind, title, season, episode, year, release, version, source, def __init__(self, language, page_link, download_link, movie_kind, title, season, episode, year, release, version, source,
video_codec, tvdb_id, imdb_id, asked_for_episode=None, asked_for_release_group=None, *args, **kwargs): video_codec, tvdb_id, imdb_id, asked_for_episode=None, asked_for_release_group=None, *args, **kwargs):
super(ArgenteamSubtitle, self).__init__(language, download_link, *args, **kwargs) super(ArgenteamSubtitle, self).__init__(language, page_link=page_link, *args, **kwargs)
self.page_link = page_link
self.download_link = download_link self.download_link = download_link
self.movie_kind = movie_kind self.movie_kind = movie_kind
self.title = title self.title = title
@ -135,7 +136,8 @@ class ArgenteamProvider(Provider, ProviderSubtitleArchiveMixin):
provider_name = 'argenteam' provider_name = 'argenteam'
languages = {Language.fromalpha2(l) for l in ['es']} languages = {Language.fromalpha2(l) for l in ['es']}
video_types = (Episode, Movie) video_types = (Episode, Movie)
API_URL = "http://argenteam.net/api/v1/" BASE_URL = "http://www.argenteam.net/"
API_URL = BASE_URL + "api/v1/"
subtitle_class = ArgenteamSubtitle subtitle_class = ArgenteamSubtitle
hearing_impaired_verifiable = False hearing_impaired_verifiable = False
language_list = list(languages) language_list = list(languages)
@ -240,12 +242,13 @@ class ArgenteamProvider(Provider, ProviderSubtitleArchiveMixin):
for r in content['releases']: for r in content['releases']:
for s in r['subtitles']: for s in r['subtitles']:
sub = ArgenteamSubtitle(language, s['uri'], "episode" if is_episode else "movie", returned_title, movie_kind = "episode" if is_episode else "movie"
page_link = self.BASE_URL + movie_kind + "/" + str(aid)
sub = ArgenteamSubtitle(language, page_link, s['uri'], movie_kind, returned_title,
season, episode, year, r.get('team'), r.get('tags'), season, episode, year, r.get('team'), r.get('tags'),
r.get('source'), r.get('codec'), content.get("tvdb"), imdb_id, r.get('source'), r.get('codec'), content.get("tvdb"), imdb_id,
asked_for_release_group=video.release_group, asked_for_release_group=video.release_group,
asked_for_episode=episode asked_for_episode=episode)
)
subtitles.append(sub) subtitles.append(sub)
if has_multiple_ids: if has_multiple_ids:

@ -0,0 +1,124 @@
import logging
import os
from io import BytesIO
from zipfile import ZipFile
from requests import Session
from subliminal_patch.subtitle import Subtitle
from subliminal_patch.providers import Provider
from subliminal import __short_version__
from subliminal.exceptions import AuthenticationError, ConfigurationError
from subliminal.subtitle import fix_line_ending
from subzero.language import Language
logger = logging.getLogger(__name__)
class Napisy24Subtitle(Subtitle):
'''Napisy24 Subtitle.'''
provider_name = 'napisy24'
def __init__(self, language, hash, imdb_id, napis_id):
super(Napisy24Subtitle, self).__init__(language)
self.hash = hash
self.imdb_id = imdb_id
self.napis_id = napis_id
@property
def id(self):
return self.hash
def get_matches(self, video):
matches = set()
# hash
if 'opensubtitles' in video.hashes and video.hashes['opensubtitles'] == self.hash:
matches.add('hash')
# imdb_id
if video.imdb_id and self.imdb_id == video.imdb_id:
matches.add('imdb_id')
return matches
class Napisy24Provider(Provider):
'''Napisy24 Provider.'''
languages = {Language(l) for l in ['pol']}
required_hash = 'opensubtitles'
api_url = 'http://napisy24.pl/run/CheckSubAgent.php'
def __init__(self, username=None, password=None):
if all((username, password)):
self.username = username
self.password = password
else:
self.username = 'subliminal'
self.password = 'lanimilbus'
self.session = None
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/%s' % __short_version__
self.session.headers['Content-Type'] = 'application/x-www-form-urlencoded'
def terminate(self):
self.session.close()
def query(self, language, size, name, hash):
params = {
'postAction': 'CheckSub',
'ua': self.username,
'ap': self.password,
'fs': size,
'fh': hash,
'fn': os.path.basename(name),
'n24pref': 1
}
response = self.session.post(self.api_url, data=params, timeout=10)
response.raise_for_status()
response_content = response.content.split(b'||', 1)
n24_data = response_content[0].decode()
if n24_data[:2] != 'OK':
if n24_data[:11] == 'login error':
raise AuthenticationError('Login failed')
logger.error('Unknown response: %s', response.content)
return None
n24_status = n24_data[:4]
if n24_status == 'OK-0':
logger.info('No subtitles found')
return None
subtitle_info = dict(p.split(':', 1) for p in n24_data.split('|')[1:])
logger.debug('Subtitle info: %s', subtitle_info)
if n24_status == 'OK-1':
logger.info('No subtitles found but got video info')
return None
elif n24_status == 'OK-2':
logger.info('Found subtitles')
elif n24_status == 'OK-3':
logger.info('Found subtitles but not from Napisy24 database')
return None
subtitle_content = response_content[1]
subtitle = Napisy24Subtitle(language, hash, 'tt%s' % subtitle_info['imdb'].zfill(7), subtitle_info['napisId'])
with ZipFile(BytesIO(subtitle_content)) as zf:
subtitle.content = fix_line_ending(zf.open(zf.namelist()[0]).read())
return subtitle
def list_subtitles(self, video, languages):
subtitles = [self.query(l, video.size, video.name, video.hashes['opensubtitles']) for l in languages]
return [s for s in subtitles if s is not None]
def download_subtitle(self, subtitle):
# there is no download step, content is already filled from listing subtitles
pass

@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
import io
import logging
import os
import time
import zipfile
import rarfile
from subzero.language import Language
from guessit import guessit
from requests import Session
from subliminal import __short_version__
from subliminal.providers import ParserBeautifulSoup, Provider
from subliminal.subtitle import SUBTITLE_EXTENSIONS, Subtitle, fix_line_ending,guess_matches
from subliminal.video import Episode, Movie
logger = logging.getLogger(__name__)
class SubdivxSubtitle(Subtitle):
provider_name = 'subdivx'
hash_verifiable = False
def __init__(self, language, page_link, download_link, description, title):
super(SubdivxSubtitle, self).__init__(language, hearing_impaired=False,
page_link=page_link)
self.download_link = download_link
self.description = description.lower()
self.title = title
@property
def id(self):
return self.download_link
def get_matches(self, video):
matches = set()
# episode
if isinstance(video, Episode):
# already matched in search query
matches.update(['title', 'series', 'season', 'episode', 'year'])
# movie
elif isinstance(video, Movie):
# already matched in search query
matches.update(['title', 'year'])
# release_group
if video.release_group and video.release_group.lower() in self.description:
matches.add('release_group')
# resolution
if video.resolution and video.resolution.lower() in self.description:
matches.add('resolution')
# format
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.add('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.add('video_codec')
break
return matches
class SubdivxSubtitlesProvider(Provider):
provider_name = 'subdivx'
hash_verifiable = False
languages = {Language.fromalpha2(l) for l in ['es']}
subtitle_class = SubdivxSubtitle
server_url = 'https://www.subdivx.com/'
multi_result_throttle = 2
language_list = list(languages)
def __init__(self):
self.session = None
def initialize(self):
self.session = Session()
self.session.headers['User-Agent'] = 'Subliminal/{}'.format(__short_version__)
def terminate(self):
self.session.close()
def query(self, keyword, season=None, episode=None, year=None):
query = keyword
if season and episode:
query += ' S{season:02d}E{episode:02d}'.format(season=season, episode=episode)
elif year:
query += ' {:4d}'.format(year)
params = {
'buscar': query, # search string
'accion': 5, # action search
'oxdown': 1, # order by downloads descending
'pg': 1 # page 1
}
logger.debug('Searching subtitles %r', query)
subtitles = []
language = self.language_list[0]
search_link = self.server_url + 'index.php'
while True:
r = self.session.get(search_link, params=params, timeout=10)
r.raise_for_status()
if not r.content:
logger.debug('No data returned from provider')
return []
page_soup = ParserBeautifulSoup(r.content.decode('iso-8859-1', 'ignore'), ['lxml', 'html.parser'])
title_soups = page_soup.find_all("div", {'id': 'menu_detalle_buscador'})
body_soups = page_soup.find_all("div", {'id': 'buscador_detalle'})
if len(title_soups) != len(body_soups):
logger.debug('Error in provider data')
return []
for subtitle in range(0, len(title_soups)):
title_soup, body_soup = title_soups[subtitle], body_soups[subtitle]
# title
title = title_soup.find("a").text.replace("Subtitulo de ", "")
page_link = title_soup.find("a")["href"].replace('http://', 'https://')
# body
description = body_soup.find("div", {'id': 'buscador_detalle_sub'}).text
download_link = body_soup.find("div", {'id': 'buscador_detalle_sub_datos'}
).find("a", {'target': 'new'})["href"].replace('http://', 'https://')
subtitle = self.subtitle_class(language, page_link, download_link, description, title)
logger.debug('Found subtitle %r', subtitle)
subtitles.append(subtitle)
if len(title_soups) >= 20:
params['pg'] += 1 # search next page
time.sleep(self.multi_result_throttle)
else:
break
return subtitles
def list_subtitles(self, video, languages):
if isinstance(video, Episode):
titles = [video.series] + video.alternative_series
elif isinstance(video, Movie):
titles = [video.title] + video.alternative_titles
else:
titles = []
subtitles = []
for title in titles:
if isinstance(video, Episode):
subtitles += [s for s in self.query(title, season=video.season,
episode=video.episode, year=video.year)
if s.language in languages]
elif isinstance(video, Movie):
subtitles += [s for s in self.query(title, year=video.year)
if s.language in languages]
return subtitles
def download_subtitle(self, subtitle):
if isinstance(subtitle, SubdivxSubtitle):
# download the subtitle
logger.info('Downloading subtitle %r', subtitle)
r = self.session.get(subtitle.download_link, headers={'Referer': subtitle.page_link},
timeout=30)
r.raise_for_status()
if not r.content:
logger.debug('Unable to download subtitle. No data returned from provider')
return
archive = _get_archive(r.content)
subtitle_content = _get_subtitle_from_archive(archive)
if subtitle_content:
subtitle.content = fix_line_ending(subtitle_content)
else:
logger.debug('Could not extract subtitle from %r', archive)
def _get_archive(content):
# open the archive
archive_stream = io.BytesIO(content)
archive = None
if rarfile.is_rarfile(archive_stream):
logger.debug('Identified rar archive')
archive = rarfile.RarFile(archive_stream)
elif zipfile.is_zipfile(archive_stream):
logger.debug('Identified zip archive')
archive = zipfile.ZipFile(archive_stream)
return archive
def _get_subtitle_from_archive(archive):
for name in archive.namelist():
# discard hidden files
if os.path.split(name)[-1].startswith('.'):
continue
# discard non-subtitle files
if not name.lower().endswith(SUBTITLE_EXTENSIONS):
continue
return archive.read(name)
return None

@ -5,16 +5,11 @@ import logging
import os import os
import time import time
import inflect import inflect
import cfscrape
from random import randint
from zipfile import ZipFile from zipfile import ZipFile
from babelfish import language_converters from babelfish import language_converters
from guessit import guessit from guessit import guessit
from dogpile.cache.api import NO_VALUE
from subliminal import Episode, ProviderError from subliminal import Episode, ProviderError
from subliminal.cache import region
from subliminal.utils import sanitize_release_group from subliminal.utils import sanitize_release_group
from subliminal_patch.http import RetryingCFSession from subliminal_patch.http import RetryingCFSession
from subliminal_patch.providers import Provider from subliminal_patch.providers import Provider
@ -195,7 +190,7 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
return subtitles return subtitles
def query(self, video): def query(self, video):
vfn = get_video_filename(video) #vfn = get_video_filename(video)
subtitles = [] subtitles = []
#logger.debug(u"Searching for: %s", vfn) #logger.debug(u"Searching for: %s", vfn)
# film = search(vfn, session=self.session) # film = search(vfn, session=self.session)
@ -215,7 +210,7 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
for series in [video.series] + video.alternative_series: for series in [video.series] + video.alternative_series:
term = u"%s - %s Season" % (series, p.number_to_words("%sth" % video.season).capitalize()) term = u"%s - %s Season" % (series, p.number_to_words("%sth" % video.season).capitalize())
logger.debug('Searching for alternative results: %s', term) logger.debug('Searching for alternative results: %s', term)
film = search(term, session=self.session, release=False) film = search(term, session=self.session, release=False, throttle=self.search_throttle)
if film and film.subtitles: if film and film.subtitles:
logger.debug('Alternative results found: %s', len(film.subtitles)) logger.debug('Alternative results found: %s', len(film.subtitles))
subtitles += self.parse_results(video, film) subtitles += self.parse_results(video, film)
@ -223,25 +218,26 @@ class SubsceneProvider(Provider, ProviderSubtitleArchiveMixin):
logger.debug('No alternative results found') logger.debug('No alternative results found')
# packs # packs
if video.season_fully_aired: # if video.season_fully_aired:
term = u"%s S%02i" % (series, video.season) # term = u"%s S%02i" % (series, video.season)
logger.debug('Searching for packs: %s', term) # logger.debug('Searching for packs: %s', term)
time.sleep(self.search_throttle) # time.sleep(self.search_throttle)
film = search(term, session=self.session) # film = search(term, session=self.session, throttle=self.search_throttle)
if film and film.subtitles: # if film and film.subtitles:
logger.debug('Pack results found: %s', len(film.subtitles)) # logger.debug('Pack results found: %s', len(film.subtitles))
subtitles += self.parse_results(video, film) # subtitles += self.parse_results(video, film)
else: # else:
logger.debug('No pack results found') # logger.debug('No pack results found')
else: # else:
logger.debug("Not searching for packs, because the season hasn't fully aired") # logger.debug("Not searching for packs, because the season hasn't fully aired")
if more_than_one: if more_than_one:
time.sleep(self.search_throttle) time.sleep(self.search_throttle)
else: else:
more_than_one = len([video.title] + video.alternative_titles) > 1 more_than_one = len([video.title] + video.alternative_titles) > 1
for title in [video.title] + video.alternative_titles: for title in [video.title] + video.alternative_titles:
logger.debug('Searching for movie results: %s', title) logger.debug('Searching for movie results: %s', title)
film = search(title, year=video.year, session=self.session, limit_to=None, release=False) film = search(title, year=video.year, session=self.session, limit_to=None, release=False,
throttle=self.search_throttle)
if film and film.subtitles: if film and film.subtitles:
subtitles += self.parse_results(video, film) subtitles += self.parse_results(video, film)
if more_than_one: if more_than_one:

@ -225,7 +225,8 @@ class TitloviProvider(Provider, ProviderSubtitleArchiveMixin):
# page link # page link
page_link = self.server_url + sub.a.attrs['href'] page_link = self.server_url + sub.a.attrs['href']
# subtitle language # subtitle language
match = lang_re.search(sub.select_one('.lang').attrs['src']) _lang = sub.select_one('.lang')
match = lang_re.search(_lang.attrs.get('src', _lang.attrs.get('cfsrc', '')))
if match: if match:
try: try:
# decode language # decode language

@ -123,7 +123,8 @@ class Subtitle(Subtitle_):
# http://scratchpad.wikia.com/wiki/Character_Encoding_Recommendation_for_Languages # http://scratchpad.wikia.com/wiki/Character_Encoding_Recommendation_for_Languages
if self.language.alpha3 == 'zho': if self.language.alpha3 == 'zho':
encodings.extend(['cp936', 'gb2312', 'cp950', 'gb18030', 'big5', 'big5hkscs']) encodings.extend(['cp936', 'gb2312', 'gbk', 'gb18030', 'hz', 'iso2022_jp_2', 'cp950', 'gb18030', 'big5',
'big5hkscs', 'utf-16'])
elif self.language.alpha3 == 'jpn': elif self.language.alpha3 == 'jpn':
encodings.extend(['shift-jis', 'cp932', 'euc_jp', 'iso2022_jp', 'iso2022_jp_1', 'iso2022_jp_2', encodings.extend(['shift-jis', 'cp932', 'euc_jp', 'iso2022_jp', 'iso2022_jp_1', 'iso2022_jp_2',
'iso2022_jp_2004', 'iso2022_jp_3', 'iso2022_jp_ext', ]) 'iso2022_jp_2004', 'iso2022_jp_3', 'iso2022_jp_ext', ])
@ -132,7 +133,7 @@ class Subtitle(Subtitle_):
# arabian/farsi # arabian/farsi
elif self.language.alpha3 in ('ara', 'fas', 'per'): elif self.language.alpha3 in ('ara', 'fas', 'per'):
encodings.append('windows-1256') encodings.extend(['windows-1256', 'utf-16'])
elif self.language.alpha3 == 'heb': elif self.language.alpha3 == 'heb':
encodings.extend(['windows-1255', 'iso-8859-8']) encodings.extend(['windows-1255', 'iso-8859-8'])
elif self.language.alpha3 == 'tur': elif self.language.alpha3 == 'tur':
@ -250,8 +251,7 @@ class Subtitle(Subtitle_):
subs = pysubs2.SSAFile.from_string(text, fps=self.plex_media_fps) subs = pysubs2.SSAFile.from_string(text, fps=self.plex_media_fps)
unicontent = self.pysubs2_to_unicode(subs) unicontent = self.pysubs2_to_unicode(subs)
self.content = unicontent.encode("utf-8") self.content = unicontent.encode(self._guessed_encoding)
self._guessed_encoding = "utf-8"
except: except:
logger.exception("Couldn't convert subtitle %s to .srt format: %s", self, traceback.format_exc()) logger.exception("Couldn't convert subtitle %s to .srt format: %s", self, traceback.format_exc())
return False return False
@ -319,7 +319,8 @@ class Subtitle(Subtitle_):
:return: string :return: string
""" """
if not self.mods: if not self.mods:
return fix_text(self.content.decode("utf-8"), **ftfy_defaults).encode(encoding="utf-8") return fix_text(self.content.decode(encoding=self._guessed_encoding), **ftfy_defaults).encode(
encoding=self._guessed_encoding)
submods = SubtitleModifications(debug=debug) submods = SubtitleModifications(debug=debug)
if submods.load(content=self.text, language=self.language): if submods.load(content=self.text, language=self.language):
@ -328,7 +329,7 @@ class Subtitle(Subtitle_):
self.mods = submods.mods_used self.mods = submods.mods_used
content = fix_text(self.pysubs2_to_unicode(submods.f, format=format), **ftfy_defaults)\ content = fix_text(self.pysubs2_to_unicode(submods.f, format=format), **ftfy_defaults)\
.encode(encoding="utf-8") .encode(encoding=self._guessed_encoding)
submods.f = None submods.f = None
del submods del submods
return content return content

@ -28,6 +28,9 @@ import re
import enum import enum
import sys import sys
import requests
import time
import logging
is_PY2 = sys.version_info[0] < 3 is_PY2 = sys.version_info[0] < 3
if is_PY2: if is_PY2:
@ -37,8 +40,13 @@ else:
from contextlib import suppress from contextlib import suppress
from urllib2.request import Request, urlopen from urllib2.request import Request, urlopen
from dogpile.cache.api import NO_VALUE
from subliminal.cache import region
from bs4 import BeautifulSoup, NavigableString from bs4 import BeautifulSoup, NavigableString
logger = logging.getLogger(__name__)
# constants # constants
HEADERS = { HEADERS = {
} }
@ -48,6 +56,13 @@ DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWeb"\
"Kit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36" "Kit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36"
ENDPOINT_RE = re.compile(ur'(?uis)<form action="/subtitles/(.+)">.*?<input type="text"')
class NewEndpoint(Exception):
pass
# utils # utils
def soup_for(url, session=None, user_agent=DEFAULT_USER_AGENT): def soup_for(url, session=None, user_agent=DEFAULT_USER_AGENT):
url = re.sub("\s", "+", url) url = re.sub("\s", "+", url)
@ -55,7 +70,19 @@ def soup_for(url, session=None, user_agent=DEFAULT_USER_AGENT):
r = Request(url, data=None, headers=dict(HEADERS, **{"User-Agent": user_agent})) r = Request(url, data=None, headers=dict(HEADERS, **{"User-Agent": user_agent}))
html = urlopen(r).read().decode("utf-8") html = urlopen(r).read().decode("utf-8")
else: else:
html = session.get(url).text ret = session.get(url)
try:
ret.raise_for_status()
except requests.HTTPError, e:
if e.response.status_code == 404:
m = ENDPOINT_RE.search(ret.text)
if m:
try:
raise NewEndpoint(m.group(1))
except:
pass
raise
html = ret.text
return BeautifulSoup(html, "html.parser") return BeautifulSoup(html, "html.parser")
@ -243,17 +270,45 @@ def get_first_film(soup, section, year=None, session=None):
return Film.from_url(url, session=session) return Film.from_url(url, session=session)
def search(term, release=True, session=None, year=None, limit_to=SearchTypes.Exact): def search(term, release=True, session=None, year=None, limit_to=SearchTypes.Exact, throttle=0):
soup = soup_for("%s/subtitles/%s?q=%s" % (SITE_DOMAIN, "release" if release else "title", term), session=session) # note to subscene: if you actually start to randomize the endpoint, we'll have to query your server even more
endpoints = ["searching", "search", "srch", "find"]
if "Subtitle search by" in str(soup):
rows = soup.find("table").tbody.find_all("tr")
subtitles = Subtitle.from_rows(rows)
return Film(term, subtitles=subtitles)
for junk, search_type in SearchTypes.__members__.items(): if release:
if section_exists(soup, search_type): endpoints = ["release"]
return get_first_film(soup, search_type, year=year, session=session) else:
endpoint = region.get("subscene_endpoint")
if endpoint is not NO_VALUE and endpoint not in endpoints:
endpoints.insert(0, endpoint)
if limit_to == search_type: soup = None
return for endpoint in endpoints:
try:
soup = soup_for("%s/subtitles/%s?q=%s" % (SITE_DOMAIN, endpoint, term),
session=session)
except NewEndpoint, e:
new_endpoint = e.message
if new_endpoint not in endpoints:
new_endpoint = new_endpoint.strip()
logger.debug("Switching main endpoint to %s", new_endpoint)
region.set("subscene_endpoint", new_endpoint)
time.sleep(throttle)
return search(term, release=release, session=session, year=year, limit_to=limit_to, throttle=throttle)
else:
region.delete("subscene_endpoint")
raise Exception("New endpoint %s didn't work; exiting" % new_endpoint)
break
if soup:
if "Subtitle search by" in str(soup):
rows = soup.find("table").tbody.find_all("tr")
subtitles = Subtitle.from_rows(rows)
return Film(term, subtitles=subtitles)
for junk, search_type in SearchTypes.__members__.items():
if section_exists(soup, search_type):
return get_first_film(soup, search_type, year=year, session=session)
if limit_to == search_type:
return

@ -2,7 +2,8 @@
OS_PLEX_USERAGENT = 'plexapp.com v9.0' OS_PLEX_USERAGENT = 'plexapp.com v9.0'
DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'subzero', 'libfilebot', 'cfscrape'] DEPENDENCY_MODULE_NAMES = ['subliminal', 'subliminal_patch', 'enzyme', 'guessit', 'subzero', 'libfilebot',
'cloudscraper']
PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none" PERSONAL_MEDIA_IDENTIFIER = "com.plexapp.agents.none"
PLUGIN_IDENTIFIER_SHORT = "subzero" PLUGIN_IDENTIFIER_SHORT = "subzero"
PLUGIN_IDENTIFIER = "com.plexapp.agents.%s" % PLUGIN_IDENTIFIER_SHORT PLUGIN_IDENTIFIER = "com.plexapp.agents.%s" % PLUGIN_IDENTIFIER_SHORT

@ -152,7 +152,7 @@ def get_localzone():
if _cache_tz is None: if _cache_tz is None:
_cache_tz = _get_localzone() _cache_tz = _get_localzone()
utils.assert_tz_offset(_cache_tz) #utils.assert_tz_offset(_cache_tz)
return _cache_tz return _cache_tz
@ -160,5 +160,5 @@ def reload_localzone():
"""Reload the cached localzone. You need to call this if the timezone has changed.""" """Reload the cached localzone. You need to call this if the timezone has changed."""
global _cache_tz global _cache_tz
_cache_tz = _get_localzone() _cache_tz = _get_localzone()
utils.assert_tz_offset(_cache_tz) #utils.assert_tz_offset(_cache_tz)
return _cache_tz return _cache_tz

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 358 KiB

@ -0,0 +1,174 @@
$('#settings_form')
.form({
fields: {
settings_general_ip : {
rules : [
{
type : 'regExp[/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/]',
prompt : '"General / Start-Up / Listening IP address" must be a valid IPv4 address'
},
{
type : 'empty',
prompt : '"General / Start-Up / Listening IP address" must have a value'
}
]
},
settings_general_port : {
rules : [
{
type : 'integer[1..65535]',
prompt : '"General / Start-Up / Listening port" must be an integer between 1 and 65535'
},
{
type : 'empty',
prompt : '"General / Start-Up / Listening port" must have a value'
}
]
},
settings_general_chmod: {
rules: [
{
type: 'regExp[^([0-7]{4})$]',
prompt : '"General / Start-Up / Set subtitle file permissions to" must be a 4-digit octal (e.g.: 0775)'
}
]
},
settings_auth_password : {
depends: 'settings_auth_username',
rules : [
{
type : 'empty',
prompt : '"General / Security settings / Password" must have a value and you must type it again if you change your username.'
}
]
},
sonarr_validated_checkbox : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'checked',
prompt : '"Sonarr / Connection settings / Test" must be successful before going further'
}
]
},
settings_sonarr_ip : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'empty',
prompt : '"Sonarr / Connection settings / Hostname or IP address" must have a value'
}
]
},
settings_sonarr_port : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'integer[1..65535]',
prompt : '"Sonarr / Connection settings / Listening port" must be an integer between 1 and 65535'
},
{
type : 'empty',
prompt : '"Sonarr / Connection settings / Listening port" must have a value'
}
]
},
settings_sonarr_apikey : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'exactLength[32]',
prompt : '"Sonarr / Connection settings / API key" must be exactly {ruleValue} characters'
},
{
type : 'empty',
prompt : '"Sonarr / Connection settings / API key" must have a value'
}
]
},
radarr_validated_checkbox : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'checked',
prompt : '"Radarr / Connection settings / Test" must be successful before going further'
}
]
},
settings_radarr_ip : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'empty',
prompt : '"Radarr / Connection settings / Hostname or IP address" must have a value'
}
]
},
settings_radarr_port : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'integer[1..65535]',
prompt : '"Radarr / Connection settings / Listening port" must be an integer between 1 and 65535'
},
{
type : 'empty',
prompt : '"Radarr / Connection settings / Listening port" must have a value'
}
]
},
settings_radarr_apikey : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'exactLength[32]',
prompt : '"Radarr / Connection settings / API key" must be exactly {ruleValue} characters'
},
{
type : 'empty',
prompt : '"Radarr / Connection settings / API key" must have a value'
}
]
},
settings_subliminal_providers : {
rules : [
{
type : 'minCount[1]',
prompt : '"Subtitles / Subtitles providers" must have at least one enabled provider'
}
]
},
settings_subliminal_languages : {
rules : [
{
type : 'minCount[1]',
prompt : '"Subtitles / Subtitles languages / Enabled languages" must have at least one enabled language'
}
]
},
settings_days_to_upgrade_subs : {
depends: 'settings_upgrade_subs',
rules : [
{
type : 'integer[1..30]',
prompt : '"Subtitles / Subtitles options / Number of days to go back in history..." must be an integer between 1 and 30'
}
]
}
},
inline : false,
selector : {
message: '#form_validation_error'
},
on : 'change',
onFailure: function(){
$('.submit').addClass('disabled');
return false;
},
onSuccess: function(){
$('.submit').removeClass('disabled');
$('#loader').addClass('active');
}
})
;

@ -0,0 +1,156 @@
$('#wizard_form')
.form({
fields: {
settings_general_ip : {
rules : [
{
type : 'regExp[/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/]',
prompt : '"General / Start-Up / Listening IP address" must be a valid IPv4 address'
},
{
type : 'empty',
prompt : '"General / Start-Up / Listening IP address" must have a value'
}
]
},
settings_general_port : {
rules : [
{
type : 'integer[1..65535]',
prompt : '"General / Start-Up / Listening port" must be an integer between 1 and 65535'
},
{
type : 'empty',
prompt : '"General / Start-Up / Listening port" must have a value'
}
]
},
sonarr_validated_checkbox : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'checked',
prompt : '"Sonarr / Connection settings / Test" must be successful before going further'
}
]
},
settings_sonarr_ip : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'empty',
prompt : '"Sonarr / Connection settings / Hostname or IP address" must have a value'
}
]
},
settings_sonarr_port : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'integer[1..65535]',
prompt : '"Sonarr / Connection settings / Listening port" must be an integer between 1 and 65535'
},
{
type : 'empty',
prompt : '"Sonarr / Connection settings / Listening port" must have a value'
}
]
},
settings_sonarr_apikey : {
depends: 'settings_general_use_sonarr',
rules : [
{
type : 'exactLength[32]',
prompt : '"Sonarr / Connection settings / API key" must be exactly {ruleValue} characters'
},
{
type : 'empty',
prompt : '"Sonarr / Connection settings / API key" must have a value'
}
]
},
radarr_validated_checkbox : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'checked',
prompt : '"Radarr / Connection settings / Test" must be successful before going further'
}
]
},
settings_radarr_ip : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'empty',
prompt : '"Radarr / Connection settings / Hostname or IP address" must have a value'
}
]
},
settings_radarr_port : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'integer[1..65535]',
prompt : '"Radarr / Connection settings / Listening port" must be an integer between 1 and 65535'
},
{
type : 'empty',
prompt : '"Radarr / Connection settings / Listening port" must have a value'
}
]
},
settings_radarr_apikey : {
depends: 'settings_general_use_radarr',
rules : [
{
type : 'exactLength[32]',
prompt : '"Radarr / Connection settings / API key" must be exactly {ruleValue} characters'
},
{
type : 'empty',
prompt : '"Radarr / Connection settings / API key" must have a value'
}
]
},
settings_subliminal_providers : {
rules : [
{
type : 'minCount[1]',
prompt : '"Subtitles / Subtitles providers" must have at least one enabled provider'
}
]
},
settings_subliminal_languages : {
rules : [
{
type : 'minCount[1]',
prompt : '"Subtitles / Subtitles languages / Enabled languages" must have at least one enabled language'
}
]
}
},
inline : false,
selector : {
message: '#form_validation_error'
},
on : 'change',
onFailure: function(){
$('#submit').addClass('disabled');
$('.prev2').addClass('disabled');
$('.prev3').addClass('disabled');
$('.next2').addClass('disabled');
$('.next3').addClass('disabled');
return false;
},
onSuccess: function(){
$('#submit').removeClass('disabled');
$('.prev2').removeClass('disabled');
$('.prev3').removeClass('disabled');
$('.next2').removeClass('disabled');
$('.next3').removeClass('disabled');
}
})
;

@ -224,7 +224,7 @@
% elif restart_required[0] == '1': % elif restart_required[0] == '1':
<div class='ui center aligned grid'><div class='fifteen wide column'><div class="ui red message">Bazarr need to be restarted to apply changes to general settings. Click <a href=# id="restart_link">here</a> to restart.</div></div></div> <div class='ui center aligned grid'><div class='fifteen wide column'><div class="ui red message">Bazarr need to be restarted to apply changes to general settings. Click <a href=# id="restart_link">here</a> to restart.</div></div></div>
% end % end
</div> </div>
</body> </body>
</html> </html>

@ -0,0 +1,753 @@
<div class="ui dividing header">Subtitles providers</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Addic7ed (require anti-captcha)</label>
</div>
<div class="one wide column">
<div id="addic7ed" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="addic7ed_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_addic7ed_username" type="text" value="{{settings.addic7ed.username if settings.addic7ed.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_addic7ed_password" type="password" value="{{settings.addic7ed.password if settings.addic7ed.password != None else ''}}">
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Random user-agents</label>
</div>
<div class="one wide column">
<div id="settings_addic7ed_random_agents" class="ui toggle checkbox" data-randomagents={{settings.addic7ed.getboolean('random_agents')}}>
<input type="checkbox" name="settings_addic7ed_random_agents">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Use random user agents" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Argenteam</label>
</div>
<div class="one wide column">
<div id="argenteam" 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="Spanish subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="argenteam_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Assrt</label>
</div>
<div class="one wide column">
<div id="assrt" 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="Chinese subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="assrt_option" class="ui grid container">
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Token</label>
</div>
<div class="six wide column">
<div class="ui fluid input">
<input name="settings_assrt_token" type="text" value="{{settings.assrt.token if settings.assrt.token != None else ''}}">
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>GreekSubtitles</label>
</div>
<div class="one wide column">
<div id="greeksubtitles" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Greek subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="greeksubtitles_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Hosszupuska</label>
</div>
<div class="one wide column">
<div id="hosszupuska" 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="Hungarian subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="hosszupuska_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Nekur</label>
</div>
<div class="one wide column">
<div id="nekur" 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="Latvian subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="nekur_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>LegendasTV</label>
</div>
<div class="one wide column">
<div id="legendastv" 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="legendastv_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_legendastv_username" type="text" value="{{settings.legendastv.username if settings.legendastv.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_legendastv_password" type="password" value="{{settings.legendastv.password if settings.legendastv.password != None else ''}}">
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Napiprojekt</label>
</div>
<div class="one wide column">
<div id="napiprojekt" 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="Polish subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="napiprojekt_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Napisy24</label>
</div>
<div class="one wide column">
<div id="napisy24" 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="Polish subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="napisy24_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_napisy24_username" type="text" value="{{settings.napisy24.username if settings.napisy24.username != None else ''}}">
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div data-tooltip="The provided credentials must have api access. Leave empty to use the defaults." data-inverted="" class="ui basic icon">
<i class="yellow warning circle large icon"></i>
</div>
</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_napisy24_password" type="password" value="{{settings.napisy24.password if settings.napisy24.password != None else ''}}">
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>OpenSubtitles</label>
</div>
<div class="one wide column">
<div id="opensubtitles" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="opensubtitles_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_opensubtitles_username" type="text" value="{{settings.opensubtitles.username if settings.opensubtitles.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_opensubtitles_password" type="password" value="{{settings.opensubtitles.password if settings.opensubtitles.password != None else ''}}">
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>VIP</label>
</div>
<div class="one wide column">
<div id="settings_opensubtitles_vip" class="ui toggle checkbox" data-osvip={{settings.opensubtitles.getboolean('vip')}}>
<input type="checkbox" name="settings_opensubtitles_vip">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="ad-free subs, 1000 subs/day, no-cache VIP server: http://v.ht/osvip" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Use SSL</label>
</div>
<div class="one wide column">
<div id="settings_opensubtitles_ssl" class="ui toggle checkbox" data-osssl={{settings.opensubtitles.getboolean('ssl')}}>
<input type="checkbox" name="settings_opensubtitles_ssl">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Use SSL to connect to OpenSubtitles" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Skip wrong FPS</label>
</div>
<div class="one wide column">
<div id="settings_opensubtitles_skip_wrong_fps" class="ui toggle checkbox" data-osfps={{settings.opensubtitles.getboolean('skip_wrong_fps')}}>
<input type="checkbox" name="settings_opensubtitles_skip_wrong_fps">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Skip subtitles with a mismatched FPS value; might lead to more results when disabled but also to more false-positives." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Podnapisi</label>
</div>
<div class="one wide column">
<div id="podnapisi" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="podnapisi_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subdivx</label>
</div>
<div class="one wide column">
<div id="subdivx" 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="Spanish subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subdivx_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subs.sab.bz</label>
</div>
<div class="one wide column">
<div id="subssabbz" 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="Bulgarian mostly subtitle provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subssabbz_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subscene</label>
</div>
<div class="one wide column">
<div id="subscene" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="subscene_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subscenter</label>
</div>
<div class="one wide column">
<div id="subscenter" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="subscenter_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subsunacs.net</label>
</div>
<div class="one wide column">
<div id="subsunacs" 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="Bulgarian mostly subtitle provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subsunacs_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subs4Free</label>
</div>
<div class="one wide column">
<div id="subs4free" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Greek subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subs4free_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subs4Series</label>
</div>
<div class="one wide column">
<div id="subs4series" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Greek subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subs4series_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>subtitri.id.lv</label>
</div>
<div class="one wide column">
<div id="subtitriid" 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="Latvian subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subtitriid_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>SubZ</label>
</div>
<div class="one wide column">
<div id="subz" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Greek subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="subz_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Supersubtitles</label>
</div>
<div class="one wide column">
<div id="supersubtitles" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="supersubtitles_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Titlovi (require anti-captcha)</label>
</div>
<div class="one wide column">
<div id="titlovi" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="titlovi_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>TVSubtitles</label>
</div>
<div class="one wide column">
<div id="tvsubtitles" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="tvsubtitles_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>XSubs</label>
</div>
<div class="one wide column">
<div id="xsubs" class="ui toggle checkbox provider">
<input type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Greek subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="xsubs_option" class="ui grid container">
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Username</label>
</div>
<div class="six wide column">
<div class="ui fluid input">
<input name="settings_xsubs_username" type="text" value="{{settings.xsubs.username if settings.xsubs.username != None else ''}}">
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned six wide column">
<label>Password</label>
</div>
<div class="six wide column">
<div class="ui fluid input">
<input name="settings_xsubs_password" type="password" value="{{settings.xsubs.password if settings.xsubs.password != None else ''}}">
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Zimuku</label>
</div>
<div class="one wide column">
<div id="zimuku" 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="Chinese subtitles provider." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div id="zimuku_option" class="ui grid container">
</div>
<div class="middle aligned row">
<div class="eleven wide column">
<div class='field' hidden>
<select name="settings_subliminal_providers" id="settings_providers" multiple="" class="ui fluid search selection dropdown">
<option value="">Providers</option>
%enabled_providers = []
%providers = settings.general.enabled_providers.lower().split(',')
%for provider in settings_providers:
<option value="{{provider}}">{{provider}}</option>
%end
%for provider in providers:
%enabled_providers.append(str(provider))
%end
</select>
</div>
</div>
</div>
</div>
</div>
<script>
if ($('#settings_addic7ed_random_agents').data("randomagents") === "True") {
$("#settings_addic7ed_random_agents").checkbox('check');
} else {
$("#settings_addic7ed_random_agents").checkbox('uncheck');
}
if ($('#settings_opensubtitles_vip').data("osvip") === "True") {
$("#settings_opensubtitles_vip").checkbox('check');
} else {
$("#settings_opensubtitles_vip").checkbox('uncheck');
}
if ($('#settings_opensubtitles_ssl').data("osssl") === "True") {
$("#settings_opensubtitles_ssl").checkbox('check');
} else {
$("#settings_opensubtitles_ssl").checkbox('uncheck');
}
if ($('#settings_opensubtitles_skip_wrong_fps').data("osfps") === "True") {
$("#settings_opensubtitles_skip_wrong_fps").checkbox('check');
} else {
$("#settings_opensubtitles_skip_wrong_fps").checkbox('uncheck');
}
$('#settings_providers').dropdown('clear');
$('#settings_providers').dropdown('set selected',{{!enabled_providers}});
$('#settings_providers').dropdown();
$('#settings_providers').dropdown('setting', 'onChange', function(){
$('.form').form('validate field', 'settings_subliminal_providers');
});
$("#settings_providers > option").each(function() {
$('#'+$(this).val()+'_option').hide();
});
$("#settings_providers > option:selected").each(function() {
$('[id='+this.value+']').checkbox('check');
$('#'+$(this).val()+'_option').show();
});
$('.provider').checkbox({
onChecked: function() {
$('#settings_providers').dropdown('set selected', $(this).parent().attr('id'));
$('#'+$(this).parent().attr('id')+'_option').show();
},
onUnchecked: function() {
$('#settings_providers').dropdown('remove selected', $(this).parent().attr('id'));
$('#'+$(this).parent().attr('id')+'_option').hide();
}
});
</script>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,834 @@
<div class="ui dividing header">Start-Up</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Listening IP address</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input name="settings_general_ip" type="text" value="{{settings.general.ip}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Valid IP4 address or '0.0.0.0' for all interfaces" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Listening port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input name="settings_general_port" type="text" value="{{settings.general.port}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Valid TCP port (default: 6767)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Base URL</label>
</div>
<div class="five wide column">
<div class="ui fluid input">
%if settings.general.base_url is None:
% base_url = "/"
%else:
% base_url = settings.general.base_url
%end
<input name="settings_general_baseurl" type="text" value="{{base_url}}">
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="For reverse proxy support, default is '/'" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Enable debug logging</label>
</div>
<div class="five wide column">
<div id="settings_debug" class="ui toggle checkbox" data-debug={{settings.general.getboolean('debug')}}>
<input name="settings_general_debug" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Debug logging should only be enabled temporarily" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div id="chmod_enabled" class="middle aligned row">
<div class="right aligned four wide column">
<label>Enable chmod</label>
</div>
<div class="five wide column">
<div id="settings_chmod_enabled" class="ui toggle checkbox" data-chmod={{settings.general.getboolean('chmod_enabled')}}>
<input name="settings_general_chmod_enabled" type="checkbox">
<label></label>
</div>
</div>
</div>
<div id="chmod" class="middle aligned row">
<div class="right aligned four wide column">
<label>Set subtitle file permissions to</label>
</div>
<div class="five wide column">
<div class='field'>
<div id="settings_chmod" class="ui fluid input">
<input name="settings_general_chmod" type="text"
value={{ settings.general.chmod }}>
<label></label>
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Must be 4 digit octal, e.g.: 0775" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Page size</label>
</div>
<div class="five wide column">
<select name="settings_page_size" id="settings_page_size" class="ui fluid selection dropdown">
<option value="">Page Size</option>
<option value="-1">Unlimited</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="250">250</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="How many items to show in a list." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Proxy settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Proxy type</label>
</div>
<div class="five wide column">
<select name="settings_proxy_type" id="settings_proxy_type" class="ui fluid selection dropdown">
<option value="None">None</option>
<option value="http">HTTP(S)</option>
<option value="socks4">Socks4</option>
<option value="socks5">Socks5</option>
</select>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Type of your proxy." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="proxy_option middle aligned row">
<div class="right aligned four wide column">
<label>Hostname</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_proxy_url" name="settings_proxy_url" type="text" value="{{settings.proxy.url}}">
</div>
</div>
</div>
</div>
<div class="proxy_option middle aligned row">
<div class="right aligned four wide column">
<label>Port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_proxy_port" name="settings_proxy_port" type="text" value="{{settings.proxy.port}}">
</div>
</div>
</div>
</div>
<div class="proxy_option middle aligned row">
<div class="right aligned four wide column">
<label>Username</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_proxy_username" name="settings_proxy_username" type="text" value="{{settings.proxy.username}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="UYou only need to enter a username and password if one is required. Leave them blank otherwise" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="proxy_option middle aligned row">
<div class="right aligned four wide column">
<label>Password</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_proxy_password" name="settings_proxy_password" type="password" value="{{settings.proxy.password}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="You only need to enter a username and password if one is required. Leave them blank otherwise" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="proxy_option middle aligned row">
<div class="right aligned four wide column">
<label>Ignored Addresses</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_proxy_exclude" name="settings_proxy_exclude" type="text" value="{{settings.proxy.exclude}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Use ',' as a separator, and '*.' as a wildcard for subdomains" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Security settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Authentication</label>
</div>
<div class="five wide column">
<select name="settings_auth_type" id="settings_auth_type" class="ui fluid selection dropdown">
<option value="None">None</option>
<option value="basic">Basic (Browser Popup)</option>
<option value="form">Forms (Login Page)</option>
</select>
<label></label>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Require Username and Password to access Bazarr." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="auth_option middle aligned row">
<div class="right aligned four wide column">
<label>Username</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_auth_username" name="settings_auth_username" type="text" autocomplete="nope" value="{{settings.auth.username}}">
</div>
</div>
</div>
</div>
<div class="auth_option middle aligned row">
<div class="right aligned four wide column">
<label>Password</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_auth_password" name="settings_auth_password" type="password" autocomplete="new-password" value="{{settings.auth.password}}">
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Authentication send username and password in clear over the network. You should add SSL encryption trough a reverse proxy." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Integration settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Use Sonarr</label>
</div>
<div class="one wide column">
<div id="settings_use_sonarr" class="ui toggle checkbox" data-enabled={{settings.general.getboolean('use_sonarr')}}>
<input name="settings_general_use_sonarr" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Enable Sonarr integration." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Use Radarr</label>
</div>
<div class="one wide column">
<div id="settings_use_radarr" class="ui toggle checkbox" data-enabled={{settings.general.getboolean('use_radarr')}}>
<input name="settings_general_use_radarr" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Enable Radarr integration." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Path Mappings for shows</div>
<div class="twelve wide column">
<div class="ui grid">
%import ast
%if settings.general.path_mappings is not None:
% path_substitutions = ast.literal_eval(settings.general.path_mappings)
%else:
% path_substitutions = []
%end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Sonarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Root path to the directory Sonarr accesses." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="two wide center aligned column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Bazarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Path that Bazarr should use to access the same directory remotely." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
%for x in range(0, 5):
% path = []
% try:
% path = path_substitutions[x]
% except IndexError:
% path = ["", ""]
% end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_sourcepath" type="text" value="{{path[0]}}">
</div>
</div>
<div class="center aligned column">
<i class="arrow circle right icon"></i>
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_destpath" type="text" value="{{path[1]}}">
</div>
</div>
</div>
%end
</div>
</div>
<div class="ui dividing header">Path Mappings for movies</div>
<div class="twelve wide column">
<div class="ui grid">
%import ast
%if settings.general.path_mappings_movie is not None:
% path_substitutions_movie = ast.literal_eval(settings.general.path_mappings_movie)
%else:
% path_substitutions_movie = []
%end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Radarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Root path to the directory Radarr accesses." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="two wide center aligned column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Bazarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Path that Bazarr should use to access the same directory remotely." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
%for x in range(0, 5):
% path_movie = []
% try:
% path_movie = path_substitutions_movie[x]
% except IndexError:
% path_movie = ["", ""]
% end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_sourcepath_movie" type="text" value="{{path_movie[0]}}">
</div>
</div>
<div class="center aligned column">
<i class="arrow circle right icon"></i>
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_destpath_movie" type="text" value="{{path_movie[1]}}">
</div>
</div>
</div>
%end
</div>
</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>
</div>
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<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')}}>
<input name="settings_general_use_postprocessing" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Enable the post-processing execution after downloading a subtitles." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row postprocessing">
<div class="right aligned four wide column">
<label>Post-processing command</label>
</div>
<div class="five wide column">
<div id="settings_general_postprocessing_cmd_div" class="ui fluid input">
<input name="settings_general_postprocessing_cmd" type="text" value="{{settings.general.postprocessing_cmd if settings.general.postprocessing_cmd != None else ''}}">
</div>
</div>
</div>
<div class="middle aligned row postprocessing">
<div class="right aligned four wide column">
<label>Variables you can use in your command (include the double curly brace):</label>
</div>
<div class="ten wide column">
<div class="ui list">
<div class="item">
<div class="header">&lbrace;&lbrace;directory&rbrace;&rbrace;</div>
The full path of the episode file parent directory.
</div>
<div class="item">
<div class="header">&lbrace;&lbrace;episode&rbrace;&rbrace;</div>
The full path of the episode file.
</div>
<div class="item">
<div class="header">&lbrace;&lbrace;episode_name&rbrace;&rbrace;</div>
The filename of the episode without parent directory or extension.
</div>
<div class="item">
<div class="header">&lbrace;&lbrace;subtitles&rbrace;&rbrace;</div>
The full path of the subtitles file.
</div>
<div class="item">
<div class="header">&lbrace;&lbrace;subtitles_language&rbrace;&rbrace;</div>
The language of the subtitles file.
</div>
<div class="item">
<div class="header">&lbrace;&lbrace;subtitles_language_code2&rbrace;&rbrace;</div>
The 2-letter ISO-639 language code of the subtitles language.
</div>
<div class="item">
<div class="header">&lbrace;&lbrace;subtitles_language_code3&rbrace;&rbrace;</div>
The 3-letter ISO-639 language code of the subtitles language.
</div>
</div>
</div>
</div>
</div>
</div>
<div id="div_update">
<div class="ui dividing header">Updates</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row" id="div_branch">
<div class="right aligned four wide column">
<label>Branch</label>
</div>
<div class="five wide column">
<select name="settings_general_branch" id="settings_branch" class="ui fluid selection dropdown">
<option value="">Branch</option>
<option value="master">master</option>
<option value="development">development</option>
</select>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Only select development branch if you want to live on the edge." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Automatic</label>
</div>
<div class="one wide column">
<div id="settings_automatic_div" class="ui toggle checkbox" data-automatic={{settings.general.getboolean('auto_update')}}>
<input name="settings_general_automatic" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Automatically download and install updates. You will still be able to install from System: Tasks" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Restart after update</label>
</div>
<div class="one wide column">
<div id="settings_update_restart" class="ui toggle checkbox"
data-update-restart={{settings.general.getboolean('update_restart')}}>
<input name="settings_general_update_restart" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip="Automatically restart after download and install updates. You will still be able to restart manualy"
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
% from get_args import args
% if args.no_update:
$("#div_update").hide();
% elif args.release_update:
$("#div_branch").hide();
% end
% import sys
% if sys.platform.startswith('win'):
$("#chmod").hide();
$("#chmod_enabled").hide();
% end
if ($('#settings_automatic_div').data("automatic") === "True") {
$("#settings_automatic_div").checkbox('check');
} else {
$("#settings_automatic_div").checkbox('uncheck');
}
if ($('#settings_update_restart').data("update-restart") === "True") {
$("#settings_update_restart").checkbox('check');
} else {
$("#settings_update_restart").checkbox('uncheck');
}
if ($('#settings_debug').data("debug") === "True") {
$("#settings_debug").checkbox('check');
} else {
$("#settings_debug").checkbox('uncheck');
}
if ($('#settings_chmod_enabled').data("chmod") === "True") {
$("#settings_chmod_enabled").checkbox('check');
} else {
$("#settings_chmod_enabled").checkbox('uncheck');
}
if ($('#settings_use_postprocessing').data("postprocessing") === "True") {
$("#settings_use_postprocessing").checkbox('check');
$("#settings_general_postprocessing_cmd_div").removeClass('disabled');
} else {
$("#settings_use_postprocessing").checkbox('uncheck');
$("#settings_general_postprocessing_cmd_div").addClass('disabled');
}
$("#settings_use_postprocessing").on('change', function(i, obj) {
if ($("#settings_use_postprocessing").checkbox('is checked')) {
$("#settings_general_postprocessing_cmd_div").removeClass('disabled');
} else {
$("#settings_general_postprocessing_cmd_div").addClass('disabled');
}
});
if ($('#settings_use_postprocessing').data("postprocessing") === "True") {
$('.postprocessing').show();
} else {
$('.postprocessing').hide();
}
$('#settings_use_postprocessing').checkbox({
onChecked: function() {
$('.postprocessing').show();
},
onUnchecked: function() {
$('.postprocessing').hide();
}
});
if ($('#settings_use_sonarr').data("enabled") === "True") {
$("#settings_use_sonarr").checkbox('check');
$("#sonarr_tab").removeClass('disabled');
} else {
$("#settings_use_sonarr").checkbox('uncheck');
$("#sonarr_tab").addClass('disabled');
}
$('#settings_use_sonarr').checkbox({
onChecked: function() {
$("#sonarr_tab").removeClass('disabled');
$('#sonarr_validated').checkbox('uncheck');
$('.form').form('validate form');
$('#loader').removeClass('active');
},
onUnchecked: function() {
$("#sonarr_tab").addClass('disabled');
}
});
if ($('#settings_use_radarr').data("enabled") === "True") {
$("#settings_use_radarr").checkbox('check');
$("#radarr_tab").removeClass('disabled');
} else {
$("#settings_use_radarr").checkbox('uncheck');
$("#radarr_tab").addClass('disabled');
}
$('#settings_use_radarr').checkbox({
onChecked: function() {
$("#radarr_tab").removeClass('disabled');
$('#radarr_validated').checkbox('uncheck');
$('.form').form('validate form');
$('#loader').removeClass('active');
},
onUnchecked: function() {
$("#radarr_tab").addClass('disabled');
}
});
if ($('#settings_chmod_enabled').data("chmod") === "True") {
$('#chmod').show();
} else {
$('#chmod').hide();
}
$('#settings_chmod_enabled').checkbox({
onChecked: function() {
$('#chmod').show();
},
onUnchecked: function() {
$('#chmod').hide();
}
});
if ($('#settings_auth_type').val() === "None") {
$('.auth_option').hide();
}
$('#settings_auth_type').dropdown('setting', 'onChange', function(){
if ($('#settings_auth_type').val() === "None") {
$('.auth_option').hide();
} else {
$('.auth_option').show();
}
});
// Load default value for Settings_auth_type
$('#settings_auth_type').dropdown('clear');
$('#settings_auth_type').dropdown('set selected','{{!settings.auth.type}}');
// Remove value from Password input when changing to Form login to prevent bad password saving
$("#settings_auth_type").on('change', function() {
if ($(this).val() === 'form'){
$('#settings_auth_password').val('');
} else {
$('#settings_auth_password').val('{{settings.auth.password}}');
}
});
$('#settings_loglevel').dropdown('clear');
$('#settings_loglevel').dropdown('set selected','{{!settings.general.getboolean('debug')}}');
$('#settings_page_size').dropdown('clear');
$('#settings_page_size').dropdown('set selected','{{!settings.general.page_size}}');
$('#settings_proxy_type').dropdown('clear');
$('#settings_proxy_type').dropdown('set selected','{{!settings.proxy.type}}');
$('#settings_branch').dropdown('clear');
$('#settings_branch').dropdown('set selected','{{!settings.general.branch}}');
$('#settings_auth_username').on('keyup', function() {
$('#settings_auth_password').val('');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
</script>

@ -0,0 +1,72 @@
<div class="ui dividing header">Notifications settings</div>
<div class="twelve wide column">
<div class="ui info message">
<p>Thanks to caronc for his work on <a href="https://github.com/caronc/apprise" target="_blank">apprise</a> on which is based the notifications system.</p>
</div>
<div class="ui info message">
<p>Please follow instructions on his <a href="https://github.com/caronc/apprise/wiki" target="_blank">wiki</a> to configure your notifications providers.</p>
</div>
<div class="ui grid">
%for notifier in settings_notifier:
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>{{notifier[0]}}</label>
</div>
<div class="one wide column">
<div id="settings_notifier_{{notifier[0]}}_enabled" class="notifier_enabled ui toggle checkbox" data-notifier-url-div="settings_notifier_{{notifier[0]}}_url_div" data-enabled={{notifier[2]}}>
<input name="settings_notifier_{{notifier[0]}}_enabled" type="checkbox">
<label></label>
</div>
</div>
<div class="eight wide column">
<div class='field'>
<div id="settings_notifier_{{notifier[0]}}_url_div" class="ui fluid input">
<input name="settings_notifier_{{notifier[0]}}_url" type="text" value="{{notifier[1] if notifier[1] != None else ''}}">
<div class="test_notification ui blue button" data-notification="{{notifier[1]}}">Test Notification</div>
</div>
</div>
</div>
</div>
%end
</div>
</div>
<script>
$('.test_notification').on('click', function() {
const url_field = $(this).prev().val();
const url_protocol = url_field.split(':')[0];
const url_string = url_field.split('://')[1];
$.ajax({
url: "{{base_url}}test_notification/" + url_protocol + "/" + encodeURIComponent(url_string),
beforeSend: function () {
$('#loader').addClass('active');
},
complete: function () {
$('#loader').removeClass('active');
},
cache: false
});
});
$('.notifier_enabled').each(function() {
if ($(this).data("enabled") === 1) {
$(this).checkbox('check');
$('[id=\"' + $(this).data("notifier-url-div") + '\"]').removeClass('disabled');
} else {
$(this).checkbox('uncheck');
$('[id=\"' + $(this).data("notifier-url-div") + '\"]').addClass('disabled');
}
});
$('.notifier_enabled').on('change', function() {
if ($(this).checkbox('is checked')) {
$('[id=\"' + $(this).data("notifier-url-div") + '\"]').removeClass('disabled');
} else {
$('[id=\"' + $(this).data("notifier-url-div") + '\"]').addClass('disabled');
}
});
</script>

@ -0,0 +1,198 @@
<div class="ui dividing header">Connection settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Settings validation:</label>
</div>
<div class="two wide column">
<button id="radarr_validate" class="test ui blue button" type="button">
Test
</button>
</div>
<div class="seven wide column">
<div id="radarr_validated" class="ui read-only checkbox">
<input id="radarr_validated_checkbox" type="checkbox">
<label id="radarr_validation_result">Not tested recently</label>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Hostname or IP address</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_radarr_ip" name="settings_radarr_ip" type="text" class="radarr_config" value="{{settings.radarr.ip}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Hostname or IP4 address of Radarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Listening port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_radarr_port" name="settings_radarr_port" type="text" class="radarr_config" value="{{settings.radarr.port}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="TCP port of Radarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Base URL</label>
</div>
<div class="five wide column">
<div class="ui fluid input">
<input id="settings_radarr_baseurl" name="settings_radarr_baseurl" type="text" class="radarr_config" value="{{settings.radarr.base_url}}">
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Base URL for Radarr (default: '/')" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>SSL enabled</label>
</div>
<div class="one wide column">
<div id="radarr_ssl_div" class="ui toggle checkbox" data-ssl={{settings.radarr.getboolean('ssl')}}>
<input id="settings_radarr_ssl" name="settings_radarr_ssl" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>API key</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_radarr_apikey" name="settings_radarr_apikey" type="text" class="radarr_config" value="{{settings.radarr.apikey}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="API key for Radarr (32 alphanumeric characters)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Download only monitored</label>
</div>
<div class="one wide column">
<div id="settings_only_monitored_radarr" class="ui toggle checkbox" data-monitored={{settings.radarr.getboolean('only_monitored')}}>
<input name="settings_radarr_only_monitored" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Automatic download of subtitles will happen only for monitored movies in Radarr." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Synchronization</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Full sync frequency</label>
</div>
<div class="five wide column">
<div class='field'>
<select name="settings_radarr_sync" id="settings_radarr_sync" class="ui fluid selection dropdown">
<option value="Manually">Manually</option>
<option value="Daily">Daily (at 5am)</option>
<option value="Weekly">Weekly (sunday at 5am)</option>
</select>
</div>
</div>
</div>
</div>
</div>
<script>
if ($('#radarr_ssl_div').data("ssl") === "True") {
$("#radarr_ssl_div").checkbox('check');
} else {
$("#radarr_ssl_div").checkbox('uncheck');
}
if ($('#settings_only_monitored_radarr').data("monitored") === "True") {
$("#settings_only_monitored_radarr").checkbox('check');
} else {
$("#settings_only_monitored_radarr").checkbox('uncheck');
}
$('#settings_radarr_sync').dropdown('clear');
$('#settings_radarr_sync').dropdown('set selected','{{!settings.radarr.full_update}}');
$('#radarr_validate').on('click', function() {
if ($('#radarr_ssl_div').checkbox('is checked')) {
protocol = 'https';
} else {
protocol = 'http';
}
const radarr_url = $('#settings_radarr_ip').val() + ":" + $('#settings_radarr_port').val() + $('#settings_radarr_baseurl').val().replace(/\/$/, "") + "/api/system/status?apikey=" + $('#settings_radarr_apikey').val();
$.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');
$('.form').form('validate form');
$('#loader').removeClass('active');
} else {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('Test failed').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
}
});
});
$('.radarr_config').on('keyup', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Radarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$('#settings_radarr_ssl').on('change', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Radarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$("#radarr_validated").checkbox('check');
</script>

@ -0,0 +1,200 @@
<div class="ui dividing header">Connection settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Settings validation:</label>
</div>
<div class="two wide column">
<button id="sonarr_validate" class="test ui blue button" type="button">
Test
</button>
</div>
<div class="seven wide column">
<div id="sonarr_validated" class="ui read-only checkbox">
<input id="sonarr_validated_checkbox" type="checkbox">
<label id="sonarr_validation_result">Not tested recently</label>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Hostname or IP address</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_sonarr_ip" name="settings_sonarr_ip" class="sonarr_config" type="text" value="{{settings.sonarr.ip}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Hostname or IP4 address of Sonarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Listening port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_sonarr_port" name="settings_sonarr_port" class="sonarr_config" type="text" value="{{settings.sonarr.port}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="TCP port of Sonarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Base URL</label>
</div>
<div class="five wide column">
<div class="ui fluid input">
<input id="settings_sonarr_baseurl" name="settings_sonarr_baseurl" class="sonarr_config" type="text" value="{{settings.sonarr.base_url}}">
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Base URL for Sonarr (default: '/')" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>SSL enabled</label>
</div>
<div class="one wide column">
<div id="sonarr_ssl_div" class="ui toggle checkbox" data-ssl={{settings.sonarr.getboolean('ssl')}}>
<input id="settings_sonarr_ssl" name="settings_sonarr_ssl" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>API key</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_sonarr_apikey" name="settings_sonarr_apikey" class="sonarr_config" type="text" value="{{settings.sonarr.apikey}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="API key for Sonarr (32 alphanumeric characters)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Download only monitored</label>
</div>
<div class="one wide column">
<div id="settings_only_monitored_sonarr" class="ui toggle checkbox" data-monitored={{settings.sonarr.getboolean('only_monitored')}}>
<input name="settings_sonarr_only_monitored" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Automatic download of subtitles will happen only for monitored episodes in Sonarr." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Synchronization</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Full sync frequency</label>
</div>
<div class="five wide column">
<div class='field'>
<select name="settings_sonarr_sync" id="settings_sonarr_sync" class="ui fluid selection dropdown">
<option value="Manually">Manually</option>
<option value="Daily">Daily (at 4am)</option>
<option value="Weekly">Weekly (sunday at 4am)</option>
</select>
</div>
</div>
</div>
</div>
</div>
<script>
if ($('#sonarr_ssl_div').data("ssl") === "True") {
$("#sonarr_ssl_div").checkbox('check');
} else {
$("#sonarr_ssl_div").checkbox('uncheck');
}
if ($('#settings_only_monitored_sonarr').data("monitored") === "True") {
$("#settings_only_monitored_sonarr").checkbox('check');
} else {
$("#settings_only_monitored_sonarr").checkbox('uncheck');
}
$('#settings_sonarr_sync').dropdown('clear');
$('#settings_sonarr_sync').dropdown('set selected','{{!settings.sonarr.full_update}}');
$('#sonarr_validate').on('click', function() {
if ($('#sonarr_ssl_div').checkbox('is checked')) {
protocol = 'https';
} else {
protocol = 'http';
}
const sonarr_url = $('#settings_sonarr_ip').val() + ":" + $('#settings_sonarr_port').val() + $('#settings_sonarr_baseurl').val().replace(/\/$/, "") + "/api/system/status?apikey=" + $('#settings_sonarr_apikey').val();
$.getJSON("{{base_url}}test_url/" + protocol + "/" + encodeURIComponent(sonarr_url), function (data) {
if (data.status) {
$('#sonarr_validated').checkbox('check');
$('#sonarr_validation_result').text('Test successful: Sonarr v' + data.version).css('color', 'green');
$('.form').form('validate form');
$('#loader').removeClass('active');
} else {
$('#sonarr_validated').checkbox('uncheck');
$('#sonarr_validation_result').text('Test failed').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
}
});
});
$('.sonarr_config').on('keyup', function() {
$('#sonarr_validated').checkbox('uncheck');
$('#sonarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$('#settings_sonarr_ssl').on('change', function() {
$('#sonarr_validated').checkbox('uncheck');
$('#sonarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$("#sonarr_validated").checkbox('check');
</script>

@ -0,0 +1,726 @@
<div class="ui dividing header">Subtitles options</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<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')}}>
<input name="settings_general_scenename" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Use the scene name from Sonarr/Radarr if available to circumvent usage of episode file renaming." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Minimum score for episodes</label>
</div>
<div class="two wide column">
<div class='field'>
<div class="ui input">
<input name="settings_general_minimum_score" type="number" min="0" max="100" step="1" onkeydown="return false" value="{{settings.general.minimum_score}}">
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Minimum score for an episode subtitle to be downloaded (0 to 100)." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Minimum score for movies</label>
</div>
<div class="two wide column">
<div class='field'>
<div class="ui input">
<input name="settings_general_minimum_score_movies" type="number" min="0" max="100" step="1" onkeydown="return false" value="{{settings.general.minimum_score_movie}}">
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Minimum score for a movie subtitle to be downloaded (0 to 100)." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subtitle folder</label>
</div>
<div class="five wide column">
<select name="settings_subfolder" id="settings_subfolder"
class="ui fluid selection dropdown">
<option value="current">Alongside media file</option>
<option value="relative">Relative path to media file</option>
<option value="absolute">Absolute path</option>
</select>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Choose folder where you want to store/read the subtitles'
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row subfolder">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Custom Subtitle folder</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_subfolder_custom" name="settings_subfolder_custom"
type="text" value="{{ settings.general.subfolder_custom }}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Choose your own folder for the subtitles' data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Upgrade previously downloaded subtitles</label>
</div>
<div class="one wide column">
<div id="settings_upgrade_subs" class="ui toggle checkbox" data-upgrade={{settings.general.getboolean('upgrade_subs')}}>
<input name="settings_upgrade_subs" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Schedule a task that run every 12 hours to upgrade subtitles previously downloaded by Bazarr.'
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row upgrade_subs">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Number of days to go back in history to upgrade subtitles (up to 30)</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_days_to_upgrade_subs" name="settings_days_to_upgrade_subs"
type="text" value="{{ settings.general.days_to_upgrade_subs }}">
</div>
</div>
</div>
</div>
<div class="middle aligned row upgrade_subs">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Upgrade manually downloaded subtitles</label>
</div>
<div class="one wide column">
<div id="settings_upgrade_manual" class="ui toggle checkbox" data-upgrade-manual={{settings.general.getboolean('upgrade_manual')}}>
<input name="settings_upgrade_manual" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Enable or disable upgrade of manually searched and downloaded subtitles.'
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Use embedded subtitles</label>
</div>
<div class="one wide column">
<div id="settings_embedded" class="ui toggle checkbox" data-embedded={{settings.general.getboolean('use_embedded_subs')}}>
<input name="settings_general_embedded" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Use embedded subtitles in media files when determining missing ones." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Ignore embedded PGS subtitles</label>
</div>
<div class="one wide column">
<div id="settings_ignore_pgs" class="ui toggle checkbox" data-ignorepgs={{settings.general.getboolean('ignore_pgs_subs')}}>
<input name="settings_general_ignore_pgs" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Ignores pgs subtitles in embedded subtitles detection. Only relevant if 'Use embedded subtitles' is enabled." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Adaptive searching</label>
</div>
<div class="one wide column">
<div id="settings_adaptive_searching" class="ui toggle checkbox" data-adaptive={{settings.general.getboolean('adaptive_searching')}}>
<input name="settings_general_adaptive_searching" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="When searching for subtitles, Bazarr will search less frequently after sometime to limit call to providers." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Search enabled providers simultaneously</label>
</div>
<div class="one wide column">
<div id="settings_multithreading" class="ui toggle checkbox"
data-multithreading={{ settings.general.getboolean('multithreading') }}>
<input name="settings_general_multithreading" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Search multi providers at once (Don't choose this on low powered devices)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Anti-captcha options</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Provider</label>
</div>
<div class="five wide column">
<select name="settings_anti_captcha_provider" id="settings_anti_captcha_provider" class="ui fluid selection dropdown">
<option value="None">None</option>
<option value="anti-captcha">Anti-Captcha</option>
<option value="death-by-captcha">Death by Captcha</option>
</select>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Choose the anti-captcha provider you want to use.'
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row anticaptcha">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Provider website</label>
</div>
<div class="five wide column">
<a href="http://getcaptchasolution.com/eixxo1rsnw" target="_blank">Anti-Captcha.com</a>
</div>
</div>
<div class="middle aligned row anticaptcha">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Account Key</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_anti_captcha_key" name="settings_anti_captcha_key"
type="text" value="{{ settings.anticaptcha.anti_captcha_key }}">
</div>
</div>
</div>
</div>
<div class="middle aligned row deathbycaptcha">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Provider website</label>
</div>
<div class="five wide column">
<a href="https://www.deathbycaptcha.com" target="_blank">DeathByCaptcha.com</a>
</div>
</div>
<div class="middle aligned row deathbycaptcha">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Username</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_death_by_captcha_username" name="settings_death_by_captcha_username"
type="text" value="{{ settings.deathbycaptcha.username }}">
</div>
</div>
</div>
</div>
<div class="middle aligned row deathbycaptcha">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Password</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_death_by_captcha_password" name="settings_death_by_captcha_password"
type="password" value="{{ settings.deathbycaptcha.password }}">
</div>
</div>
</div>
</div>
</div>
</div>
% include('providers.tpl')
<div class="ui dividing header">Subtitles languages</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Single language</label>
</div>
<div class="one wide column">
<div id="settings_single_language" class="ui toggle checkbox" data-single-language={{settings.general.getboolean('single_language')}}>
<input name="settings_general_single_language" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Download a single subtitles file and don't add the language code to the filename." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Enabled languages</label>
</div>
<div class="eleven wide column">
<div class='field'>
<select name="settings_subliminal_languages" id="settings_languages" multiple="" class="ui fluid search selection dropdown">
<option value="">Languages</option>
%enabled_languages = []
%for language in settings_languages:
<option value="{{language[1]}}">{{language[2]}}</option>
%if language[3] == True:
% enabled_languages.append(str(language[1]))
%end
%end
</select>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Series default settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Default enabled</label>
</div>
<div class="one wide column">
<div class="nine wide column">
<div id="settings_serie_default_enabled_div" class="ui toggle checkbox" data-enabled="{{settings.general.getboolean('serie_default_enabled')}}">
<input name="settings_serie_default_enabled" id="settings_serie_default_enabled" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Apply only to series added to Bazarr after enabling this option." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Languages</label>
</div>
<div class="eleven wide column">
<div class='field'>
<select name="settings_serie_default_languages" id="settings_serie_default_languages" multiple="" class="ui fluid search selection dropdown">
%if settings.general.getboolean('single_language') is False:
<option value="">Languages</option>
%else:
<option value="None">None</option>
%end
</select>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Hearing-impaired</label>
</div>
<div class="eleven wide column">
<div class="nine wide column">
<div id="settings_serie_default_hi_div" class="ui toggle checkbox" data-hi="{{settings.general.getboolean('serie_default_hi')}}">
<input name="settings_serie_default_hi" id="settings_serie_default_hi" type="checkbox">
<label></label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Movies default settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Default enabled</label>
</div>
<div class="one wide column">
<div class="nine wide column">
<div id="settings_movie_default_enabled_div" class="ui toggle checkbox" data-enabled="{{settings.general.getboolean('movie_default_enabled')}}">
<input name="settings_movie_default_enabled" id="settings_movie_default_enabled" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Apply only to movies added to Bazarr after enabling this option." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div id="movie_default_languages_label" class="right aligned four wide column">
<label>Languages</label>
</div>
<div class="eleven wide column">
<div class='field'>
<select name="settings_movie_default_languages" id="settings_movie_default_languages" multiple="" class="ui fluid search selection dropdown">
%if settings.general.getboolean('single_language') is False:
<option value="">Languages</option>
%else:
<option value="None">None</option>
%end
</select>
</div>
</div>
</div>
<div class="middle aligned row">
<div id="movie_default_hi_label" class="right aligned four wide column">
<label>Hearing-impaired</label>
</div>
<div class="eleven wide column">
<div class="nine wide column">
<div id="settings_movie_default_hi_div" class="ui toggle checkbox" data-hi="{{settings.general.getboolean('movie_default_hi')}}">
<input name="settings_movie_default_hi" id="settings_movie_default_hi" type="checkbox">
<label></label>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
if ($('#settings_single_language').data("single-language") === "True") {
$("#settings_single_language").checkbox('check');
} else {
$("#settings_single_language").checkbox('uncheck');
}
if ($('#settings_scenename').data("scenename") === "True") {
$("#settings_scenename").checkbox('check');
} else {
$("#settings_scenename").checkbox('uncheck');
}
if ($('#settings_upgrade_subs').data("upgrade") === "True") {
$("#settings_upgrade_subs").checkbox('check');
} else {
$("#settings_upgrade_subs").checkbox('uncheck');
}
if ($('#settings_upgrade_manual').data("upgrade-manual") === "True") {
$("#settings_upgrade_manual").checkbox('check');
} else {
$("#settings_upgrade_manual").checkbox('uncheck');
}
if ($('#settings_embedded').data("embedded") === "True") {
$("#settings_embedded").checkbox('check');
} else {
$("#settings_embedded").checkbox('uncheck');
}
if ($('#settings_ignore_pgs').data("ignorepgs") === "True") {
$("#settings_ignore_pgs").checkbox('check');
} else {
$("#settings_ignore_pgs").checkbox('uncheck');
}
if ($('#settings_adaptive_searching').data("adaptive") === "True") {
$("#settings_adaptive_searching").checkbox('check');
} else {
$("#settings_adaptive_searching").checkbox('uncheck');
}
if ($('#settings_multithreading').data("multithreading") === "True") {
$("#settings_multithreading").checkbox('check');
} else {
$("#settings_multithreading").checkbox('uncheck');
}
if (($('#settings_subfolder').val() !== "relative") && ($('#settings_subfolder').val() !== "absolute")) {
$('.subfolder').hide();
}
$('#settings_subfolder').dropdown('setting', 'onChange', function(){
if (($('#settings_subfolder').val() !== "relative") && ($('#settings_subfolder').val() !== "absolute")) {
$('.subfolder').hide();
}
else {
$('.subfolder').show();
}
});
if ($('#settings_anti_captcha_provider').val() === "None") {
$('.anticaptcha').hide();
$('.deathbycaptcha').hide();
} else if ($('#settings_anti_captcha_provider').val() === "anti-captcha") {
$('.anticaptcha').show();
$('.deathbycaptcha').hide();
} else if ($('#settings_anti_captcha_provider').val() === "death-by-cCaptcha") {
$('.deathbycaptcha').show();
$('.anticaptcha').hide();
}
$('#settings_anti_captcha_provider').dropdown('setting', 'onChange', function(){
if ($('#settings_anti_captcha_provider').val() === "None") {
$('.anticaptcha').hide();
$('.deathbycaptcha').hide();
} else if ($('#settings_anti_captcha_provider').val() === "anti-captcha") {
$('.anticaptcha').show();
$('.deathbycaptcha').hide();
} else if ($('#settings_anti_captcha_provider').val() === "death-by-captcha") {
$('.deathbycaptcha').show();
$('.anticaptcha').hide();
}
});
if ($('#settings_upgrade_subs').data("upgrade") === "True") {
$('.upgrade_subs').show();
} else {
$('.upgrade_subs').hide();
}
$('#settings_upgrade_subs').checkbox({
onChecked: function() {
$('.upgrade_subs').show();
},
onUnchecked: function() {
$('.upgrade_subs').hide();
}
});
$('#settings_languages').dropdown('setting', 'onAdd', function(val, txt){
$("#settings_serie_default_languages").append(
$("<option></option>").attr("value", val).text(txt)
);
$("#settings_movie_default_languages").append(
$("<option></option>").attr("value", val).text(txt)
)
});
$('#settings_languages').dropdown('setting', 'onRemove', function(val, txt){
$("#settings_serie_default_languages").dropdown('remove selected', val);
$("#settings_serie_default_languages option[value='" + val + "']").remove();
$("#settings_movie_default_languages").dropdown('remove selected', val);
$("#settings_movie_default_languages option[value='" + val + "']").remove();
});
if ($('#settings_serie_default_enabled_div').data("enabled") === "True") {
$("#settings_serie_default_enabled_div").checkbox('check');
} else {
$("#settings_serie_default_enabled_div").checkbox('uncheck');
}
if ($('#settings_serie_default_enabled_div').data("enabled") === "True") {
$("#settings_serie_default_languages").removeClass('disabled');
$("#settings_serie_default_hi_div").removeClass('disabled');
} else {
$("#settings_serie_default_languages").addClass('disabled');
$("#settings_serie_default_hi_div").addClass('disabled');
}
$('#settings_serie_default_enabled_div').checkbox({
onChecked: function() {
$("#settings_serie_default_languages").parent().removeClass('disabled');
$("#settings_serie_default_hi_div").removeClass('disabled');
},
onUnchecked: function() {
$("#settings_serie_default_languages").parent().addClass('disabled');
$("#settings_serie_default_hi_div").addClass('disabled');
}
});
if ($('#settings_serie_default_hi_div').data("hi") === "True") {
$("#settings_serie_default_hi_div").checkbox('check');
} else {
$("#settings_serie_default_hi_div").checkbox('uncheck');
}
if ($('#settings_movie_default_enabled_div').data("enabled") === "True") {
$("#settings_movie_default_enabled_div").checkbox('check');
} else {
$("#settings_movie_default_enabled_div").checkbox('uncheck');
}
if ($('#settings_movie_default_enabled_div').data("enabled") === "True") {
$("#settings_movie_default_languages").removeClass('disabled');
$("#settings_movie_default_hi_div").removeClass('disabled');
} else {
$("#settings_movie_default_languages").addClass('disabled');
$("#settings_movie_default_hi_div").addClass('disabled');
}
$('#settings_movie_default_enabled_div').checkbox({
onChecked: function() {
$("#settings_movie_default_languages").parent().removeClass('disabled');
$("#settings_movie_default_hi_div").removeClass('disabled');
},
onUnchecked: function() {
$("#settings_movie_default_languages").parent().addClass('disabled');
$("#settings_movie_default_hi_div").addClass('disabled');
}
});
if ($('#settings_movie_default_hi_div').data("hi") === "True") {
$("#settings_movie_default_hi_div").checkbox('check');
} else {
$("#settings_movie_default_hi_div").checkbox('uncheck');
}
if ($("#settings_single_language").checkbox('is checked')) {
$("#settings_serie_default_languages").parent().removeClass('multiple');
$("#settings_serie_default_languages").removeAttr('multiple');
$("#settings_movie_default_languages").parent().removeClass('multiple');
$("#settings_movie_default_languages").removeAttr('multiple');
} else {
$("#settings_serie_default_languages").parent().addClass('multiple');
$("#settings_serie_default_languages").attr('multiple');
$("#settings_movie_default_languages").parent().addClass('multiple');
$("#settings_movie_default_languages").attr('multiple');
}
$("#settings_single_language").on('change', function() {
if ($("#settings_single_language").checkbox('is checked')) {
$("#settings_serie_default_languages").dropdown('clear');
$("#settings_movie_default_languages").dropdown('clear');
$("#settings_serie_default_languages").prepend("<option value='None' selected='selected'>None</option>");
$("#settings_movie_default_languages").prepend("<option value='None' selected='selected'>None</option>");
$("#settings_serie_default_languages").parent().removeClass('multiple');
$("#settings_serie_default_languages").removeAttr('multiple');
$("#settings_movie_default_languages").parent().removeClass('multiple');
$("#settings_movie_default_languages").removeAttr('multiple');
} else {
$("#settings_serie_default_languages").dropdown('clear');
$("#settings_movie_default_languages").dropdown('clear');
$("#settings_serie_default_languages option[value='None']").remove();
$("#settings_movie_default_languages option[value='None']").remove();
$("#settings_serie_default_languages").parent().addClass('multiple');
$("#settings_serie_default_languages").attr('multiple');
$("#settings_movie_default_languages").parent().addClass('multiple');
$("#settings_movie_default_languages").attr('multiple');
}
});
$('#settings_subfolder').dropdown('clear');
$('#settings_subfolder').dropdown('set selected', '{{!settings.general.subfolder}}');
$('#settings_anti_captcha_provider').dropdown('clear');
$('#settings_anti_captcha_provider').dropdown('set selected', '{{!settings.general.anti_captcha_provider}}');
$('#settings_languages').dropdown('clear');
$('#settings_languages').dropdown('set selected',{{!enabled_languages}});
%if settings.general.serie_default_language != 'None':
$('#settings_serie_default_languages').dropdown('set selected',{{!settings.general.serie_default_language}});
%end
%if settings.general.movie_default_language != 'None':
$('#settings_movie_default_languages').dropdown('set selected',{{!settings.general.movie_default_language}});
%end
$('#settings_languages').dropdown('setting', 'onChange', function(){
$('.form').form('validate field', 'settings_subliminal_languages');
});
</script>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,231 @@
<div class="ui dividing header">Start-Up</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Listening IP address</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input name="settings_general_ip" type="text" value="{{settings.general.ip}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Valid IP4 address or '0.0.0.0' for all interfaces" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Listening port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input name="settings_general_port" type="text" value="{{settings.general.port}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Valid TCP port (default: 6767)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Base URL</label>
</div>
<div class="five wide column">
<div class="ui fluid input">
%if settings.general.base_url is None:
% base_url = "/"
%else:
% base_url = settings.general.base_url
%end
<input name="settings_general_baseurl" type="text">
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
<i class="yellow warning sign icon"></i>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="For reverse proxy support, default is '/'" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Path Mappings for shows</div>
<div class="twelve wide column">
<div class="ui grid">
%import ast
%if settings.general.path_mappings is not None:
% path_substitutions = ast.literal_eval(settings.general.path_mappings)
%else:
% path_substitutions = []
%end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Sonarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Root path to the directory Sonarr accesses." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="two wide center aligned column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Bazarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Path that Bazarr should use to access the same directory remotely." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
%for x in range(0, 5):
% path = []
% try:
% path = path_substitutions[x]
% except IndexError:
% path = ["", ""]
% end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_sourcepath" type="text">
</div>
</div>
<div class="center aligned column">
<i class="arrow circle right icon"></i>
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_destpath" type="text">
</div>
</div>
</div>
%end
</div>
</div>
<div class="ui dividing header">Path Mappings for movies</div>
<div class="twelve wide column">
<div class="ui grid">
%import ast
%if settings.general.path_mappings_movie is not None:
% path_substitutions_movie = ast.literal_eval(settings.general.path_mappings_movie)
%else:
% path_substitutions_movie = []
%end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Radarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Root path to the directory Radarr accesses." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="two wide center aligned column">
</div>
<div class="two wide column">
<div class="ui fluid input">
<h4 class="ui header">
Path for Bazarr:
</h4>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Path that Bazarr should use to access the same directory remotely." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
%for x in range(0, 5):
% path_movie = []
% try:
% path_movie = path_substitutions_movie[x]
% except IndexError:
% path_movie = ["", ""]
% end
<div class="middle aligned row">
<div class="right aligned four wide column">
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_sourcepath_movie" type="text">
</div>
</div>
<div class="center aligned column">
<i class="arrow circle right icon"></i>
</div>
<div class="four wide column">
<div class="ui fluid input">
<input name="settings_general_destpath_movie" type="text">
</div>
</div>
</div>
%end
</div>
</div>
<script>
</script>

@ -0,0 +1,204 @@
<div class="ui dividing header">Connection settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Settings validation:</label>
</div>
<div class="two wide column">
<button id="radarr_validate" class="test ui blue button" type="button">
Test
</button>
</div>
<div class="seven wide column">
<div id="radarr_validated" class="ui read-only checkbox">
<input id="radarr_validated_checkbox" type="checkbox">
<label id="radarr_validation_result">Not tested recently</label>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Use Radarr</label>
</div>
<div class="one wide column">
<div id="settings_use_radarr" class="ui toggle checkbox">
<input name="settings_general_use_radarr" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Enable Radarr integration." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="radarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Hostname or IP address</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_radarr_ip" name="settings_radarr_ip" type="text" class="radarr_config" value="{{settings.radarr.ip}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Hostname or IP4 address of Radarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="radarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Listening port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_radarr_port" name="settings_radarr_port" type="text" class="radarr_config" value="{{settings.radarr.port}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="TCP port of Radarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="radarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Base URL</label>
</div>
<div class="five wide column">
<div class="ui fluid input">
<input id="settings_radarr_baseurl" name="settings_radarr_baseurl" type="text" class="radarr_config">
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Base URL for Radarr (default: '/')" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="radarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>SSL enabled</label>
</div>
<div class="one wide column">
<div id="radarr_ssl_div" class="ui toggle checkbox">
<input id="settings_radarr_ssl" name="settings_radarr_ssl" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="radarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>API key</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_radarr_apikey" name="settings_radarr_apikey" type="text" class="radarr_config">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="API key for Radarr (32 alphanumeric characters)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="radarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Download only monitored</label>
</div>
<div class="one wide column">
<div id="settings_only_monitored_radarr" class="ui toggle checkbox" data-monitored={{settings.radarr.getboolean('only_monitored')}}>
<input name="settings_radarr_only_monitored" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Automatic download of subtitles will happen only for monitored movies in Radarr." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$(".radarr_hide").hide();
$('#settings_use_radarr').checkbox({
onChecked: function() {
$(".radarr_hide").show();
},
onUnchecked: function() {
$(".radarr_hide").hide();
}
});
if ($('#radarr_ssl_div').data("ssl") === "True") {
$("#radarr_ssl_div").checkbox('check');
} else {
$("#radarr_ssl_div").checkbox('uncheck');
}
if ($('#settings_only_monitored_radarr').data("monitored") === "True") {
$("#settings_only_monitored_radarr").checkbox('check');
} else {
$("#settings_only_monitored_radarr").checkbox('uncheck');
}
$('#radarr_validate').on('click', function() {
if ($('#radarr_ssl_div').checkbox('is checked')) {
protocol = 'https';
} else {
protocol = 'http';
}
radarr_url = $('#settings_radarr_ip').val() + ":" + $('#settings_radarr_port').val() + $('#settings_radarr_baseurl').val().replace(/\/$/, "") + "/api/system/status?apikey=" + $('#settings_radarr_apikey').val();
$.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');
$('.form').form('validate form');
$('#loader').removeClass('active');
} else {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('Test failed').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
}
});
});
$('.radarr_config').on('keyup', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Radarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$('#settings_radarr_ssl').on('change', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Radarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$("#radarr_validated").checkbox('check');
</script>

@ -0,0 +1,204 @@
<div class="ui dividing header">Connection settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Settings validation:</label>
</div>
<div class="two wide column">
<button id="sonarr_validate" class="test ui blue button" type="button">
Test
</button>
</div>
<div class="seven wide column">
<div id="sonarr_validated" class="ui read-only checkbox">
<input id="sonarr_validated_checkbox" type="checkbox">
<label id="sonarr_validation_result">Not tested recently</label>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Use Sonarr</label>
</div>
<div class="one wide column">
<div id="settings_use_sonarr" class="ui toggle checkbox">
<input name="settings_general_use_sonarr" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Enable Sonarr integration." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="sonarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Hostname or IP address</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_sonarr_ip" name="settings_sonarr_ip" class="sonarr_config" type="text" value="{{settings.sonarr.ip}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Hostname or IP4 address of Sonarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="sonarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Listening port</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_sonarr_port" name="settings_sonarr_port" class="sonarr_config" type="text" value="{{settings.sonarr.port}}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="TCP port of Sonarr" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="sonarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Base URL</label>
</div>
<div class="five wide column">
<div class="ui fluid input">
<input id="settings_sonarr_baseurl" name="settings_sonarr_baseurl" class="sonarr_config" type="text">
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Base URL for Sonarr (default: '/')" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="sonarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>SSL enabled</label>
</div>
<div class="one wide column">
<div id="sonarr_ssl_div" class="ui toggle checkbox">
<input id="settings_sonarr_ssl" name="settings_sonarr_ssl" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="sonarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>API key</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_sonarr_apikey" name="settings_sonarr_apikey" class="sonarr_config" type="text">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="API key for Sonarr (32 alphanumeric characters)" data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="sonarr_hide middle aligned row">
<div class="right aligned four wide column">
<label>Download only monitored</label>
</div>
<div class="one wide column">
<div id="settings_only_monitored_sonarr" class="ui toggle checkbox" data-monitored={{settings.sonarr.getboolean('only_monitored')}}>
<input name="settings_sonarr_only_monitored" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Automatic download of subtitles will happen only for monitored episodes in Sonarr." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$(".sonarr_hide").hide();
$('#settings_use_sonarr').checkbox({
onChecked: function() {
$(".sonarr_hide").show();
},
onUnchecked: function() {
$(".sonarr_hide").hide();
}
});
if ($('#sonarr_ssl_div').data("ssl") === "True") {
$("#sonarr_ssl_div").checkbox('check');
} else {
$("#sonarr_ssl_div").checkbox('uncheck');
}
if ($('#settings_only_monitored_sonarr').data("monitored") === "True") {
$("#settings_only_monitored_sonarr").checkbox('check');
} else {
$("#settings_only_monitored_sonarr").checkbox('uncheck');
}
$('#sonarr_validate').on('click', function() {
if ($('#sonarr_ssl_div').checkbox('is checked')) {
protocol = 'https';
} else {
protocol = 'http';
}
sonarr_url = $('#settings_sonarr_ip').val() + ":" + $('#settings_sonarr_port').val() + $('#settings_sonarr_baseurl').val().replace(/\/$/, "") + "/api/system/status?apikey=" + $('#settings_sonarr_apikey').val();
$.getJSON("{{base_url}}test_url/" + protocol + "/" + encodeURIComponent(sonarr_url), function (data) {
if (data.status) {
$('#sonarr_validated').checkbox('check');
$('#sonarr_validation_result').text('Test successful: Sonarr v' + data.version).css('color', 'green');
$('.form').form('validate form');
$('#loader').removeClass('active');
} else {
$('#sonarr_validated').checkbox('uncheck');
$('#sonarr_validation_result').text('Test failed').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
}
});
});
$('.sonarr_config').on('keyup', function() {
$('#sonarr_validated').checkbox('uncheck');
$('#sonarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$('#settings_sonarr_ssl').on('change', function() {
$('#sonarr_validated').checkbox('uncheck');
$('#sonarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$("#sonarr_validated").checkbox('check');
</script>

@ -0,0 +1,387 @@
<div class="ui dividing header">Subtitles options</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Subtitle folder</label>
</div>
<div class="five wide column">
<select name="settings_subfolder" id="settings_subfolder"
class="ui fluid selection dropdown">
<option value="current">Alongside media file</option>
<option value="relative">Relative path to media file</option>
<option value="absolute">Absolute path</option>
</select>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Choose folder where you want to store/read the subtitles'
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row subfolder">
<div class="two wide column"></div>
<div class="right aligned four wide column">
<label>Custom Subtitle folder</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<input id="settings_subfolder_custom" name="settings_subfolder_custom"
type="text" value="{{ settings.general.subfolder_custom }}">
</div>
</div>
</div>
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip='Choose your own folder for the subtitles' data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Use embedded subtitles</label>
</div>
<div class="one wide column">
<div id="settings_embedded" class="ui toggle checkbox"
data-embedded={{ settings.general.getboolean('use_embedded_subs') }}>
<input name="settings_general_embedded" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon"
data-tooltip="Use embedded subtitles in media files when determining missing ones."
data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
% include('providers.tpl')
<div class="ui dividing header">Subtitles languages</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Single language</label>
</div>
<div class="one wide column">
<div id="settings_single_language" class="ui toggle checkbox" data-single-language={{settings.general.getboolean('single_language')}}>
<input name="settings_general_single_language" type="checkbox">
<label></label>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Download a single subtitles file and don't add the language code to the filename." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Enabled languages</label>
</div>
<div class="eleven wide column">
<div class='field'>
<select name="settings_subliminal_languages" id="settings_languages" multiple="" class="ui fluid search selection dropdown">
<option value="">Languages</option>
%enabled_languages = []
%for language in settings_languages:
<option value="{{language[1]}}">{{language[2]}}</option>
%if language[3] == True:
% enabled_languages.append(str(language[1]))
%end
%end
</select>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Series default settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Default enabled</label>
</div>
<div class="one wide column">
<div class="nine wide column">
<div id="settings_serie_default_enabled_div" class="ui toggle checkbox">
<input name="settings_serie_default_enabled" id="settings_serie_default_enabled" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Apply only to series added to Bazarr after enabling this option." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Languages</label>
</div>
<div class="eleven wide column">
<div class='field'>
<select name="settings_serie_default_languages" id="settings_serie_default_languages" multiple="" class="ui fluid search selection dropdown">
%if not settings.general.getboolean('single_language'):
<option value="">Languages</option>
%else:
<option value="None">None</option>
%end
</select>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Hearing-impaired</label>
</div>
<div class="eleven wide column">
<div class="nine wide column">
<div id="settings_serie_default_hi_div" class="ui toggle checkbox">
<input name="settings_serie_default_hi" id="settings_serie_default_hi" type="checkbox">
<label></label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">Movies default settings</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Default enabled</label>
</div>
<div class="one wide column">
<div class="nine wide column">
<div id="settings_movie_default_enabled_div" class="ui toggle checkbox">
<input name="settings_movie_default_enabled" id="settings_movie_default_enabled" type="checkbox">
<label></label>
</div>
</div>
</div>
<div class="collapsed column">
<div class="collapsed center aligned column">
<div class="ui basic icon" data-tooltip="Apply only to movies added to Bazarr after enabling this option." data-inverted="">
<i class="help circle large icon"></i>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div id="movie_default_languages_label" class="right aligned four wide column">
<label>Languages</label>
</div>
<div class="eleven wide column">
<div class='field'>
<select name="settings_movie_default_languages" id="settings_movie_default_languages" multiple="" class="ui fluid search selection dropdown">
%if not settings.general.getboolean('single_language'):
<option value="">Languages</option>
%else:
<option value="None">None</option>
%end
</select>
</div>
</div>
</div>
<div class="middle aligned row">
<div id="movie_default_hi_label" class="right aligned four wide column">
<label>Hearing-impaired</label>
</div>
<div class="eleven wide column">
<div class="nine wide column">
<div id="settings_movie_default_hi_div" class="ui toggle checkbox">
<input name="settings_movie_default_hi" id="settings_movie_default_hi" type="checkbox">
<label></label>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
if ($('#settings_embedded').data("embedded") === "True") {
$("#settings_embedded").checkbox('check');
} else {
$("#settings_embedded").checkbox('uncheck');
}
if ($('#settings_single_language').data("single-language") === "True") {
$("#settings_single_language").checkbox('check');
} else {
$("#settings_single_language").checkbox('uncheck');
}
$('#settings_languages').dropdown('setting', 'onAdd', function(val, txt){
$("#settings_serie_default_languages").append(
$("<option></option>").attr("value", val).text(txt)
);
$("#settings_movie_default_languages").append(
$("<option></option>").attr("value", val).text(txt)
)
});
$('#settings_languages').dropdown('setting', 'onRemove', function(val){
$("#settings_serie_default_languages").dropdown('remove selected', val);
$("#settings_serie_default_languages option[value='" + val + "']").remove();
$("#settings_movie_default_languages").dropdown('remove selected', val);
$("#settings_movie_default_languages option[value='" + val + "']").remove();
});
if ($('#settings_serie_default_enabled_div').data("enabled") === "True") {
$("#settings_serie_default_enabled_div").checkbox('check');
} else {
$("#settings_serie_default_enabled_div").checkbox('uncheck');
}
if ($('#settings_serie_default_enabled_div').data("enabled") === "True") {
$("#settings_serie_default_languages").removeClass('disabled');
$("#settings_serie_default_hi_div").removeClass('disabled');
} else {
$("#settings_serie_default_languages").addClass('disabled');
$("#settings_serie_default_hi_div").addClass('disabled');
}
$('#settings_serie_default_enabled_div').checkbox({
onChecked: function() {
$("#settings_serie_default_languages").parent().removeClass('disabled');
$("#settings_serie_default_hi_div").removeClass('disabled');
},
onUnchecked: function() {
$("#settings_serie_default_languages").parent().addClass('disabled');
$("#settings_serie_default_hi_div").addClass('disabled');
}
});
if ($('#settings_serie_default_hi_div').data("hi") === "True") {
$("#settings_serie_default_hi_div").checkbox('check');
} else {
$("#settings_serie_default_hi_div").checkbox('uncheck');
}
if ($('#settings_movie_default_enabled_div').data("enabled") === "True") {
$("#settings_movie_default_enabled_div").checkbox('check');
} else {
$("#settings_movie_default_enabled_div").checkbox('uncheck');
}
if ($('#settings_movie_default_enabled_div').data("enabled") === "True") {
$("#settings_movie_default_languages").removeClass('disabled');
$("#settings_movie_default_hi_div").removeClass('disabled');
} else {
$("#settings_movie_default_languages").addClass('disabled');
$("#settings_movie_default_hi_div").addClass('disabled');
}
$('#settings_movie_default_enabled_div').checkbox({
onChecked: function() {
$("#settings_movie_default_languages").parent().removeClass('disabled');
$("#settings_movie_default_hi_div").removeClass('disabled');
},
onUnchecked: function() {
$("#settings_movie_default_languages").parent().addClass('disabled');
$("#settings_movie_default_hi_div").addClass('disabled');
}
});
if ($('#settings_movie_default_hi_div').data("hi") === "True") {
$("#settings_movie_default_hi_div").checkbox('check');
} else {
$("#settings_movie_default_hi_div").checkbox('uncheck');
}
if ($("#settings_single_language").checkbox('is checked')) {
$("#settings_serie_default_languages").parent().removeClass('multiple');
$("#settings_serie_default_languages").removeAttr('multiple');
$("#settings_movie_default_languages").parent().removeClass('multiple');
$("#settings_movie_default_languages").removeAttr('multiple');
} else {
$("#settings_serie_default_languages").parent().addClass('multiple');
$("#settings_serie_default_languages").attr('multiple');
$("#settings_movie_default_languages").parent().addClass('multiple');
$("#settings_movie_default_languages").attr('multiple');
}
$("#settings_single_language").on('change', function() {
if ($("#settings_single_language").checkbox('is checked')) {
$("#settings_serie_default_languages").dropdown('clear');
$("#settings_movie_default_languages").dropdown('clear');
$("#settings_serie_default_languages").prepend("<option value='None' selected='selected'>None</option>");
$("#settings_movie_default_languages").prepend("<option value='None' selected='selected'>None</option>");
$("#settings_serie_default_languages").parent().removeClass('multiple');
$("#settings_serie_default_languages").removeAttr('multiple');
$("#settings_movie_default_languages").parent().removeClass('multiple');
$("#settings_movie_default_languages").removeAttr('multiple');
} else {
$("#settings_serie_default_languages").dropdown('clear');
$("#settings_movie_default_languages").dropdown('clear');
$("#settings_serie_default_languages option[value='None']").remove();
$("#settings_movie_default_languages option[value='None']").remove();
$("#settings_serie_default_languages").parent().addClass('multiple');
$("#settings_serie_default_languages").attr('multiple');
$("#settings_movie_default_languages").parent().addClass('multiple');
$("#settings_movie_default_languages").attr('multiple');
}
});
$('#settings_languages').dropdown('clear');
$('#settings_languages').dropdown('set selected',{{!enabled_languages}});
$('#settings_subfolder').dropdown('clear');
$('#settings_subfolder').dropdown('set selected', '{{!settings.general.subfolder}}');
%if settings.general.serie_default_language != 'None':
$('#settings_serie_default_languages').dropdown('set selected',{{!settings.general.serie_default_language}});
%end
%if settings.general.movie_default_language != 'None':
$('#settings_movie_default_languages').dropdown('set selected',{{!settings.general.movie_default_language}});
%end
$('#settings_languages').dropdown('setting', 'onChange', function(){
$('.form').form('validate field', 'settings_subliminal_languages');
});
if (($('#settings_subfolder').val() !== "relative") && ($('#settings_subfolder').val() !== "absolute")) {
$('.subfolder').hide();
}
$('#settings_subfolder').dropdown('setting', 'onChange', function(){
if (($('#settings_subfolder').val() !== "relative") && ($('#settings_subfolder').val() !== "absolute")) {
$('.subfolder').hide();
} else {
$('.subfolder').show();
}
});
</script>
Loading…
Cancel
Save