Merge remote-tracking branch 'origin/development' into subliminal_patch

# Conflicts:
#	bazarr/check_update.py
#	bazarr/get_episodes.py
#	bazarr/get_movies.py
#	bazarr/get_series.py
#	bazarr/get_subtitle.py
#	bazarr/init.py
#	bazarr/list_subtitles.py
#	bazarr/main.py
#	views/menu.tpl
#	views/settings.tpl
#	views/wanted.tpl
This commit is contained in:
Halali 2019-01-01 21:15:12 +01:00
commit 67bf114098
46 changed files with 3672 additions and 1242 deletions

View File

@ -6,7 +6,8 @@ import sqlite3
import json
import requests
import git
if not no_update:
import git
from get_args import args
from get_settings import get_general_settings

View File

@ -4,6 +4,7 @@ import os
import sqlite3
import requests
import logging
from queueconfig import q4ws
from get_args import args
from get_settings import path_replace
@ -26,7 +27,8 @@ def update_all_movies():
def sync_episodes():
logging.debug('BAZARR Starting episode sync from Sonarr.')
q4ws.append('Episodes sync from Sonarr started...')
logging.debug('BAZARR Starting episodes sync from Sonarr.')
from get_settings import get_sonarr_settings
url_sonarr = get_sonarr_settings()[6]
apikey_sonarr = get_sonarr_settings()[4]
@ -44,12 +46,13 @@ def sync_episodes():
episodes_to_add = []
# Get sonarrId for each series from database
seriesIdList = c.execute("SELECT sonarrSeriesId FROM table_shows").fetchall()
seriesIdList = c.execute("SELECT sonarrSeriesId, title FROM table_shows").fetchall()
# Close database connection
c.close()
for seriesId in seriesIdList:
q4ws.append('Getting episodes data for this show: ' + seriesId[1])
# Get episodes data for a series from Sonarr
url_sonarr_api_episode = url_sonarr + "/api/episode?seriesId=" + str(seriesId[0]) + "&apikey=" + apikey_sonarr
try:
@ -123,3 +126,5 @@ def sync_episodes():
list_missing_subtitles()
logging.debug('BAZARR All missing subtitles updated in database.')
q4ws.append('Episodes sync from Sonarr ended.')

View File

@ -4,6 +4,7 @@ import os
import sqlite3
import requests
import logging
from queueconfig import q4ws
from get_args import args
from get_settings import get_general_settings, path_replace_movie
@ -11,6 +12,7 @@ from list_subtitles import store_subtitles_movie, list_missing_subtitles_movies
def update_movies():
q4ws.append("Update movies list from Radarr is running...")
logging.debug('BAZARR Starting movie sync from Radarr.')
from get_settings import get_radarr_settings
url_radarr = get_radarr_settings()[6]
@ -50,6 +52,7 @@ def update_movies():
movies_to_add = []
for movie in r.json():
q4ws.append("Getting data for this movie: " + movie['title'])
if movie['hasFile'] is True:
if 'movieFile' in movie:
if movie["path"] != None and movie['movieFile']['relativePath'] != None:
@ -118,14 +121,17 @@ def update_movies():
for added_movie in movies_to_add:
store_subtitles_movie(path_replace_movie(added_movie[1]))
for updated_movie in movies_to_update:
store_subtitles_movie(path_replace_movie(updated_movie[1]))
# TODO: Commented until I find a way to make it store only episodes really updated.
# for updated_movie in movies_to_update:
# store_subtitles_movie(path_replace_movie(updated_movie[1]))
logging.debug('BAZARR All movies synced from Radarr into database.')
list_missing_subtitles_movies()
logging.debug('BAZARR All movie missing subtitles updated in database.')
q4ws.append("Update movies list from Radarr is ended.")
def get_profile_list():
from get_settings import get_radarr_settings

View File

@ -4,6 +4,8 @@ import os
import sqlite3
import requests
import logging
from queueconfig import q4ws
import datetime
from get_args import args
from get_settings import get_general_settings
@ -11,6 +13,7 @@ from list_subtitles import list_missing_subtitles
def update_series():
q4ws.append("Update series list from Sonarr is running...")
from get_settings import get_sonarr_settings
url_sonarr = get_sonarr_settings()[6]
apikey_sonarr = get_sonarr_settings()[4]
@ -53,6 +56,7 @@ def update_series():
series_to_add = []
for show in r.json():
q4ws.append("Getting series data for this show: " + show['title'])
try:
overview = unicode(show['overview'])
except:
@ -120,6 +124,8 @@ def update_series():
db.commit()
db.close()
q4ws.append("Update series list from Sonarr is ended.")
def get_profile_list():
from get_settings import get_sonarr_settings

View File

@ -20,7 +20,7 @@ from subliminal import region, score as subliminal_scores, \
list_subtitles
from subliminal_patch.core import SZAsyncProviderPool, download_best_subtitles, save_subtitles, download_subtitles
from subliminal_patch.score import compute_score
from get_languages import language_from_alpha3, alpha2_from_alpha3, alpha3_from_alpha2
from get_languages import language_from_alpha3, alpha2_from_alpha3, alpha3_from_alpha2, language_from_alpha2
from bs4 import UnicodeDammit
from get_settings import get_general_settings, pp_replace, path_replace, path_replace_movie, path_replace_reverse, \
path_replace_reverse_movie
@ -29,6 +29,7 @@ from utils import history_log, history_log_movie
from notifier import send_notifications, send_notifications_movie
from get_providers import get_providers, get_providers_auth
from get_args import args
from queueconfig import q4ws
# configure the cache
@ -137,10 +138,10 @@ def download_subtitle(path, language, hi, providers, providers_auth, sceneName,
"""
AsyncProviderPool:
implement:
blacklist=None,
blacklist=None,
throttle_callback=None,
pre_download_hook=None,
post_download_hook=None,
pre_download_hook=None,
post_download_hook=None,
language_hook=None
"""
@ -182,7 +183,7 @@ def download_subtitle(path, language, hi, providers, providers_auth, sceneName,
if not throttle_data:
return
throttle_delta, throttle_description = throttle_data
throttle_delta, throttle_description = throttle_data
throttle_until = datetime.datetime.now() + throttle_delta
# save throttle_until together with provider name somewhere, then implement dynamic provider_list based on
@ -553,6 +554,7 @@ def wanted_download_subtitles(path):
for i in range(len(attempt)):
if attempt[i][0] == language:
if search_active(attempt[i][1]) is True:
q4ws.append('Searching ' + str(language_from_alpha2(language)) + ' subtitles for this file: ' + path)
message = download_subtitle(path_replace(episode[0]), str(alpha3_from_alpha2(language)),
episode[4], providers_list, providers_auth, str(episode[5]),
episode[7], 'series')
@ -599,6 +601,7 @@ def wanted_download_subtitles_movie(path):
for i in range(len(attempt)):
if attempt[i][0] == language:
if search_active(attempt[i][1]) is True:
q4ws.append('Searching ' + str(language_from_alpha2(language)) + ' subtitles for this file: ' + path)
message = download_subtitle(path_replace_movie(movie[0]), str(alpha3_from_alpha2(language)),
movie[4], providers_list, providers_auth, str(movie[5]), movie[7],
'movie')

View File

@ -20,14 +20,18 @@ from get_settings import path_replace_reverse, path_replace, path_replace_revers
get_general_settings
from get_languages import alpha2_from_alpha3
from queueconfig import q4ws
gc.enable()
def store_subtitles(file):
# languages = []
logging.debug('BAZARR started subtitles indexing for this file: ' + file)
actual_subtitles = []
if os.path.exists(file):
q4ws.append('Analyzing this file for subtitles: ' + file)
if os.path.splitext(file)[1] == '.mkv':
logging.debug("BAZARR is trying to index embedded subtitles.")
try:
with open(file, 'rb') as f:
mkv = enzyme.MKV(f)
@ -35,62 +39,80 @@ def store_subtitles(file):
for subtitle_track in mkv.subtitle_tracks:
try:
if alpha2_from_alpha3(subtitle_track.language) != None:
actual_subtitles.append([str(alpha2_from_alpha3(subtitle_track.language)), None])
lang = str(alpha2_from_alpha3(subtitle_track.language))
logging.debug("BAZARR embedded subtitles detected: " + lang)
actual_subtitles.append([lang, None])
except:
logging.debug("BAZARR unable to index this unrecognized language: " + subtitle_track.language)
pass
except:
except Exception as e:
logging.exception("BAZARR error when trying to analyze this mkv file: " + file)
pass
else:
logging.debug("BAZARR This file isn't an .mkv file.")
brazilian_portuguese = [".pt-br", ".pob", "pb"]
try:
# fixme: set subliminal_patch.core.CUSTOM_PATHS to a list of absolute folders or subfolders to support
# subtitles outside the media file folder
subtitles = search_external_subtitles(file)
except:
except Exception as e:
logging.exception("BAZARR unable to index external subtitles.")
pass
else:
for subtitle, language in subtitles.iteritems():
if str(os.path.splitext(subtitle)[0]).lower().endswith(tuple(brazilian_portuguese)) is True:
logging.debug("BAZARR external subtitles detected: " + "pb")
actual_subtitles.append(
[str("pb"), path_replace_reverse(os.path.join(os.path.dirname(file), subtitle))])
elif str(language) != 'und':
logging.debug("BAZARR external subtitles detected: " + str(language))
actual_subtitles.append(
[str(language), path_replace_reverse(os.path.join(os.path.dirname(file), subtitle))])
else:
with open(path_replace(os.path.join(os.path.dirname(file), subtitle)), 'r') as f:
text = list(islice(f, 100))
text = ' '.join(text)
encoding = UnicodeDammit(text)
try:
text = text.decode(encoding.original_encoding)
detected_language = langdetect.detect(text)
except Exception as e:
logging.exception(
'BAZARR Error trying to detect character encoding for this subtitles file: ' + path_replace(
if os.path.splitext(subtitle)[1] != ".sub":
logging.debug("BAZARR falling back to file content analysis to detect language.")
with open(path_replace(os.path.join(os.path.dirname(file), subtitle)), 'r') as f:
text = list(islice(f, 100))
text = ' '.join(text)
encoding = UnicodeDammit(text)
try:
text = text.decode(encoding.original_encoding)
detected_language = langdetect.detect(text)
except Exception as e:
logging.exception(
'BAZARR Error trying to detect language for this subtitles file: ' + path_replace(
os.path.join(os.path.dirname(file),
subtitle)) + ' You should try to delete this subtitles file manually and ask Bazarr to download it again.')
else:
if len(detected_language) > 0:
actual_subtitles.append([str(detected_language), path_replace_reverse(
else:
if len(detected_language) > 0:
logging.debug("BAZARR external subtitles detected and analysis guessed this language: " + str(detected_language))
actual_subtitles.append([str(detected_language), path_replace_reverse(
os.path.join(os.path.dirname(file), subtitle))])
conn_db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c_db = conn_db.cursor()
c_db.execute("UPDATE table_episodes SET subtitles = ? WHERE path = ?",
conn_db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c_db = conn_db.cursor()
logging.debug("BAZARR storing those languages to DB: " + str(actual_subtitles))
c_db.execute("UPDATE table_episodes SET subtitles = ? WHERE path = ?",
(str(actual_subtitles), path_replace_reverse(file)))
conn_db.commit()
conn_db.commit()
c_db.close()
c_db.close()
else:
logging.debug("BAZARR this file doesn't seems to exist or isn't accessible.")
logging.debug('BAZARR ended subtitles indexing for this file: ' + file)
return actual_subtitles
def store_subtitles_movie(file):
# languages = []
logging.debug('BAZARR started subtitles indexing for this file: ' + file)
actual_subtitles = []
if os.path.exists(file):
q4ws.append('Analyzing this file for subtitles: ' + file)
if os.path.splitext(file)[1] == '.mkv':
logging.debug("BAZARR is trying to index embedded subtitles.")
try:
with open(file, 'rb') as f:
mkv = enzyme.MKV(f)
@ -98,51 +120,70 @@ def store_subtitles_movie(file):
for subtitle_track in mkv.subtitle_tracks:
try:
if alpha2_from_alpha3(subtitle_track.language) != None:
actual_subtitles.append([str(alpha2_from_alpha3(subtitle_track.language)), None])
lang = str(alpha2_from_alpha3(subtitle_track.language))
logging.debug("BAZARR embedded subtitles detected: " + lang)
actual_subtitles.append([lang, None])
except:
logging.debug("BAZARR unable to index this unrecognized language: " + subtitle_track.language)
pass
except:
except Exception as e:
logging.exception("BAZARR error when trying to analyze this mkv file: " + file)
pass
else:
logging.debug("BAZARR This file isn't an .mkv file.")
# fixme: set subliminal_patch.core.CUSTOM_PATHS to a list of absolute folders or subfolders to support
# subtitles outside the media file folder
subtitles = search_external_subtitles(file)
brazilian_portuguese = [".pt-br", ".pob", "pb"]
for subtitle, language in subtitles.iteritems():
if str(os.path.splitext(subtitle)[0]).lower().endswith(tuple(brazilian_portuguese)) is True:
actual_subtitles.append(
try:
subtitles = core.search_external_subtitles(file)
except Exception as e:
logging.exception("BAZARR unable to index external subtitles.")
pass
else:
for subtitle, language in subtitles.iteritems():
if str(os.path.splitext(subtitle)[0]).lower().endswith(tuple(brazilian_portuguese)) is True:
logging.debug("BAZARR external subtitles detected: " + "pb")
actual_subtitles.append(
[str("pb"), path_replace_reverse_movie(os.path.join(os.path.dirname(file), subtitle))])
elif str(language) != 'und':
actual_subtitles.append(
elif str(language) != 'und':
logging.debug("BAZARR external subtitles detected: " + str(language))
actual_subtitles.append(
[str(language), path_replace_reverse_movie(os.path.join(os.path.dirname(file), subtitle))])
else:
if os.path.splitext(subtitle)[1] != ".sub":
with open(path_replace_movie(os.path.join(os.path.dirname(file), subtitle)), 'r') as f:
text = list(islice(f, 100))
text = ' '.join(text)
encoding = UnicodeDammit(text)
try:
text = text.decode(encoding.original_encoding)
detected_language = langdetect.detect(text)
except Exception as e:
logging.exception(
'BAZARR Error trying to detect character encoding for this subtitles file: ' + path_replace_movie(
else:
if os.path.splitext(subtitle)[1] != ".sub":
logging.debug("BAZARR falling back to file content analysis to detect language.")
with open(path_replace_movie(os.path.join(os.path.dirname(file), subtitle)), 'r') as f:
text = list(islice(f, 100))
text = ' '.join(text)
encoding = UnicodeDammit(text)
try:
text = text.decode(encoding.original_encoding)
detected_language = langdetect.detect(text)
except Exception as e:
logging.exception(
'BAZARR Error trying to detect language for this subtitles file: ' + path_replace(
os.path.join(os.path.dirname(file),
subtitle)) + ' You should try to delete this subtitles file manually and ask Bazarr to download it again.')
else:
if len(detected_language) > 0:
actual_subtitles.append([str(detected_language), path_replace_reverse_movie(
else:
if len(detected_language) > 0:
logging.debug("BAZARR external subtitles detected and analysis guessed this language: " + str(detected_language))
actual_subtitles.append([str(detected_language), path_replace_reverse_movie(
os.path.join(os.path.dirname(file), subtitle))])
conn_db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30)
c_db = conn_db.cursor()
logging.debug("BAZARR storing those languages to DB: " + str(actual_subtitles))
c_db.execute("UPDATE table_movies SET subtitles = ? WHERE path = ?",
(str(actual_subtitles), path_replace_reverse_movie(file)))
conn_db.commit()
c_db.close()
else:
logging.debug("BAZARR this file doesn't seems to exist or isn't accessible.")
logging.debug('BAZARR ended subtitles indexing for this file: ' + file)
return actual_subtitles

View File

@ -80,6 +80,7 @@ def configure_logging(debug=False):
logging.getLogger("guessit").setLevel(logging.WARNING)
logging.getLogger("rebulk").setLevel(logging.WARNING)
logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL)
logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING)
fh.setLevel(log_level)
logger.addHandler(fh)

View File

@ -12,6 +12,7 @@ import ast
import hashlib
import urllib
import warnings
import queueconfig
from get_args import args
from init import *
@ -19,12 +20,15 @@ from update_db import *
from notifier import update_notifier
from get_settings import get_general_settings, get_proxy_settings
from logger import configure_logging, empty_log
from cherrypy.wsgiserver import CherryPyWSGIServer
from gevent.pywsgi import WSGIServer
from geventwebsocket import WebSocketError
from geventwebsocket.handler import WebSocketHandler
#from cherrypy.wsgiserver import CherryPyWSGIServer
from io import BytesIO
from six import text_type
from beaker.middleware import SessionMiddleware
from cork import Cork
from bottle import route, run, template, static_file, request, redirect, response, HTTPError, app, hook
from bottle import route, run, template, static_file, request, redirect, response, HTTPError, app, hook, abort
from datetime import datetime, timedelta
from get_languages import load_language_in_db, language_from_alpha3
from get_providers import load_providers, get_providers, get_providers_auth
@ -48,7 +52,7 @@ sys.setdefaultencoding('utf8')
gc.enable()
update_notifier()
bazarr_version = '0.6.9'
bazarr_version = '0.6.9.5'
configure_logging(get_general_settings()[4] or args.debug)
if get_proxy_settings()[0] != 'None':
@ -68,6 +72,20 @@ if "PYCHARM_HOSTED" in os.environ:
else:
bottle.ERROR_PAGE_TEMPLATE = bottle.ERROR_PAGE_TEMPLATE.replace('if DEBUG and', 'if')
# Install gevent under user directory if it'S not already available. This one is required to use websocket.
try:
import gevent
except ImportError as e:
logging.exception('BAZARR require gevent Python module to be installed using pip.')
try:
stop_file = open(os.path.join(config_dir, "bazarr.stop"), "w")
except Exception as e:
logging.error('BAZARR Cannot create bazarr.stop file.')
else:
stop_file.write('')
stop_file.close()
os._exit(0)
load_providers()
# Reset restart required warning on start
@ -77,6 +95,11 @@ c.execute("UPDATE system SET configured = 0, updated = 0")
conn.commit()
c.close()
logging.debug('Bazarr version: %s', bazarr_version)
logging.debug('Bazarr branch: %s', get_general_settings()[5])
logging.debug('Operating system: %s', platform.platform())
logging.debug('Python version: %s', platform.python_version())
# Load languages in database
load_language_in_db()
@ -208,7 +231,7 @@ def wizard():
settings_sonarr = get_sonarr_settings()
settings_radarr = get_radarr_settings()
return template('wizard', __file__=__file__, bazarr_version=bazarr_version, settings_general=settings_general,
return template('wizard', bazarr_version=bazarr_version, settings_general=settings_general,
settings_languages=settings_languages, settings_providers=settings_providers,
settings_sonarr=settings_sonarr, settings_radarr=settings_radarr, base_url=base_url)
@ -550,7 +573,7 @@ def serieseditor():
c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1")
languages = c.fetchall()
c.close()
output = template('serieseditor', __file__=__file__, bazarr_version=bazarr_version, rows=data, languages=languages,
output = template('serieseditor', bazarr_version=bazarr_version, rows=data, languages=languages,
missing_count=missing_count, base_url=base_url, single_language=single_language,
current_port=port)
return output
@ -684,11 +707,10 @@ def episodes(no):
for key, season in itertools.groupby(episodes, operator.itemgetter(2)):
seasons_list.append(list(season))
return template('episodes', __file__=__file__, bazarr_version=bazarr_version, no=no, details=series_details,
return template('episodes', bazarr_version=bazarr_version, no=no, details=series_details,
languages=languages, seasons=seasons_list, url_sonarr_short=url_sonarr_short, base_url=base_url,
tvdbid=tvdbid, number=number, current_port=port)
@route(base_url + 'movies')
@custom_auth_basic(check_credentials)
def movies():
@ -716,7 +738,7 @@ def movies():
c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1")
languages = c.fetchall()
c.close()
output = template('movies', __file__=__file__, bazarr_version=bazarr_version, rows=data, languages=languages,
output = template('movies', bazarr_version=bazarr_version, rows=data, languages=languages,
missing_count=missing_count, page=page, max_page=max_page, base_url=base_url,
single_language=single_language, page_size=page_size, current_port=port)
return output
@ -742,7 +764,7 @@ def movieseditor():
c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1")
languages = c.fetchall()
c.close()
output = template('movieseditor', __file__=__file__, bazarr_version=bazarr_version, rows=data, languages=languages,
output = template('movieseditor', bazarr_version=bazarr_version, rows=data, languages=languages,
missing_count=missing_count, base_url=base_url, single_language=single_language,
current_port=port)
return output
@ -834,11 +856,10 @@ def movie(no):
languages = c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1").fetchall()
c.close()
return template('movie', __file__=__file__, bazarr_version=bazarr_version, no=no, details=movies_details,
return template('movie', bazarr_version=bazarr_version, no=no, details=movies_details,
languages=languages, url_radarr_short=url_radarr_short, base_url=base_url, tmdbid=tmdbid,
current_port=port)
@route(base_url + 'scan_disk/<no:int>', method='GET')
@custom_auth_basic(check_credentials)
def scan_disk(no):
@ -887,7 +908,7 @@ def search_missing_subtitles_movie(no):
@custom_auth_basic(check_credentials)
def history():
authorize()
return template('history', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url, current_port=port)
return template('history', bazarr_version=bazarr_version, base_url=base_url, current_port=port)
@route(base_url + 'historyseries')
@ -928,11 +949,10 @@ def historyseries():
data = c.fetchall()
c.close()
data = reversed(sorted(data, key=operator.itemgetter(4)))
return template('historyseries', __file__=__file__, bazarr_version=bazarr_version, rows=data, row_count=row_count,
return template('historyseries', bazarr_version=bazarr_version, rows=data, row_count=row_count,
page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size,
current_port=port)
@route(base_url + 'historymovies')
@custom_auth_basic(check_credentials)
def historymovies():
@ -971,16 +991,15 @@ def historymovies():
data = c.fetchall()
c.close()
data = reversed(sorted(data, key=operator.itemgetter(2)))
return template('historymovies', __file__=__file__, bazarr_version=bazarr_version, rows=data, row_count=row_count,
return template('historymovies', bazarr_version=bazarr_version, rows=data, row_count=row_count,
page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size,
current_port=port)
@route(base_url + 'wanted')
@custom_auth_basic(check_credentials)
def wanted():
authorize()
return template('wanted', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url, current_port=port)
return template('wanted', bazarr_version=bazarr_version, base_url=base_url, current_port=port)
@route(base_url + 'wantedseries')
@ -1011,11 +1030,10 @@ def wantedseries():
(page_size, offset,))
data = c.fetchall()
c.close()
return template('wantedseries', __file__=__file__, bazarr_version=bazarr_version, rows=data,
return template('wantedseries', bazarr_version=bazarr_version, rows=data,
missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, page_size=page_size,
current_port=port)
@route(base_url + 'wantedmovies')
@custom_auth_basic(check_credentials)
def wantedmovies():
@ -1044,11 +1062,10 @@ def wantedmovies():
(page_size, offset,))
data = c.fetchall()
c.close()
return template('wantedmovies', __file__=__file__, bazarr_version=bazarr_version, rows=data,
return template('wantedmovies', bazarr_version=bazarr_version, rows=data,
missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, page_size=page_size,
current_port=port)
@route(base_url + 'wanted_search_missing_subtitles')
@custom_auth_basic(check_credentials)
def wanted_search_missing_subtitles_list():
@ -1082,13 +1099,12 @@ def settings():
settings_sonarr = get_sonarr_settings()
settings_radarr = get_radarr_settings()
return template('settings', __file__=__file__, bazarr_version=bazarr_version, settings_general=settings_general,
return template('settings', bazarr_version=bazarr_version, settings_general=settings_general,
settings_proxy=settings_proxy, settings_auth=settings_auth, settings_languages=settings_languages,
settings_providers=settings_providers, settings_sonarr=settings_sonarr,
settings_radarr=settings_radarr, settings_notifier=settings_notifier, base_url=base_url,
current_port=port)
@route(base_url + 'save_settings', method='POST')
@custom_auth_basic(check_credentials)
def save_settings():
@ -1525,20 +1541,25 @@ def system():
url_sonarr = get_sonarr_settings()[6]
apikey_sonarr = get_sonarr_settings()[4]
sv = url_sonarr + "/api/system/status?apikey=" + apikey_sonarr
try:
sonarr_version = requests.get(sv, timeout=15, verify=False).json()['version']
except:
sonarr_version = ''
sonarr_version = ''
if use_sonarr:
try:
sonarr_version = requests.get(sv, timeout=15, verify=False).json()['version']
except:
pass
url_radarr = get_radarr_settings()[6]
apikey_radarr = get_radarr_settings()[4]
sv = url_radarr + "/api/system/status?apikey=" + apikey_radarr
try:
radarr_version = requests.get(sv, timeout=15, verify=False).json()['version']
except:
radarr_version = ''
rv = url_radarr + "/api/system/status?apikey=" + apikey_radarr
radarr_version = ''
if use_radarr:
try:
radarr_version = requests.get(rv, timeout=15, verify=False).json()['version']
except:
pass
return template('system', __file__=__file__, bazarr_version=bazarr_version,
return template('system', bazarr_version=bazarr_version,
sonarr_version=sonarr_version, radarr_version=radarr_version,
operating_system=platform.platform(), python_version=platform.python_version(),
config_dir=args.config_dir, bazarr_dir=os.path.normcase(os.getcwd()),
@ -1832,13 +1853,44 @@ def test_url(protocol, url):
return dict(status=True, version=result)
@route(base_url + 'test_notification/<protocol>/<provider:path>', method='GET')
@custom_auth_basic(check_credentials)
def test_notification(protocol, provider):
provider = urllib.unquote(provider)
apobj = apprise.Apprise()
apobj.add(protocol + "://" + provider)
apobj.notify(
title='Bazarr test notification',
body=('Test notification')
)
@route(base_url + 'websocket')
@custom_auth_basic(check_credentials)
def handle_websocket():
wsock = request.environ.get('wsgi.websocket')
if not wsock:
abort(400, 'Expected WebSocket request.')
queueconfig.q4ws.clear()
while True:
try:
if len(queueconfig.q4ws) > 0:
wsock.send(queueconfig.q4ws.popleft())
gevent.sleep(0)
except WebSocketError:
break
# Mute DeprecationWarning
warnings.simplefilter("ignore", DeprecationWarning)
server = CherryPyWSGIServer((str(ip), int(port)), app)
server = WSGIServer((str(ip), int(port)), app, handler_class=WebSocketHandler)
try:
logging.info('BAZARR is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url))
# print 'Bazarr is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url)
server.start()
server.serve_forever()
except KeyboardInterrupt:
shutdown()

4
bazarr/queueconfig.py Normal file
View File

@ -0,0 +1,4 @@
from collections import deque
global q4ws
q4ws = deque(maxlen=10)

View File

@ -0,0 +1,21 @@
VERSION = (0, 10, 1, 'final', 0)
__all__ = [
'WebSocketApplication',
'Resource',
'WebSocketServer',
'WebSocketError',
'get_version'
]
def get_version(*args, **kwargs):
from .utils import get_version
return get_version(*args, **kwargs)
try:
from .resource import WebSocketApplication, Resource
from .server import WebSocketServer
from .exceptions import WebSocketError
except ImportError:
pass

View File

@ -0,0 +1,23 @@
from __future__ import absolute_import, division, print_function
import sys
import codecs
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
if PY2:
bytes = str
text_type = unicode
string_types = basestring
range_type = xrange
iteritems = lambda x: x.iteritems()
# b = lambda x: x
else:
text_type = str
string_types = str,
range_type = range
iteritems = lambda x: iter(x.items())
# b = lambda x: codecs.latin_1_encode(x)[0]

View File

@ -0,0 +1,19 @@
from socket import error as socket_error
class WebSocketError(socket_error):
"""
Base class for all websocket errors.
"""
class ProtocolError(WebSocketError):
"""
Raised if an error occurs when de/encoding the websocket protocol.
"""
class FrameTooLargeException(ProtocolError):
"""
Raised if a frame is received that is too large.
"""

View File

@ -0,0 +1,6 @@
from geventwebsocket.handler import WebSocketHandler
from gunicorn.workers.ggevent import GeventPyWSGIWorker
class GeventWebSocketWorker(GeventPyWSGIWorker):
wsgi_handler = WebSocketHandler

View File

@ -0,0 +1,283 @@
import base64
import hashlib
from gevent.pywsgi import WSGIHandler
from ._compat import PY3
from .websocket import WebSocket, Stream
from .logging import create_logger
class Client(object):
def __init__(self, address, ws):
self.address = address
self.ws = ws
class WebSocketHandler(WSGIHandler):
"""
Automatically upgrades the connection to a websocket.
To prevent the WebSocketHandler to call the underlying WSGI application,
but only setup the WebSocket negotiations, do:
mywebsockethandler.prevent_wsgi_call = True
before calling run_application(). This is useful if you want to do more
things before calling the app, and want to off-load the WebSocket
negotiations to this library. Socket.IO needs this for example, to send
the 'ack' before yielding the control to your WSGI app.
"""
SUPPORTED_VERSIONS = ('13', '8', '7')
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def run_websocket(self):
"""
Called when a websocket has been created successfully.
"""
if getattr(self, 'prevent_wsgi_call', False):
return
# In case WebSocketServer is not used
if not hasattr(self.server, 'clients'):
self.server.clients = {}
# Since we're now a websocket connection, we don't care what the
# application actually responds with for the http response
try:
self.server.clients[self.client_address] = Client(
self.client_address, self.websocket)
list(self.application(self.environ, lambda s, h, e=None: []))
finally:
del self.server.clients[self.client_address]
if not self.websocket.closed:
self.websocket.close()
self.environ.update({
'wsgi.websocket': None
})
self.websocket = None
def run_application(self):
if (hasattr(self.server, 'pre_start_hook') and self.server.pre_start_hook):
self.logger.debug("Calling pre-start hook")
if self.server.pre_start_hook(self):
return super(WebSocketHandler, self).run_application()
self.logger.debug("Initializing WebSocket")
self.result = self.upgrade_websocket()
if hasattr(self, 'websocket'):
if self.status and not self.headers_sent:
self.write('')
self.run_websocket()
else:
if self.status:
# A status was set, likely an error so just send the response
if not self.result:
self.result = []
self.process_result()
return
# This handler did not handle the request, so defer it to the
# underlying application object
return super(WebSocketHandler, self).run_application()
def upgrade_websocket(self):
"""
Attempt to upgrade the current environ into a websocket enabled
connection. If successful, the environ dict with be updated with two
new entries, `wsgi.websocket` and `wsgi.websocket_version`.
:returns: Whether the upgrade was successful.
"""
# Some basic sanity checks first
self.logger.debug("Validating WebSocket request")
if self.environ.get('REQUEST_METHOD', '') != 'GET':
# This is not a websocket request, so we must not handle it
self.logger.debug('Can only upgrade connection if using GET method.')
return
upgrade = self.environ.get('HTTP_UPGRADE', '').lower()
if upgrade == 'websocket':
connection = self.environ.get('HTTP_CONNECTION', '').lower()
if 'upgrade' not in connection:
# This is not a websocket request, so we must not handle it
self.logger.warning("Client didn't ask for a connection "
"upgrade")
return
else:
# This is not a websocket request, so we must not handle it
return
if self.request_version != 'HTTP/1.1':
self.start_response('402 Bad Request', [])
self.logger.warning("Bad server protocol in headers")
return ['Bad protocol version']
if self.environ.get('HTTP_SEC_WEBSOCKET_VERSION'):
return self.upgrade_connection()
else:
self.logger.warning("No protocol defined")
self.start_response('426 Upgrade Required', [
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))])
return ['No Websocket protocol version defined']
def upgrade_connection(self):
"""
Validate and 'upgrade' the HTTP request to a WebSocket request.
If an upgrade succeeded then then handler will have `start_response`
with a status of `101`, the environ will also be updated with
`wsgi.websocket` and `wsgi.websocket_version` keys.
:param environ: The WSGI environ dict.
:param start_response: The callable used to start the response.
:param stream: File like object that will be read from/written to by
the underlying WebSocket object, if created.
:return: The WSGI response iterator is something went awry.
"""
self.logger.debug("Attempting to upgrade connection")
version = self.environ.get("HTTP_SEC_WEBSOCKET_VERSION")
if version not in self.SUPPORTED_VERSIONS:
msg = "Unsupported WebSocket Version: {0}".format(version)
self.logger.warning(msg)
self.start_response('400 Bad Request', [
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))
])
return [msg]
key = self.environ.get("HTTP_SEC_WEBSOCKET_KEY", '').strip()
if not key:
# 5.2.1 (3)
msg = "Sec-WebSocket-Key header is missing/empty"
self.logger.warning(msg)
self.start_response('400 Bad Request', [])
return [msg]
try:
key_len = len(base64.b64decode(key))
except TypeError:
msg = "Invalid key: {0}".format(key)
self.logger.warning(msg)
self.start_response('400 Bad Request', [])
return [msg]
if key_len != 16:
# 5.2.1 (3)
msg = "Invalid key: {0}".format(key)
self.logger.warning(msg)
self.start_response('400 Bad Request', [])
return [msg]
# Check for WebSocket Protocols
requested_protocols = self.environ.get(
'HTTP_SEC_WEBSOCKET_PROTOCOL', '')
protocol = None
if hasattr(self.application, 'app_protocol'):
allowed_protocol = self.application.app_protocol(
self.environ['PATH_INFO'])
if allowed_protocol and allowed_protocol in requested_protocols:
protocol = allowed_protocol
self.logger.debug("Protocol allowed: {0}".format(protocol))
self.websocket = WebSocket(self.environ, Stream(self), self)
self.environ.update({
'wsgi.websocket_version': version,
'wsgi.websocket': self.websocket
})
if PY3:
accept = base64.b64encode(
hashlib.sha1((key + self.GUID).encode("latin-1")).digest()
).decode("latin-1")
else:
accept = base64.b64encode(hashlib.sha1(key + self.GUID).digest())
headers = [
("Upgrade", "websocket"),
("Connection", "Upgrade"),
("Sec-WebSocket-Accept", accept)
]
if protocol:
headers.append(("Sec-WebSocket-Protocol", protocol))
self.logger.debug("WebSocket request accepted, switching protocols")
self.start_response("101 Switching Protocols", headers)
@property
def logger(self):
if not hasattr(self.server, 'logger'):
self.server.logger = create_logger(__name__)
return self.server.logger
def log_request(self):
if '101' not in str(self.status):
self.logger.info(self.format_request())
@property
def active_client(self):
return self.server.clients[self.client_address]
def start_response(self, status, headers, exc_info=None):
"""
Called when the handler is ready to send a response back to the remote
endpoint. A websocket connection may have not been created.
"""
writer = super(WebSocketHandler, self).start_response(
status, headers, exc_info=exc_info)
self._prepare_response()
return writer
def _prepare_response(self):
"""
Sets up the ``pywsgi.Handler`` to work with a websocket response.
This is used by other projects that need to support WebSocket
connections as part of a larger effort.
"""
assert not self.headers_sent
if not self.environ.get('wsgi.websocket'):
# a WebSocket connection is not established, do nothing
return
# So that `finalize_headers` doesn't write a Content-Length header
self.provided_content_length = False
# The websocket is now controlling the response
self.response_use_chunked = False
# Once the request is over, the connection must be closed
self.close_connection = True
# Prevents the Date header from being written
self.provided_date = True

View File

@ -0,0 +1,31 @@
from __future__ import absolute_import
from logging import getLogger, StreamHandler, getLoggerClass, Formatter, DEBUG
def create_logger(name, debug=False, format=None):
Logger = getLoggerClass()
class DebugLogger(Logger):
def getEffectiveLevel(x):
if x.level == 0 and debug:
return DEBUG
else:
return Logger.getEffectiveLevel(x)
class DebugHandler(StreamHandler):
def emit(x, record):
StreamHandler.emit(x, record) if debug else None
handler = DebugHandler()
handler.setLevel(DEBUG)
if format:
handler.setFormatter(Formatter(format))
logger = getLogger(name)
del logger.handlers[:]
logger.__class__ = DebugLogger
logger.addHandler(handler)
return logger

View File

@ -0,0 +1,35 @@
class BaseProtocol(object):
PROTOCOL_NAME = ''
def __init__(self, app):
self._app = app
def on_open(self):
self.app.on_open()
def on_message(self, message):
self.app.on_message(message)
def on_close(self, reason=None):
self.app.on_close(reason)
@property
def app(self):
if self._app:
return self._app
else:
raise Exception("No application coupled")
@property
def server(self):
if not hasattr(self.app, 'ws'):
return None
return self.app.ws.handler.server
@property
def handler(self):
if not hasattr(self.app, 'ws'):
return None
return self.app.ws.handler

View File

@ -0,0 +1,235 @@
import inspect
import random
import string
import types
try:
import ujson as json
except ImportError:
try:
import simplejson as json
except ImportError:
import json
from .._compat import range_type, string_types
from ..exceptions import WebSocketError
from .base import BaseProtocol
def export_rpc(arg=None):
if isinstance(arg, types.FunctionType):
arg._rpc = arg.__name__
return arg
def serialize(data):
return json.dumps(data)
class Prefixes(object):
def __init__(self):
self.prefixes = {}
def add(self, prefix, uri):
self.prefixes[prefix] = uri
def resolve(self, curie_or_uri):
if "http://" in curie_or_uri:
return curie_or_uri
elif ':' in curie_or_uri:
prefix, proc = curie_or_uri.split(':', 1)
return self.prefixes[prefix] + proc
else:
raise Exception(curie_or_uri)
class RemoteProcedures(object):
def __init__(self):
self.calls = {}
def register_procedure(self, uri, proc):
self.calls[uri] = proc
def register_object(self, uri, obj):
for k in inspect.getmembers(obj, inspect.ismethod):
if '_rpc' in k[1].__dict__:
proc_uri = uri + k[1]._rpc
self.calls[proc_uri] = (obj, k[1])
def call(self, uri, args):
if uri in self.calls:
proc = self.calls[uri]
# Do the correct call whether it's a function or instance method.
if isinstance(proc, tuple):
if proc[1].__self__ is None:
# Create instance of object and call method
return proc[1](proc[0](), *args)
else:
# Call bound method on instance
return proc[1](*args)
else:
return self.calls[uri](*args)
else:
raise Exception("no such uri '{}'".format(uri))
class Channels(object):
def __init__(self):
self.channels = {}
def create(self, uri, prefix_matching=False):
if uri not in self.channels:
self.channels[uri] = []
# TODO: implement prefix matching
def subscribe(self, uri, client):
if uri in self.channels:
self.channels[uri].append(client)
def unsubscribe(self, uri, client):
if uri not in self.channels:
return
client_index = self.channels[uri].index(client)
self.channels[uri].pop(client_index)
if len(self.channels[uri]) == 0:
del self.channels[uri]
def publish(self, uri, event, exclude=None, eligible=None):
if uri not in self.channels:
return
# TODO: exclude & eligible
msg = [WampProtocol.MSG_EVENT, uri, event]
for client in self.channels[uri]:
try:
client.ws.send(serialize(msg))
except WebSocketError:
# Seems someone didn't unsubscribe before disconnecting
self.channels[uri].remove(client)
class WampProtocol(BaseProtocol):
MSG_WELCOME = 0
MSG_PREFIX = 1
MSG_CALL = 2
MSG_CALL_RESULT = 3
MSG_CALL_ERROR = 4
MSG_SUBSCRIBE = 5
MSG_UNSUBSCRIBE = 6
MSG_PUBLISH = 7
MSG_EVENT = 8
PROTOCOL_NAME = "wamp"
def __init__(self, *args, **kwargs):
self.procedures = RemoteProcedures()
self.prefixes = Prefixes()
self.session_id = ''.join(
[random.choice(string.digits + string.letters)
for i in range_type(16)])
super(WampProtocol, self).__init__(*args, **kwargs)
def register_procedure(self, *args, **kwargs):
self.procedures.register_procedure(*args, **kwargs)
def register_object(self, *args, **kwargs):
self.procedures.register_object(*args, **kwargs)
def register_pubsub(self, *args, **kwargs):
if not hasattr(self.server, 'channels'):
self.server.channels = Channels()
self.server.channels.create(*args, **kwargs)
def do_handshake(self):
from geventwebsocket import get_version
welcome = [
self.MSG_WELCOME,
self.session_id,
1,
'gevent-websocket/' + get_version()
]
self.app.ws.send(serialize(welcome))
def _get_exception_info(self, e):
uri = 'http://TODO#generic'
desc = str(type(e))
details = str(e)
return [uri, desc, details]
def rpc_call(self, data):
call_id, curie_or_uri = data[1:3]
args = data[3:]
if not isinstance(call_id, string_types):
raise Exception()
if not isinstance(curie_or_uri, string_types):
raise Exception()
uri = self.prefixes.resolve(curie_or_uri)
try:
result = self.procedures.call(uri, args)
result_msg = [self.MSG_CALL_RESULT, call_id, result]
except Exception as e:
result_msg = [self.MSG_CALL_ERROR,
call_id] + self._get_exception_info(e)
self.app.on_message(serialize(result_msg))
def pubsub_action(self, data):
action = data[0]
curie_or_uri = data[1]
if not isinstance(action, int):
raise Exception()
if not isinstance(curie_or_uri, string_types):
raise Exception()
uri = self.prefixes.resolve(curie_or_uri)
if action == self.MSG_SUBSCRIBE and len(data) == 2:
self.server.channels.subscribe(data[1], self.handler.active_client)
elif action == self.MSG_UNSUBSCRIBE and len(data) == 2:
self.server.channels.unsubscribe(
data[1], self.handler.active_client)
elif action == self.MSG_PUBLISH and len(data) >= 3:
payload = data[2] if len(data) >= 3 else None
exclude = data[3] if len(data) >= 4 else None
eligible = data[4] if len(data) >= 5 else None
self.server.channels.publish(uri, payload, exclude, eligible)
def on_open(self):
self.app.on_open()
self.do_handshake()
def on_message(self, message):
data = json.loads(message)
if not isinstance(data, list):
raise Exception('incoming data is no list')
if data[0] == self.MSG_PREFIX and len(data) == 3:
prefix, uri = data[1:3]
self.prefixes.add(prefix, uri)
elif data[0] == self.MSG_CALL and len(data) >= 3:
return self.rpc_call(data)
elif data[0] in (self.MSG_SUBSCRIBE, self.MSG_UNSUBSCRIBE,
self.MSG_PUBLISH):
return self.pubsub_action(data)
else:
raise Exception("Unknown call")

View File

@ -0,0 +1,100 @@
import re
import warnings
from .protocols.base import BaseProtocol
from .exceptions import WebSocketError
try:
from collections import OrderedDict
except ImportError:
class OrderedDict:
pass
class WebSocketApplication(object):
protocol_class = BaseProtocol
def __init__(self, ws):
self.protocol = self.protocol_class(self)
self.ws = ws
def handle(self):
self.protocol.on_open()
while True:
try:
message = self.ws.receive()
except WebSocketError:
self.protocol.on_close()
break
self.protocol.on_message(message)
def on_open(self, *args, **kwargs):
pass
def on_close(self, *args, **kwargs):
pass
def on_message(self, message, *args, **kwargs):
self.ws.send(message, **kwargs)
@classmethod
def protocol_name(cls):
return cls.protocol_class.PROTOCOL_NAME
class Resource(object):
def __init__(self, apps=None):
self.apps = apps if apps else []
if isinstance(apps, dict):
if not isinstance(apps, OrderedDict):
warnings.warn("Using an unordered dictionary for the "
"app list is discouraged and may lead to "
"undefined behavior.", UserWarning)
self.apps = apps.items()
# An app can either be a standard WSGI application (an object we call with
# __call__(self, environ, start_response)) or a class we instantiate
# (and which can handle websockets). This function tells them apart.
# Override this if you have apps that can handle websockets but don't
# fulfill these criteria.
def _is_websocket_app(self, app):
return isinstance(app, type) and issubclass(app, WebSocketApplication)
def _app_by_path(self, environ_path, is_websocket_request):
# Which app matched the current path?
for path, app in self.apps:
if re.match(path, environ_path):
if is_websocket_request == self._is_websocket_app(app):
return app
return None
def app_protocol(self, path):
# app_protocol will only be called for websocket apps
app = self._app_by_path(path, True)
if hasattr(app, 'protocol_name'):
return app.protocol_name()
else:
return ''
def __call__(self, environ, start_response):
environ = environ
is_websocket_call = 'wsgi.websocket' in environ
current_app = self._app_by_path(environ['PATH_INFO'], is_websocket_call)
if current_app is None:
raise Exception("No apps defined")
if is_websocket_call:
ws = environ['wsgi.websocket']
current_app = current_app(ws)
current_app.ws = ws # TODO: needed?
current_app.handle()
# Always return something, calling WSGI middleware may rely on it
return []
else:
return current_app(environ, start_response)

View File

@ -0,0 +1,34 @@
from gevent.pywsgi import WSGIServer
from .handler import WebSocketHandler
from .logging import create_logger
class WebSocketServer(WSGIServer):
handler_class = WebSocketHandler
debug_log_format = (
'-' * 80 + '\n' +
'%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' +
'%(message)s\n' +
'-' * 80
)
def __init__(self, *args, **kwargs):
self.debug = kwargs.pop('debug', False)
self.pre_start_hook = kwargs.pop('pre_start_hook', None)
self._logger = None
self.clients = {}
super(WebSocketServer, self).__init__(*args, **kwargs)
def handle(self, socket, address):
handler = self.handler_class(socket, address, self)
handler.handle()
@property
def logger(self):
if not self._logger:
self._logger = create_logger(
__name__, self.debug, self.debug_log_format)
return self._logger

View File

@ -0,0 +1,224 @@
from ._compat import PY3
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Crossbar.io Technologies GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
# Note: This code is a Python implementation of the algorithm
# "Flexible and Economical UTF-8 Decoder" by Bjoern Hoehrmann
# bjoern@hoehrmann.de, http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
__all__ = ("Utf8Validator",)
# DFA transitions
UTF8VALIDATOR_DFA = (
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 00..1f
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 20..3f
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 40..5f
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 60..7f
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, # 80..9f
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, # a0..bf
8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # c0..df
0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, # e0..ef
0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, # f0..ff
0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, # s0..s0
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, # s1..s2
1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, # s3..s4
1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, # s5..s6
1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # s7..s8
)
UTF8_ACCEPT = 0
UTF8_REJECT = 1
# use Cython implementation of UTF8 validator if available
#
try:
from wsaccel.utf8validator import Utf8Validator
except ImportError:
#
# Fallback to pure Python implementation - also for PyPy.
#
# Do NOT touch this code unless you know what you are doing!
# https://github.com/oberstet/scratchbox/tree/master/python/utf8
#
if PY3:
# Python 3 and above
# convert DFA table to bytes (performance)
UTF8VALIDATOR_DFA_S = bytes(UTF8VALIDATOR_DFA)
class Utf8Validator(object):
"""
Incremental UTF-8 validator with constant memory consumption (minimal state).
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
"""
def __init__(self):
self.reset()
def decode(self, b):
"""
Eat one UTF-8 octet, and validate on the fly.
Returns ``UTF8_ACCEPT`` when enough octets have been consumed, in which case
``self.codepoint`` contains the decoded Unicode code point.
Returns ``UTF8_REJECT`` when invalid UTF-8 was encountered.
Returns some other positive integer when more octets need to be eaten.
"""
tt = UTF8VALIDATOR_DFA_S[b]
if self.state != UTF8_ACCEPT:
self.codepoint = (b & 0x3f) | (self.codepoint << 6)
else:
self.codepoint = (0xff >> tt) & b
self.state = UTF8VALIDATOR_DFA_S[256 + self.state * 16 + tt]
return self.state
def reset(self):
"""
Reset validator to start new incremental UTF-8 decode/validation.
"""
self.state = UTF8_ACCEPT # the empty string is valid UTF8
self.codepoint = 0
self.i = 0
def validate(self, ba):
"""
Incrementally validate a chunk of bytes provided as string.
Will return a quad ``(valid?, endsOnCodePoint?, currentIndex, totalIndex)``.
As soon as an octet is encountered which renders the octet sequence
invalid, a quad with ``valid? == False`` is returned. ``currentIndex`` returns
the index within the currently consumed chunk, and ``totalIndex`` the
index within the total consumed sequence that was the point of bail out.
When ``valid? == True``, currentIndex will be ``len(ba)`` and ``totalIndex`` the
total amount of consumed bytes.
"""
#
# The code here is written for optimal JITting in PyPy, not for best
# readability by your grandma or particular elegance. Do NOT touch!
#
l = len(ba)
i = 0
state = self.state
while i < l:
# optimized version of decode(), since we are not interested in actual code points
state = UTF8VALIDATOR_DFA_S[256 + (state << 4) + UTF8VALIDATOR_DFA_S[ba[i]]]
if state == UTF8_REJECT:
self.state = state
self.i += i
return False, False, i, self.i
i += 1
self.state = state
self.i += l
return True, state == UTF8_ACCEPT, l, self.i
else:
# convert DFA table to string (performance)
UTF8VALIDATOR_DFA_S = ''.join([chr(c) for c in UTF8VALIDATOR_DFA])
class Utf8Validator(object):
"""
Incremental UTF-8 validator with constant memory consumption (minimal state).
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
"""
def __init__(self):
self.reset()
def decode(self, b):
"""
Eat one UTF-8 octet, and validate on the fly.
Returns ``UTF8_ACCEPT`` when enough octets have been consumed, in which case
``self.codepoint`` contains the decoded Unicode code point.
Returns ``UTF8_REJECT`` when invalid UTF-8 was encountered.
Returns some other positive integer when more octets need to be eaten.
"""
tt = ord(UTF8VALIDATOR_DFA_S[b])
if self.state != UTF8_ACCEPT:
self.codepoint = (b & 0x3f) | (self.codepoint << 6)
else:
self.codepoint = (0xff >> tt) & b
self.state = ord(UTF8VALIDATOR_DFA_S[256 + self.state * 16 + tt])
return self.state
def reset(self):
"""
Reset validator to start new incremental UTF-8 decode/validation.
"""
self.state = UTF8_ACCEPT # the empty string is valid UTF8
self.codepoint = 0
self.i = 0
def validate(self, ba):
"""
Incrementally validate a chunk of bytes provided as string.
Will return a quad ``(valid?, endsOnCodePoint?, currentIndex, totalIndex)``.
As soon as an octet is encountered which renders the octet sequence
invalid, a quad with ``valid? == False`` is returned. ``currentIndex`` returns
the index within the currently consumed chunk, and ``totalIndex`` the
index within the total consumed sequence that was the point of bail out.
When ``valid? == True``, currentIndex will be ``len(ba)`` and ``totalIndex`` the
total amount of consumed bytes.
"""
#
# The code here is written for optimal JITting in PyPy, not for best
# readability by your grandma or particular elegance. Do NOT touch!
#
l = len(ba)
i = 0
state = self.state
while i < l:
# optimized version of decode(), since we are not interested in actual code points
try:
state = ord(UTF8VALIDATOR_DFA_S[256 + (state << 4) + ord(UTF8VALIDATOR_DFA_S[ba[i]])])
except:
import ipdb; ipdb.set_trace()
if state == UTF8_REJECT:
self.state = state
self.i += i
return False, False, i, self.i
i += 1
self.state = state
self.i += l
return True, state == UTF8_ACCEPT, l, self.i

View File

@ -0,0 +1,45 @@
import subprocess
def get_version(version=None):
"Returns a PEP 386-compliant version number from VERSION."
if version is None:
from geventwebsocket import VERSION as version
else:
assert len(version) == 5
assert version[3] in ('alpha', 'beta', 'rc', 'final')
# Now build the two parts of the version number:
# main = X.Y[.Z]
# sub = .devN - for pre-alpha releases
# | {a|b|c}N - for alpha, beta and rc releases
parts = 2 if version[2] == 0 else 3
main = '.'.join(str(x) for x in version[:parts])
sub = ''
if version[3] == 'alpha' and version[4] == 0:
hg_changeset = get_hg_changeset()
if hg_changeset:
sub = '.dev{0}'.format(hg_changeset)
elif version[3] != 'final':
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
sub = mapping[version[3]] + str(version[4])
return str(main + sub)
def get_hg_changeset():
rev, err = subprocess.Popen(
'hg id -i',
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
).communicate()
if err:
return None
else:
return rev.strip().replace('+', '')

View File

@ -0,0 +1,565 @@
import struct
from socket import error
from ._compat import string_types, range_type, text_type
from .exceptions import ProtocolError
from .exceptions import WebSocketError
from .exceptions import FrameTooLargeException
from .utf8validator import Utf8Validator
MSG_SOCKET_DEAD = "Socket is dead"
MSG_ALREADY_CLOSED = "Connection is already closed"
MSG_CLOSED = "Connection closed"
class WebSocket(object):
"""
Base class for supporting websocket operations.
:ivar environ: The http environment referenced by this connection.
:ivar closed: Whether this connection is closed/closing.
:ivar stream: The underlying file like object that will be read from /
written to by this WebSocket object.
"""
__slots__ = ('utf8validator', 'utf8validate_last', 'environ', 'closed',
'stream', 'raw_write', 'raw_read', 'handler')
OPCODE_CONTINUATION = 0x00
OPCODE_TEXT = 0x01
OPCODE_BINARY = 0x02
OPCODE_CLOSE = 0x08
OPCODE_PING = 0x09
OPCODE_PONG = 0x0a
def __init__(self, environ, stream, handler):
self.environ = environ
self.closed = False
self.stream = stream
self.raw_write = stream.write
self.raw_read = stream.read
self.utf8validator = Utf8Validator()
self.handler = handler
def __del__(self):
try:
self.close()
except:
# close() may fail if __init__ didn't complete
pass
def _decode_bytes(self, bytestring):
"""
Internal method used to convert the utf-8 encoded bytestring into
unicode.
If the conversion fails, the socket will be closed.
"""
if not bytestring:
return ''
try:
return bytestring.decode('utf-8')
except UnicodeDecodeError:
self.close(1007)
raise
def _encode_bytes(self, text):
"""
:returns: The utf-8 byte string equivalent of `text`.
"""
if not isinstance(text, str):
text = text_type(text or '')
return text.encode("utf-8")
def _is_valid_close_code(self, code):
"""
:returns: Whether the returned close code is a valid hybi return code.
"""
if code < 1000:
return False
if 1004 <= code <= 1006:
return False
if 1012 <= code <= 1016:
return False
if code == 1100:
# not sure about this one but the autobahn fuzzer requires it.
return False
if 2000 <= code <= 2999:
return False
return True
@property
def current_app(self):
if hasattr(self.handler.server.application, 'current_app'):
return self.handler.server.application.current_app
else:
# For backwards compatibility reasons
class MockApp():
def on_close(self, *args):
pass
return MockApp()
@property
def origin(self):
if not self.environ:
return
return self.environ.get('HTTP_ORIGIN')
@property
def protocol(self):
if not self.environ:
return
return self.environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')
@property
def version(self):
if not self.environ:
return
return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION')
@property
def path(self):
if not self.environ:
return
return self.environ.get('PATH_INFO')
@property
def logger(self):
return self.handler.logger
def handle_close(self, header, payload):
"""
Called when a close frame has been decoded from the stream.
:param header: The decoded `Header`.
:param payload: The bytestring payload associated with the close frame.
"""
if not payload:
self.close(1000, None)
return
if len(payload) < 2:
raise ProtocolError('Invalid close frame: {0} {1}'.format(
header, payload))
code = struct.unpack('!H', payload[:2])[0]
payload = payload[2:]
if payload:
validator = Utf8Validator()
val = validator.validate(payload)
if not val[0]:
raise UnicodeError
if not self._is_valid_close_code(code):
raise ProtocolError('Invalid close code {0}'.format(code))
self.close(code, payload)
def handle_ping(self, header, payload):
self.send_frame(payload, self.OPCODE_PONG)
def handle_pong(self, header, payload):
pass
def read_frame(self):
"""
Block until a full frame has been read from the socket.
This is an internal method as calling this will not cleanup correctly
if an exception is called. Use `receive` instead.
:return: The header and payload as a tuple.
"""
header = Header.decode_header(self.stream)
if header.flags:
raise ProtocolError
if not header.length:
return header, b''
try:
payload = self.raw_read(header.length)
except error:
payload = b''
except Exception:
# TODO log out this exception
payload = b''
if len(payload) != header.length:
raise WebSocketError('Unexpected EOF reading frame payload')
if header.mask:
payload = header.unmask_payload(payload)
return header, payload
def validate_utf8(self, payload):
# Make sure the frames are decodable independently
self.utf8validate_last = self.utf8validator.validate(payload)
if not self.utf8validate_last[0]:
raise UnicodeError("Encountered invalid UTF-8 while processing "
"text message at payload octet index "
"{0:d}".format(self.utf8validate_last[3]))
def read_message(self):
"""
Return the next text or binary message from the socket.
This is an internal method as calling this will not cleanup correctly
if an exception is called. Use `receive` instead.
"""
opcode = None
message = bytearray()
while True:
header, payload = self.read_frame()
f_opcode = header.opcode
if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY):
# a new frame
if opcode:
raise ProtocolError("The opcode in non-fin frame is "
"expected to be zero, got "
"{0!r}".format(f_opcode))
# Start reading a new message, reset the validator
self.utf8validator.reset()
self.utf8validate_last = (True, True, 0, 0)
opcode = f_opcode
elif f_opcode == self.OPCODE_CONTINUATION:
if not opcode:
raise ProtocolError("Unexpected frame with opcode=0")
elif f_opcode == self.OPCODE_PING:
self.handle_ping(header, payload)
continue
elif f_opcode == self.OPCODE_PONG:
self.handle_pong(header, payload)
continue
elif f_opcode == self.OPCODE_CLOSE:
self.handle_close(header, payload)
return
else:
raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode))
if opcode == self.OPCODE_TEXT:
self.validate_utf8(payload)
message += payload
if header.fin:
break
if opcode == self.OPCODE_TEXT:
self.validate_utf8(message)
return self._decode_bytes(message)
else:
return message
def receive(self):
"""
Read and return a message from the stream. If `None` is returned, then
the socket is considered closed/errored.
"""
if self.closed:
self.current_app.on_close(MSG_ALREADY_CLOSED)
raise WebSocketError(MSG_ALREADY_CLOSED)
try:
return self.read_message()
except UnicodeError:
self.close(1007)
except ProtocolError:
self.close(1002)
except error:
self.close()
self.current_app.on_close(MSG_CLOSED)
return None
def send_frame(self, message, opcode):
"""
Send a frame over the websocket with message as its payload
"""
if self.closed:
self.current_app.on_close(MSG_ALREADY_CLOSED)
raise WebSocketError(MSG_ALREADY_CLOSED)
if opcode in (self.OPCODE_TEXT, self.OPCODE_PING):
message = self._encode_bytes(message)
elif opcode == self.OPCODE_BINARY:
message = bytes(message)
header = Header.encode_header(True, opcode, b'', len(message), 0)
try:
self.raw_write(header + message)
except error:
raise WebSocketError(MSG_SOCKET_DEAD)
except:
raise
def send(self, message, binary=None):
"""
Send a frame over the websocket with message as its payload
"""
if binary is None:
binary = not isinstance(message, string_types)
opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT
try:
self.send_frame(message, opcode)
except WebSocketError:
self.current_app.on_close(MSG_SOCKET_DEAD)
raise WebSocketError(MSG_SOCKET_DEAD)
def close(self, code=1000, message=b''):
"""
Close the websocket and connection, sending the specified code and
message. The underlying socket object is _not_ closed, that is the
responsibility of the initiator.
"""
if self.closed:
self.current_app.on_close(MSG_ALREADY_CLOSED)
try:
message = self._encode_bytes(message)
self.send_frame(message, opcode=self.OPCODE_CLOSE)
except WebSocketError:
# Failed to write the closing frame but it's ok because we're
# closing the socket anyway.
self.logger.debug("Failed to write closing frame -> closing socket")
finally:
self.logger.debug("Closed WebSocket")
self.closed = True
self.stream = None
self.raw_write = None
self.raw_read = None
self.environ = None
#self.current_app.on_close(MSG_ALREADY_CLOSED)
class Stream(object):
"""
Wraps the handler's socket/rfile attributes and makes it in to a file like
object that can be read from/written to by the lower level websocket api.
"""
__slots__ = ('handler', 'read', 'write')
def __init__(self, handler):
self.handler = handler
self.read = handler.rfile.read
self.write = handler.socket.sendall
class Header(object):
__slots__ = ('fin', 'mask', 'opcode', 'flags', 'length')
FIN_MASK = 0x80
OPCODE_MASK = 0x0f
MASK_MASK = 0x80
LENGTH_MASK = 0x7f
RSV0_MASK = 0x40
RSV1_MASK = 0x20
RSV2_MASK = 0x10
# bitwise mask that will determine the reserved bits for a frame header
HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK
def __init__(self, fin=0, opcode=0, flags=0, length=0):
self.mask = ''
self.fin = fin
self.opcode = opcode
self.flags = flags
self.length = length
def mask_payload(self, payload):
payload = bytearray(payload)
mask = bytearray(self.mask)
for i in range_type(self.length):
payload[i] ^= mask[i % 4]
return payload
# it's the same operation
unmask_payload = mask_payload
def __repr__(self):
opcodes = {
0: 'continuation(0)',
1: 'text(1)',
2: 'binary(2)',
8: 'close(8)',
9: 'ping(9)',
10: 'pong(10)'
}
flags = {
0x40: 'RSV1 MASK',
0x20: 'RSV2 MASK',
0x10: 'RSV3 MASK'
}
return ("<Header fin={0} opcode={1} length={2} flags={3} mask={4} at "
"0x{5:x}>").format(
self.fin,
opcodes.get(self.opcode, 'reserved({})'.format(self.opcode)),
self.length,
flags.get(self.flags, 'reserved({})'.format(self.flags)),
self.mask, id(self)
)
@classmethod
def decode_header(cls, stream):
"""
Decode a WebSocket header.
:param stream: A file like object that can be 'read' from.
:returns: A `Header` instance.
"""
read = stream.read
data = read(2)
if len(data) != 2:
raise WebSocketError("Unexpected EOF while decoding header")
first_byte, second_byte = struct.unpack('!BB', data)
header = cls(
fin=first_byte & cls.FIN_MASK == cls.FIN_MASK,
opcode=first_byte & cls.OPCODE_MASK,
flags=first_byte & cls.HEADER_FLAG_MASK,
length=second_byte & cls.LENGTH_MASK)
has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK
if header.opcode > 0x07:
if not header.fin:
raise ProtocolError(
"Received fragmented control frame: {0!r}".format(data))
# Control frames MUST have a payload length of 125 bytes or less
if header.length > 125:
raise FrameTooLargeException(
"Control frame cannot be larger than 125 bytes: "
"{0!r}".format(data))
if header.length == 126:
# 16 bit length
data = read(2)
if len(data) != 2:
raise WebSocketError('Unexpected EOF while decoding header')
header.length = struct.unpack('!H', data)[0]
elif header.length == 127:
# 64 bit length
data = read(8)
if len(data) != 8:
raise WebSocketError('Unexpected EOF while decoding header')
header.length = struct.unpack('!Q', data)[0]
if has_mask:
mask = read(4)
if len(mask) != 4:
raise WebSocketError('Unexpected EOF while decoding header')
header.mask = mask
return header
@classmethod
def encode_header(cls, fin, opcode, mask, length, flags):
"""
Encodes a WebSocket header.
:param fin: Whether this is the final frame for this opcode.
:param opcode: The opcode of the payload, see `OPCODE_*`
:param mask: Whether the payload is masked.
:param length: The length of the frame.
:param flags: The RSV* flags.
:return: A bytestring encoded header.
"""
first_byte = opcode
second_byte = 0
extra = b""
result = bytearray()
if fin:
first_byte |= cls.FIN_MASK
if flags & cls.RSV0_MASK:
first_byte |= cls.RSV0_MASK
if flags & cls.RSV1_MASK:
first_byte |= cls.RSV1_MASK
if flags & cls.RSV2_MASK:
first_byte |= cls.RSV2_MASK
# now deal with length complexities
if length < 126:
second_byte += length
elif length <= 0xffff:
second_byte += 126
extra = struct.pack('!H', length)
elif length <= 0xffffffffffffffff:
second_byte += 127
extra = struct.pack('!Q', length)
else:
raise FrameTooLargeException
if mask:
second_byte |= cls.MASK_MASK
result.append(first_byte)
result.append(second_byte)
result.extend(extra)
if mask:
result.extend(mask)
return result

View File

@ -9,6 +9,7 @@ chardet=3.0.4
configparser=3.5.0
dogpile.cache=0.6.5
enzyme=0.4.1
geventwebsocker=0.10.1
gitpython=2.1.9
guessit=2.1.4
langdetect=1.0.7

File diff suppressed because one or more lines are too long

697
static/noty/noty.css Normal file
View File

@ -0,0 +1,697 @@
.noty_layout_mixin, #noty_layout__top, #noty_layout__topLeft, #noty_layout__topCenter, #noty_layout__topRight, #noty_layout__bottom, #noty_layout__bottomLeft, #noty_layout__bottomCenter, #noty_layout__bottomRight, #noty_layout__center, #noty_layout__centerLeft, #noty_layout__centerRight {
position: fixed;
margin: 0;
padding: 0;
z-index: 9999999;
-webkit-transform: translateZ(0) scale(1, 1);
transform: translateZ(0) scale(1, 1);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-font-smoothing: subpixel-antialiased;
filter: blur(0);
-webkit-filter: blur(0);
max-width: 90%; }
#noty_layout__top {
top: 0;
left: 5%;
width: 90%; }
#noty_layout__topLeft {
top: 20px;
left: 20px;
width: 325px; }
#noty_layout__topCenter {
top: 5%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__topRight {
top: 20px;
right: 20px;
width: 325px; }
#noty_layout__bottom {
bottom: 0;
left: 5%;
width: 90%; }
#noty_layout__bottomLeft {
bottom: 20px;
left: 20px;
width: 325px; }
#noty_layout__bottomCenter {
bottom: 5%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__bottomRight {
bottom: 20px;
right: 20px;
width: 325px; }
#noty_layout__center {
top: 50%;
left: 50%;
width: 325px;
-webkit-transform: translate(-webkit-calc(-50% - .5px), -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(calc(-50% - .5px), calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__centerLeft {
top: 50%;
left: 20px;
width: 325px;
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
#noty_layout__centerRight {
top: 50%;
right: 20px;
width: 325px;
-webkit-transform: translate(0, -webkit-calc(-50% - .5px)) translateZ(0) scale(1, 1);
transform: translate(0, calc(-50% - .5px)) translateZ(0) scale(1, 1); }
.noty_progressbar {
display: none; }
.noty_has_timeout.noty_has_progressbar .noty_progressbar {
display: block;
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background-color: #646464;
opacity: 0.2;
filter: alpha(opacity=10); }
.noty_bar {
-webkit-backface-visibility: hidden;
-webkit-transform: translate(0, 0) translateZ(0) scale(1, 1);
-ms-transform: translate(0, 0) scale(1, 1);
transform: translate(0, 0) scale(1, 1);
-webkit-font-smoothing: subpixel-antialiased;
overflow: hidden; }
.noty_effects_open {
opacity: 0;
-webkit-transform: translate(50%);
-ms-transform: translate(50%);
transform: translate(50%);
-webkit-animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
.noty_effects_close {
-webkit-animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: noty_anim_out 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
.noty_fix_effects_height {
-webkit-animation: noty_anim_height 75ms ease-out;
animation: noty_anim_height 75ms ease-out; }
.noty_close_with_click {
cursor: pointer; }
.noty_close_button {
position: absolute;
top: 2px;
right: 2px;
font-weight: bold;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 2px;
cursor: pointer;
-webkit-transition: all .2s ease-out;
transition: all .2s ease-out; }
.noty_close_button:hover {
background-color: rgba(0, 0, 0, 0.1); }
.noty_modal {
position: fixed;
width: 100%;
height: 100%;
background-color: #000;
z-index: 10000;
opacity: .3;
left: 0;
top: 0; }
.noty_modal.noty_modal_open {
opacity: 0;
-webkit-animation: noty_modal_in .3s ease-out;
animation: noty_modal_in .3s ease-out; }
.noty_modal.noty_modal_close {
-webkit-animation: noty_modal_out .3s ease-out;
animation: noty_modal_out .3s ease-out;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards; }
@-webkit-keyframes noty_modal_in {
100% {
opacity: .3; } }
@keyframes noty_modal_in {
100% {
opacity: .3; } }
@-webkit-keyframes noty_modal_out {
100% {
opacity: 0; } }
@keyframes noty_modal_out {
100% {
opacity: 0; } }
@keyframes noty_modal_out {
100% {
opacity: 0; } }
@-webkit-keyframes noty_anim_in {
100% {
-webkit-transform: translate(0);
transform: translate(0);
opacity: 1; } }
@keyframes noty_anim_in {
100% {
-webkit-transform: translate(0);
transform: translate(0);
opacity: 1; } }
@-webkit-keyframes noty_anim_out {
100% {
-webkit-transform: translate(50%);
transform: translate(50%);
opacity: 0; } }
@keyframes noty_anim_out {
100% {
-webkit-transform: translate(50%);
transform: translate(50%);
opacity: 0; } }
@-webkit-keyframes noty_anim_height {
100% {
height: 0; } }
@keyframes noty_anim_height {
100% {
height: 0; } }
.noty_theme__relax.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__relax.noty_bar .noty_body {
padding: 10px; }
.noty_theme__relax.noty_bar .noty_buttons {
border-top: 1px solid #e7e7e7;
padding: 5px 10px; }
.noty_theme__relax.noty_type__alert,
.noty_theme__relax.noty_type__notification {
background-color: #fff;
border: 1px solid #dedede;
color: #444; }
.noty_theme__relax.noty_type__warning {
background-color: #FFEAA8;
border: 1px solid #FFC237;
color: #826200; }
.noty_theme__relax.noty_type__warning .noty_buttons {
border-color: #dfaa30; }
.noty_theme__relax.noty_type__error {
background-color: #FF8181;
border: 1px solid #e25353;
color: #FFF; }
.noty_theme__relax.noty_type__error .noty_buttons {
border-color: darkred; }
.noty_theme__relax.noty_type__info,
.noty_theme__relax.noty_type__information {
background-color: #78C5E7;
border: 1px solid #3badd6;
color: #FFF; }
.noty_theme__relax.noty_type__info .noty_buttons,
.noty_theme__relax.noty_type__information .noty_buttons {
border-color: #0B90C4; }
.noty_theme__relax.noty_type__success {
background-color: #BCF5BC;
border: 1px solid #7cdd77;
color: darkgreen; }
.noty_theme__relax.noty_type__success .noty_buttons {
border-color: #50C24E; }
.noty_theme__metroui.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
box-shadow: rgba(0, 0, 0, 0.298039) 0 0 5px 0; }
.noty_theme__metroui.noty_bar .noty_progressbar {
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background-color: #000;
opacity: 0.2;
filter: alpha(opacity=20); }
.noty_theme__metroui.noty_bar .noty_body {
padding: 1.25em;
font-size: 14px; }
.noty_theme__metroui.noty_bar .noty_buttons {
padding: 0 10px .5em 10px; }
.noty_theme__metroui.noty_type__alert,
.noty_theme__metroui.noty_type__notification {
background-color: #fff;
color: #1d1d1d; }
.noty_theme__metroui.noty_type__warning {
background-color: #FA6800;
color: #fff; }
.noty_theme__metroui.noty_type__error {
background-color: #CE352C;
color: #FFF; }
.noty_theme__metroui.noty_type__info,
.noty_theme__metroui.noty_type__information {
background-color: #1BA1E2;
color: #FFF; }
.noty_theme__metroui.noty_type__success {
background-color: #60A917;
color: #fff; }
.noty_theme__mint.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__mint.noty_bar .noty_body {
padding: 10px;
font-size: 14px; }
.noty_theme__mint.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__mint.noty_type__alert,
.noty_theme__mint.noty_type__notification {
background-color: #fff;
border-bottom: 1px solid #D1D1D1;
color: #2F2F2F; }
.noty_theme__mint.noty_type__warning {
background-color: #FFAE42;
border-bottom: 1px solid #E89F3C;
color: #fff; }
.noty_theme__mint.noty_type__error {
background-color: #DE636F;
border-bottom: 1px solid #CA5A65;
color: #fff; }
.noty_theme__mint.noty_type__info,
.noty_theme__mint.noty_type__information {
background-color: #7F7EFF;
border-bottom: 1px solid #7473E8;
color: #fff; }
.noty_theme__mint.noty_type__success {
background-color: #AFC765;
border-bottom: 1px solid #A0B55C;
color: #fff; }
.noty_theme__sunset.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__sunset.noty_bar .noty_body {
padding: 10px;
font-size: 14px;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }
.noty_theme__sunset.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__sunset.noty_type__alert,
.noty_theme__sunset.noty_type__notification {
background-color: #073B4C;
color: #fff; }
.noty_theme__sunset.noty_type__alert .noty_progressbar,
.noty_theme__sunset.noty_type__notification .noty_progressbar {
background-color: #fff; }
.noty_theme__sunset.noty_type__warning {
background-color: #FFD166;
color: #fff; }
.noty_theme__sunset.noty_type__error {
background-color: #EF476F;
color: #fff; }
.noty_theme__sunset.noty_type__error .noty_progressbar {
opacity: .4; }
.noty_theme__sunset.noty_type__info,
.noty_theme__sunset.noty_type__information {
background-color: #118AB2;
color: #fff; }
.noty_theme__sunset.noty_type__info .noty_progressbar,
.noty_theme__sunset.noty_type__information .noty_progressbar {
opacity: .6; }
.noty_theme__sunset.noty_type__success {
background-color: #06D6A0;
color: #fff; }
.noty_theme__bootstrap-v3.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
border: 1px solid transparent;
border-radius: 4px; }
.noty_theme__bootstrap-v3.noty_bar .noty_body {
padding: 15px; }
.noty_theme__bootstrap-v3.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__bootstrap-v3.noty_bar .noty_close_button {
font-size: 21px;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: .2;
background: transparent; }
.noty_theme__bootstrap-v3.noty_bar .noty_close_button:hover {
background: transparent;
text-decoration: none;
cursor: pointer;
filter: alpha(opacity=50);
opacity: .5; }
.noty_theme__bootstrap-v3.noty_type__alert,
.noty_theme__bootstrap-v3.noty_type__notification {
background-color: #fff;
color: inherit; }
.noty_theme__bootstrap-v3.noty_type__warning {
background-color: #fcf8e3;
color: #8a6d3b;
border-color: #faebcc; }
.noty_theme__bootstrap-v3.noty_type__error {
background-color: #f2dede;
color: #a94442;
border-color: #ebccd1; }
.noty_theme__bootstrap-v3.noty_type__info,
.noty_theme__bootstrap-v3.noty_type__information {
background-color: #d9edf7;
color: #31708f;
border-color: #bce8f1; }
.noty_theme__bootstrap-v3.noty_type__success {
background-color: #dff0d8;
color: #3c763d;
border-color: #d6e9c6; }
.noty_theme__bootstrap-v4.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
border: 1px solid transparent;
border-radius: .25rem; }
.noty_theme__bootstrap-v4.noty_bar .noty_body {
padding: .75rem 1.25rem; }
.noty_theme__bootstrap-v4.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__bootstrap-v4.noty_bar .noty_close_button {
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: .5;
background: transparent; }
.noty_theme__bootstrap-v4.noty_bar .noty_close_button:hover {
background: transparent;
text-decoration: none;
cursor: pointer;
filter: alpha(opacity=50);
opacity: .75; }
.noty_theme__bootstrap-v4.noty_type__alert,
.noty_theme__bootstrap-v4.noty_type__notification {
background-color: #fff;
color: inherit; }
.noty_theme__bootstrap-v4.noty_type__warning {
background-color: #fcf8e3;
color: #8a6d3b;
border-color: #faebcc; }
.noty_theme__bootstrap-v4.noty_type__error {
background-color: #f2dede;
color: #a94442;
border-color: #ebccd1; }
.noty_theme__bootstrap-v4.noty_type__info,
.noty_theme__bootstrap-v4.noty_type__information {
background-color: #d9edf7;
color: #31708f;
border-color: #bce8f1; }
.noty_theme__bootstrap-v4.noty_type__success {
background-color: #dff0d8;
color: #3c763d;
border-color: #d6e9c6; }
.noty_theme__semanticui.noty_bar {
margin: 4px 0;
overflow: hidden;
position: relative;
border: 1px solid transparent;
font-size: 1em;
border-radius: .28571429rem;
box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_bar .noty_body {
padding: 1em 1.5em;
line-height: 1.4285em; }
.noty_theme__semanticui.noty_bar .noty_buttons {
padding: 10px; }
.noty_theme__semanticui.noty_type__alert,
.noty_theme__semanticui.noty_type__notification {
background-color: #f8f8f9;
color: rgba(0, 0, 0, 0.87); }
.noty_theme__semanticui.noty_type__warning {
background-color: #fffaf3;
color: #573a08;
box-shadow: 0 0 0 1px #c9ba9b inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_type__error {
background-color: #fff6f6;
color: #9f3a38;
box-shadow: 0 0 0 1px #e0b4b4 inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_type__info,
.noty_theme__semanticui.noty_type__information {
background-color: #f8ffff;
color: #276f86;
box-shadow: 0 0 0 1px #a9d5de inset, 0 0 0 0 transparent; }
.noty_theme__semanticui.noty_type__success {
background-color: #fcfff5;
color: #2c662d;
box-shadow: 0 0 0 1px #a3c293 inset, 0 0 0 0 transparent; }
.noty_theme__nest.noty_bar {
margin: 0 0 15px 0;
overflow: hidden;
border-radius: 2px;
position: relative;
box-shadow: rgba(0, 0, 0, 0.098039) 5px 4px 10px 0; }
.noty_theme__nest.noty_bar .noty_body {
padding: 10px;
font-size: 14px;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }
.noty_theme__nest.noty_bar .noty_buttons {
padding: 10px; }
.noty_layout .noty_theme__nest.noty_bar {
z-index: 5; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(2) {
position: absolute;
top: 0;
margin-top: 4px;
margin-right: -4px;
margin-left: 4px;
z-index: 4;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(3) {
position: absolute;
top: 0;
margin-top: 8px;
margin-right: -8px;
margin-left: 8px;
z-index: 3;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(4) {
position: absolute;
top: 0;
margin-top: 12px;
margin-right: -12px;
margin-left: 12px;
z-index: 2;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(5) {
position: absolute;
top: 0;
margin-top: 16px;
margin-right: -16px;
margin-left: 16px;
z-index: 1;
width: 100%; }
.noty_layout .noty_theme__nest.noty_bar:nth-child(n+6) {
position: absolute;
top: 0;
margin-top: 20px;
margin-right: -20px;
margin-left: 20px;
z-index: -1;
width: 100%; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(2),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(2) {
margin-top: 4px;
margin-left: -4px;
margin-right: 4px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(3),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(3) {
margin-top: 8px;
margin-left: -8px;
margin-right: 8px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(4),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(4) {
margin-top: 12px;
margin-left: -12px;
margin-right: 12px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(5),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(5) {
margin-top: 16px;
margin-left: -16px;
margin-right: 16px; }
#noty_layout__bottomLeft .noty_theme__nest.noty_bar:nth-child(n+6),
#noty_layout__topLeft .noty_theme__nest.noty_bar:nth-child(n+6) {
margin-top: 20px;
margin-left: -20px;
margin-right: 20px; }
.noty_theme__nest.noty_type__alert,
.noty_theme__nest.noty_type__notification {
background-color: #073B4C;
color: #fff; }
.noty_theme__nest.noty_type__alert .noty_progressbar,
.noty_theme__nest.noty_type__notification .noty_progressbar {
background-color: #fff; }
.noty_theme__nest.noty_type__warning {
background-color: #FFD166;
color: #fff; }
.noty_theme__nest.noty_type__error {
background-color: #EF476F;
color: #fff; }
.noty_theme__nest.noty_type__error .noty_progressbar {
opacity: .4; }
.noty_theme__nest.noty_type__info,
.noty_theme__nest.noty_type__information {
background-color: #118AB2;
color: #fff; }
.noty_theme__nest.noty_type__info .noty_progressbar,
.noty_theme__nest.noty_type__information .noty_progressbar {
opacity: .6; }
.noty_theme__nest.noty_type__success {
background-color: #06D6A0;
color: #fff; }
.noty_theme__light.noty_bar {
margin: 4px 0;
overflow: hidden;
border-radius: 2px;
position: relative; }
.noty_theme__light.noty_bar .noty_body {
padding: 10px; }
.noty_theme__light.noty_bar .noty_buttons {
border-top: 1px solid #e7e7e7;
padding: 5px 10px; }
.noty_theme__light.noty_type__alert,
.noty_theme__light.noty_type__notification {
background-color: #fff;
border: 1px solid #dedede;
color: #444; }
.noty_theme__light.noty_type__warning {
background-color: #FFEAA8;
border: 1px solid #FFC237;
color: #826200; }
.noty_theme__light.noty_type__warning .noty_buttons {
border-color: #dfaa30; }
.noty_theme__light.noty_type__error {
background-color: #ED7000;
border: 1px solid #e25353;
color: #FFF; }
.noty_theme__light.noty_type__error .noty_buttons {
border-color: darkred; }
.noty_theme__light.noty_type__info,
.noty_theme__light.noty_type__information {
background-color: #78C5E7;
border: 1px solid #3badd6;
color: #FFF; }
.noty_theme__light.noty_type__info .noty_buttons,
.noty_theme__light.noty_type__information .noty_buttons {
border-color: #0B90C4; }
.noty_theme__light.noty_type__success {
background-color: #57C880;
border: 1px solid #7cdd77;
color: darkgreen; }
.noty_theme__light.noty_type__success .noty_buttons {
border-color: #50C24E; }
/*# sourceMappingURL=noty.css.map*/

17
static/noty/noty.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -9,7 +9,6 @@
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.css">
<link rel="stylesheet" type="text/css" href="{{base_url}}static/datatables/datatables.min.css"/>
<link rel="stylesheet" type="text/css" href="{{base_url}}static/datatables/semanticui.min.css"/>
<link rel="apple-touch-icon" sizes="120x120" href="{{base_url}}static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{base_url}}static/favicon-32x32.png">
@ -37,14 +36,14 @@
margin-bottom: 3em;
padding: 2em;
border-radius: 1px;
box-shadow: 0px 0px 5px 5px #000000;
box-shadow: 0 0 5px 5px #000000;
min-height: calc(250px + 4em);
}
#fondblanc {
background-color: #ffffff;
opacity: 0.9;
border-radius: 1px;
box-shadow: 0px 0px 3px 3px #ffffff;
box-shadow: 0 0 3px 3px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding-top: 2em;
@ -78,11 +77,11 @@
</style>
<script>
$(document).ready(function(){
$(function(){
$('.ui.accordion').accordion();
var first_season_acc_title = document.getElementsByClassName("title")[0];
const first_season_acc_title = document.getElementsByClassName("title")[0];
first_season_acc_title.className += " active";
var first_season_acc_content = document.getElementsByClassName("content")[0];
const first_season_acc_content = document.getElementsByClassName("content")[0];
first_season_acc_content.className += " active";
});
</script>
@ -230,12 +229,12 @@
%>
<a data-episodePath="{{episode[1]}}" data-scenename="{{episode[8]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{details[4]}}" data-sonarrSeriesId="{{episode[5]}}" data-sonarrEpisodeId="{{episode[7]}}" class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%else:
<a data-tooltip="Automatic searching delayed (adaptive search)" data-position="top right" data-inverted="" data-episodePath="{{episode[1]}}" data-scenename="{{episode[8]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{details[4]}}" data-sonarrSeriesId="{{episode[5]}}" data-sonarrEpisodeId="{{episode[7]}}" class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search red icon"></i>
<i style="margin-left:3px; margin-right:0" class="search red icon"></i>
</a>
%end
%end
@ -243,7 +242,7 @@
%else:
<a data-episodePath="{{episode[1]}}" data-scenename="{{episode[8]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{details[4]}}" data-sonarrSeriesId="{{episode[5]}}" data-sonarrEpisodeId="{{episode[7]}}" class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%end
%end
@ -356,18 +355,18 @@
</html>
<script>
$('#scan_disk').click(function(){
$('#scan_disk').on('click', function(){
$('#loader_text').text("Scanning disk for existing subtitles...");
window.location = '{{base_url}}scan_disk/{{no}}';
})
});
$('#search_missing_subtitles').click(function(){
$('#search_missing_subtitles').on('click', function(){
$('#loader_text').text("Searching for missing subtitles...");
window.location = '{{base_url}}search_missing_subtitles/{{no}}';
})
});
$('.remove_subtitles').click(function(){
var values = {
$('.remove_subtitles').on('click', function(){
const values = {
episodePath: $(this).attr("data-episodePath"),
language: $(this).attr("data-language"),
subtitlesPath: $(this).attr("data-subtitlesPath"),
@ -390,10 +389,10 @@
$(document).ajaxStop(function(){
window.location.reload();
});
})
});
$('.get_subtitle').click(function(){
var values = {
$('.get_subtitle').on('click', function(){
const values = {
episodePath: $(this).attr("data-episodePath"),
sceneName: $(this).attr("data-sceneName"),
language: $(this).attr("data-language"),
@ -417,19 +416,18 @@
$(document).ajaxStop(function(){
window.location.reload();
});
})
});
$('a:not(.manual_search), .menu .item, button:not(#config, .cancel)').click(function(){
$('a:not(.manual_search), .menu .item, button:not(#config, .cancel)').on('click', function(){
$('#loader').addClass('active');
})
});
$('.modal')
.modal({
autofocus: false
})
;
});
$('#config').click(function(){
$('#config').on('click', function(){
$('#series_form').attr('action', '{{base_url}}edit_series/{{no}}');
$("#series_title").html($(this).data("title"));
@ -438,10 +436,10 @@
$("#series_audio_language").html($(this).data("audio"));
$('#series_languages').dropdown('clear');
var languages_array = eval($(this).data("languages"));
const languages_array = eval($(this).data("languages"));
$('#series_languages').dropdown('set selected',languages_array);
if ($(this).data("hearing-impaired") == "True") {
if ($(this).data("hearing-impaired") === "True") {
$("#series_hearing-impaired_div").checkbox('check');
} else {
$("#series_hearing-impaired_div").checkbox('uncheck');
@ -451,24 +449,23 @@
.modal({
centered: true
})
.modal('show')
;
})
.modal('show');
});
$('.manual_search').click(function(){
$('.manual_search').on('click', function(){
$("#series_title_span").html($(this).data("series_title"));
$("#season").html($(this).data("season"));
$("#episode").html($(this).data("episode"));
$("#episode_title").html($(this).data("episode_title"));
episodePath = $(this).attr("data-episodePath"),
sceneName = $(this).attr("data-sceneName"),
language = $(this).attr("data-language"),
hi = $(this).attr("data-hi"),
sonarrSeriesId = $(this).attr("data-sonarrSeriesId"),
sonarrEpisodeId = $(this).attr("data-sonarrEpisodeId")
episodePath = $(this).attr("data-episodePath");
sceneName = $(this).attr("data-sceneName");
language = $(this).attr("data-language");
hi = $(this).attr("data-hi");
sonarrSeriesId = $(this).attr("data-sonarrSeriesId");
sonarrEpisodeId = $(this).attr("data-sonarrEpisodeId");
var values = {
const values = {
episodePath: episodePath,
sceneName: sceneName,
language: language,
@ -513,19 +510,19 @@
},
{ data: null,
render: function ( data, type, row ) {
var array_matches = data.matches;
var array_dont_matches = data.dont_matches;
var i;
text = '<div class="ui inline dropdown"><i class="green check icon"></i><div class="text">'
const array_matches = data.matches;
const array_dont_matches = data.dont_matches;
let i;
let text = '<div class="ui inline dropdown"><i class="green check icon"></i><div class="text">';
text += array_matches.length;
text += '</div><i class="dropdown icon"></i><div class="menu">'
text += '</div><i class="dropdown icon"></i><div class="menu">';
for (i = 0; i < array_matches.length; i++) {
text += '<div class="criteria_matched disabled item">' + array_matches[i] + '</div>';
}
text += '</div></div>';
text += '<div class="ui inline dropdown"><i class="red times icon"></i><div class="text">'
text += '<div class="ui inline dropdown"><i class="red times icon"></i><div class="text">';
text += array_dont_matches.length;
text += '</div><i class="dropdown icon"></i><div class="menu">'
text += '</div><i class="dropdown icon"></i><div class="menu">';
for (i = 0; i < array_dont_matches.length; i++) {
text += '<div class="criteria_not_matched disabled item">' + array_dont_matches[i] + '</div>';
}
@ -545,12 +542,11 @@
.modal({
centered: false
})
.modal('show')
;
})
.modal('show');
});
function manual_get(button, episodePath, sceneName, hi, sonarrSeriesId, sonarrEpisodeId){
var values = {
const values = {
subtitle: $(button).attr("data-subtitle"),
provider: $(button).attr("data-provider"),
episodePath: episodePath,

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -23,16 +23,13 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 1em;
overflow-x:auto;
}
#logs {
margin-top: 4em;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
@ -76,16 +73,15 @@
<script>
$('.menu .item')
.tab()
;
.tab();
$('#series_tab').click(function() {
$('#series_tab').on('click', function() {
loadURLseries(1);
})
});
$('#movies_tab').click(function() {
$('#movies_tab').on('click', function() {
loadURLmovies(1);
})
});
function loadURLseries(page) {
$.ajax({
@ -109,25 +105,25 @@
});
}
$('a:not(.tabs), button:not(.cancel, #download_log)').click(function(){
$('a:not(.tabs), button:not(.cancel, #download_log)').on('click', function(){
$('#loader').addClass('active');
})
});
if ($('#series_tab').data("enabled") == "True") {
if ($('#series_tab').data("enabled") === "True") {
$("#series_tab").removeClass('disabled');
} else {
$("#series_tab").addClass('disabled');
}
if ($('#movies_tab').data("enabled") == "True") {
if ($('#movies_tab').data("enabled") === "True") {
$("#movies_tab").removeClass('disabled');
} else {
$("#movies_tab").addClass('disabled');
}
if ($('#series_tab').data("enabled") == "True") {
if ($('#series_tab').data("enabled") === "True") {
$( "#series_tab" ).trigger( "click" );
}
if ($('#series_tab').data("enabled") == "False" && $('#movies_tab').data("enabled") == "True") {
if ($('#series_tab').data("enabled") === "False" && $('#movies_tab').data("enabled") === "True") {
$( "#movies_tab" ).trigger( "click" );
}
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -21,15 +21,6 @@
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 3em;
overflow-x:auto;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
@ -183,22 +174,22 @@
sessionStorage.clear();
}
$('a, i').click(function(){
$('a, i').on('click', function(){
sessionStorage.scrolly=$(window).scrollTop();
$('#loader').addClass('active');
})
});
$('.fast.backward').click(function(){
$('.fast.backward').on('click', function(){
loadURLseries(1);
})
$('.backward:not(.fast)').click(function(){
});
$('.backward:not(.fast)').on('click', function(){
loadURLseries({{int(page)-1}});
})
$('.forward:not(.fast)').click(function(){
});
$('.forward:not(.fast)').on('click', function(){
loadURLseries({{int(page)+1}});
})
$('.fast.forward').click(function(){
});
$('.fast.forward').on('click', function(){
loadURLseries({{int(max_page)}});
})
});
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -21,15 +21,6 @@
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 3em;
overflow-x:auto;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
@ -198,22 +189,22 @@
sessionStorage.clear();
}
$('a, i').click(function(){
$('a, i').on('click', function(){
sessionStorage.scrolly=$(window).scrollTop();
$('#loader').addClass('active');
})
});
$('.fast.backward').click(function(){
$('.fast.backward').on('click', function(){
loadURLseries(1);
})
$('.backward:not(.fast)').click(function(){
});
$('.backward:not(.fast)').on('click', function(){
loadURLseries({{int(page)-1}});
})
$('.forward:not(.fast)').click(function(){
});
$('.forward:not(.fast)').on('click', function(){
loadURLseries({{int(page)+1}});
})
$('.fast.forward').click(function(){
});
$('.fast.forward').on('click', function(){
loadURLseries({{int(max_page)}});
})
});
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -24,7 +24,7 @@
height: 100%;
}
.image {
margin-top: 0px;
margin-top: 0;
}
.column {
max-width: 450px;
@ -64,8 +64,5 @@
</div>
</div>
</form>
</body>
</html>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -105,17 +105,16 @@ bug icon \\
$('.modal')
.modal({
autofocus: false
})
;
});
$('.log').click(function(){
$('.log').on('click', function(){
$("#message").html($(this).data("message"));
exception = $(this).data("exception");
let exception = $(this).data("exception");
exception = exception.replace(/'/g,"");
exception = exception.replace(/\\n\s\s\s\s/g, "\\n&emsp;&emsp;");
exception = exception.replace(/\\n\s\s\s\s/g, "\\n&emsp;&emsp;");
exception = exception.replace(/\\n\s\s/g, "\\n&emsp;");
exception = exception.replace(/\\n/g, "<br />")
exception = exception.replace(/\\n/g, "<br />");
$("#exception").html(exception);
$('#modal').modal('show');
})
});
</script>

View File

@ -1,6 +1,8 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<link href="{{base_url}}static/noty/noty.css" rel="stylesheet">
<script src="{{base_url}}static/noty/noty.min.js" type="text/javascript"></script>
<style>
#divmenu {
background-color: #000000;
@ -26,7 +28,7 @@
% import sqlite3
% from get_settings import get_general_settings
%if get_general_settings()[24] is True:
%if get_general_settings()[24]:
% monitored_only_query_string = ' AND monitored = "True"'
%else:
% monitored_only_query_string = ""
@ -50,13 +52,13 @@
<div class="sixteen wide column">
<div class="ui inverted borderless labeled icon massive menu six item">
<div class="ui container">
% if get_general_settings()[12] is True:
% if get_general_settings()[12]:
<a class="item" href="{{base_url}}series">
<i class="play icon"></i>
Series
</a>
% end
% if get_general_settings()[13] is True:
% if get_general_settings()[13]:
<a class="item" href="{{base_url}}movies">
<i class="film icon"></i>
Movies
@ -68,12 +70,12 @@
</a>
<a class="item" href="{{base_url}}wanted">
<i class="warning sign icon">
% if get_general_settings()[12] is True:
% if get_general_settings()[12]:
<div class="floating ui tiny yellow label" style="left:90% !important;top:0.5em !important;">
{{wanted_series[0]}}
</div>
% end
% if get_general_settings()[13] is True:
% if get_general_settings()[13]:
<div class="floating ui tiny green label" style="left:90% !important;top:3em !important;">
{{wanted_movies[0]}}
</div>
@ -94,7 +96,7 @@
</div>
</div>
<div style='padding-top:0rem;' class="row">
<div style='padding-top:0;' class="row">
<div class="three wide column"></div>
<div class="ten wide column">
@ -133,7 +135,7 @@
apiSettings: {
url: '{{base_url}}search_json/{query}',
onResponse: function(results) {
var response = {
const response = {
results : []
};
$.each(results.items, function(index, item) {
@ -154,21 +156,21 @@
$('.menu').css('opacity', '0.8');
$('#divmenu').css('background', '#000000');
$('#divmenu').css('opacity', '0.8');
$('#divmenu').css('box-shadow', '0px 0px 5px 5px #000000');
$('#divmenu').css('box-shadow', '0 0 5px 5px #000000');
}
else if (window.location.href.indexOf("movie/") > -1) {
$('.menu').css('background', '#000000');
$('.menu').css('opacity', '0.8');
$('#divmenu').css('background', '#000000');
$('#divmenu').css('opacity', '0.8');
$('#divmenu').css('box-shadow', '0px 0px 5px 5px #000000');
$('#divmenu').css('box-shadow', '0 0 5px 5px #000000');
}
else {
$('.menu').css('background', '#272727');
$('#divmenu').css('background', '#272727');
}
$('#restart_link').click(function(){
$('#restart_link').on('click', function(){
$('#loader_text').text("Bazarr is restarting, please wait...");
$.ajax({
url: "{{base_url}}restart",
@ -177,14 +179,14 @@
.done(function(){
setTimeout(function(){ setInterval(ping, 2000); },8000);
});
})
});
% from get_settings import get_general_settings
% ip = get_general_settings()[0]
% port = get_general_settings()[1]
% base_url = get_general_settings()[2]
if ("{{ip}}" == "0.0.0.0") {
if ("{{ip}}" === "0.0.0.0") {
public_ip = window.location.hostname;
} else {
public_ip = "{{ip}}";
@ -192,7 +194,7 @@
protocol = window.location.protocol;
if (window.location.port == '{{current_port}}') {
if (window.location.port === '{{current_port}}') {
public_port = '{{port}}';
} else {
public_port = window.location.port;
@ -206,4 +208,29 @@
}
});
}
</script>
<script type="text/javascript">
if (location.protocol != 'https:')
{
var ws = new WebSocket("ws://" + window.location.host + "{{base_url}}websocket");
} else {
var ws = new WebSocket("wss://" + window.location.host + "{{base_url}}websocket");
}
ws.onmessage = function (evt) {
new Noty({
text: evt.data,
timeout: 3000,
progressBar: false,
animation: {
open: null,
close: null
},
killer: true,
type: 'info',
layout: 'bottomRight',
theme: 'semanticui'
}).show();
};
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -36,14 +36,14 @@
margin-bottom: 3em;
padding: 2em;
border-radius: 1px;
box-shadow: 0px 0px 5px 5px #000000;
box-shadow: 0 0 5px 5px #000000;
min-height: calc(250px + 4em);
}
#fondblanc {
background-color: #ffffff;
opacity: 0.9;
border-radius: 1px;
box-shadow: 0px 0px 3px 3px #ffffff;
box-shadow: 0 0 3px 3px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding-top: 2em;
@ -194,12 +194,12 @@
%>
<a class="get_subtitle ui small blue label" data-moviePath="{{details[8]}}" data-scenename="{{details[12]}}" data-language="{{alpha3_from_alpha2(str(missing_subs_language))}}" data-hi="{{details[4]}}" data-radarrId={{details[10]}}>
{{language_from_alpha2(str(missing_subs_language))}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%else:
<a data-tooltip="Automatic searching delayed (adaptive search)" data-position="top left" data-inverted="" class="get_subtitle ui small red label" data-moviePath="{{details[8]}}" data-scenename="{{details[12]}}" data-language="{{alpha3_from_alpha2(str(missing_subs_language))}}" data-hi="{{details[4]}}" data-radarrId={{details[10]}}>
{{language_from_alpha2(str(missing_subs_language))}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
<%
end
@ -209,7 +209,7 @@
%>
<a class="get_subtitle ui small blue label" data-moviePath="{{details[8]}}" data-scenename="{{details[12]}}" data-language="{{alpha3_from_alpha2(str(missing_subs_language))}}" data-hi="{{details[4]}}" data-radarrId={{details[10]}}>
{{language_from_alpha2(str(missing_subs_language))}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
<%
end
@ -305,18 +305,18 @@
</html>
<script>
$('#scan_disk').click(function(){
$('#scan_disk').on('click', function(){
$('#loader_text').text("Scanning disk for existing subtitles...");
window.location = '{{base_url}}scan_disk_movie/{{no}}';
})
});
$('#search_missing_subtitles').click(function(){
$('#search_missing_subtitles').on('click', function(){
$('#loader_text').text("Searching for missing subtitles...");
window.location = '{{base_url}}search_missing_subtitles_movie/{{no}}';
})
});
$('.remove_subtitles').click(function(){
var values = {
$('.remove_subtitles').on('click', function(){
const values = {
moviePath: $(this).attr("data-moviePath"),
language: $(this).attr("data-language"),
subtitlesPath: $(this).attr("data-subtitlesPath"),
@ -338,10 +338,10 @@
$(document).ajaxStop(function(){
window.location.reload();
});
})
});
$('.get_subtitle').click(function(){
var values = {
$('.get_subtitle').on('click', function(){
const values = {
moviePath: $(this).attr("data-moviePath"),
sceneName: $(this).attr("data-sceneName"),
language: $(this).attr("data-language"),
@ -365,19 +365,18 @@
$(document).ajaxStop(function(){
window.location.reload();
});
})
});
$('a, .menu .item, button:not(#config, .cancel, .manual_search)').click(function(){
$('a, .menu .item, button:not(#config, .cancel, .manual_search)').on('click', function(){
$('#loader').addClass('active');
})
});
$('.modal')
.modal({
autofocus: false
})
;
});
$('#config').click(function(){
$('#config').on('click', function(){
$('#movie_form').attr('action', '{{base_url}}edit_movie/{{no}}');
$("#movie_title").html($(this).data("title"));
@ -386,10 +385,10 @@
$("#movie_audio_language").html($(this).data("audio"));
$('#movie_languages').dropdown('clear');
var languages_array = eval($(this).data("languages"));
const languages_array = eval($(this).data("languages"));
$('#movie_languages').dropdown('set selected',languages_array);
if ($(this).data("hearing-impaired") == "True") {
if ($(this).data("hearing-impaired") === "True") {
$("#movie_hearing-impaired_div").checkbox('check');
} else {
$("#movie_hearing-impaired_div").checkbox('uncheck');
@ -399,20 +398,19 @@
.modal({
centered: true
})
.modal('show')
;
})
.modal('show');
});
$('.manual_search').click(function(){
$('.manual_search').on('click', function(){
$("#movie_title_span").html($(this).data("movie_title"));
moviePath = $(this).attr("data-moviePath"),
sceneName = $(this).attr("data-sceneName"),
language = $(this).attr("data-language"),
hi = $(this).attr("data-hi"),
radarrId = $(this).attr("data-radarrId")
moviePath = $(this).attr("data-moviePath");
sceneName = $(this).attr("data-sceneName");
language = $(this).attr("data-language");
hi = $(this).attr("data-hi");
radarrId = $(this).attr("data-radarrId");
var values = {
const values = {
moviePath: moviePath,
sceneName: sceneName,
language: language,
@ -456,19 +454,19 @@
},
{ data: null,
render: function ( data, type, row ) {
var array_matches = data.matches;
var array_dont_matches = data.dont_matches;
var i;
text = '<div class="ui inline dropdown"><i class="green check icon"></i><div class="text">'
const array_matches = data.matches;
const array_dont_matches = data.dont_matches;
let i;
let text = '<div class="ui inline dropdown"><i class="green check icon"></i><div class="text">';
text += array_matches.length;
text += '</div><i class="dropdown icon"></i><div class="menu">'
text += '</div><i class="dropdown icon"></i><div class="menu">';
for (i = 0; i < array_matches.length; i++) {
text += '<div class="criteria_matched disabled item">' + array_matches[i] + '</div>';
}
text += '</div></div>';
text += '<div class="ui inline dropdown"><i class="red times icon"></i><div class="text">'
text += '<div class="ui inline dropdown"><i class="red times icon"></i><div class="text">';
text += array_dont_matches.length;
text += '</div><i class="dropdown icon"></i><div class="menu">'
text += '</div><i class="dropdown icon"></i><div class="menu">';
for (i = 0; i < array_dont_matches.length; i++) {
text += '<div class="criteria_not_matched disabled item">' + array_dont_matches[i] + '</div>';
}
@ -490,10 +488,10 @@
})
.modal('show')
;
})
});
function manual_get(button, episodePath, sceneName, hi, sonarrSeriesId, sonarrEpisodeId){
var values = {
const values = {
subtitle: $(button).attr("data-subtitle"),
provider: $(button).attr("data-provider"),
moviePath: moviePath,
@ -518,5 +516,5 @@
$(document).ajaxStop(function(){
window.location.reload();
});
}
};
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -23,8 +23,8 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
@ -100,8 +100,8 @@
%end
%end
</td>
<td>{{!"" if row[4] == None else row[4]}}</td>
<td {{!"style='background-color: #e8e8e8;'" if row[4] == None else ""}}>
<td>{{!"" if row[4] is None else row[4]}}</td>
<td {{!"style='background-color: #e8e8e8;'" if row[4] is None else ""}}>
<%
subs_languages_list = []
if subs_languages is not None:
@ -223,26 +223,26 @@
$('table').tablesort();
$('a, button:not(.cancel)').click(function(){
$('a, button:not(.cancel)').on('click', function(){
$('#loader').addClass('active');
})
});
$('.fast.backward').click(function(){
$('.fast.backward').on('click', function(){
location.href="?page=1";
})
$('.backward:not(.fast)').click(function(){
});
$('.backward:not(.fast)').on('click', function(){
location.href="?page={{int(page)-1}}";
})
$('.forward:not(.fast)').click(function(){
});
$('.forward:not(.fast)').on('click', function(){
location.href="?page={{int(page)+1}}";
})
$('.fast.forward').click(function(){
});
$('.fast.forward').on('click', function(){
location.href="?page={{int(max_page)}}";
})
});
$('#movieseditor').click(function(){
$('#movieseditor').on('click', function(){
window.location = '{{base_url}}movieseditor';
})
});
$('.modal')
.modal({
@ -250,7 +250,7 @@
})
;
$('.config').click(function(){
$('.config').on('click', function(){
sessionStorage.scrolly=$(window).scrollTop();
$('#movies_form').attr('action', '{{base_url}}edit_movie/' + $(this).data("no"));
@ -264,14 +264,14 @@
var languages_array = eval($(this).data("languages"));
$('#movies_languages').dropdown('set selected',languages_array);
if ($(this).data("hearing-impaired") == "True") {
if ($(this).data("hearing-impaired") === "True") {
$("#movies_hearing-impaired_div").checkbox('check');
} else {
$("#movies_hearing-impaired_div").checkbox('uncheck');
}
$('.small.modal').modal('show');
})
});
$('#movies_languages').dropdown();
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -23,8 +23,8 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
@ -32,9 +32,6 @@
#tablemovies {
padding-top: 1em;
}
#divdetails {
min-height: 250px;
}
#bottommenu {
background-color: #333333;
box-shadow: 0 0 10px 1px #333;
@ -140,9 +137,9 @@
$('table').tablesort();
$('a, button').click(function(){
$('a, button').on('click', function(){
$('#loader').addClass('active');
})
});
$('.modal')
.modal({
@ -150,7 +147,7 @@
})
;
$('.selected').change(function() {
$('.selected').on('change', function() {
$("#count").text($('.selected:checked').length);
if ( $('.selected:checked').length > 0 ) {
$('.select').removeClass('disabled');
@ -161,19 +158,19 @@
$('#save').addClass('disabled');
}
var result = [];
const result = [];
$('.selected:checked').each(function(i){
result.push($(this).attr('id'));
});
$("#checked").val(result);
});
$('#selectall').change(function() {
$('#selectall').on('change', function() {
if ( $('#selectall').is(":checked") ) {
$('.selected').prop('checked', true).change();
$('.selected').prop('checked', true).trigger('change');
}
else {
$('.selected').prop('checked', false).change();
$('.selected').prop('checked', false).trigger('change');
}
});

View File

@ -1,314 +1,313 @@
<html>
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
<script src="{{base_url}}static/semantic/semantic.min.js"></script>
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.min.css">
<link rel="apple-touch-icon" sizes="120x120" href="{{base_url}}static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{base_url}}static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{base_url}}static/favicon-16x16.png">
<link rel="manifest" href="{{base_url}}static/manifest.json">
<link rel="mask-icon" href="{{base_url}}static/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="{{base_url}}static/favicon.ico">
<meta name="msapplication-config" content="{{base_url}}static/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<title>Series - Bazarr</title>
<style>
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
overflow-x:auto;
}
#tableseries {
padding-top: 1em;
}
#divdetails {
min-height: 250px;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
.ui.progress:last-child {margin: 0 0 0em !important;}
.ui.progress .bar>.progress {
right: auto;
left: .5em;
color: rgba(0, 0, 0, 0.60);
}
</style>
</head>
<body>
<div id='loader' class="ui page dimmer">
<div id="loader_text" class="ui indeterminate text loader">Loading...</div>
</div>
% include('menu.tpl')
<div id="fondblanc" class="ui container">
<div class="ui basic buttons">
<button id="serieseditor" class="ui button"><i class="configure icon"></i>Series Editor</button>
</div>
<table id="tableseries" class="ui very basic selectable table">
<thead>
<tr>
<th>Name</th>
<th>Path</th>
<th>Audio language</th>
<th>Subtitles languages</th>
<th>Hearing-impaired</th>
<th class="two wide">Subtitles</th>
<th></th>
</tr>
</thead>
<tbody>
%import ast
%import os
%for row in rows:
<tr class="selectable">
<td><a href="{{base_url}}episodes/{{row[5]}}">{{row[1]}}</a></td>
<td>
%if os.path.isdir(row[2]):
<span data-tooltip="This path seems to be valid." data-inverted="" data-position="top left"><i class="checkmark icon"></i></span>
%else:
<span data-tooltip="This path doesn't seems to be valid." data-inverted="" data-position="top left"><i class="warning sign icon"></i></span>
%end
{{row[2]}}
</td>
<td>{{row[7]}}</td>
<td>
%subs_languages = ast.literal_eval(str(row[3]))
%if subs_languages is not None:
%for subs_language in subs_languages:
<div class="ui tiny label">{{subs_language}}</div>
%end
%end
</td>
<td>{{!"" if row[4] == None else row[4]}}</td>
<td>
%total_subs = 0
%missing_subs = 0
%if not total_subtitles_list:
% pass
%else:
% for total_subtitles in total_subtitles_list:
% if total_subtitles[0] == row[5]:
% total_subs = total_subtitles[1]
% end
% end
% for missing_subtitles in missing_subtitles_list:
% if missing_subtitles[0] == row[5]:
% missing_subs = missing_subtitles[1]
% end
% end
%end
<div class="ui progress" data-value="{{total_subs - missing_subs}}" data-total="{{total_subs}}">
<div class="bar">
<div class="progress"></div>
</div>
</div>
</td>
<td {{!"style='background-color: #e8e8e8;'" if row[4] == None else ""}}>
<%
subs_languages_list = []
if subs_languages is not None:
for subs_language in subs_languages:
subs_languages_list.append(subs_language)
end
end
%>
<div class="config ui inverted basic compact icon" data-tooltip="Edit series" data-inverted="" data-position="top right" data-no="{{row[5]}}" data-title="{{row[1]}}" data-poster="{{row[6]}}" data-languages="{{!subs_languages_list}}" data-hearing-impaired="{{row[4]}}" data-audio="{{row[7]}}">
<i class="ui black configure icon"></i>
</div>
</td>
</tr>
%end
</tbody>
</table>
%try: page_size
%except NameError: page_size = "25"
%end
%if page_size != -1:
<div class="ui grid">
<div class="three column row">
<div class="column"></div>
<div class="center aligned column">
<i class="\\
%if page == "1":
disabled\\
%end
fast backward icon"></i>
<i class="\\
%if page == "1":
disabled\\
%end
backward icon"></i>
{{page}} / {{max_page}}
<i class="\\
%if int(page) == int(max_page):
disabled\\
%end
forward icon"></i>
<i class="\\
%if int(page) == int(max_page):
disabled\\
%end
fast forward icon"></i>
</div>
<div class="right floated right aligned column">Total records: {{missing_count}}</div>
</div>
</div>
%end
</div>
<div class="ui small modal">
<i class="close icon"></i>
<div class="header">
<div id="series_title"></div>
</div>
<div class="content">
<form name="series_form" id="series_form" action="" method="post" class="ui form">
<div id="divdetails" class="ui grid">
<div class="four wide column">
<img id="series_poster" class="ui image" src="">
</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned five wide column">
<label>Audio language</label>
</div>
<div class="nine wide column">
<div id="series_audio_language"></div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned five wide column">
<label>Subtitles languages</label>
</div>
<div class="nine wide column">
<select name="languages" id="series_languages" {{!'multiple="" ' if single_language is False else ''}}class="ui fluid selection dropdown">
<option value="">Languages</option>
%if single_language is True:
<option value="None">None</option>
%end
%for language in languages:
<option value="{{language[0]}}">{{language[1]}}</option>
%end
</select>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned five wide column">
<label>Hearing-impaired</label>
</div>
<div class="nine wide column">
<div id="series_hearing-impaired_div" class="ui toggle checkbox">
<input name="hearing_impaired" id="series_hearing-impaired" type="checkbox">
<label></label>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui cancel button" >Cancel</button>
<button type="submit" name="save" value="save" form="series_form" class="ui blue approve button">Save</button>
</div>
</div>
% include('footer.tpl')
</body>
</html>
<script>
if (sessionStorage.scrolly) {
$(window).scrollTop(sessionStorage.scrolly);
sessionStorage.clear();
}
$('a, button:not(.cancel)').click(function(){
$('#loader').addClass('active');
})
$('.fast.backward').click(function(){
location.href="?page=1";
})
$('.backward:not(.fast)').click(function(){
location.href="?page={{int(page)-1}}";
})
$('.forward:not(.fast)').click(function(){
location.href="?page={{int(page)+1}}";
})
$('.fast.forward').click(function(){
location.href="?page={{int(max_page)}}";
})
$('#serieseditor').click(function(){
window.location = '{{base_url}}serieseditor';
})
$('.modal')
.modal({
autofocus: false
})
;
$('.config').click(function(){
sessionStorage.scrolly=$(window).scrollTop();
$('#series_form').attr('action', '{{base_url}}edit_series/' + $(this).data("no"));
$("#series_title").html($(this).data("title"));
$("#series_poster").attr("src", "{{base_url}}image_proxy" + $(this).data("poster"));
$("#series_audio_language").html($(this).data("audio"));
$('#series_languages').dropdown('clear');
var languages_array = eval($(this).data("languages"));
$('#series_languages').dropdown('set selected',languages_array);
if ($(this).data("hearing-impaired") == "True") {
$("#series_hearing-impaired_div").checkbox('check');
} else {
$("#series_hearing-impaired_div").checkbox('uncheck');
}
$('.small.modal').modal('show');
})
$('#series_languages').dropdown();
$('.progress').progress({
label: 'ratio',
text: {
ratio: '{value} / {total}'
},
showActivity: false
});
$( ".progress" ).each(function() {
if ($(this).progress('is complete') != true) {
$(this).addClass('yellow');
}
if ($(this).progress('get total') == 0) {
$(this).progress('update progress', '99');
$(this).addClass('grey disabled');
$(this).progress('set bar label', '0 / 0');
}
});
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
<script src="{{base_url}}static/semantic/semantic.min.js"></script>
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.min.css">
<link rel="apple-touch-icon" sizes="120x120" href="{{base_url}}static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{base_url}}static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{base_url}}static/favicon-16x16.png">
<link rel="manifest" href="{{base_url}}static/manifest.json">
<link rel="mask-icon" href="{{base_url}}static/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="{{base_url}}static/favicon.ico">
<meta name="msapplication-config" content="{{base_url}}static/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<title>Series - Bazarr</title>
<style>
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
overflow-x:auto;
}
#tableseries {
padding-top: 1em;
}
#divdetails {
min-height: 250px;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
.ui.progress:last-child {margin: 0 0 0 !important;}
.ui.progress .bar>.progress {
right: auto;
left: .5em;
color: rgba(0, 0, 0, 0.60);
}
</style>
</head>
<body>
<div id='loader' class="ui page dimmer">
<div id="loader_text" class="ui indeterminate text loader">Loading...</div>
</div>
% include('menu.tpl')
<div id="fondblanc" class="ui container">
<div class="ui basic buttons">
<button id="serieseditor" class="ui button"><i class="configure icon"></i>Series Editor</button>
</div>
<table id="tableseries" class="ui very basic selectable table">
<thead>
<tr>
<th>Name</th>
<th>Path</th>
<th>Audio language</th>
<th>Subtitles languages</th>
<th>Hearing-impaired</th>
<th class="two wide">Subtitles</th>
<th></th>
</tr>
</thead>
<tbody>
%import ast
%import os
%for row in rows:
<tr class="selectable">
<td><a href="{{base_url}}episodes/{{row[5]}}">{{row[1]}}</a></td>
<td>
%if os.path.isdir(row[2]):
<span data-tooltip="This path seems to be valid." data-inverted="" data-position="top left"><i class="checkmark icon"></i></span>
%else:
<span data-tooltip="This path doesn't seems to be valid." data-inverted="" data-position="top left"><i class="warning sign icon"></i></span>
%end
{{row[2]}}
</td>
<td>{{row[7]}}</td>
<td>
%subs_languages = ast.literal_eval(str(row[3]))
%if subs_languages is not None:
%for subs_language in subs_languages:
<div class="ui tiny label">{{subs_language}}</div>
%end
%end
</td>
<td>{{!"" if row[4] == None else row[4]}}</td>
<td>
%total_subs = 0
%missing_subs = 0
%if not total_subtitles_list:
% pass
%else:
% for total_subtitles in total_subtitles_list:
% if total_subtitles[0] == row[5]:
% total_subs = total_subtitles[1]
% end
% end
% for missing_subtitles in missing_subtitles_list:
% if missing_subtitles[0] == row[5]:
% missing_subs = missing_subtitles[1]
% end
% end
%end
<div class="ui progress" data-value="{{total_subs - missing_subs}}" data-total="{{total_subs}}">
<div class="bar">
<div class="progress"></div>
</div>
</div>
</td>
<td {{!"style='background-color: #e8e8e8;'" if row[4] == None else ""}}>
<%
subs_languages_list = []
if subs_languages is not None:
for subs_language in subs_languages:
subs_languages_list.append(subs_language)
end
end
%>
<div class="config ui inverted basic compact icon" data-tooltip="Edit series" data-inverted="" data-position="top right" data-no="{{row[5]}}" data-title="{{row[1]}}" data-poster="{{row[6]}}" data-languages="{{!subs_languages_list}}" data-hearing-impaired="{{row[4]}}" data-audio="{{row[7]}}">
<i class="ui black configure icon"></i>
</div>
</td>
</tr>
%end
</tbody>
</table>
%try: page_size
%except NameError: page_size = "25"
%end
%if page_size != -1:
<div class="ui grid">
<div class="three column row">
<div class="column"></div>
<div class="center aligned column">
<i class="\\
%if page == "1":
disabled\\
%end
fast backward icon"></i>
<i class="\\
%if page == "1":
disabled\\
%end
backward icon"></i>
{{page}} / {{max_page}}
<i class="\\
%if int(page) == int(max_page):
disabled\\
%end
forward icon"></i>
<i class="\\
%if int(page) == int(max_page):
disabled\\
%end
fast forward icon"></i>
</div>
<div class="right floated right aligned column">Total records: {{missing_count}}</div>
</div>
</div>
%end
</div>
<div class="ui small modal">
<i class="close icon"></i>
<div class="header">
<div id="series_title"></div>
</div>
<div class="content">
<form name="series_form" id="series_form" action="" method="post" class="ui form">
<div id="divdetails" class="ui grid">
<div class="four wide column">
<img id="series_poster" class="ui image" src="">
</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned five wide column">
<label>Audio language</label>
</div>
<div class="nine wide column">
<div id="series_audio_language"></div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned five wide column">
<label>Subtitles languages</label>
</div>
<div class="nine wide column">
<select name="languages" id="series_languages" {{!'multiple="" ' if single_language is False else ''}}class="ui fluid selection dropdown">
<option value="">Languages</option>
%if single_language is True:
<option value="None">None</option>
%end
%for language in languages:
<option value="{{language[0]}}">{{language[1]}}</option>
%end
</select>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned five wide column">
<label>Hearing-impaired</label>
</div>
<div class="nine wide column">
<div id="series_hearing-impaired_div" class="ui toggle checkbox">
<input name="hearing_impaired" id="series_hearing-impaired" type="checkbox">
<label></label>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui cancel button" >Cancel</button>
<button type="submit" name="save" value="save" form="series_form" class="ui blue approve button">Save</button>
</div>
</div>
% include('footer.tpl')
</body>
</html>
<script>
if (sessionStorage.scrolly) {
$(window).scrollTop(sessionStorage.scrolly);
sessionStorage.clear();
}
$('a, button:not(.cancel)').on('click', function(){
$('#loader').addClass('active');
});
$('.fast.backward').on('click', function(){
location.href="?page=1";
});
$('.backward:not(.fast)').on('click', function(){
location.href="?page={{int(page)-1}}";
});
$('.forward:not(.fast)').on('click', function(){
location.href="?page={{int(page)+1}}";
});
$('.fast.forward').on('click', function(){
location.href="?page={{int(max_page)}}";
});
$('#serieseditor').on('click', function(){
window.location = '{{base_url}}serieseditor';
});
$('.modal')
.modal({
autofocus: false
});
$('.config').on('click', function(){
sessionStorage.scrolly=$(window).scrollTop();
$('#series_form').attr('action', '{{base_url}}edit_series/' + $(this).data("no"));
$("#series_title").html($(this).data("title"));
$("#series_poster").attr("src", "{{base_url}}image_proxy" + $(this).data("poster"));
$("#series_audio_language").html($(this).data("audio"));
$('#series_languages').dropdown('clear');
const languages_array = eval($(this).data("languages"));
$('#series_languages').dropdown('set selected',languages_array);
if ($(this).data("hearing-impaired") === "True") {
$("#series_hearing-impaired_div").checkbox('check');
} else {
$("#series_hearing-impaired_div").checkbox('uncheck');
}
$('.small.modal').modal('show');
});
$('#series_languages').dropdown();
$('.progress').progress({
label: 'ratio',
text: {
ratio: '{value} / {total}'
},
showActivity: false
});
$( ".progress" ).each(function() {
if ($(this).progress('is complete') !== true) {
$(this).addClass('yellow');
}
if ($(this).progress('get total') == 0) {
$(this).progress('update progress', '99');
$(this).addClass('grey disabled');
$(this).progress('set bar label', '0 / 0');
}
});
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -23,8 +23,8 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
@ -32,9 +32,6 @@
#tableseries {
padding-top: 1em;
}
#divdetails {
min-height: 250px;
}
#bottommenu {
background-color: #333333;
box-shadow: 0 0 10px 1px #333;
@ -90,7 +87,7 @@
%end
%end
</td>
<td>{{!"" if row[4] == None else row[4]}}</td>
<td>{{!"" if row[4] is None else row[4]}}</td>
</tr>
%end
</tbody>
@ -140,17 +137,16 @@
$('table').tablesort();
$('a, button').click(function(){
$('a, button').on('click', function(){
$('#loader').addClass('active');
})
});
$('.modal')
.modal({
autofocus: false
})
;
});
$('.selected').change(function() {
$('.selected').on('change', function() {
$("#count").text($('.selected:checked').length);
if ( $('.selected:checked').length > 0 ) {
$('.select').removeClass('disabled');
@ -161,19 +157,19 @@
$('#save').addClass('disabled');
}
var result = [];
const result = [];
$('.selected:checked').each(function(i){
result.push($(this).attr('id'));
});
$("#checked").val(result);
});
$('#selectall').change(function() {
$('#selectall').on('change', function() {
if ( $('#selectall').is(":checked") ) {
$('.selected').prop('checked', true).change();
$('.selected').prop('checked', true).trigger('change');
}
else {
$('.selected').prop('checked', false).change();
$('.selected').prop('checked', false).trigger('change');
}
});

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -23,8 +23,8 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 1em;
@ -115,7 +115,7 @@
</div>
<div class="five wide column">
<div class="ui fluid input">
%if settings_general[2] == None:
%if settings_general[2] is None:
% base_url = "/"
%else:
% base_url = settings_general[2]
@ -1348,6 +1348,7 @@
<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>
@ -1364,6 +1365,25 @@
<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];
alert(url_protocol);
alert(url_string);
$.ajax({
url: "{{base_url}}test_notification/" + url_protocol + "/" + encodeURIComponent(url_string),
beforeSend: function () {
$('#loader').addClass('active');
},
complete: function () {
$('#loader').removeClass('active');
},
cache: false
});
});
% from get_args import args
% if args.no_update:
$("#div_update").hide();
@ -1373,69 +1393,69 @@
.tab()
;
$('a:not(.tabs), button:not(.cancel, .test)').click(function(){
$('a:not(.tabs), button:not(.cancel, .test)').on('click', function(){
$('#loader').addClass('active');
})
});
$('a[target="_blank"]').click(function(){
$('a[target="_blank"]').on('click', function(){
$('#loader').removeClass('active');
})
});
if ($('#sonarr_ssl_div').data("ssl") == "True") {
if ($('#sonarr_ssl_div').data("ssl") === "True") {
$("#sonarr_ssl_div").checkbox('check');
} else {
$("#sonarr_ssl_div").checkbox('uncheck');
}
if ($('#radarr_ssl_div').data("ssl") == "True") {
if ($('#radarr_ssl_div').data("ssl") === "True") {
$("#radarr_ssl_div").checkbox('check');
} else {
$("#radarr_ssl_div").checkbox('uncheck');
}
if ($('#settings_automatic_div').data("automatic") == "True") {
if ($('#settings_automatic_div').data("automatic") === "True") {
$("#settings_automatic_div").checkbox('check');
} else {
$("#settings_automatic_div").checkbox('uncheck');
}
if ($('#settings_debug').data("debug") == "True") {
if ($('#settings_debug').data("debug") === "True") {
$("#settings_debug").checkbox('check');
} else {
$("#settings_debug").checkbox('uncheck');
}
if ($('#settings_single_language').data("single-language") == "True") {
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") {
if ($('#settings_scenename').data("scenename") === "True") {
$("#settings_scenename").checkbox('check');
} else {
$("#settings_scenename").checkbox('uncheck');
}
if ($('#settings_embedded').data("embedded") == "True") {
if ($('#settings_embedded').data("embedded") === "True") {
$("#settings_embedded").checkbox('check');
} else {
$("#settings_embedded").checkbox('uncheck');
}
if ($('#settings_only_monitored').data("monitored") == "True") {
if ($('#settings_only_monitored').data("monitored") === "True") {
$("#settings_only_monitored").checkbox('check');
} else {
$("#settings_only_monitored").checkbox('uncheck');
}
if ($('#settings_adaptive_searching').data("adaptive") == "True") {
if ($('#settings_adaptive_searching').data("adaptive") === "True") {
$("#settings_adaptive_searching").checkbox('check');
} else {
$("#settings_adaptive_searching").checkbox('uncheck');
}
if ($('#settings_use_postprocessing').data("postprocessing") == "True") {
if ($('#settings_use_postprocessing').data("postprocessing") === "True") {
$("#settings_use_postprocessing").checkbox('check');
$("#settings_general_postprocessing_cmd_div").removeClass('disabled');
} else {
@ -1443,7 +1463,7 @@
$("#settings_general_postprocessing_cmd_div").addClass('disabled');
}
$("#settings_use_postprocessing").change(function(i, obj) {
$("#settings_use_postprocessing").on('change', function(i, obj) {
if ($("#settings_use_postprocessing").checkbox('is checked')) {
$("#settings_general_postprocessing_cmd_div").removeClass('disabled');
} else {
@ -1451,7 +1471,7 @@
}
});
if ($('#settings_use_sonarr').data("enabled") == "True") {
if ($('#settings_use_sonarr').data("enabled") === "True") {
$("#settings_use_sonarr").checkbox('check');
$("#sonarr_tab").removeClass('disabled');
} else {
@ -1471,7 +1491,7 @@
}
});
if ($('#settings_use_radarr').data("enabled") == "True") {
if ($('#settings_use_radarr').data("enabled") === "True") {
$("#settings_use_radarr").checkbox('check');
$("#radarr_tab").removeClass('disabled');
} else {
@ -1491,17 +1511,17 @@
}
});
if ($('#settings_auth_type').val() == "None") {
if ($('#settings_auth_type').val() === "None") {
$('.auth_option').hide();
};
}
$('#settings_auth_type').dropdown('setting', 'onChange', function(){
if ($('#settings_auth_type').val() == "None") {
if ($('#settings_auth_type').val() === "None") {
$('.auth_option').hide();
}
else {
$('.auth_option').show();
};
}
});
// Load default value for Settings_auth_type
@ -1509,8 +1529,8 @@
$('#settings_auth_type').dropdown('set selected','{{!settings_auth[0]}}');
// Remove value from Password input when changing to Form login to prevent bad password saving
$("#settings_auth_type").change(function() {
if ($(this).val() == 'form'){
$("#settings_auth_type").on('change', function() {
if ($(this).val() === 'form'){
$('#settings_auth_password').val('');
}
else {
@ -1521,7 +1541,7 @@
$('#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)
)
@ -1535,13 +1555,13 @@
$("#settings_movie_default_languages option[value='" + val + "']").remove();
});
if ($('#settings_serie_default_enabled_div').data("enabled") == "True") {
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") {
if ($('#settings_serie_default_enabled_div').data("enabled") === "True") {
$("#settings_serie_default_languages").removeClass('disabled');
$("#settings_serie_default_hi_div").removeClass('disabled');
} else {
@ -1560,19 +1580,19 @@
}
});
if ($('#settings_serie_default_hi_div').data("hi") == "True") {
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") {
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") {
if ($('#settings_movie_default_enabled_div').data("enabled") === "True") {
$("#settings_movie_default_languages").removeClass('disabled');
$("#settings_movie_default_hi_div").removeClass('disabled');
} else {
@ -1591,7 +1611,7 @@
}
});
if ($('#settings_movie_default_hi_div').data("hi") == "True") {
if ($('#settings_movie_default_hi_div').data("hi") === "True") {
$("#settings_movie_default_hi_div").checkbox('check');
} else {
$("#settings_movie_default_hi_div").checkbox('uncheck');
@ -1609,7 +1629,7 @@
$("#settings_movie_default_languages").attr('multiple');
}
$("#settings_single_language").change(function(i, obj) {
$("#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');
@ -1631,8 +1651,8 @@
}
});
$('.notifier_enabled').each(function(i, obj) {
if ($(this).data("enabled") == 1) {
$('.notifier_enabled').each(function() {
if ($(this).data("enabled") === 1) {
$(this).checkbox('check');
$('[id=\"' + $(this).data("notifier-url-div") + '\"]').removeClass('disabled');
} else {
@ -1641,7 +1661,7 @@
}
});
$('.notifier_enabled').change(function(i, obj) {
$('.notifier_enabled').on('change', function() {
if ($(this).checkbox('is checked')) {
$('[id=\"' + $(this).data("notifier-url-div") + '\"]').removeClass('disabled');
} else {
@ -1829,31 +1849,31 @@
})
;
if ($('#settings_proxy_type').val() == "None") {
if ($('#settings_proxy_type').val() === "None") {
$('.proxy_option').hide();
$('#settings_form').form('remove rule', 'settings_proxy_url', 'empty')
$('#settings_form').form('remove rule', 'settings_proxy_port', 'empty')
$('#settings_form').form('remove rule', 'settings_proxy_port', 'integer[1..65535]')
$('#settings_form').form('remove rule', 'settings_proxy_url', 'empty');
$('#settings_form').form('remove rule', 'settings_proxy_port', 'empty');
$('#settings_form').form('remove rule', 'settings_proxy_port', 'integer[1..65535]');
}
else {
$('#settings_form').form('add rule', 'settings_proxy_url', 'empty')
$('#settings_form').form('add rule', 'settings_proxy_port', 'empty')
$('#settings_form').form('add rule', 'settings_proxy_port', 'integer[1..65535]')
};
$('#settings_form').form('add rule', 'settings_proxy_url', 'empty');
$('#settings_form').form('add rule', 'settings_proxy_port', 'empty');
$('#settings_form').form('add rule', 'settings_proxy_port', 'integer[1..65535]');
}
$('#settings_proxy_type').dropdown('setting', 'onChange', function(){
if ($('#settings_proxy_type').val() == "None") {
if ($('#settings_proxy_type').val() === "None") {
$('.proxy_option').hide();
$('#settings_form').form('remove rule', 'settings_proxy_url', 'empty')
$('#settings_form').form('remove rule', 'settings_proxy_port', 'empty')
$('#settings_form').form('remove rule', 'settings_proxy_port', 'integer[1..65535]')
$('#settings_form').form('remove rule', 'settings_proxy_url', 'empty');
$('#settings_form').form('remove rule', 'settings_proxy_port', 'empty');
$('#settings_form').form('remove rule', 'settings_proxy_port', 'integer[1..65535]');
}
else {
$('.proxy_option').show();
$('#settings_form').form('add rule', 'settings_proxy_url', 'empty')
$('#settings_form').form('add rule', 'settings_proxy_port', 'empty')
$('#settings_form').form('add rule', 'settings_proxy_port', 'integer[1..65535]')
};
$('#settings_form').form('add rule', 'settings_proxy_url', 'empty');
$('#settings_form').form('add rule', 'settings_proxy_port', 'empty');
$('#settings_form').form('add rule', 'settings_proxy_port', 'integer[1..65535]');
}
});
$('#settings_providers').dropdown('setting', 'onChange', function(){
@ -1863,33 +1883,33 @@
$('.form').form('validate field', 'settings_subliminal_languages');
});
$('.submit').click(function() {
$('.submit').on('click', function() {
alert('Settings saved.');
})
});
$( document ).ready(function() {
$(function() {
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$('#settings_form').focusout(function() {
$('#settings_form').on('focusout', function() {
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$('#settings_auth_username').keyup(function() {
$('#settings_auth_username').on('keyup', function() {
$('#settings_auth_password').val('');
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$('#sonarr_validate').click(function() {
$('#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();
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) {
@ -1904,31 +1924,31 @@
$('#loader').removeClass('active');
}
});
})
});
$('.sonarr_config').keyup(function() {
$('.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').change(function() {
$('#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');
$('#radarr_validate').click(function() {
$('#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();
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) {
@ -1943,21 +1963,21 @@
$('#loader').removeClass('active');
}
});
})
});
$('.radarr_config').keyup(function() {
$('.radarr_config').on('keyup', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$('#settings_radarr_ssl').change(function() {
$('#settings_radarr_ssl').on('change', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$("#radarr_validated").checkbox('check');
</script>

View File

@ -1,412 +1,415 @@
<html>
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
<script src="{{base_url}}static/semantic/semantic.min.js"></script>
<script src="{{base_url}}static/jquery/tablesort.js"></script>
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.min.css">
<link rel="apple-touch-icon" sizes="120x120" href="{{base_url}}static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{base_url}}static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{base_url}}static/favicon-16x16.png">
<link rel="manifest" href="{{base_url}}static/manifest.json">
<link rel="mask-icon" href="{{base_url}}static/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="{{base_url}}static/favicon.ico">
<meta name="msapplication-config" content="{{base_url}}static/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<title>System - Bazarr</title>
<style>
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 1em;
}
#logs {
margin-top: 4em;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
</style>
</head>
<body>
<div id='loader' class="ui page dimmer">
<div id='loader_text' class="ui indeterminate text loader">Loading...</div>
</div>
% include('menu.tpl')
<div id="fondblanc" class="ui container">
<div class="ui basic icon buttons" style="float: right;">
<div id="shutdown" class="ui icon button" data-tooltip="Shutdown" data-inverted=""><i class="red power off icon"></i></div>
<div id="restart" class="ui icon button" data-tooltip="Restart" data-inverted=""><i class="redo alternate icon"></i></div>
% from get_settings import get_auth_settings
% if get_auth_settings()[0] != 'None':
<div id="logout" class="ui icon button" data-tooltip="Logout" data-inverted=""><i class="sign-out icon"></i></div>
% end
</div>
<div class="ui top attached tabular menu">
<a class="tabs item active" data-tab="tasks">Tasks</a>
<a class="tabs item" data-tab="logs">Logs</a>
<a class="tabs item" data-tab="status">Status</a>
<a class="tabs item" data-tab="releases">Releases</a>
</div>
<div class="ui bottom attached tab segment active" data-tab="tasks">
<div class="content">
<table class="ui very basic selectable table">
<thead>
<tr>
<th>Name</th>
<th>Execution Frequency</th>
<th>Next Execution</th>
<th class="collapsing"></th>
</tr>
</thead>
<tbody>
%for task in task_list:
<tr>
<td>{{task[0]}}</td>
<td>{{task[1]}}</td>
<td>{{task[2]}}</td>
<td class="collapsing">
<div class="execute ui inverted basic compact icon" data-tooltip="Execute {{task[0]}}" data-inverted="" data-taskid='{{task[3]}}'>
<i class="ui black refresh icon"></i>
</div>
</td>
</tr>
%end
</tbody>
</table>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="logs">
<div class="ui left floated basic buttons">
<button id="refresh_log" class="ui button"><i class="refresh icon"></i>Refresh current page</button>
</div>
<div class="ui right floated basic buttons">
<button id="download_log" class="ui button"><i class="download icon"></i>Download log file</button>
<button id="empty_log" class="ui button"><i class="download icon"></i>Empty log file</button>
</div>
<div class="content">
<div id="logs"></div>
%try: page_size
%except NameError: page_size = "25"
%end
%if page_size != -1:
<div class="ui grid">
<div class="three column row">
<div class="column"></div>
<div class="center aligned column">
<i class="fast backward icon"></i>
<i class="backward icon"></i>
<span id="page"></span> / {{max_page}}
<i class="forward icon"></i>
<i class="fast forward icon"></i>
</div>
<div class="right floated right aligned column">Total records: {{row_count}}</div>
</div>
</div>
%end
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="status">
<div class="ui dividing header">About</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Bazarr version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{bazarr_version}}
</div>
</div>
</div>
</div>
% from get_settings import get_general_settings
% if get_general_settings()[12]:
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Sonarr version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{sonarr_version}}
</div>
</div>
</div>
</div>
% end
% if get_general_settings()[13]:
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Radarr version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{radarr_version}}
</div>
</div>
</div>
</div>
% end
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Operating system</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{operating_system}}
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Python version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{python_version}}
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Bazarr directory</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{bazarr_dir}}
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Bazarr config directory</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{config_dir}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">More info</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Source</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<i class="github icon"></i><a href="https://github.com/morpheus65535/bazarr">Bazarr on GitHub</a>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Wiki</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<i class="wikipedia w icon"></i><a href="https://github.com/morpheus65535/bazarr/wiki">Bazarr Wiki</a>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Discord</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<i class="discord icon"></i><a href="https://discord.gg/MH2e2eb">Bazarr on Discord</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="releases">
%for release in releases:
<h2 class="ui header">
%if release[0][1:] == bazarr_version:
{{release[0]}} <div class="ui green label">Current version</div>
%else:
{{release[0]}}
%end
</h2>
<div class="ui list">
%release_lines = release[1].split('\r\n')
%for i, release_line in enumerate(release_lines):
%if i == 0:
<div class="item">
<div><h4>{{release_line}}</h4></div>
<div class="list">
%else:
<div class="item">{{release_line}}</div>
%end
%end
</div>
</div>
</div>
%end
</div>
</div>
% include('footer.tpl')
</body>
</html>
<script>
$('.menu .item')
.tab()
;
function loadURL(page) {
$.ajax({
url: "{{base_url}}logs/" + page,
beforeSend: function() { $('#loader').addClass('active'); },
complete: function() { $('#loader').removeClass('active'); },
cache: false
}).done(function(data) {
$("#logs").html(data);
});
current_page = page;
$("#page").text(current_page);
if (current_page == 1) {
$(".backward, .fast.backward").addClass("disabled");
}
if (current_page == {{int(max_page)}}) {
$(".forward, .fast.forward").addClass("disabled");
}
if (current_page > 1 && current_page < {{int(max_page)}}) {
$(".backward, .fast.backward").removeClass("disabled");
$(".forward, .fast.forward").removeClass("disabled");
}
}
loadURL(1);
$('.backward').click(function(){
loadURL(current_page - 1);
})
$('.fast.backward').click(function(){
loadURL(1);
})
$('.forward').click(function(){
loadURL(current_page + 1);
})
$('.fast.forward').click(function(){
loadURL({{int(max_page)}});
})
$('#refresh_log').click(function(){
loadURL(current_page);
})
$('#download_log').click(function(){
window.location = '{{base_url}}bazarr.log';
})
$('#empty_log').click(function(){
window.location = '{{base_url}}emptylog';
})
$('.execute').click(function(){
window.location = '{{base_url}}execute/' + $(this).data("taskid");
})
$('a:not(.tabs), button:not(.cancel, #download_log), #restart').click(function(){
$('#loader').addClass('active');
})
$('#shutdown').click(function(){
$.ajax({
url: "{{base_url}}shutdown",
async: false
})
.always(function(){
document.open();
document.write('Bazarr has shutdown.');
document.close();
});
})
$('#logout').click(function(){
window.location = '{{base_url}}logout';
})
$('#restart').click(function(){
$('#loader_text').text("Bazarr is restarting, please wait...");
$.ajax({
url: "{{base_url}}restart",
async: true
})
.done(function(){
setTimeout(function(){ setInterval(ping, 2000); },8000);
});
})
% from get_settings import get_general_settings
% ip = get_general_settings()[0]
% port = get_general_settings()[1]
% base_url = get_general_settings()[2]
if ("{{ip}}" == "0.0.0.0") {
public_ip = window.location.hostname;
} else {
public_ip = "{{ip}}";
}
protocol = window.location.protocol;
if (window.location.port == '{{current_port}}') {
public_port = '{{port}}';
} else {
public_port = window.location.port;
}
function ping() {
$.ajax({
url: protocol + '//' + public_ip + ':' + public_port + '{{base_url}}',
success: function(result) {
window.location.href= protocol + '//' + public_ip + ':' + public_port + '{{base_url}}';
}
});
}
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
<script src="{{base_url}}static/semantic/semantic.min.js"></script>
<script src="{{base_url}}static/jquery/tablesort.js"></script>
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.min.css">
<link rel="apple-touch-icon" sizes="120x120" href="{{base_url}}static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{base_url}}static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{base_url}}static/favicon-16x16.png">
<link rel="manifest" href="{{base_url}}static/manifest.json">
<link rel="mask-icon" href="{{base_url}}static/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="{{base_url}}static/favicon.ico">
<meta name="msapplication-config" content="{{base_url}}static/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<title>System - Bazarr</title>
<style>
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 1em;
}
#logs {
margin-top: 4em;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
</style>
</head>
<body>
<div id='loader' class="ui page dimmer">
<div id='loader_text' class="ui indeterminate text loader">Loading...</div>
</div>
% include('menu.tpl')
<div id="fondblanc" class="ui container">
<div class="ui basic icon buttons" style="float: right;">
<div id="shutdown" class="ui icon button" data-tooltip="Shutdown" data-inverted=""><i class="red power off icon"></i></div>
<div id="restart" class="ui icon button" data-tooltip="Restart" data-inverted=""><i class="redo alternate icon"></i></div>
% from get_settings import get_auth_settings
% if get_auth_settings()[0] is not 'None':
<div id="logout" class="ui icon button" data-tooltip="Logout" data-inverted=""><i class="sign-out icon"></i></div>
% end
</div>
<div class="ui top attached tabular menu">
<a class="tabs item active" data-tab="tasks">Tasks</a>
<a class="tabs item" data-tab="logs">Logs</a>
<a class="tabs item" data-tab="status">Status</a>
<a class="tabs item" data-tab="releases">Releases</a>
</div>
<div class="ui bottom attached tab segment active" data-tab="tasks">
<div class="content">
<table class="ui very basic selectable table">
<thead>
<tr>
<th>Name</th>
<th>Execution Frequency</th>
<th>Next Execution</th>
<th class="collapsing"></th>
</tr>
</thead>
<tbody>
%for task in task_list:
<tr>
<td>{{task[0]}}</td>
<td>{{task[1]}}</td>
<td>{{task[2]}}</td>
<td class="collapsing">
<div class="execute ui inverted basic compact icon" data-tooltip="Execute {{task[0]}}" data-inverted="" data-taskid='{{task[3]}}'>
<i class="ui black refresh icon"></i>
</div>
</td>
</tr>
%end
</tbody>
</table>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="logs">
<div class="ui left floated basic buttons">
<button id="refresh_log" class="ui button"><i class="refresh icon"></i>Refresh current page</button>
</div>
<div class="ui right floated basic buttons">
<button id="download_log" class="ui button"><i class="download icon"></i>Download log file</button>
<button id="empty_log" class="ui button"><i class="download icon"></i>Empty log file</button>
</div>
<div class="content">
<div id="logs"></div>
%try: page_size
%except NameError: page_size = "25"
%end
%if page_size != -1:
<div class="ui grid">
<div class="three column row">
<div class="column"></div>
<div class="center aligned column">
<i class="fast backward icon"></i>
<i class="backward icon"></i>
<span id="page"></span> / {{max_page}}
<i class="forward icon"></i>
<i class="fast forward icon"></i>
</div>
<div class="right floated right aligned column">Total records: {{row_count}}</div>
</div>
</div>
%end
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="status">
<div class="ui dividing header">About</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Bazarr version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{bazarr_version}}
</div>
</div>
</div>
</div>
% from get_settings import get_general_settings
% if get_general_settings()[12]:
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Sonarr version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{sonarr_version}}
</div>
</div>
</div>
</div>
% end
% if get_general_settings()[13]:
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Radarr version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{radarr_version}}
</div>
</div>
</div>
</div>
% end
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Operating system</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{operating_system}}
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Python version</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{python_version}}
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Bazarr directory</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{bazarr_dir}}
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Bazarr config directory</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
{{config_dir}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui dividing header">More info</div>
<div class="twelve wide column">
<div class="ui grid">
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Source</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<i class="github icon"></i><a href="https://github.com/morpheus65535/bazarr" target="_blank">Bazarr on GitHub</a>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Wiki</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<i class="wikipedia w icon"></i><a href="https://github.com/morpheus65535/bazarr/wiki" target="_blank">Bazarr Wiki</a>
</div>
</div>
</div>
</div>
<div class="middle aligned row">
<div class="right aligned four wide column">
<label>Discord</label>
</div>
<div class="five wide column">
<div class='field'>
<div class="ui fluid input">
<i class="discord icon"></i><a href="https://discord.gg/MH2e2eb" target="_blank">Bazarr on Discord</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="releases">
%for release in releases:
<h2 class="ui header">
%if release[0][1:] == bazarr_version:
{{release[0]}} <div class="ui green label">Current version</div>
%else:
{{release[0]}}
%end
</h2>
<div class="ui list">
%release_lines = release[1].split('\r\n')
%for i, release_line in enumerate(release_lines):
%if i == 0:
<div class="item">
<div><h4>{{release_line}}</h4></div>
<div class="list">
%else:
<div class="item">{{release_line}}</div>
%end
%end
</div>
</div>
</div>
%end
</div>
</div>
% include('footer.tpl')
</body>
</html>
<script>
$('.menu .item')
.tab();
function loadURL(page) {
$.ajax({
url: "{{base_url}}logs/" + page,
beforeSend: function() { $('#loader').addClass('active'); },
complete: function() { $('#loader').removeClass('active'); },
cache: false
}).done(function(data) {
$("#logs").html(data);
});
current_page = page;
$("#page").text(current_page);
if (current_page == 1) {
$(".backward, .fast.backward").addClass("disabled");
}
if (current_page == {{int(max_page)}}) {
$(".forward, .fast.forward").addClass("disabled");
}
if (current_page > 1 && current_page < {{int(max_page)}}) {
$(".backward, .fast.backward").removeClass("disabled");
$(".forward, .fast.forward").removeClass("disabled");
}
}
loadURL(1);
$('.backward').on('click', function(){
loadURL(current_page - 1);
});
$('.fast.backward').on('click', function(){
loadURL(1);
});
$('.forward').on('click', function(){
loadURL(current_page + 1);
});
$('.fast.forward').on('click', function(){
loadURL({{int(max_page)}});
});
$('#refresh_log').on('click', function(){
loadURL(current_page);
});
$('#download_log').on('click', function(){
window.location = '{{base_url}}bazarr.log';
});
$('#empty_log').on('click', function(){
window.location = '{{base_url}}emptylog';
});
$('.execute').on('click', function(){
window.location = '{{base_url}}execute/' + $(this).data("taskid");
});
$('a:not(.tabs), button:not(.cancel, #download_log), #restart').on('click', function(){
$('#loader').addClass('active');
});
$('a[target="_blank"]').on('click', function(){
$('#loader').removeClass('active');
});
$('#shutdown').on('click', function(){
$.ajax({
url: "{{base_url}}shutdown",
async: false
})
.always(function(){
document.open();
document.write('Bazarr has shutdown.');
document.close();
});
});
$('#logout').on('click', function(){
window.location = '{{base_url}}logout';
});
$('#restart').on('click', function(){
$('#loader_text').text("Bazarr is restarting, please wait...");
$.ajax({
url: "{{base_url}}restart",
async: true
})
.done(function(){
setTimeout(function(){ setInterval(ping, 2000); },8000);
})
});
% from get_settings import get_general_settings
% ip = get_general_settings()[0]
% port = get_general_settings()[1]
% base_url = get_general_settings()[2]
if ("{{ip}}" === "0.0.0.0") {
public_ip = window.location.hostname;
} else {
public_ip = "{{ip}}";
}
protocol = window.location.protocol;
if (window.location.port === '{{current_port}}') {
public_port = '{{port}}';
} else {
public_port = window.location.port;
}
function ping() {
$.ajax({
url: protocol + '//' + public_ip + ':' + public_port + '{{base_url}}',
success: function(result) {
window.location.href= protocol + '//' + public_ip + ':' + public_port + '{{base_url}}';
}
});
}
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -23,24 +23,12 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 1em;
}
#logs {
margin-top: 4em;
}
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
.ui.tabular.menu > .disabled.item {
opacity: 0.45 !important;
pointer-events: none !important;
}
</style>
</head>
<body>
@ -50,7 +38,7 @@
% import sqlite3
% from get_settings import get_general_settings
%if get_general_settings()[24] is True:
%if get_general_settings()[24]:
% monitored_only_query_string = ' AND monitored = "True"'
%else:
% monitored_only_query_string = ""
@ -103,13 +91,13 @@
.tab()
;
$('#series_tab').click(function() {
$('#series_tab').on('click', function() {
loadURLseries(1);
})
});
$('#movies_tab').click(function() {
$('#movies_tab').on('click', function() {
loadURLmovies(1);
})
});
function loadURLseries(page) {
$.ajax({
@ -133,25 +121,25 @@
});
}
$('a:not(.tabs), button:not(.cancel, #download_log)').click(function(){
$('a:not(.tabs), button:not(.cancel, #download_log)').on('click', function(){
$('#loader').addClass('active');
})
});
if ($('#series_tab').data("enabled") == "True") {
if ($('#series_tab').data("enabled") === "True") {
$("#series_tab").removeClass('disabled');
} else {
$("#series_tab").addClass('disabled');
}
if ($('#movies_tab').data("enabled") == "True") {
if ($('#movies_tab').data("enabled") === "True") {
$("#movies_tab").removeClass('disabled');
} else {
$("#movies_tab").addClass('disabled');
}
if ($('#series_tab').data("enabled") == "True") {
if ($('#series_tab').data("enabled") === "True") {
$( "#series_tab" ).trigger( "click" );
}
if ($('#series_tab').data("enabled") == "False" && $('#movies_tab').data("enabled") == "True") {
if ($('#series_tab').data("enabled") === "False" && $('#movies_tab').data("enabled") === "True") {
$( "#movies_tab" ).trigger( "click" );
}
</script>

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -21,14 +21,6 @@
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
}
#tablehistory {
padding-top: 2em;
}
@ -83,12 +75,12 @@
%>
<a data-moviePath="{{row[3]}}" data-sceneName="{{row[5]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{row[4]}}" data-radarrId={{row[2]}} class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%else:
<a data-tooltip="Automatic searching delayed (adaptive search)" data-position="top right" data-inverted="" data-moviePath="{{row[3]}}" data-sceneName="{{row[5]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{row[4]}}" data-radarrId={{row[2]}} class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search red icon"></i>
<i style="margin-left:3px; margin-right:0" class="search red icon"></i>
</a>
%end
%end
@ -96,7 +88,7 @@
%else:
<a data-moviePath="{{row[3]}}" data-sceneName="{{row[5]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{row[4]}}" data-radarrId={{row[2]}} class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%end
@ -147,30 +139,30 @@
<script>
$('a, button').click(function(){
$('a, button').on('click', function(){
$('#loader').addClass('active');
})
});
$('.fast.backward').click(function(){
$('.fast.backward').on('click', function(){
loadURLmovies(1);
})
$('.backward:not(.fast)').click(function(){
});
$('.backward:not(.fast)').on('click', function(){
loadURLmovies({{int(page)-1}});
})
$('.forward:not(.fast)').click(function(){
});
$('.forward:not(.fast)').on('click', function(){
loadURLmovies({{int(page)+1}});
})
$('.fast.forward').click(function(){
});
$('.fast.forward').on('click', function(){
loadURLmovies({{int(max_page)}});
})
});
$('#wanted_search_missing_subtitles_movies').click(function(){
$('#wanted_search_missing_subtitles_movies').on('click', function(){
$('#loader_text').text("Searching for missing subtitles...");
window.location = '{{base_url}}wanted_search_missing_subtitles';
})
});
$('.get_subtitle').click(function(){
var values = {
$('.get_subtitle').on('click', function(){
const values = {
moviePath: $(this).attr("data-moviePath"),
sceneName: $(this).attr("data-sceneName"),
language: $(this).attr("data-language"),

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -21,14 +21,6 @@
body {
background-color: #272727;
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 2em 3em 2em 3em;
}
#tablehistory {
padding-top: 2em;
}
@ -90,12 +82,12 @@
%>
<a data-episodePath="{{row[5]}}" data-sceneName="{{row[8]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{row[6]}}" data-sonarrSeriesId={{row[4]}} data-sonarrEpisodeId={{row[7]}} class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%else:
<a data-tooltip="Automatic searching delayed (adaptive search)" data-position="top right" data-inverted="" data-episodePath="{{row[5]}}" data-sceneName="{{row[8]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{row[6]}}" data-sonarrSeriesId={{row[4]}} data-sonarrEpisodeId={{row[7]}} class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search red icon"></i>
<i style="margin-left:3px; margin-right:0" class="search red icon"></i>
</a>
%end
%end
@ -103,7 +95,7 @@
%else:
<a data-episodePath="{{row[5]}}" data-sceneName="{{row[8]}}" data-language="{{alpha3_from_alpha2(str(language))}}" data-hi="{{row[6]}}" data-sonarrSeriesId={{row[4]}} data-sonarrEpisodeId={{row[7]}} class="get_subtitle ui tiny label">
{{language}}
<i style="margin-left:3px; margin-right:0px" class="search icon"></i>
<i style="margin-left:3px; margin-right:0" class="search icon"></i>
</a>
%end
@ -154,30 +146,30 @@
<script>
$('a, button').click(function(){
$('a, button').on('click', function(){
$('#loader').addClass('active');
})
});
$('.fast.backward').click(function(){
$('.fast.backward').on('click', function(){
loadURLseries(1);
})
$('.backward:not(.fast)').click(function(){
});
$('.backward:not(.fast)').on('click', function(){
loadURLseries({{int(page)-1}});
})
$('.forward:not(.fast)').click(function(){
});
$('.forward:not(.fast)').on('click', function(){
loadURLseries({{int(page)+1}});
})
$('.fast.forward').click(function(){
});
$('.fast.forward').on('click', function(){
loadURLseries({{int(max_page)}});
})
});
$('#wanted_search_missing_subtitles').click(function(){
$('#wanted_search_missing_subtitles').on('click', function(){
$('#loader_text').text("Searching for missing subtitles...");
window.location = '{{base_url}}wanted_search_missing_subtitles';
})
});
$('.get_subtitle').click(function(){
var values = {
$('.get_subtitle').on('click', function(){
const values = {
episodePath: $(this).attr("data-episodePath"),
sceneName: $(this).attr("data-sceneName"),
language: $(this).attr("data-language"),

View File

@ -1,4 +1,4 @@
<html>
<html lang="en">
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
@ -24,8 +24,8 @@
}
#fondblanc {
background-color: #ffffff;
border-radius: 0px;
box-shadow: 0px 0px 5px 5px #ffffff;
border-radius: 0;
box-shadow: 0 0 5px 5px #ffffff;
margin-top: 32px;
margin-bottom: 3em;
padding: 1em;
@ -791,9 +791,9 @@
</html>
<script>
$(document).ready(function() {
$(function() {
$('.next1').click(function(e) {
$('.next1').on('click', function(e) {
e.preventDefault();
@ -805,7 +805,7 @@ $(document).ready(function() {
});
$('.prev1').click(function(m) {
$('.prev1').on('click', function(m) {
m.preventDefault();
@ -817,7 +817,7 @@ $(document).ready(function() {
});
$('.next2').click(function(e) {
$('.next2').on('click', function(e) {
e.preventDefault();
@ -829,7 +829,7 @@ $(document).ready(function() {
});
$('.prev2').click(function(m) {
$('.prev2').on('click', function(m) {
m.preventDefault();
@ -841,7 +841,7 @@ $(document).ready(function() {
});
$('.next3').click(function(e) {
$('.next3').on('click', function(e) {
e.preventDefault();
@ -853,7 +853,7 @@ $(document).ready(function() {
});
$('.prev3').click(function(m) {
$('.prev3').on('click', function(m) {
m.preventDefault();
@ -866,9 +866,7 @@ $(document).ready(function() {
});
});
</script>
<script>
$(".sonarr_hide").hide();
$('#settings_use_sonarr').checkbox({
onChecked: function() {
@ -889,19 +887,19 @@ $(document).ready(function() {
}
});
if ($('#sonarr_ssl_div').data("ssl") == "True") {
if ($('#sonarr_ssl_div').data("ssl") === "True") {
$("#sonarr_ssl_div").checkbox('check');
} else {
$("#sonarr_ssl_div").checkbox('uncheck');
}
if ($('#radarr_ssl_div').data("ssl") == "True") {
if ($('#radarr_ssl_div').data("ssl") === "True") {
$("#radarr_ssl_div").checkbox('check');
} else {
$("#radarr_ssl_div").checkbox('uncheck');
}
if ($('#settings_single_language').data("single-language") == "True") {
if ($('#settings_single_language').data("single-language") === "True") {
$("#settings_single_language").checkbox('check');
} else {
$("#settings_single_language").checkbox('uncheck');
@ -910,13 +908,13 @@ $(document).ready(function() {
$('#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_languages').dropdown('setting', 'onRemove', function(val){
$("#settings_serie_default_languages").dropdown('remove selected', val);
$("#settings_serie_default_languages option[value='" + val + "']").remove();
@ -924,13 +922,13 @@ $(document).ready(function() {
$("#settings_movie_default_languages option[value='" + val + "']").remove();
});
if ($('#settings_serie_default_enabled_div').data("enabled") == "True") {
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") {
if ($('#settings_serie_default_enabled_div').data("enabled") === "True") {
$("#settings_serie_default_languages").removeClass('disabled');
$("#settings_serie_default_hi_div").removeClass('disabled');
} else {
@ -949,19 +947,19 @@ $(document).ready(function() {
}
});
if ($('#settings_serie_default_hi_div').data("hi") == "True") {
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") {
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") {
if ($('#settings_movie_default_enabled_div').data("enabled") === "True") {
$("#settings_movie_default_languages").removeClass('disabled');
$("#settings_movie_default_hi_div").removeClass('disabled');
} else {
@ -980,7 +978,7 @@ $(document).ready(function() {
}
});
if ($('#settings_movie_default_hi_div').data("hi") == "True") {
if ($('#settings_movie_default_hi_div').data("hi") === "True") {
$("#settings_movie_default_hi_div").checkbox('check');
} else {
$("#settings_movie_default_hi_div").checkbox('uncheck');
@ -998,7 +996,7 @@ $(document).ready(function() {
$("#settings_movie_default_languages").attr('multiple');
}
$("#settings_single_language").change(function(i, obj) {
$("#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');
@ -1035,9 +1033,7 @@ $(document).ready(function() {
%if settings_general[19] is not None:
$('#settings_movie_default_languages').dropdown('set selected',{{!settings_general[19]}});
%end
</script>
<script>
// form validation
$('#wizard_form')
.form({
@ -1179,7 +1175,6 @@ $(document).ready(function() {
$('.prev3').removeClass('disabled');
$('.next2').removeClass('disabled');
$('.next3').removeClass('disabled');
$('#loader').addClass('active');
}
})
;
@ -1191,18 +1186,18 @@ $(document).ready(function() {
$('.form').form('validate field', 'settings_subliminal_languages');
});
$( document ).ready(function() {
$(function() {
$('.form').form('validate form');
$('#loader').removeClass('active');
});
$('#wizard_form').focusout(function() {
$('#wizard_form').on('focusout', function() {
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$('#sonarr_validate').click(function() {
$('#sonarr_validate').on('click', function() {
if ($('#sonarr_ssl_div').checkbox('is checked')) {
protocol = 'https';
} else {
@ -1223,25 +1218,25 @@ $(document).ready(function() {
$('#loader').removeClass('active');
}
});
})
});
$('.sonarr_config').keyup(function() {
$('.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').change(function() {
$('#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');
$('#radarr_validate').click(function() {
$('#radarr_validate').on('click', function() {
if ($('#radarr_ssl_div').checkbox('is checked')) {
protocol = 'https';
} else {
@ -1262,21 +1257,21 @@ $(document).ready(function() {
$('#loader').removeClass('active');
}
});
})
});
$('.radarr_config').keyup(function() {
$('.radarr_config').on('keyup', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$('#settings_radarr_ssl').change(function() {
$('#settings_radarr_ssl').on('change', function() {
$('#radarr_validated').checkbox('uncheck');
$('#radarr_validation_result').text('You must test your Sonarr connection settings before saving settings.').css('color', 'red');
$('.form').form('validate form');
$('#loader').removeClass('active');
})
});
$("#radarr_validated").checkbox('check');
</script>