diff --git a/bazarr.py b/bazarr.py index 3cecdbfcd..fa3e2de7d 100644 --- a/bazarr.py +++ b/bazarr.py @@ -1,1597 +1,1706 @@ -bazarr_version = '0.6.2' - -import gc -gc.enable() - -from get_argv import config_dir - -import os -import sys -reload(sys) -sys.setdefaultencoding('utf8') -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'libs/')) - -import sqlite3 -from update_modules import * -from init import * -from update_db import * - - -from get_settings import get_general_settings -import logging -from logging.handlers import TimedRotatingFileHandler - -logger = logging.getLogger('waitress') -log_level = get_general_settings()[4] -if log_level is None: - log_level = "INFO" - -class OneLineExceptionFormatter(logging.Formatter): - def formatException(self, exc_info): - """ - Format an exception so that it prints on a single line. - """ - result = super(OneLineExceptionFormatter, self).formatException(exc_info) - return repr(result) # or format into one line however you want to - - def format(self, record): - s = super(OneLineExceptionFormatter, self).format(record) - if record.exc_text: - s = s.replace('\n', '') + '|' - return s - -def configure_logging(): - global fh - fh = TimedRotatingFileHandler(os.path.join(config_dir, 'log/bazarr.log'), when="midnight", interval=1, backupCount=7) - f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|', - '%d/%m/%Y %H:%M:%S') - fh.setFormatter(f) - logging.getLogger("enzyme").setLevel(logging.CRITICAL) - logging.getLogger("apscheduler").setLevel(logging.WARNING) - logging.getLogger("subliminal").setLevel(logging.CRITICAL) - logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL) - root = logging.getLogger() - root.setLevel(log_level) - root.addHandler(fh) - -configure_logging() - -from bottle import route, run, template, static_file, request, redirect, response, HTTPError -import bottle -bottle.TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(__file__), 'views/')) -bottle.debug(True) -bottle.TEMPLATES.clear() - -import itertools -import operator -import requests -import pretty -from datetime import datetime, timedelta -from io import BytesIO -import math -import ast -import hashlib - -from get_languages import load_language_in_db, language_from_alpha3 -from get_providers import * - -from get_series import * -from get_episodes import * -from get_settings import base_url, ip, port, path_replace, path_replace_movie -from check_update import check_and_apply_update -from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_subtitles, movies_scan_subtitles, list_missing_subtitles, list_missing_subtitles_movies -from get_subtitle import download_subtitle, series_download_subtitles, movies_download_subtitles, wanted_download_subtitles, wanted_search_missing_subtitles, manual_search, manual_download_subtitle -from utils import history_log, history_log_movie -from scheduler import * -from notifier import send_notifications, send_notifications_movie - -# Reset restart required warning on start -conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) -c = conn.cursor() -c.execute("UPDATE system SET configured = 0, updated = 0") -conn.commit() -c.close() - -# Load languages in database -load_language_in_db() - -from get_settings import get_auth_settings - -def custom_auth_basic(check): - def decorator(func): - def wrapper(*a, **ka): - if get_auth_settings()[0] is True: - user, password = request.auth or (None, None) - if user is None or not check(user, password): - err = HTTPError(401, "Access denied") - err.add_header('WWW-Authenticate', 'Basic realm="Bazarr"') - return err - return func(*a, **ka) - else: - return func(*a, **ka) - - return wrapper - return decorator - -def check_credentials(user, pw): - from get_settings import get_auth_settings - - username = get_auth_settings()[1] - password = get_auth_settings()[2] - if hashlib.md5(pw).hexdigest() == password and user == username: - return True - return False - -@route('/') -@custom_auth_basic(check_credentials) -def redirect_root(): - redirect (base_url) - -@route(base_url + 'static/:path#.+#', name='static') -@custom_auth_basic(check_credentials) -def static(path): - return static_file(path, root=os.path.join(os.path.dirname(__file__), 'static')) - -@route(base_url + 'emptylog') -@custom_auth_basic(check_credentials) -def emptylog(): - ref = request.environ['HTTP_REFERER'] - - fh.doRollover() - logging.info('Log file emptied') - - redirect(ref) - -@route(base_url + 'bazarr.log') -@custom_auth_basic(check_credentials) -def download_log(): - return static_file('bazarr.log', root=os.path.join(config_dir, 'log/'), download='bazarr.log') - -@route(base_url + 'image_proxy/', method='GET') -@custom_auth_basic(check_credentials) -def image_proxy(url): - url_sonarr = get_sonarr_settings()[6] - url_sonarr_short = get_sonarr_settings()[7] - apikey = get_sonarr_settings()[4] - url_image = url_sonarr_short + '/' + url + '?apikey=' + apikey - try: - image_buffer = BytesIO(requests.get(url_sonarr + '/api' + url_image.split(url_sonarr)[1], timeout=15, verify=False).content) - except: - return None - else: - image_buffer.seek(0) - bytes = image_buffer.read() - response.set_header('Content-type', 'image/jpeg') - return bytes - -@route(base_url + 'image_proxy_movies/', method='GET') -@custom_auth_basic(check_credentials) -def image_proxy_movies(url): - url_radarr = get_radarr_settings()[6] - url_radarr_short = get_radarr_settings()[7] - apikey = get_radarr_settings()[4] - try: - url_image = (url_radarr_short + '/' + url + '?apikey=' + apikey).replace('/fanart.jpg', '/banner.jpg') - image_buffer = BytesIO(requests.get(url_radarr + '/api' + url_image.split(url_radarr)[1], timeout=15, verify=False).content) - except: - url_image = url_radarr_short + '/' + url + '?apikey=' + apikey - image_buffer = BytesIO(requests.get(url_radarr + '/api' + url_image.split(url_radarr)[1], timeout=15, verify=False).content) - else: - image_buffer.seek(0) - bytes = image_buffer.read() - response.set_header('Content-type', 'image/jpeg') - return bytes - - -@route(base_url) -@custom_auth_basic(check_credentials) -def redirect_root(): - - if get_general_settings()[12] is True: - redirect(base_url + 'series') - elif get_general_settings()[13] is True: - redirect(base_url + 'movies') - else: - redirect(base_url + 'settings') - - -@route(base_url + 'series') -@custom_auth_basic(check_credentials) -def series(): - single_language = get_general_settings()[7] - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - db.create_function("path_substitution", 1, path_replace) - c = db.cursor() - - c.execute("SELECT COUNT(*) FROM table_shows") - missing_count = c.fetchone() - missing_count = missing_count[0] - page = request.GET.page - if page == "": - page = "1" - page_size = int(get_general_settings()[21]) - offset = (int(page) - 1) * page_size - max_page = int(math.ceil(missing_count / (page_size + 0.0))) - - c.execute("SELECT tvdbId, title, path_substitution(path), languages, hearing_impaired, sonarrSeriesId, poster, audio_language FROM table_shows ORDER BY sortTitle ASC LIMIT ? OFFSET ?", (page_size, offset,)) - data = c.fetchall() - c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1") - languages = c.fetchall() - c.execute("SELECT table_shows.sonarrSeriesId, COUNT(table_episodes.missing_subtitles) FROM table_shows LEFT JOIN table_episodes ON table_shows.sonarrSeriesId=table_episodes.sonarrSeriesId WHERE table_shows.languages IS NOT 'None' AND table_episodes.missing_subtitles IS NOT '[]' GROUP BY table_shows.sonarrSeriesId") - missing_subtitles_list = c.fetchall() - c.execute("SELECT table_shows.sonarrSeriesId, COUNT(table_episodes.missing_subtitles) FROM table_shows LEFT JOIN table_episodes ON table_shows.sonarrSeriesId=table_episodes.sonarrSeriesId WHERE table_shows.languages IS NOT 'None' GROUP BY table_shows.sonarrSeriesId") - total_subtitles_list = c.fetchall() - c.close() - output = template('series', __file__=__file__, bazarr_version=bazarr_version, rows=data, missing_subtitles_list=missing_subtitles_list, total_subtitles_list=total_subtitles_list, languages=languages, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, single_language=single_language, page_size=page_size) - return output - -@route(base_url + 'serieseditor') -@custom_auth_basic(check_credentials) -def serieseditor(): - single_language = get_general_settings()[7] - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - db.create_function("path_substitution", 1, path_replace) - c = db.cursor() - - c.execute("SELECT COUNT(*) FROM table_shows") - missing_count = c.fetchone() - missing_count = missing_count[0] - - c.execute("SELECT tvdbId, title, path_substitution(path), languages, hearing_impaired, sonarrSeriesId, poster, audio_language FROM table_shows ORDER BY title ASC") - data = c.fetchall() - 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, missing_count=missing_count, base_url=base_url, single_language=single_language) - return output - -@route(base_url + 'search_json/', method='GET') -@custom_auth_basic(check_credentials) -def search_json(query): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - - c.execute("SELECT title, sonarrSeriesId FROM table_shows WHERE title LIKE ? ORDER BY title", ('%'+query+'%',)) - series = c.fetchall() - - c.execute("SELECT title, radarrId FROM table_movies WHERE title LIKE ? ORDER BY title", ('%' + query + '%',)) - movies = c.fetchall() - - search_list = [] - for serie in series: - search_list.append(dict([('name', serie[0]), ('url', base_url + 'episodes/' + str(serie[1]))])) - - for movie in movies: - search_list.append(dict([('name', movie[0]), ('url', base_url + 'movie/' + str(movie[1]))])) - - response.content_type = 'application/json' - return dict(items=search_list) - - -@route(base_url + 'edit_series/', method='POST') -@custom_auth_basic(check_credentials) -def edit_series(no): - ref = request.environ['HTTP_REFERER'] - - lang = request.forms.getall('languages') - if len(lang) > 0: - pass - else: - lang = 'None' - - single_language = get_general_settings()[7] - if single_language is True: - if str(lang) == "['None']": - lang = 'None' - else: - lang = str(lang) - else: - if str(lang) == "['']": - lang = '[]' - - hi = request.forms.get('hearing_impaired') - - if hi == "on": - hi = "True" - else: - hi = "False" - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = conn.cursor() - c.execute("UPDATE table_shows SET languages = ?, hearing_impaired = ? WHERE sonarrSeriesId LIKE ?", (str(lang), hi, no)) - conn.commit() - c.close() - - list_missing_subtitles(no) - - redirect(ref) - -@route(base_url + 'edit_serieseditor', method='POST') -@custom_auth_basic(check_credentials) -def edit_serieseditor(): - ref = request.environ['HTTP_REFERER'] - - series = request.forms.get('series') - series = ast.literal_eval(str('[' + series + ']')) - lang = request.forms.getall('languages') - hi = request.forms.get('hearing_impaired') - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = conn.cursor() - - for serie in series: - if str(lang) != "[]" and str(lang) != "['']": - if str(lang) == "['None']": - lang = 'None' - else: - lang = str(lang) - c.execute("UPDATE table_shows SET languages = ? WHERE sonarrSeriesId LIKE ?", (lang, serie)) - if hi != '': - c.execute("UPDATE table_shows SET hearing_impaired = ? WHERE sonarrSeriesId LIKE ?", (hi, serie)) - - conn.commit() - c.close() - - for serie in series: - list_missing_subtitles(serie) - - redirect(ref) - -@route(base_url + 'episodes/', method='GET') -@custom_auth_basic(check_credentials) -def episodes(no): - # single_language = get_general_settings()[7] - url_sonarr_short = get_sonarr_settings()[7] - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - conn.create_function("path_substitution", 1, path_replace) - c = conn.cursor() - - series_details = [] - series_details = c.execute("SELECT title, overview, poster, fanart, hearing_impaired, tvdbid, audio_language, languages, path_substitution(path) FROM table_shows WHERE sonarrSeriesId LIKE ?", (str(no),)).fetchone() - tvdbid = series_details[5] - - episodes = c.execute("SELECT title, path_substitution(path), season, episode, subtitles, sonarrSeriesId, missing_subtitles, sonarrEpisodeId, scene_name, monitored FROM table_episodes WHERE sonarrSeriesId LIKE ? ORDER BY episode ASC", (str(no),)).fetchall() - number = len(episodes) - languages = c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1").fetchall() - c.close() - episodes = reversed(sorted(episodes, key=operator.itemgetter(2))) - seasons_list = [] - 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, languages=languages, seasons=seasons_list, url_sonarr_short=url_sonarr_short, base_url=base_url, tvdbid=tvdbid, number=number) - -@route(base_url + 'movies') -@custom_auth_basic(check_credentials) -def movies(): - single_language = get_general_settings()[7] - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - db.create_function("path_substitution", 1, path_replace_movie) - c = db.cursor() - - c.execute("SELECT COUNT(*) FROM table_movies") - missing_count = c.fetchone() - missing_count = missing_count[0] - page = request.GET.page - if page == "": - page = "1" - page_size = int(get_general_settings()[21]) - offset = (int(page) - 1) * page_size - max_page = int(math.ceil(missing_count / (page_size + 0.0))) - - c.execute("SELECT tmdbId, title, path_substitution(path), languages, hearing_impaired, radarrId, poster, audio_language, monitored FROM table_movies ORDER BY title ASC LIMIT ? OFFSET ?", (page_size, offset,)) - data = c.fetchall() - 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, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, single_language=single_language, page_size=page_size) - return output - -@route(base_url + 'movieseditor') -@custom_auth_basic(check_credentials) -def movieseditor(): - single_language = get_general_settings()[7] - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - db.create_function("path_substitution", 1, path_replace_movie) - c = db.cursor() - - c.execute("SELECT COUNT(*) FROM table_movies") - missing_count = c.fetchone() - missing_count = missing_count[0] - - c.execute("SELECT tmdbId, title, path_substitution(path), languages, hearing_impaired, radarrId, poster, audio_language FROM table_movies ORDER BY title ASC") - data = c.fetchall() - 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, missing_count=missing_count, base_url=base_url, single_language=single_language) - return output - -@route(base_url + 'edit_movieseditor', method='POST') -@custom_auth_basic(check_credentials) -def edit_movieseditor(): - ref = request.environ['HTTP_REFERER'] - - movies = request.forms.get('movies') - movies = ast.literal_eval(str('[' + movies + ']')) - lang = request.forms.getall('languages') - hi = request.forms.get('hearing_impaired') - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = conn.cursor() - - for movie in movies: - if str(lang) != "[]" and str(lang) != "['']": - if str(lang) == "['None']": - lang = 'None' - else: - lang = str(lang) - c.execute("UPDATE table_movies SET languages = ? WHERE radarrId LIKE ?", (lang, movie)) - if hi != '': - c.execute("UPDATE table_movies SET hearing_impaired = ? WHERE radarrId LIKE ?", (hi, movie)) - - conn.commit() - c.close() - - for movie in movies: - list_missing_subtitles_movies(movie) - - redirect(ref) - -@route(base_url + 'edit_movie/', method='POST') -@custom_auth_basic(check_credentials) -def edit_movie(no): - ref = request.environ['HTTP_REFERER'] - - lang = request.forms.getall('languages') - if len(lang) > 0: - pass - else: - lang = 'None' - - if str(lang) == "['']": - lang = '[]' - - hi = request.forms.get('hearing_impaired') - - if hi == "on": - hi = "True" - else: - hi = "False" - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = conn.cursor() - c.execute("UPDATE table_movies SET languages = ?, hearing_impaired = ? WHERE radarrId LIKE ?", (str(lang), hi, no)) - conn.commit() - c.close() - - list_missing_subtitles_movies(no) - - redirect(ref) - -@route(base_url + 'movie/', method='GET') -@custom_auth_basic(check_credentials) -def movie(no): - # single_language = get_general_settings()[7] - url_radarr_short = get_radarr_settings()[7] - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - conn.create_function("path_substitution", 1, path_replace_movie) - c = conn.cursor() - - movies_details = [] - movies_details = c.execute("SELECT title, overview, poster, fanart, hearing_impaired, tmdbid, audio_language, languages, path_substitution(path), subtitles, radarrId, missing_subtitles, sceneName, monitored FROM table_movies WHERE radarrId LIKE ?", (str(no),)).fetchone() - tmdbid = movies_details[5] - - 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, languages=languages, url_radarr_short=url_radarr_short, base_url=base_url, tmdbid=tmdbid) - -@route(base_url + 'scan_disk/', method='GET') -@custom_auth_basic(check_credentials) -def scan_disk(no): - ref = request.environ['HTTP_REFERER'] - - series_scan_subtitles(no) - - redirect(ref) - -@route(base_url + 'scan_disk_movie/', method='GET') -@custom_auth_basic(check_credentials) -def scan_disk_movie(no): - ref = request.environ['HTTP_REFERER'] - - movies_scan_subtitles(no) - - redirect(ref) - -@route(base_url + 'search_missing_subtitles/', method='GET') -@custom_auth_basic(check_credentials) -def search_missing_subtitles(no): - ref = request.environ['HTTP_REFERER'] - - series_download_subtitles(no) - - redirect(ref) - -@route(base_url + 'search_missing_subtitles_movie/', method='GET') -@custom_auth_basic(check_credentials) -def search_missing_subtitles_movie(no): - ref = request.environ['HTTP_REFERER'] - - movies_download_subtitles(no) - - redirect(ref) - -@route(base_url + 'history') -@custom_auth_basic(check_credentials) -def history(): - return template('history', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url) - -@route(base_url + 'historyseries') -@custom_auth_basic(check_credentials) -def historyseries(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - - c.execute("SELECT COUNT(*) FROM table_history") - row_count = c.fetchone() - row_count = row_count[0] - page = request.GET.page - if page == "": - page = "1" - page_size = int(get_general_settings()[21]) - offset = (int(page) - 1) * page_size - max_page = int(math.ceil(row_count / (page_size + 0.0))) - - now = datetime.now() - today = [] - thisweek = [] - thisyear = [] - stats = c.execute("SELECT timestamp FROM table_history WHERE action LIKE '1'").fetchall() - total = len(stats) - for stat in stats: - if now - timedelta(hours=24) <= datetime.fromtimestamp(stat[0]) <= now: - today.append(datetime.fromtimestamp(stat[0]).date()) - if now - timedelta(weeks=1) <= datetime.fromtimestamp(stat[0]) <= now: - thisweek.append(datetime.fromtimestamp(stat[0]).date()) - if now - timedelta(weeks=52) <= datetime.fromtimestamp(stat[0]) <= now: - thisyear.append(datetime.fromtimestamp(stat[0]).date()) - stats = [len(today), len(thisweek), len(thisyear), total] - - c.execute("SELECT table_history.action, table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_history.timestamp, table_history.description, table_history.sonarrSeriesId FROM table_history LEFT JOIN table_shows on table_shows.sonarrSeriesId = table_history.sonarrSeriesId LEFT JOIN table_episodes on table_episodes.sonarrEpisodeId = table_history.sonarrEpisodeId ORDER BY id DESC LIMIT ? OFFSET ?", (page_size, offset,)) - 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, page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size) - -@route(base_url + 'historymovies') -@custom_auth_basic(check_credentials) -def historymovies(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - - c.execute("SELECT COUNT(*) FROM table_history_movie") - row_count = c.fetchone() - row_count = row_count[0] - page = request.GET.page - if page == "": - page = "1" - page_size = int(get_general_settings()[21]) - offset = (int(page) - 1) * page_size - max_page = int(math.ceil(row_count / (page_size + 0.0))) - - now = datetime.now() - today = [] - thisweek = [] - thisyear = [] - stats = c.execute("SELECT timestamp FROM table_history_movie WHERE action LIKE '1'").fetchall() - total = len(stats) - for stat in stats: - if now - timedelta(hours=24) <= datetime.fromtimestamp(stat[0]) <= now: - today.append(datetime.fromtimestamp(stat[0]).date()) - if now - timedelta(weeks=1) <= datetime.fromtimestamp(stat[0]) <= now: - thisweek.append(datetime.fromtimestamp(stat[0]).date()) - if now - timedelta(weeks=52) <= datetime.fromtimestamp(stat[0]) <= now: - thisyear.append(datetime.fromtimestamp(stat[0]).date()) - stats = [len(today), len(thisweek), len(thisyear), total] - - c.execute("SELECT table_history_movie.action, table_movies.title, table_history_movie.timestamp, table_history_movie.description, table_history_movie.radarrId FROM table_history_movie LEFT JOIN table_movies on table_movies.radarrId = table_history_movie.radarrId ORDER BY id DESC LIMIT ? OFFSET ?", (page_size, offset,)) - 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, page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size) - -@route(base_url + 'wanted') -@custom_auth_basic(check_credentials) -def wanted(): - return template('wanted', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url) - -@route(base_url + 'wantedseries') -@custom_auth_basic(check_credentials) -def wantedseries(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - db.create_function("path_substitution", 1, path_replace) - c = db.cursor() - - if get_general_settings()[24] is True: - monitored_only_query_string = ' AND monitored = "True"' - else: - monitored_only_query_string = "" - - c.execute("SELECT COUNT(*) FROM table_episodes WHERE missing_subtitles != '[]'" + monitored_only_query_string) - missing_count = c.fetchone() - missing_count = missing_count[0] - page = request.GET.page - if page == "": - page = "1" - page_size = int(get_general_settings()[21]) - offset = (int(page) - 1) * page_size - max_page = int(math.ceil(missing_count / (page_size + 0.0))) - - c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles, table_episodes.sonarrSeriesId, path_substitution(table_episodes.path), table_shows.hearing_impaired, table_episodes.sonarrEpisodeId, table_episodes.scene_name FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY table_episodes._rowid_ DESC LIMIT ? OFFSET ?", (page_size, offset,)) - data = c.fetchall() - c.close() - return template('wantedseries', __file__=__file__, bazarr_version=bazarr_version, rows=data, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, page_size=page_size) - -@route(base_url + 'wantedmovies') -@custom_auth_basic(check_credentials) -def wantedmovies(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - db.create_function("path_substitution", 1, path_replace_movie) - c = db.cursor() - - if get_general_settings()[24] is True: - monitored_only_query_string = ' AND monitored = "True"' - else: - monitored_only_query_string = "" - - c.execute("SELECT COUNT(*) FROM table_movies WHERE missing_subtitles != '[]'" + monitored_only_query_string) - missing_count = c.fetchone() - missing_count = missing_count[0] - page = request.GET.page - if page == "": - page = "1" - page_size = int(get_general_settings()[21]) - offset = (int(page) - 1) * page_size - max_page = int(math.ceil(missing_count / (page_size + 0.0))) - - c.execute("SELECT title, missing_subtitles, radarrId, path_substitution(path), hearing_impaired, sceneName FROM table_movies WHERE missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY _rowid_ DESC LIMIT ? OFFSET ?", (page_size, offset,)) - data = c.fetchall() - c.close() - return template('wantedmovies', __file__=__file__, bazarr_version=bazarr_version, rows=data, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, page_size=page_size) - -@route(base_url + 'wanted_search_missing_subtitles') -@custom_auth_basic(check_credentials) -def wanted_search_missing_subtitles_list(): - ref = request.environ['HTTP_REFERER'] - - wanted_search_missing_subtitles() - - redirect(ref) - -@route(base_url + 'settings') -@custom_auth_basic(check_credentials) -def settings(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - c.execute("SELECT * FROM table_settings_languages ORDER BY name") - settings_languages = c.fetchall() - c.execute("SELECT * FROM table_settings_providers ORDER BY name") - settings_providers = c.fetchall() - c.execute("SELECT * FROM table_settings_notifier ORDER BY name") - settings_notifier = c.fetchall() - c.close() - from get_settings import get_general_settings, get_auth_settings, get_radarr_settings, get_sonarr_settings - settings_general = get_general_settings() - settings_auth = get_auth_settings() - settings_sonarr = get_sonarr_settings() - settings_radarr = get_radarr_settings() - - return template('settings', __file__=__file__, bazarr_version=bazarr_version, settings_general=settings_general, 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) - -@route(base_url + 'save_settings', method='POST') -@custom_auth_basic(check_credentials) -def save_settings(): - ref = request.environ['HTTP_REFERER'] - - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = conn.cursor() - - settings_general_ip = request.forms.get('settings_general_ip') - settings_general_port = request.forms.get('settings_general_port') - settings_general_baseurl = request.forms.get('settings_general_baseurl') - settings_general_loglevel = request.forms.get('settings_general_loglevel') - settings_general_auth_enabled = request.forms.get('settings_general_auth_enabled') - if settings_general_auth_enabled is None: - settings_general_auth_enabled = 'False' - else: - settings_general_auth_enabled = 'True' - settings_general_auth_username = request.forms.get('settings_general_auth_username') - settings_general_auth_password = request.forms.get('settings_general_auth_password') - settings_general_sourcepath = request.forms.getall('settings_general_sourcepath') - settings_general_destpath = request.forms.getall('settings_general_destpath') - settings_general_pathmapping = [] - settings_general_pathmapping.extend([list(a) for a in zip(settings_general_sourcepath, settings_general_destpath)]) - settings_general_sourcepath_movie = request.forms.getall('settings_general_sourcepath_movie') - settings_general_destpath_movie = request.forms.getall('settings_general_destpath_movie') - settings_general_pathmapping_movie = [] - settings_general_pathmapping_movie.extend([list(a) for a in zip(settings_general_sourcepath_movie, settings_general_destpath_movie)]) - settings_general_branch = request.forms.get('settings_general_branch') - settings_general_automatic = request.forms.get('settings_general_automatic') - if settings_general_automatic is None: - settings_general_automatic = 'False' - else: - settings_general_automatic = 'True' - settings_general_single_language = request.forms.get('settings_general_single_language') - if settings_general_single_language is None: - settings_general_single_language = 'False' - else: - settings_general_single_language = 'True' - settings_general_scenename = request.forms.get('settings_general_scenename') - if settings_general_scenename is None: - settings_general_scenename = 'False' - else: - settings_general_scenename = 'True' - settings_general_embedded = request.forms.get('settings_general_embedded') - if settings_general_embedded is None: - settings_general_embedded = 'False' - else: - settings_general_embedded = 'True' - settings_general_only_monitored = request.forms.get('settings_general_only_monitored') - if settings_general_only_monitored is None: - settings_general_only_monitored = 'False' - else: - settings_general_only_monitored = 'True' - settings_general_adaptive_searching = request.forms.get('settings_general_adaptive_searching') - if settings_general_adaptive_searching is None: - settings_general_adaptive_searching = 'False' - else: - settings_general_adaptive_searching = 'True' - settings_general_minimum_score = request.forms.get('settings_general_minimum_score') - settings_general_minimum_score_movies = request.forms.get('settings_general_minimum_score_movies') - settings_general_use_postprocessing = request.forms.get('settings_general_use_postprocessing') - if settings_general_use_postprocessing is None: - settings_general_use_postprocessing = 'False' - else: - settings_general_use_postprocessing = 'True' - settings_general_postprocessing_cmd = request.forms.get('settings_general_postprocessing_cmd') - settings_general_use_sonarr = request.forms.get('settings_general_use_sonarr') - if settings_general_use_sonarr is None: - settings_general_use_sonarr = 'False' - else: - settings_general_use_sonarr = 'True' - settings_general_use_radarr = request.forms.get('settings_general_use_radarr') - if settings_general_use_radarr is None: - settings_general_use_radarr = 'False' - else: - settings_general_use_radarr = 'True' - settings_page_size = request.forms.get('settings_page_size') - - settings_general = get_general_settings() - - before = (unicode(settings_general[0]), int(settings_general[1]), unicode(settings_general[2]), unicode(settings_general[4]), unicode(settings_general[3]), unicode(settings_general[12]), unicode(settings_general[13]), unicode(settings_general[14])) - after = (unicode(settings_general_ip), int(settings_general_port), unicode(settings_general_baseurl), unicode(settings_general_loglevel), unicode(settings_general_pathmapping), unicode(settings_general_use_sonarr), unicode(settings_general_use_radarr), unicode(settings_general_pathmapping_movie)) - from six import text_type - - cfg = ConfigParser() - - with open(config_file, 'r') as f: - cfg.read_file(f) - - cfg.set('general', 'ip', text_type(settings_general_ip)) - cfg.set('general', 'port', text_type(settings_general_port)) - cfg.set('general', 'base_url', text_type(settings_general_baseurl)) - cfg.set('general', 'path_mappings', text_type(settings_general_pathmapping)) - cfg.set('general', 'log_level', text_type(settings_general_loglevel)) - cfg.set('general', 'branch', text_type(settings_general_branch)) - cfg.set('general', 'auto_update', text_type(settings_general_automatic)) - cfg.set('general', 'single_language', text_type(settings_general_single_language)) - cfg.set('general', 'minimum_score', text_type(settings_general_minimum_score)) - cfg.set('general', 'use_scenename', text_type(settings_general_scenename)) - cfg.set('general', 'use_postprocessing', text_type(settings_general_use_postprocessing)) - cfg.set('general', 'postprocessing_cmd', text_type(settings_general_postprocessing_cmd)) - cfg.set('general', 'use_sonarr', text_type(settings_general_use_sonarr)) - cfg.set('general', 'use_radarr', text_type(settings_general_use_radarr)) - cfg.set('general', 'path_mappings_movie', text_type(settings_general_pathmapping_movie)) - cfg.set('general', 'page_size', text_type(settings_page_size)) - cfg.set('general', 'minimum_score_movie', text_type(settings_general_minimum_score_movies)) - cfg.set('general', 'use_embedded_subs', text_type(settings_general_embedded)) - cfg.set('general', 'only_monitored', text_type(settings_general_only_monitored)) - cfg.set('general', 'adaptive_searching', text_type(settings_general_adaptive_searching)) - - - if after != before: - configured() - get_general_settings() - - settings_auth = get_auth_settings() - - before_auth_password = (unicode(settings_auth[0]), unicode(settings_auth[2])) - if before_auth_password[0] != settings_general_auth_enabled: - configured() - if before_auth_password[1] == settings_general_auth_password: - cfg.set('auth', 'enabled', text_type(settings_general_auth_enabled)) - cfg.set('auth', 'username', text_type(settings_general_auth_username)) - else: - cfg.set('auth', 'enabled', text_type(settings_general_auth_enabled)) - cfg.set('auth', 'username', text_type(settings_general_auth_username)) - cfg.set('auth', 'password', hashlib.md5(settings_general_auth_password).hexdigest()) - - settings_sonarr_ip = request.forms.get('settings_sonarr_ip') - settings_sonarr_port = request.forms.get('settings_sonarr_port') - settings_sonarr_baseurl = request.forms.get('settings_sonarr_baseurl') - settings_sonarr_ssl = request.forms.get('settings_sonarr_ssl') - if settings_sonarr_ssl is None: - settings_sonarr_ssl = 'False' - else: - settings_sonarr_ssl = 'True' - settings_sonarr_apikey = request.forms.get('settings_sonarr_apikey') - settings_sonarr_sync = request.forms.get('settings_sonarr_sync') - - cfg.set('sonarr', 'ip', text_type(settings_sonarr_ip)) - cfg.set('sonarr', 'port', text_type(settings_sonarr_port)) - cfg.set('sonarr', 'base_url', text_type(settings_sonarr_baseurl)) - cfg.set('sonarr', 'ssl', text_type(settings_sonarr_ssl)) - cfg.set('sonarr', 'apikey', text_type(settings_sonarr_apikey)) - cfg.set('sonarr', 'full_update', text_type(settings_sonarr_sync)) - - settings_radarr_ip = request.forms.get('settings_radarr_ip') - settings_radarr_port = request.forms.get('settings_radarr_port') - settings_radarr_baseurl = request.forms.get('settings_radarr_baseurl') - settings_radarr_ssl = request.forms.get('settings_radarr_ssl') - if settings_radarr_ssl is None: - settings_radarr_ssl = 'False' - else: - settings_radarr_ssl = 'True' - settings_radarr_apikey = request.forms.get('settings_radarr_apikey') - settings_radarr_sync = request.forms.get('settings_radarr_sync') - - cfg.set('radarr', 'ip', text_type(settings_radarr_ip)) - cfg.set('radarr', 'port', text_type(settings_radarr_port)) - cfg.set('radarr', 'base_url', text_type(settings_radarr_baseurl)) - cfg.set('radarr', 'ssl', text_type(settings_radarr_ssl)) - cfg.set('radarr', 'apikey', text_type(settings_radarr_apikey)) - cfg.set('radarr', 'full_update', text_type(settings_radarr_sync)) - - settings_subliminal_providers = request.forms.getall('settings_subliminal_providers') - c.execute("UPDATE table_settings_providers SET enabled = 0") - for item in settings_subliminal_providers: - c.execute("UPDATE table_settings_providers SET enabled = '1' WHERE name = ?", (item,)) - - settings_addic7ed_username = request.forms.get('settings_addic7ed_username') - settings_addic7ed_password = request.forms.get('settings_addic7ed_password') - c.execute("UPDATE table_settings_providers SET username = ?, password = ? WHERE name = 'addic7ed'", (settings_addic7ed_username, settings_addic7ed_password)) - settings_legendastv_username = request.forms.get('settings_legendastv_username') - settings_legendastv_password = request.forms.get('settings_legendastv_password') - c.execute("UPDATE table_settings_providers SET username = ?, password = ? WHERE name = 'legendastv'", (settings_legendastv_username, settings_legendastv_password)) - settings_opensubtitles_username = request.forms.get('settings_opensubtitles_username') - settings_opensubtitles_password = request.forms.get('settings_opensubtitles_password') - c.execute("UPDATE table_settings_providers SET username = ?, password = ? WHERE name = 'opensubtitles'", (settings_opensubtitles_username, settings_opensubtitles_password)) - - settings_subliminal_languages = request.forms.getall('settings_subliminal_languages') - c.execute("UPDATE table_settings_languages SET enabled = 0") - for item in settings_subliminal_languages: - c.execute("UPDATE table_settings_languages SET enabled = '1' WHERE code2 = ?", (item,)) - - settings_serie_default_enabled = request.forms.get('settings_serie_default_enabled') - if settings_serie_default_enabled is None: - settings_serie_default_enabled = 'False' - else: - settings_serie_default_enabled = 'True' - cfg.set('general', 'serie_default_enabled', text_type(settings_serie_default_enabled)) - - settings_serie_default_languages = str(request.forms.getall('settings_serie_default_languages')) - if settings_serie_default_languages == "['None']": - settings_serie_default_languages = 'None' - cfg.set('general', 'serie_default_language', text_type(settings_serie_default_languages)) - - settings_serie_default_hi = request.forms.get('settings_serie_default_hi') - if settings_serie_default_hi is None: - settings_serie_default_hi = 'False' - else: - settings_serie_default_hi = 'True' - cfg.set('general', 'serie_default_hi', text_type(settings_serie_default_hi)) - - settings_movie_default_enabled = request.forms.get('settings_movie_default_enabled') - if settings_movie_default_enabled is None: - settings_movie_default_enabled = 'False' - else: - settings_movie_default_enabled = 'True' - cfg.set('general', 'movie_default_enabled', text_type(settings_movie_default_enabled)) - - settings_movie_default_languages = str(request.forms.getall('settings_movie_default_languages')) - if settings_movie_default_languages == "['None']": - settings_movie_default_languages = 'None' - cfg.set('general', 'movie_default_language', text_type(settings_movie_default_languages)) - - settings_movie_default_hi = request.forms.get('settings_movie_default_hi') - if settings_movie_default_hi is None: - settings_movie_default_hi = 'False' - else: - settings_movie_default_hi = 'True' - cfg.set('general', 'movie_default_hi', text_type(settings_movie_default_hi)) - - with open(config_file, 'wb') as f: - cfg.write(f) - - settings_notifier_Boxcar_enabled = request.forms.get('settings_notifier_Boxcar_enabled') - if settings_notifier_Boxcar_enabled == 'on': - settings_notifier_Boxcar_enabled = 1 - else: - settings_notifier_Boxcar_enabled = 0 - settings_notifier_Boxcar_url = request.forms.get('settings_notifier_Boxcar_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Boxcar'", (settings_notifier_Boxcar_enabled, settings_notifier_Boxcar_url)) - - settings_notifier_Faast_enabled = request.forms.get('settings_notifier_Faast_enabled') - if settings_notifier_Faast_enabled == 'on': - settings_notifier_Faast_enabled = 1 - else: - settings_notifier_Faast_enabled = 0 - settings_notifier_Faast_url = request.forms.get('settings_notifier_Faast_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Faast'", (settings_notifier_Faast_enabled, settings_notifier_Faast_url)) - - settings_notifier_Growl_enabled = request.forms.get('settings_notifier_Growl_enabled') - if settings_notifier_Growl_enabled == 'on': - settings_notifier_Growl_enabled = 1 - else: - settings_notifier_Growl_enabled = 0 - settings_notifier_Growl_url = request.forms.get('settings_notifier_Growl_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Growl'", (settings_notifier_Growl_enabled, settings_notifier_Growl_url)) - - settings_notifier_Join_enabled = request.forms.get('settings_notifier_Join_enabled') - if settings_notifier_Join_enabled == 'on': - settings_notifier_Join_enabled = 1 - else: - settings_notifier_Join_enabled = 0 - settings_notifier_Join_url = request.forms.get('settings_notifier_Join_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Join'", (settings_notifier_Join_enabled, settings_notifier_Join_url)) - - settings_notifier_KODI_enabled = request.forms.get('settings_notifier_KODI_enabled') - if settings_notifier_KODI_enabled == 'on': - settings_notifier_KODI_enabled = 1 - else: - settings_notifier_KODI_enabled = 0 - settings_notifier_KODI_url = request.forms.get('settings_notifier_KODI_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'KODI'", (settings_notifier_KODI_enabled, settings_notifier_KODI_url)) - - settings_notifier_Mattermost_enabled = request.forms.get('settings_notifier_Mattermost_enabled') - if settings_notifier_Mattermost_enabled == 'on': - settings_notifier_Mattermost_enabled = 1 - else: - settings_notifier_Mattermost_enabled = 0 - settings_notifier_Mattermost_url = request.forms.get('settings_notifier_Mattermost_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Mattermost'", (settings_notifier_Mattermost_enabled, settings_notifier_Mattermost_url)) - - settings_notifier_NMA_enabled = request.forms.get('settings_notifier_Notify My Android_enabled') - if settings_notifier_NMA_enabled == 'on': - settings_notifier_NMA_enabled = 1 - else: - settings_notifier_NMA_enabled = 0 - settings_notifier_NMA_url = request.forms.get('settings_notifier_Notify My Android_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Notify My Android'", (settings_notifier_NMA_enabled, settings_notifier_NMA_url)) - - settings_notifier_Prowl_enabled = request.forms.get('settings_notifier_Prowl_enabled') - if settings_notifier_Prowl_enabled == 'on': - settings_notifier_Prowl_enabled = 1 - else: - settings_notifier_Prowl_enabled = 0 - settings_notifier_Prowl_url = request.forms.get('settings_notifier_Prowl_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Prowl'", (settings_notifier_Prowl_enabled, settings_notifier_Prowl_url)) - - settings_notifier_Pushalot_enabled = request.forms.get('settings_notifier_Pushalot_enabled') - if settings_notifier_Pushalot_enabled == 'on': - settings_notifier_Pushalot_enabled = 1 - else: - settings_notifier_Pushalot_enabled = 0 - settings_notifier_Pushalot_url = request.forms.get('settings_notifier_Pushalot_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Pushalot'", (settings_notifier_Pushalot_enabled, settings_notifier_Pushalot_url)) - - settings_notifier_PushBullet_enabled = request.forms.get('settings_notifier_PushBullet_enabled') - if settings_notifier_PushBullet_enabled == 'on': - settings_notifier_PushBullet_enabled = 1 - else: - settings_notifier_PushBullet_enabled = 0 - settings_notifier_PushBullet_url = request.forms.get('settings_notifier_PushBullet_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'PushBullet'", (settings_notifier_PushBullet_enabled, settings_notifier_PushBullet_url)) - - settings_notifier_Pushjet_enabled = request.forms.get('settings_notifier_Pushjet_enabled') - if settings_notifier_Pushjet_enabled == 'on': - settings_notifier_Pushjet_enabled = 1 - else: - settings_notifier_Pushjet_enabled = 0 - settings_notifier_Pushjet_url = request.forms.get('settings_notifier_Pushjet_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Pushjet'", (settings_notifier_Pushjet_enabled, settings_notifier_Pushjet_url)) - - settings_notifier_Pushover_enabled = request.forms.get('settings_notifier_Pushover_enabled') - if settings_notifier_Pushover_enabled == 'on': - settings_notifier_Pushover_enabled = 1 - else: - settings_notifier_Pushover_enabled = 0 - settings_notifier_Pushover_url = request.forms.get('settings_notifier_Pushover_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Pushover'", (settings_notifier_Pushover_enabled, settings_notifier_Pushover_url)) - - settings_notifier_RocketChat_enabled = request.forms.get('settings_notifier_Rocket.Chat_enabled') - if settings_notifier_RocketChat_enabled == 'on': - settings_notifier_RocketChat_enabled = 1 - else: - settings_notifier_RocketChat_enabled = 0 - settings_notifier_RocketChat_url = request.forms.get('settings_notifier_Rocket.Chat_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Rocket.Chat'", (settings_notifier_RocketChat_enabled, settings_notifier_RocketChat_url)) - - settings_notifier_Slack_enabled = request.forms.get('settings_notifier_Slack_enabled') - if settings_notifier_Slack_enabled == 'on': - settings_notifier_Slack_enabled = 1 - else: - settings_notifier_Slack_enabled = 0 - settings_notifier_Slack_url = request.forms.get('settings_notifier_Slack_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Slack'", (settings_notifier_Slack_enabled, settings_notifier_Slack_url)) - - settings_notifier_SuperToasty_enabled = request.forms.get('settings_notifier_Super Toasty_enabled') - if settings_notifier_SuperToasty_enabled == 'on': - settings_notifier_SuperToasty_enabled = 1 - else: - settings_notifier_SuperToasty_enabled = 0 - settings_notifier_SuperToasty_url = request.forms.get('settings_notifier_Super Toasty_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Super Toasty'", (settings_notifier_SuperToasty_enabled, settings_notifier_SuperToasty_url)) - - settings_notifier_Telegram_enabled = request.forms.get('settings_notifier_Telegram_enabled') - if settings_notifier_Telegram_enabled == 'on': - settings_notifier_Telegram_enabled = 1 - else: - settings_notifier_Telegram_enabled = 0 - settings_notifier_Telegram_url = request.forms.get('settings_notifier_Telegram_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Telegram'", (settings_notifier_Telegram_enabled, settings_notifier_Telegram_url)) - - settings_notifier_Twitter_enabled = request.forms.get('settings_notifier_Twitter_enabled') - if settings_notifier_Twitter_enabled == 'on': - settings_notifier_Twitter_enabled = 1 - else: - settings_notifier_Twitter_enabled = 0 - settings_notifier_Twitter_url = request.forms.get('settings_notifier_Twitter_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Twitter'", (settings_notifier_Twitter_enabled, settings_notifier_Twitter_url)) - - settings_notifier_XBMC_enabled = request.forms.get('settings_notifier_XBMC_enabled') - if settings_notifier_XBMC_enabled == 'on': - settings_notifier_XBMC_enabled = 1 - else: - settings_notifier_XBMC_enabled = 0 - settings_notifier_XBMC_url = request.forms.get('settings_notifier_XBMC_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'XBMC'", (settings_notifier_XBMC_enabled, settings_notifier_XBMC_url)) - - settings_notifier_Discord_enabled = request.forms.get('settings_notifier_Discord_enabled') - if settings_notifier_Discord_enabled == 'on': - settings_notifier_Discord_enabled = 1 - else: - settings_notifier_Discord_enabled = 0 - settings_notifier_Discord_url = request.forms.get('settings_notifier_Discord_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Discord'", (settings_notifier_Discord_enabled, settings_notifier_Discord_url)) - - settings_notifier_E_Mail_enabled = request.forms.get('settings_notifier_E-Mail_enabled') - if settings_notifier_E_Mail_enabled == 'on': - settings_notifier_E_Mail_enabled = 1 - else: - settings_notifier_E_Mail_enabled = 0 - settings_notifier_E_Mail_url = request.forms.get('settings_notifier_E-Mail_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'E-Mail'", (settings_notifier_E_Mail_enabled, settings_notifier_E_Mail_url)) - - settings_notifier_Emby_enabled = request.forms.get('settings_notifier_Emby_enabled') - if settings_notifier_Emby_enabled == 'on': - settings_notifier_Emby_enabled = 1 - else: - settings_notifier_Emby_enabled = 0 - settings_notifier_Emby_url = request.forms.get('settings_notifier_Emby_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Emby'", (settings_notifier_Emby_enabled, settings_notifier_Emby_url)) - - settings_notifier_IFTTT_enabled = request.forms.get('settings_notifier_IFTTT_enabled') - if settings_notifier_IFTTT_enabled == 'on': - settings_notifier_IFTTT_enabled = 1 - else: - settings_notifier_IFTTT_enabled = 0 - settings_notifier_IFTTT_url = request.forms.get('settings_notifier_IFTTT_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'IFTTT'", (settings_notifier_IFTTT_enabled, settings_notifier_IFTTT_url)) - - settings_notifier_Stride_enabled = request.forms.get('settings_notifier_Stride_enabled') - if settings_notifier_Stride_enabled == 'on': - settings_notifier_Stride_enabled = 1 - else: - settings_notifier_Stride_enabled = 0 - settings_notifier_Stride_url = request.forms.get('settings_notifier_Stride_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Stride'", (settings_notifier_Stride_enabled, settings_notifier_Stride_url)) - - settings_notifier_Windows_enabled = request.forms.get('settings_notifier_Windows_enabled') - if settings_notifier_Windows_enabled == 'on': - settings_notifier_Windows_enabled = 1 - else: - settings_notifier_Windows_enabled = 0 - settings_notifier_Windows_url = request.forms.get('settings_notifier_Windows_url') - c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Windows'", (settings_notifier_Windows_enabled, settings_notifier_Windows_url)) - - conn.commit() - c.close() - - sonarr_full_update() - radarr_full_update() - - logging.info('Settings saved succesfully.') - - # reschedule full update task according to settings - sonarr_full_update() - - redirect(ref) - -@route(base_url + 'check_update') -@custom_auth_basic(check_credentials) -def check_update(): - ref = request.environ['HTTP_REFERER'] - - check_and_apply_update() - - redirect(ref) - -@route(base_url + 'system') -@custom_auth_basic(check_credentials) -def system(): - def get_time_from_interval(interval): - interval_clean = interval.split('[') - interval_clean = interval_clean[1][:-1] - interval_split = interval_clean.split(':') - - hour = interval_split[0] - minute = interval_split[1].lstrip("0") - second = interval_split[2].lstrip("0") - - text = "every " - if hour != "0": - text = text + hour - if hour == "1": - text = text + " hour" - else: - text = text + " hours" - - if minute != "" and second != "": - text = text + ", " - elif minute == "" and second != "": - text = text + " and " - elif minute != "" and second == "": - text = text + " and " - if minute != "": - text = text + minute - if minute == "1": - text = text + " minute" - else: - text = text + " minutes" - - if second != "": - text = text + " and " - if second != "": - text = text + second - if second == "1": - text = text + " second" - else: - text = text + " seconds" - - return text - - def get_time_from_cron(cron): - text = "at " - hour = str(cron[5]) - minute = str(cron[6]) - second = str(cron[7]) - - if hour != "0" and hour != "*": - text = text + hour - if hour == "0" or hour == "1": - text = text + " hour" - else: - text = text + " hours" - - if minute != "*" and second != "0": - text = text + ", " - elif minute == "*" and second != "0": - text = text + " and " - elif minute != "0" and minute != "*" and second == "0": - text = text + " and " - if minute != "0" and minute != "*": - text = text + minute - if minute == "0" or minute == "1": - text = text + " minute" - else: - text = text + " minutes" - - if second != "0" and second != "*": - text = text + " and " - if second != "0" and second != "*": - text = text + second - if second == "0" or second == "1": - text = text + " second" - else: - text = text + " seconds" - - return text - - - task_list = [] - for job in scheduler.get_jobs(): - if job.next_run_time is not None: - next_run = pretty.date(job.next_run_time.replace(tzinfo=None)) - else: - next_run = "Never" - - if job.trigger.__str__().startswith('interval'): - task_list.append([job.name, get_time_from_interval(str(job.trigger)), next_run, job.id]) - elif job.trigger.__str__().startswith('cron'): - task_list.append([job.name, get_time_from_cron(job.trigger.fields), next_run, job.id]) - - i = 0 - with open(os.path.join(config_dir, 'log/bazarr.log')) as f: - for i, l in enumerate(f, 1): - pass - row_count = i - page_size = int(get_general_settings()[21]) - max_page = int(math.ceil(row_count / (page_size + 0.0))) - - releases = [] - url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases' - try: - r = requests.get(url_releases, timeout=15) - r.raise_for_status() - except requests.exceptions.HTTPError as errh: - logging.exception("Error trying to get releases from Github. Http error.") - except requests.exceptions.ConnectionError as errc: - logging.exception("Error trying to get releases from Github. Connection Error.") - except requests.exceptions.Timeout as errt: - logging.exception("Error trying to get releases from Github. Timeout Error.") - except requests.exceptions.RequestException as err: - logging.exception("Error trying to get releases from Github.") - else: - for release in r.json(): - releases.append([release['name'],release['body']]) - - return template('system', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url, task_list=task_list, row_count=row_count, max_page=max_page, page_size=page_size, releases=releases) - -@route(base_url + 'logs/') -@custom_auth_basic(check_credentials) -def get_logs(page): - page_size = int(get_general_settings()[21]) - begin = (page * page_size) - page_size - end = (page * page_size) - 1 - logs_complete = [] - for line in reversed(open(os.path.join(config_dir, 'log/bazarr.log')).readlines()): - logs_complete.append(line.rstrip()) - logs = logs_complete[begin:end] - - return template('logs', logs=logs, base_url=base_url) - -@route(base_url + 'execute/') -@custom_auth_basic(check_credentials) -def execute_task(taskid): - ref = request.environ['HTTP_REFERER'] - - execute_now(taskid) - - redirect(ref) - - -@route(base_url + 'remove_subtitles', method='POST') -@custom_auth_basic(check_credentials) -def remove_subtitles(): - episodePath = request.forms.get('episodePath') - language = request.forms.get('language') - subtitlesPath = request.forms.get('subtitlesPath') - sonarrSeriesId = request.forms.get('sonarrSeriesId') - sonarrEpisodeId = request.forms.get('sonarrEpisodeId') - - try: - os.remove(subtitlesPath) - result = language_from_alpha3(language) + " subtitles deleted from disk." - history_log(0, sonarrSeriesId, sonarrEpisodeId, result) - except OSError: - pass - store_subtitles(unicode(episodePath)) - list_missing_subtitles(sonarrSeriesId) - - -@route(base_url + 'remove_subtitles_movie', method='POST') -@custom_auth_basic(check_credentials) -def remove_subtitles_movie(): - moviePath = request.forms.get('moviePath') - language = request.forms.get('language') - subtitlesPath = request.forms.get('subtitlesPath') - radarrId = request.forms.get('radarrId') - - try: - os.remove(subtitlesPath) - result = language_from_alpha3(language) + " subtitles deleted from disk." - history_log_movie(0, radarrId, result) - except OSError: - pass - store_subtitles_movie(unicode(moviePath)) - list_missing_subtitles_movies(radarrId) - - -@route(base_url + 'get_subtitle', method='POST') -@custom_auth_basic(check_credentials) -def get_subtitle(): - ref = request.environ['HTTP_REFERER'] - - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') - language = request.forms.get('language') - hi = request.forms.get('hi') - sonarrSeriesId = request.forms.get('sonarrSeriesId') - sonarrEpisodeId = request.forms.get('sonarrEpisodeId') - # tvdbid = request.forms.get('tvdbid') - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") - enabled_providers = c.fetchall() - c.close() - - providers_list = [] - providers_auth = {} - if len(enabled_providers) > 0: - for provider in enabled_providers: - providers_list.append(provider[0]) - try: - if provider[2] is not '' and provider[3] is not '': - provider_auth = providers_auth.append(provider[0]) - provider_auth.update({'username':providers[2], 'password':providers[3]}) - else: - providers_auth = None - except: - providers_auth = None - else: - providers_list = None - providers_auth = None - - try: - result = download_subtitle(episodePath, language, hi, providers_list, providers_auth, sceneName, 'series') - if result is not None: - history_log(1, sonarrSeriesId, sonarrEpisodeId, result) - send_notifications(sonarrSeriesId, sonarrEpisodeId, result) - store_subtitles(unicode(episodePath)) - list_missing_subtitles(sonarrSeriesId) - redirect(ref) - except OSError: - pass - -@route(base_url + 'manual_search', method='POST') -@custom_auth_basic(check_credentials) -def manual_search_json(): - ref = request.environ['HTTP_REFERER'] - - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') - language = request.forms.get('language') - hi = request.forms.get('hi') - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") - enabled_providers = c.fetchall() - c.close() - - providers_list = [] - providers_auth = {} - if len(enabled_providers) > 0: - for provider in enabled_providers: - providers_list.append(provider[0]) - try: - if provider[2] is not '' and provider[3] is not '': - provider_auth = providers_auth.append(provider[0]) - provider_auth.update({'username':providers[2], 'password':providers[3]}) - else: - providers_auth = None - except: - providers_auth = None - else: - providers_list = None - providers_auth = None - - data = manual_search(episodePath, language, hi, providers_list, providers_auth, sceneName, 'series') - return dict(data=data) - -@route(base_url + 'manual_get_subtitle', method='POST') -@custom_auth_basic(check_credentials) -def manual_get_subtitle(): - ref = request.environ['HTTP_REFERER'] - - episodePath = request.forms.get('episodePath') - sceneName = request.forms.get('sceneName') - language = request.forms.get('language') - hi = request.forms.get('hi') - selected_provider = request.forms.get('provider') - subtitle = request.forms.get('subtitle') - sonarrSeriesId = request.forms.get('sonarrSeriesId') - sonarrEpisodeId = request.forms.get('sonarrEpisodeId') - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - provider = c.execute("SELECT * FROM table_settings_providers WHERE name = ?",(selected_provider,)).fetchone() - c.close() - providers_auth = {} - try: - if provider[2] is not '' and provider[3] is not '': - provider_auth = providers_auth.append(provider[0]) - provider_auth.update({'username':providers[2], 'password':providers[3]}) - else: - providers_auth = None - except: - providers_auth = None - - try: - result = manual_download_subtitle(episodePath, language, hi, subtitle, selected_provider, providers_auth, sceneName, 'series') - if result is not None: - history_log(1, sonarrSeriesId, sonarrEpisodeId, result) - send_notifications(sonarrSeriesId, sonarrEpisodeId, result) - store_subtitles(unicode(episodePath)) - list_missing_subtitles(sonarrSeriesId) - redirect(ref) - except OSError: - pass - -@route(base_url + 'get_subtitle_movie', method='POST') -@custom_auth_basic(check_credentials) -def get_subtitle_movie(): - ref = request.environ['HTTP_REFERER'] - - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') - language = request.forms.get('language') - hi = request.forms.get('hi') - radarrId = request.forms.get('radarrId') - # tmdbid = request.forms.get('tmdbid') - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") - enabled_providers = c.fetchall() - c.close() - - providers_list = [] - providers_auth = {} - if len(enabled_providers) > 0: - for provider in enabled_providers: - providers_list.append(provider[0]) - try: - if provider[2] is not '' and provider[3] is not '': - provider_auth = providers_auth.append(provider[0]) - provider_auth.update({'username':providers[2], 'password':providers[3]}) - else: - providers_auth = None - except: - providers_auth = None - else: - providers_list = None - providers_auth = None - - try: - result = download_subtitle(moviePath, language, hi, providers_list, providers_auth, sceneName, 'movie') - if result is not None: - history_log_movie(1, radarrId, result) - send_notifications_movie(radarrId, result) - store_subtitles_movie(unicode(moviePath)) - list_missing_subtitles_movies(radarrId) - redirect(ref) - except OSError: - pass - -@route(base_url + 'manual_search_movie', method='POST') -@custom_auth_basic(check_credentials) -def manual_search_movie_json(): - ref = request.environ['HTTP_REFERER'] - - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') - language = request.forms.get('language') - hi = request.forms.get('hi') - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") - enabled_providers = c.fetchall() - c.close() - - providers_list = [] - providers_auth = {} - if len(enabled_providers) > 0: - for provider in enabled_providers: - providers_list.append(provider[0]) - try: - if provider[2] is not '' and provider[3] is not '': - provider_auth = providers_auth.append(provider[0]) - provider_auth.update({'username':providers[2], 'password':providers[3]}) - else: - providers_auth = None - except: - providers_auth = None - else: - providers_list = None - providers_auth = None - - data = manual_search(moviePath, language, hi, providers_list, providers_auth, sceneName, 'movie') - return dict(data=data) - -@route(base_url + 'manual_get_subtitle_movie', method='POST') -@custom_auth_basic(check_credentials) -def manual_get_subtitle_movie(): - ref = request.environ['HTTP_REFERER'] - - moviePath = request.forms.get('moviePath') - sceneName = request.forms.get('sceneName') - language = request.forms.get('language') - hi = request.forms.get('hi') - selected_provider = request.forms.get('provider') - subtitle = request.forms.get('subtitle') - radarrId = request.forms.get('radarrId') - - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - provider = c.execute("SELECT * FROM table_settings_providers WHERE name = ?",(selected_provider,)).fetchone() - c.close() - providers_auth = {} - try: - if provider[2] is not '' and provider[3] is not '': - provider_auth = providers_auth.append(provider[0]) - provider_auth.update({'username':providers[2], 'password':providers[3]}) - else: - providers_auth = None - except: - providers_auth = None - - try: - result = manual_download_subtitle(moviePath, language, hi, subtitle, selected_provider, providers_auth, sceneName, 'movie') - if result is not None: - history_log_movie(1, radarrId, result) - send_notifications_movie(radarrId, result) - store_subtitles_movie(unicode(moviePath)) - list_missing_subtitles_movies(radarrId) - redirect(ref) - except OSError: - pass - -def configured(): - conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = conn.cursor() - c.execute("UPDATE system SET configured = 1") - conn.commit() - c.close() - -@route(base_url + 'api/wanted') -def api_wanted(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - data = c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]' ORDER BY table_episodes._rowid_ DESC").fetchall() - c.close() - return dict(subtitles=data) - -@route(base_url + 'api/history') -def api_history(): - db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) - c = db.cursor() - data = c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, strftime('%Y-%m-%d', datetime(table_history.timestamp, 'unixepoch')), table_history.description FROM table_history INNER JOIN table_shows on table_shows.sonarrSeriesId = table_history.sonarrSeriesId INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = table_history.sonarrEpisodeId WHERE table_history.action = '1' ORDER BY id DESC").fetchall() - c.close() - return dict(subtitles=data) - -@route(base_url + 'test_url/', method='GET') -@custom_auth_basic(check_credentials) -def test_url(url): - try: - result = requests.get(url).json()['version'] - except: - return dict(status=False, version=result) - else: - return dict(status=True, version=result) - - -logging.info('Bazarr is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url)) -run(host=ip, port=port, server='waitress') -logging.info('Bazarr has been stopped.') +bazarr_version = '0.6.2' + +import gc +gc.enable() + +from get_argv import config_dir + +import os +import sys +reload(sys) +sys.setdefaultencoding('utf8') +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'libs/')) + +import sqlite3 +from update_modules import * +from init import * +from update_db import * + + +from get_settings import get_general_settings +import logging +from logging.handlers import TimedRotatingFileHandler + +logger = logging.getLogger('waitress') +log_level = get_general_settings()[4] +if log_level is None: + log_level = "INFO" + +class OneLineExceptionFormatter(logging.Formatter): + def formatException(self, exc_info): + """ + Format an exception so that it prints on a single line. + """ + result = super(OneLineExceptionFormatter, self).formatException(exc_info) + return repr(result) # or format into one line however you want to + + def format(self, record): + s = super(OneLineExceptionFormatter, self).format(record) + if record.exc_text: + s = s.replace('\n', '') + '|' + return s + +def configure_logging(): + global fh + fh = TimedRotatingFileHandler(os.path.join(config_dir, 'log/bazarr.log'), when="midnight", interval=1, backupCount=7) + f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|', + '%d/%m/%Y %H:%M:%S') + fh.setFormatter(f) + logging.getLogger("enzyme").setLevel(logging.CRITICAL) + logging.getLogger("apscheduler").setLevel(logging.WARNING) + logging.getLogger("subliminal").setLevel(logging.CRITICAL) + logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL) + root = logging.getLogger() + root.setLevel(log_level) + root.addHandler(fh) + +configure_logging() + +from bottle import route, run, template, static_file, request, redirect, response, HTTPError, app +import bottle +bottle.TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(__file__), 'views/')) +bottle.debug(True) +bottle.TEMPLATES.clear() + +from beaker.middleware import SessionMiddleware +from cork import Cork +from json import dumps +import itertools +import operator +import requests +import pretty +from datetime import datetime, timedelta +from io import BytesIO +import math +import ast +import hashlib +import time + +from get_languages import load_language_in_db, language_from_alpha3 +from get_providers import * + +from get_series import * +from get_episodes import * +from get_settings import base_url, ip, port, path_replace, path_replace_movie +from check_update import check_and_apply_update +from list_subtitles import store_subtitles, store_subtitles_movie, series_scan_subtitles, movies_scan_subtitles, list_missing_subtitles, list_missing_subtitles_movies +from get_subtitle import download_subtitle, series_download_subtitles, movies_download_subtitles, wanted_download_subtitles, wanted_search_missing_subtitles, manual_search, manual_download_subtitle +from utils import history_log, history_log_movie +from scheduler import * +from notifier import send_notifications, send_notifications_movie + +# Reset restart required warning on start +conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) +c = conn.cursor() +c.execute("UPDATE system SET configured = 0, updated = 0") +conn.commit() +c.close() + +# Load languages in database +load_language_in_db() + +from get_settings import get_auth_settings + +aaa = Cork(os.path.normpath(os.path.join(config_dir, 'config'))) + +app = app() +session_opts = { + 'session.cookie_expires': True, + 'session.key': 'Bazarr', + 'session.httponly': True, + 'session.timeout': 3600 * 24, # 1 day + 'session.type': 'cookie', + 'session.validate_key': True +} +app = SessionMiddleware(app, session_opts) +login_auth = get_auth_settings()[0] + + +def custom_auth_basic(check): + def decorator(func): + def wrapper(*a, **ka): + if get_auth_settings()[0] == 'basic': + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, "Access denied") + err.add_header('WWW-Authenticate', 'Basic realm="Bazarr"') + return err + return func(*a, **ka) + else: + return func(*a, **ka) + + return wrapper + return decorator + +def check_credentials(user, pw): + from get_settings import get_auth_settings + + username = get_auth_settings()[1] + password = get_auth_settings()[2] + if hashlib.md5(pw).hexdigest() == password and user == username: + return True + return False + + +def authorize(): + if login_auth == 'form': + aaa = Cork(os.path.normpath(os.path.join(config_dir, 'config'))) + aaa.require(fail_redirect=(base_url + 'login')) + + +def post_get(name, default=''): + return request.POST.get(name, default).strip() + + +@route(base_url + 'login') +def login_form(): + msg = bottle.request.query.get('msg', '') + return template('login', base_url=base_url, msg=msg) + + +@route(base_url + 'login', method='POST') +def login(): + aaa = Cork(os.path.normpath(os.path.join(config_dir, 'config'))) + username = post_get('username') + password = post_get('password') + aaa.login(username, password, success_redirect=base_url, fail_redirect=(base_url + 'login?msg=fail')) + + +@route(base_url + 'logout') +def logout(): + aaa.logout(success_redirect=(base_url + 'login')) + + +@route('/') +@custom_auth_basic(check_credentials) +def redirect_root(): + authorize() + redirect (base_url) + +@route(base_url + 'static/:path#.+#', name='static') +@custom_auth_basic(check_credentials) +def static(path): + return static_file(path, root=os.path.join(os.path.dirname(__file__), 'static')) + +@route(base_url + 'emptylog') +@custom_auth_basic(check_credentials) +def emptylog(): + authorize() + ref = request.environ['HTTP_REFERER'] + + fh.doRollover() + logging.info('Log file emptied') + + redirect(ref) + +@route(base_url + 'bazarr.log') +@custom_auth_basic(check_credentials) +def download_log(): + authorize() + return static_file('bazarr.log', root=os.path.join(config_dir, 'log/'), download='bazarr.log') + +@route(base_url + 'image_proxy/', method='GET') +@custom_auth_basic(check_credentials) +def image_proxy(url): + authorize() + url_sonarr = get_sonarr_settings()[6] + url_sonarr_short = get_sonarr_settings()[7] + apikey = get_sonarr_settings()[4] + url_image = url_sonarr_short + '/' + url + '?apikey=' + apikey + try: + image_buffer = BytesIO(requests.get(url_sonarr + '/api' + url_image.split(url_sonarr)[1], timeout=15, verify=False).content) + except: + return None + else: + image_buffer.seek(0) + bytes = image_buffer.read() + response.set_header('Content-type', 'image/jpeg') + return bytes + +@route(base_url + 'image_proxy_movies/', method='GET') +@custom_auth_basic(check_credentials) +def image_proxy_movies(url): + authorize() + url_radarr = get_radarr_settings()[6] + url_radarr_short = get_radarr_settings()[7] + apikey = get_radarr_settings()[4] + try: + url_image = (url_radarr_short + '/' + url + '?apikey=' + apikey).replace('/fanart.jpg', '/banner.jpg') + image_buffer = BytesIO(requests.get(url_radarr + '/api' + url_image.split(url_radarr)[1], timeout=15, verify=False).content) + except: + url_image = url_radarr_short + '/' + url + '?apikey=' + apikey + image_buffer = BytesIO(requests.get(url_radarr + '/api' + url_image.split(url_radarr)[1], timeout=15, verify=False).content) + else: + image_buffer.seek(0) + bytes = image_buffer.read() + response.set_header('Content-type', 'image/jpeg') + return bytes + + +@route(base_url) +@custom_auth_basic(check_credentials) +def redirect_root(): + authorize() + if get_general_settings()[12] is True: + redirect(base_url + 'series') + elif get_general_settings()[13] is True: + redirect(base_url + 'movies') + else: + redirect(base_url + 'settings') + + +@route(base_url + 'series') +@custom_auth_basic(check_credentials) +def series(): + authorize() + single_language = get_general_settings()[7] + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + db.create_function("path_substitution", 1, path_replace) + c = db.cursor() + + c.execute("SELECT COUNT(*) FROM table_shows") + missing_count = c.fetchone() + missing_count = missing_count[0] + page = request.GET.page + if page == "": + page = "1" + page_size = int(get_general_settings()[21]) + offset = (int(page) - 1) * page_size + max_page = int(math.ceil(missing_count / (page_size + 0.0))) + + c.execute("SELECT tvdbId, title, path_substitution(path), languages, hearing_impaired, sonarrSeriesId, poster, audio_language FROM table_shows ORDER BY sortTitle ASC LIMIT ? OFFSET ?", (page_size, offset,)) + data = c.fetchall() + c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1") + languages = c.fetchall() + c.execute("SELECT table_shows.sonarrSeriesId, COUNT(table_episodes.missing_subtitles) FROM table_shows LEFT JOIN table_episodes ON table_shows.sonarrSeriesId=table_episodes.sonarrSeriesId WHERE table_shows.languages IS NOT 'None' AND table_episodes.missing_subtitles IS NOT '[]' GROUP BY table_shows.sonarrSeriesId") + missing_subtitles_list = c.fetchall() + c.execute("SELECT table_shows.sonarrSeriesId, COUNT(table_episodes.missing_subtitles) FROM table_shows LEFT JOIN table_episodes ON table_shows.sonarrSeriesId=table_episodes.sonarrSeriesId WHERE table_shows.languages IS NOT 'None' GROUP BY table_shows.sonarrSeriesId") + total_subtitles_list = c.fetchall() + c.close() + output = template('series', __file__=__file__, bazarr_version=bazarr_version, rows=data, missing_subtitles_list=missing_subtitles_list, total_subtitles_list=total_subtitles_list, languages=languages, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, single_language=single_language, page_size=page_size) + return output + +@route(base_url + 'serieseditor') +@custom_auth_basic(check_credentials) +def serieseditor(): + authorize() + single_language = get_general_settings()[7] + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + db.create_function("path_substitution", 1, path_replace) + c = db.cursor() + + c.execute("SELECT COUNT(*) FROM table_shows") + missing_count = c.fetchone() + missing_count = missing_count[0] + + c.execute("SELECT tvdbId, title, path_substitution(path), languages, hearing_impaired, sonarrSeriesId, poster, audio_language FROM table_shows ORDER BY title ASC") + data = c.fetchall() + 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, missing_count=missing_count, base_url=base_url, single_language=single_language) + return output + +@route(base_url + 'search_json/', method='GET') +@custom_auth_basic(check_credentials) +def search_json(query): + authorize() + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + + c.execute("SELECT title, sonarrSeriesId FROM table_shows WHERE title LIKE ? ORDER BY title", ('%'+query+'%',)) + series = c.fetchall() + + c.execute("SELECT title, radarrId FROM table_movies WHERE title LIKE ? ORDER BY title", ('%' + query + '%',)) + movies = c.fetchall() + + search_list = [] + for serie in series: + search_list.append(dict([('name', serie[0]), ('url', base_url + 'episodes/' + str(serie[1]))])) + + for movie in movies: + search_list.append(dict([('name', movie[0]), ('url', base_url + 'movie/' + str(movie[1]))])) + + response.content_type = 'application/json' + return dict(items=search_list) + + +@route(base_url + 'edit_series/', method='POST') +@custom_auth_basic(check_credentials) +def edit_series(no): + authorize() + ref = request.environ['HTTP_REFERER'] + + lang = request.forms.getall('languages') + if len(lang) > 0: + pass + else: + lang = 'None' + + single_language = get_general_settings()[7] + if single_language is True: + if str(lang) == "['None']": + lang = 'None' + else: + lang = str(lang) + else: + if str(lang) == "['']": + lang = '[]' + + hi = request.forms.get('hearing_impaired') + + if hi == "on": + hi = "True" + else: + hi = "False" + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = conn.cursor() + c.execute("UPDATE table_shows SET languages = ?, hearing_impaired = ? WHERE sonarrSeriesId LIKE ?", (str(lang), hi, no)) + conn.commit() + c.close() + + list_missing_subtitles(no) + + redirect(ref) + +@route(base_url + 'edit_serieseditor', method='POST') +@custom_auth_basic(check_credentials) +def edit_serieseditor(): + authorize() + ref = request.environ['HTTP_REFERER'] + + series = request.forms.get('series') + series = ast.literal_eval(str('[' + series + ']')) + lang = request.forms.getall('languages') + hi = request.forms.get('hearing_impaired') + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = conn.cursor() + + for serie in series: + if str(lang) != "[]" and str(lang) != "['']": + if str(lang) == "['None']": + lang = 'None' + else: + lang = str(lang) + c.execute("UPDATE table_shows SET languages = ? WHERE sonarrSeriesId LIKE ?", (lang, serie)) + if hi != '': + c.execute("UPDATE table_shows SET hearing_impaired = ? WHERE sonarrSeriesId LIKE ?", (hi, serie)) + + conn.commit() + c.close() + + for serie in series: + list_missing_subtitles(serie) + + redirect(ref) + +@route(base_url + 'episodes/', method='GET') +@custom_auth_basic(check_credentials) +def episodes(no): + authorize() + # single_language = get_general_settings()[7] + url_sonarr_short = get_sonarr_settings()[7] + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + conn.create_function("path_substitution", 1, path_replace) + c = conn.cursor() + + series_details = [] + series_details = c.execute("SELECT title, overview, poster, fanart, hearing_impaired, tvdbid, audio_language, languages, path_substitution(path) FROM table_shows WHERE sonarrSeriesId LIKE ?", (str(no),)).fetchone() + tvdbid = series_details[5] + + episodes = c.execute("SELECT title, path_substitution(path), season, episode, subtitles, sonarrSeriesId, missing_subtitles, sonarrEpisodeId, scene_name, monitored FROM table_episodes WHERE sonarrSeriesId LIKE ? ORDER BY episode ASC", (str(no),)).fetchall() + number = len(episodes) + languages = c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1").fetchall() + c.close() + episodes = reversed(sorted(episodes, key=operator.itemgetter(2))) + seasons_list = [] + 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, languages=languages, seasons=seasons_list, url_sonarr_short=url_sonarr_short, base_url=base_url, tvdbid=tvdbid, number=number) + +@route(base_url + 'movies') +@custom_auth_basic(check_credentials) +def movies(): + authorize() + single_language = get_general_settings()[7] + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + db.create_function("path_substitution", 1, path_replace_movie) + c = db.cursor() + + c.execute("SELECT COUNT(*) FROM table_movies") + missing_count = c.fetchone() + missing_count = missing_count[0] + page = request.GET.page + if page == "": + page = "1" + page_size = int(get_general_settings()[21]) + offset = (int(page) - 1) * page_size + max_page = int(math.ceil(missing_count / (page_size + 0.0))) + + c.execute("SELECT tmdbId, title, path_substitution(path), languages, hearing_impaired, radarrId, poster, audio_language, monitored FROM table_movies ORDER BY title ASC LIMIT ? OFFSET ?", (page_size, offset,)) + data = c.fetchall() + 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, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, single_language=single_language, page_size=page_size) + return output + +@route(base_url + 'movieseditor') +@custom_auth_basic(check_credentials) +def movieseditor(): + authorize() + single_language = get_general_settings()[7] + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + db.create_function("path_substitution", 1, path_replace_movie) + c = db.cursor() + + c.execute("SELECT COUNT(*) FROM table_movies") + missing_count = c.fetchone() + missing_count = missing_count[0] + + c.execute("SELECT tmdbId, title, path_substitution(path), languages, hearing_impaired, radarrId, poster, audio_language FROM table_movies ORDER BY title ASC") + data = c.fetchall() + 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, missing_count=missing_count, base_url=base_url, single_language=single_language) + return output + +@route(base_url + 'edit_movieseditor', method='POST') +@custom_auth_basic(check_credentials) +def edit_movieseditor(): + authorize() + ref = request.environ['HTTP_REFERER'] + + movies = request.forms.get('movies') + movies = ast.literal_eval(str('[' + movies + ']')) + lang = request.forms.getall('languages') + hi = request.forms.get('hearing_impaired') + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = conn.cursor() + + for movie in movies: + if str(lang) != "[]" and str(lang) != "['']": + if str(lang) == "['None']": + lang = 'None' + else: + lang = str(lang) + c.execute("UPDATE table_movies SET languages = ? WHERE radarrId LIKE ?", (lang, movie)) + if hi != '': + c.execute("UPDATE table_movies SET hearing_impaired = ? WHERE radarrId LIKE ?", (hi, movie)) + + conn.commit() + c.close() + + for movie in movies: + list_missing_subtitles_movies(movie) + + redirect(ref) + +@route(base_url + 'edit_movie/', method='POST') +@custom_auth_basic(check_credentials) +def edit_movie(no): + authorize() + ref = request.environ['HTTP_REFERER'] + + lang = request.forms.getall('languages') + if len(lang) > 0: + pass + else: + lang = 'None' + + if str(lang) == "['']": + lang = '[]' + + hi = request.forms.get('hearing_impaired') + + if hi == "on": + hi = "True" + else: + hi = "False" + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = conn.cursor() + c.execute("UPDATE table_movies SET languages = ?, hearing_impaired = ? WHERE radarrId LIKE ?", (str(lang), hi, no)) + conn.commit() + c.close() + + list_missing_subtitles_movies(no) + + redirect(ref) + +@route(base_url + 'movie/', method='GET') +@custom_auth_basic(check_credentials) +def movie(no): + authorize() + # single_language = get_general_settings()[7] + url_radarr_short = get_radarr_settings()[7] + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + conn.create_function("path_substitution", 1, path_replace_movie) + c = conn.cursor() + + movies_details = [] + movies_details = c.execute("SELECT title, overview, poster, fanart, hearing_impaired, tmdbid, audio_language, languages, path_substitution(path), subtitles, radarrId, missing_subtitles, sceneName, monitored FROM table_movies WHERE radarrId LIKE ?", (str(no),)).fetchone() + tmdbid = movies_details[5] + + 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, languages=languages, url_radarr_short=url_radarr_short, base_url=base_url, tmdbid=tmdbid) + +@route(base_url + 'scan_disk/', method='GET') +@custom_auth_basic(check_credentials) +def scan_disk(no): + authorize() + ref = request.environ['HTTP_REFERER'] + + series_scan_subtitles(no) + + redirect(ref) + +@route(base_url + 'scan_disk_movie/', method='GET') +@custom_auth_basic(check_credentials) +def scan_disk_movie(no): + authorize() + ref = request.environ['HTTP_REFERER'] + + movies_scan_subtitles(no) + + redirect(ref) + +@route(base_url + 'search_missing_subtitles/', method='GET') +@custom_auth_basic(check_credentials) +def search_missing_subtitles(no): + authorize() + ref = request.environ['HTTP_REFERER'] + + series_download_subtitles(no) + + redirect(ref) + +@route(base_url + 'search_missing_subtitles_movie/', method='GET') +@custom_auth_basic(check_credentials) +def search_missing_subtitles_movie(no): + authorize() + ref = request.environ['HTTP_REFERER'] + + movies_download_subtitles(no) + + redirect(ref) + +@route(base_url + 'history') +@custom_auth_basic(check_credentials) +def history(): + authorize() + return template('history', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url) + +@route(base_url + 'historyseries') +@custom_auth_basic(check_credentials) +def historyseries(): + authorize() + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + + c.execute("SELECT COUNT(*) FROM table_history") + row_count = c.fetchone() + row_count = row_count[0] + page = request.GET.page + if page == "": + page = "1" + page_size = int(get_general_settings()[21]) + offset = (int(page) - 1) * page_size + max_page = int(math.ceil(row_count / (page_size + 0.0))) + + now = datetime.now() + today = [] + thisweek = [] + thisyear = [] + stats = c.execute("SELECT timestamp FROM table_history WHERE action LIKE '1'").fetchall() + total = len(stats) + for stat in stats: + if now - timedelta(hours=24) <= datetime.fromtimestamp(stat[0]) <= now: + today.append(datetime.fromtimestamp(stat[0]).date()) + if now - timedelta(weeks=1) <= datetime.fromtimestamp(stat[0]) <= now: + thisweek.append(datetime.fromtimestamp(stat[0]).date()) + if now - timedelta(weeks=52) <= datetime.fromtimestamp(stat[0]) <= now: + thisyear.append(datetime.fromtimestamp(stat[0]).date()) + stats = [len(today), len(thisweek), len(thisyear), total] + + c.execute("SELECT table_history.action, table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_history.timestamp, table_history.description, table_history.sonarrSeriesId FROM table_history LEFT JOIN table_shows on table_shows.sonarrSeriesId = table_history.sonarrSeriesId LEFT JOIN table_episodes on table_episodes.sonarrEpisodeId = table_history.sonarrEpisodeId ORDER BY id DESC LIMIT ? OFFSET ?", (page_size, offset,)) + 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, page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size) + +@route(base_url + 'historymovies') +@custom_auth_basic(check_credentials) +def historymovies(): + authorize() + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + + c.execute("SELECT COUNT(*) FROM table_history_movie") + row_count = c.fetchone() + row_count = row_count[0] + page = request.GET.page + if page == "": + page = "1" + page_size = int(get_general_settings()[21]) + offset = (int(page) - 1) * page_size + max_page = int(math.ceil(row_count / (page_size + 0.0))) + + now = datetime.now() + today = [] + thisweek = [] + thisyear = [] + stats = c.execute("SELECT timestamp FROM table_history_movie WHERE action LIKE '1'").fetchall() + total = len(stats) + for stat in stats: + if now - timedelta(hours=24) <= datetime.fromtimestamp(stat[0]) <= now: + today.append(datetime.fromtimestamp(stat[0]).date()) + if now - timedelta(weeks=1) <= datetime.fromtimestamp(stat[0]) <= now: + thisweek.append(datetime.fromtimestamp(stat[0]).date()) + if now - timedelta(weeks=52) <= datetime.fromtimestamp(stat[0]) <= now: + thisyear.append(datetime.fromtimestamp(stat[0]).date()) + stats = [len(today), len(thisweek), len(thisyear), total] + + c.execute("SELECT table_history_movie.action, table_movies.title, table_history_movie.timestamp, table_history_movie.description, table_history_movie.radarrId FROM table_history_movie LEFT JOIN table_movies on table_movies.radarrId = table_history_movie.radarrId ORDER BY id DESC LIMIT ? OFFSET ?", (page_size, offset,)) + 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, page=page, max_page=max_page, stats=stats, base_url=base_url, page_size=page_size) + +@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) + +@route(base_url + 'wantedseries') +@custom_auth_basic(check_credentials) +def wantedseries(): + authorize() + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + db.create_function("path_substitution", 1, path_replace) + c = db.cursor() + + if get_general_settings()[24] is True: + monitored_only_query_string = ' AND monitored = "True"' + else: + monitored_only_query_string = "" + + c.execute("SELECT COUNT(*) FROM table_episodes WHERE missing_subtitles != '[]'" + monitored_only_query_string) + missing_count = c.fetchone() + missing_count = missing_count[0] + page = request.GET.page + if page == "": + page = "1" + page_size = int(get_general_settings()[21]) + offset = (int(page) - 1) * page_size + max_page = int(math.ceil(missing_count / (page_size + 0.0))) + + c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles, table_episodes.sonarrSeriesId, path_substitution(table_episodes.path), table_shows.hearing_impaired, table_episodes.sonarrEpisodeId, table_episodes.scene_name FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY table_episodes._rowid_ DESC LIMIT ? OFFSET ?", (page_size, offset,)) + data = c.fetchall() + c.close() + return template('wantedseries', __file__=__file__, bazarr_version=bazarr_version, rows=data, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, page_size=page_size) + +@route(base_url + 'wantedmovies') +@custom_auth_basic(check_credentials) +def wantedmovies(): + authorize() + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + db.create_function("path_substitution", 1, path_replace_movie) + c = db.cursor() + + if get_general_settings()[24] is True: + monitored_only_query_string = ' AND monitored = "True"' + else: + monitored_only_query_string = "" + + c.execute("SELECT COUNT(*) FROM table_movies WHERE missing_subtitles != '[]'" + monitored_only_query_string) + missing_count = c.fetchone() + missing_count = missing_count[0] + page = request.GET.page + if page == "": + page = "1" + page_size = int(get_general_settings()[21]) + offset = (int(page) - 1) * page_size + max_page = int(math.ceil(missing_count / (page_size + 0.0))) + + c.execute("SELECT title, missing_subtitles, radarrId, path_substitution(path), hearing_impaired, sceneName FROM table_movies WHERE missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY _rowid_ DESC LIMIT ? OFFSET ?", (page_size, offset,)) + data = c.fetchall() + c.close() + return template('wantedmovies', __file__=__file__, bazarr_version=bazarr_version, rows=data, missing_count=missing_count, page=page, max_page=max_page, base_url=base_url, page_size=page_size) + +@route(base_url + 'wanted_search_missing_subtitles') +@custom_auth_basic(check_credentials) +def wanted_search_missing_subtitles_list(): + authorize() + ref = request.environ['HTTP_REFERER'] + + wanted_search_missing_subtitles() + + redirect(ref) + +@route(base_url + 'settings') +@custom_auth_basic(check_credentials) +def settings(): + authorize() + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + c.execute("SELECT * FROM table_settings_languages ORDER BY name") + settings_languages = c.fetchall() + c.execute("SELECT * FROM table_settings_providers ORDER BY name") + settings_providers = c.fetchall() + c.execute("SELECT * FROM table_settings_notifier ORDER BY name") + settings_notifier = c.fetchall() + c.close() + + from get_settings import get_general_settings, get_auth_settings, get_radarr_settings, get_sonarr_settings + settings_general = get_general_settings() + settings_auth = get_auth_settings() + settings_sonarr = get_sonarr_settings() + settings_radarr = get_radarr_settings() + + return template('settings', __file__=__file__, bazarr_version=bazarr_version, settings_general=settings_general, 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) + +@route(base_url + 'save_settings', method='POST') +@custom_auth_basic(check_credentials) +def save_settings(): + authorize() + ref = request.environ['HTTP_REFERER'] + + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = conn.cursor() + + settings_general_ip = request.forms.get('settings_general_ip') + settings_general_port = request.forms.get('settings_general_port') + settings_general_baseurl = request.forms.get('settings_general_baseurl') + settings_general_loglevel = request.forms.get('settings_general_loglevel') + settings_general_sourcepath = request.forms.getall('settings_general_sourcepath') + settings_general_destpath = request.forms.getall('settings_general_destpath') + settings_general_pathmapping = [] + settings_general_pathmapping.extend([list(a) for a in zip(settings_general_sourcepath, settings_general_destpath)]) + settings_general_sourcepath_movie = request.forms.getall('settings_general_sourcepath_movie') + settings_general_destpath_movie = request.forms.getall('settings_general_destpath_movie') + settings_general_pathmapping_movie = [] + settings_general_pathmapping_movie.extend([list(a) for a in zip(settings_general_sourcepath_movie, settings_general_destpath_movie)]) + settings_general_branch = request.forms.get('settings_general_branch') + settings_general_automatic = request.forms.get('settings_general_automatic') + if settings_general_automatic is None: + settings_general_automatic = 'False' + else: + settings_general_automatic = 'True' + settings_general_single_language = request.forms.get('settings_general_single_language') + if settings_general_single_language is None: + settings_general_single_language = 'False' + else: + settings_general_single_language = 'True' + settings_general_scenename = request.forms.get('settings_general_scenename') + if settings_general_scenename is None: + settings_general_scenename = 'False' + else: + settings_general_scenename = 'True' + settings_general_embedded = request.forms.get('settings_general_embedded') + if settings_general_embedded is None: + settings_general_embedded = 'False' + else: + settings_general_embedded = 'True' + settings_general_only_monitored = request.forms.get('settings_general_only_monitored') + if settings_general_only_monitored is None: + settings_general_only_monitored = 'False' + else: + settings_general_only_monitored = 'True' + settings_general_adaptive_searching = request.forms.get('settings_general_adaptive_searching') + if settings_general_adaptive_searching is None: + settings_general_adaptive_searching = 'False' + else: + settings_general_adaptive_searching = 'True' + settings_general_minimum_score = request.forms.get('settings_general_minimum_score') + settings_general_minimum_score_movies = request.forms.get('settings_general_minimum_score_movies') + settings_general_use_postprocessing = request.forms.get('settings_general_use_postprocessing') + if settings_general_use_postprocessing is None: + settings_general_use_postprocessing = 'False' + else: + settings_general_use_postprocessing = 'True' + settings_general_postprocessing_cmd = request.forms.get('settings_general_postprocessing_cmd') + settings_general_use_sonarr = request.forms.get('settings_general_use_sonarr') + if settings_general_use_sonarr is None: + settings_general_use_sonarr = 'False' + else: + settings_general_use_sonarr = 'True' + settings_general_use_radarr = request.forms.get('settings_general_use_radarr') + if settings_general_use_radarr is None: + settings_general_use_radarr = 'False' + else: + settings_general_use_radarr = 'True' + settings_page_size = request.forms.get('settings_page_size') + + settings_general = get_general_settings() + + before = (unicode(settings_general[0]), int(settings_general[1]), unicode(settings_general[2]), unicode(settings_general[4]), unicode(settings_general[3]), unicode(settings_general[12]), unicode(settings_general[13]), unicode(settings_general[14])) + after = (unicode(settings_general_ip), int(settings_general_port), unicode(settings_general_baseurl), unicode(settings_general_loglevel), unicode(settings_general_pathmapping), unicode(settings_general_use_sonarr), unicode(settings_general_use_radarr), unicode(settings_general_pathmapping_movie)) + from six import text_type + + cfg = ConfigParser() + + with open(config_file, 'r') as f: + cfg.read_file(f) + + cfg.set('general', 'ip', text_type(settings_general_ip)) + cfg.set('general', 'port', text_type(settings_general_port)) + cfg.set('general', 'base_url', text_type(settings_general_baseurl)) + cfg.set('general', 'path_mappings', text_type(settings_general_pathmapping)) + cfg.set('general', 'log_level', text_type(settings_general_loglevel)) + cfg.set('general', 'branch', text_type(settings_general_branch)) + cfg.set('general', 'auto_update', text_type(settings_general_automatic)) + cfg.set('general', 'single_language', text_type(settings_general_single_language)) + cfg.set('general', 'minimum_score', text_type(settings_general_minimum_score)) + cfg.set('general', 'use_scenename', text_type(settings_general_scenename)) + cfg.set('general', 'use_postprocessing', text_type(settings_general_use_postprocessing)) + cfg.set('general', 'postprocessing_cmd', text_type(settings_general_postprocessing_cmd)) + cfg.set('general', 'use_sonarr', text_type(settings_general_use_sonarr)) + cfg.set('general', 'use_radarr', text_type(settings_general_use_radarr)) + cfg.set('general', 'path_mappings_movie', text_type(settings_general_pathmapping_movie)) + cfg.set('general', 'page_size', text_type(settings_page_size)) + cfg.set('general', 'minimum_score_movie', text_type(settings_general_minimum_score_movies)) + cfg.set('general', 'use_embedded_subs', text_type(settings_general_embedded)) + cfg.set('general', 'only_monitored', text_type(settings_general_only_monitored)) + cfg.set('general', 'adaptive_searching', text_type(settings_general_adaptive_searching)) + + if after != before: + configured() + get_general_settings() + + settings_auth = get_auth_settings() + + settings_auth_type = request.forms.get('settings_auth_type') + settings_auth_username = request.forms.get('settings_auth_username') + settings_auth_password = request.forms.get('settings_auth_password') + + if get_auth_settings()[0] != settings_auth_type: + configured() + if settings_auth[2] == settings_auth_password: + cfg.set('auth', 'type', text_type(settings_auth_type)) + cfg.set('auth', 'username', text_type(settings_auth_username)) + else: + cfg.set('auth', 'type', text_type(settings_auth_type)) + cfg.set('auth', 'username', text_type(settings_auth_username)) + cfg.set('auth', 'password', hashlib.md5(settings_auth_password).hexdigest()) + if settings_auth_username not in aaa._store.users: + cork = Cork(os.path.normpath(os.path.join(config_dir, 'config')), initialize=True) + cork._store.roles[''] = 100 + cork._store.save_roles() + cork._store.users[settings_auth_username] = { + 'role': '', + 'hash': cork._hash(settings_auth_username, settings_auth_password), + 'email_addr': '', + 'desc': '', + 'creation_date': time.time() + } + cork._store.save_users() + if settings_auth_type == 'basic' or settings_auth_type == 'None': + pass + else: + aaa._beaker_session.delete() + else: + if settings_auth[2] != settings_auth_password: + aaa.user(settings_auth_username).update(role='', pwd=settings_auth_password) + if settings_auth_type == 'basic' or settings_auth_type == 'None': + pass + else: + aaa._beaker_session.delete() + + settings_sonarr_ip = request.forms.get('settings_sonarr_ip') + settings_sonarr_port = request.forms.get('settings_sonarr_port') + settings_sonarr_baseurl = request.forms.get('settings_sonarr_baseurl') + settings_sonarr_ssl = request.forms.get('settings_sonarr_ssl') + if settings_sonarr_ssl is None: + settings_sonarr_ssl = 'False' + else: + settings_sonarr_ssl = 'True' + settings_sonarr_apikey = request.forms.get('settings_sonarr_apikey') + settings_sonarr_sync = request.forms.get('settings_sonarr_sync') + + cfg.set('sonarr', 'ip', text_type(settings_sonarr_ip)) + cfg.set('sonarr', 'port', text_type(settings_sonarr_port)) + cfg.set('sonarr', 'base_url', text_type(settings_sonarr_baseurl)) + cfg.set('sonarr', 'ssl', text_type(settings_sonarr_ssl)) + cfg.set('sonarr', 'apikey', text_type(settings_sonarr_apikey)) + cfg.set('sonarr', 'full_update', text_type(settings_sonarr_sync)) + + settings_radarr_ip = request.forms.get('settings_radarr_ip') + settings_radarr_port = request.forms.get('settings_radarr_port') + settings_radarr_baseurl = request.forms.get('settings_radarr_baseurl') + settings_radarr_ssl = request.forms.get('settings_radarr_ssl') + if settings_radarr_ssl is None: + settings_radarr_ssl = 'False' + else: + settings_radarr_ssl = 'True' + settings_radarr_apikey = request.forms.get('settings_radarr_apikey') + settings_radarr_sync = request.forms.get('settings_radarr_sync') + + cfg.set('radarr', 'ip', text_type(settings_radarr_ip)) + cfg.set('radarr', 'port', text_type(settings_radarr_port)) + cfg.set('radarr', 'base_url', text_type(settings_radarr_baseurl)) + cfg.set('radarr', 'ssl', text_type(settings_radarr_ssl)) + cfg.set('radarr', 'apikey', text_type(settings_radarr_apikey)) + cfg.set('radarr', 'full_update', text_type(settings_radarr_sync)) + + settings_subliminal_providers = request.forms.getall('settings_subliminal_providers') + c.execute("UPDATE table_settings_providers SET enabled = 0") + for item in settings_subliminal_providers: + c.execute("UPDATE table_settings_providers SET enabled = '1' WHERE name = ?", (item,)) + + settings_addic7ed_username = request.forms.get('settings_addic7ed_username') + settings_addic7ed_password = request.forms.get('settings_addic7ed_password') + c.execute("UPDATE table_settings_providers SET username = ?, password = ? WHERE name = 'addic7ed'", (settings_addic7ed_username, settings_addic7ed_password)) + settings_legendastv_username = request.forms.get('settings_legendastv_username') + settings_legendastv_password = request.forms.get('settings_legendastv_password') + c.execute("UPDATE table_settings_providers SET username = ?, password = ? WHERE name = 'legendastv'", (settings_legendastv_username, settings_legendastv_password)) + settings_opensubtitles_username = request.forms.get('settings_opensubtitles_username') + settings_opensubtitles_password = request.forms.get('settings_opensubtitles_password') + c.execute("UPDATE table_settings_providers SET username = ?, password = ? WHERE name = 'opensubtitles'", (settings_opensubtitles_username, settings_opensubtitles_password)) + + settings_subliminal_languages = request.forms.getall('settings_subliminal_languages') + c.execute("UPDATE table_settings_languages SET enabled = 0") + for item in settings_subliminal_languages: + c.execute("UPDATE table_settings_languages SET enabled = '1' WHERE code2 = ?", (item,)) + + settings_serie_default_enabled = request.forms.get('settings_serie_default_enabled') + if settings_serie_default_enabled is None: + settings_serie_default_enabled = 'False' + else: + settings_serie_default_enabled = 'True' + cfg.set('general', 'serie_default_enabled', text_type(settings_serie_default_enabled)) + + settings_serie_default_languages = str(request.forms.getall('settings_serie_default_languages')) + if settings_serie_default_languages == "['None']": + settings_serie_default_languages = 'None' + cfg.set('general', 'serie_default_language', text_type(settings_serie_default_languages)) + + settings_serie_default_hi = request.forms.get('settings_serie_default_hi') + if settings_serie_default_hi is None: + settings_serie_default_hi = 'False' + else: + settings_serie_default_hi = 'True' + cfg.set('general', 'serie_default_hi', text_type(settings_serie_default_hi)) + + settings_movie_default_enabled = request.forms.get('settings_movie_default_enabled') + if settings_movie_default_enabled is None: + settings_movie_default_enabled = 'False' + else: + settings_movie_default_enabled = 'True' + cfg.set('general', 'movie_default_enabled', text_type(settings_movie_default_enabled)) + + settings_movie_default_languages = str(request.forms.getall('settings_movie_default_languages')) + if settings_movie_default_languages == "['None']": + settings_movie_default_languages = 'None' + cfg.set('general', 'movie_default_language', text_type(settings_movie_default_languages)) + + settings_movie_default_hi = request.forms.get('settings_movie_default_hi') + if settings_movie_default_hi is None: + settings_movie_default_hi = 'False' + else: + settings_movie_default_hi = 'True' + cfg.set('general', 'movie_default_hi', text_type(settings_movie_default_hi)) + + with open(config_file, 'wb') as f: + cfg.write(f) + + settings_notifier_Boxcar_enabled = request.forms.get('settings_notifier_Boxcar_enabled') + if settings_notifier_Boxcar_enabled == 'on': + settings_notifier_Boxcar_enabled = 1 + else: + settings_notifier_Boxcar_enabled = 0 + settings_notifier_Boxcar_url = request.forms.get('settings_notifier_Boxcar_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Boxcar'", (settings_notifier_Boxcar_enabled, settings_notifier_Boxcar_url)) + + settings_notifier_Faast_enabled = request.forms.get('settings_notifier_Faast_enabled') + if settings_notifier_Faast_enabled == 'on': + settings_notifier_Faast_enabled = 1 + else: + settings_notifier_Faast_enabled = 0 + settings_notifier_Faast_url = request.forms.get('settings_notifier_Faast_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Faast'", (settings_notifier_Faast_enabled, settings_notifier_Faast_url)) + + settings_notifier_Growl_enabled = request.forms.get('settings_notifier_Growl_enabled') + if settings_notifier_Growl_enabled == 'on': + settings_notifier_Growl_enabled = 1 + else: + settings_notifier_Growl_enabled = 0 + settings_notifier_Growl_url = request.forms.get('settings_notifier_Growl_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Growl'", (settings_notifier_Growl_enabled, settings_notifier_Growl_url)) + + settings_notifier_Join_enabled = request.forms.get('settings_notifier_Join_enabled') + if settings_notifier_Join_enabled == 'on': + settings_notifier_Join_enabled = 1 + else: + settings_notifier_Join_enabled = 0 + settings_notifier_Join_url = request.forms.get('settings_notifier_Join_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Join'", (settings_notifier_Join_enabled, settings_notifier_Join_url)) + + settings_notifier_KODI_enabled = request.forms.get('settings_notifier_KODI_enabled') + if settings_notifier_KODI_enabled == 'on': + settings_notifier_KODI_enabled = 1 + else: + settings_notifier_KODI_enabled = 0 + settings_notifier_KODI_url = request.forms.get('settings_notifier_KODI_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'KODI'", (settings_notifier_KODI_enabled, settings_notifier_KODI_url)) + + settings_notifier_Mattermost_enabled = request.forms.get('settings_notifier_Mattermost_enabled') + if settings_notifier_Mattermost_enabled == 'on': + settings_notifier_Mattermost_enabled = 1 + else: + settings_notifier_Mattermost_enabled = 0 + settings_notifier_Mattermost_url = request.forms.get('settings_notifier_Mattermost_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Mattermost'", (settings_notifier_Mattermost_enabled, settings_notifier_Mattermost_url)) + + settings_notifier_NMA_enabled = request.forms.get('settings_notifier_Notify My Android_enabled') + if settings_notifier_NMA_enabled == 'on': + settings_notifier_NMA_enabled = 1 + else: + settings_notifier_NMA_enabled = 0 + settings_notifier_NMA_url = request.forms.get('settings_notifier_Notify My Android_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Notify My Android'", (settings_notifier_NMA_enabled, settings_notifier_NMA_url)) + + settings_notifier_Prowl_enabled = request.forms.get('settings_notifier_Prowl_enabled') + if settings_notifier_Prowl_enabled == 'on': + settings_notifier_Prowl_enabled = 1 + else: + settings_notifier_Prowl_enabled = 0 + settings_notifier_Prowl_url = request.forms.get('settings_notifier_Prowl_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Prowl'", (settings_notifier_Prowl_enabled, settings_notifier_Prowl_url)) + + settings_notifier_Pushalot_enabled = request.forms.get('settings_notifier_Pushalot_enabled') + if settings_notifier_Pushalot_enabled == 'on': + settings_notifier_Pushalot_enabled = 1 + else: + settings_notifier_Pushalot_enabled = 0 + settings_notifier_Pushalot_url = request.forms.get('settings_notifier_Pushalot_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Pushalot'", (settings_notifier_Pushalot_enabled, settings_notifier_Pushalot_url)) + + settings_notifier_PushBullet_enabled = request.forms.get('settings_notifier_PushBullet_enabled') + if settings_notifier_PushBullet_enabled == 'on': + settings_notifier_PushBullet_enabled = 1 + else: + settings_notifier_PushBullet_enabled = 0 + settings_notifier_PushBullet_url = request.forms.get('settings_notifier_PushBullet_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'PushBullet'", (settings_notifier_PushBullet_enabled, settings_notifier_PushBullet_url)) + + settings_notifier_Pushjet_enabled = request.forms.get('settings_notifier_Pushjet_enabled') + if settings_notifier_Pushjet_enabled == 'on': + settings_notifier_Pushjet_enabled = 1 + else: + settings_notifier_Pushjet_enabled = 0 + settings_notifier_Pushjet_url = request.forms.get('settings_notifier_Pushjet_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Pushjet'", (settings_notifier_Pushjet_enabled, settings_notifier_Pushjet_url)) + + settings_notifier_Pushover_enabled = request.forms.get('settings_notifier_Pushover_enabled') + if settings_notifier_Pushover_enabled == 'on': + settings_notifier_Pushover_enabled = 1 + else: + settings_notifier_Pushover_enabled = 0 + settings_notifier_Pushover_url = request.forms.get('settings_notifier_Pushover_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Pushover'", (settings_notifier_Pushover_enabled, settings_notifier_Pushover_url)) + + settings_notifier_RocketChat_enabled = request.forms.get('settings_notifier_Rocket.Chat_enabled') + if settings_notifier_RocketChat_enabled == 'on': + settings_notifier_RocketChat_enabled = 1 + else: + settings_notifier_RocketChat_enabled = 0 + settings_notifier_RocketChat_url = request.forms.get('settings_notifier_Rocket.Chat_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Rocket.Chat'", (settings_notifier_RocketChat_enabled, settings_notifier_RocketChat_url)) + + settings_notifier_Slack_enabled = request.forms.get('settings_notifier_Slack_enabled') + if settings_notifier_Slack_enabled == 'on': + settings_notifier_Slack_enabled = 1 + else: + settings_notifier_Slack_enabled = 0 + settings_notifier_Slack_url = request.forms.get('settings_notifier_Slack_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Slack'", (settings_notifier_Slack_enabled, settings_notifier_Slack_url)) + + settings_notifier_SuperToasty_enabled = request.forms.get('settings_notifier_Super Toasty_enabled') + if settings_notifier_SuperToasty_enabled == 'on': + settings_notifier_SuperToasty_enabled = 1 + else: + settings_notifier_SuperToasty_enabled = 0 + settings_notifier_SuperToasty_url = request.forms.get('settings_notifier_Super Toasty_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Super Toasty'", (settings_notifier_SuperToasty_enabled, settings_notifier_SuperToasty_url)) + + settings_notifier_Telegram_enabled = request.forms.get('settings_notifier_Telegram_enabled') + if settings_notifier_Telegram_enabled == 'on': + settings_notifier_Telegram_enabled = 1 + else: + settings_notifier_Telegram_enabled = 0 + settings_notifier_Telegram_url = request.forms.get('settings_notifier_Telegram_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Telegram'", (settings_notifier_Telegram_enabled, settings_notifier_Telegram_url)) + + settings_notifier_Twitter_enabled = request.forms.get('settings_notifier_Twitter_enabled') + if settings_notifier_Twitter_enabled == 'on': + settings_notifier_Twitter_enabled = 1 + else: + settings_notifier_Twitter_enabled = 0 + settings_notifier_Twitter_url = request.forms.get('settings_notifier_Twitter_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Twitter'", (settings_notifier_Twitter_enabled, settings_notifier_Twitter_url)) + + settings_notifier_XBMC_enabled = request.forms.get('settings_notifier_XBMC_enabled') + if settings_notifier_XBMC_enabled == 'on': + settings_notifier_XBMC_enabled = 1 + else: + settings_notifier_XBMC_enabled = 0 + settings_notifier_XBMC_url = request.forms.get('settings_notifier_XBMC_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'XBMC'", (settings_notifier_XBMC_enabled, settings_notifier_XBMC_url)) + + settings_notifier_Discord_enabled = request.forms.get('settings_notifier_Discord_enabled') + if settings_notifier_Discord_enabled == 'on': + settings_notifier_Discord_enabled = 1 + else: + settings_notifier_Discord_enabled = 0 + settings_notifier_Discord_url = request.forms.get('settings_notifier_Discord_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Discord'", (settings_notifier_Discord_enabled, settings_notifier_Discord_url)) + + settings_notifier_E_Mail_enabled = request.forms.get('settings_notifier_E-Mail_enabled') + if settings_notifier_E_Mail_enabled == 'on': + settings_notifier_E_Mail_enabled = 1 + else: + settings_notifier_E_Mail_enabled = 0 + settings_notifier_E_Mail_url = request.forms.get('settings_notifier_E-Mail_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'E-Mail'", (settings_notifier_E_Mail_enabled, settings_notifier_E_Mail_url)) + + settings_notifier_Emby_enabled = request.forms.get('settings_notifier_Emby_enabled') + if settings_notifier_Emby_enabled == 'on': + settings_notifier_Emby_enabled = 1 + else: + settings_notifier_Emby_enabled = 0 + settings_notifier_Emby_url = request.forms.get('settings_notifier_Emby_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Emby'", (settings_notifier_Emby_enabled, settings_notifier_Emby_url)) + + settings_notifier_IFTTT_enabled = request.forms.get('settings_notifier_IFTTT_enabled') + if settings_notifier_IFTTT_enabled == 'on': + settings_notifier_IFTTT_enabled = 1 + else: + settings_notifier_IFTTT_enabled = 0 + settings_notifier_IFTTT_url = request.forms.get('settings_notifier_IFTTT_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'IFTTT'", (settings_notifier_IFTTT_enabled, settings_notifier_IFTTT_url)) + + settings_notifier_Stride_enabled = request.forms.get('settings_notifier_Stride_enabled') + if settings_notifier_Stride_enabled == 'on': + settings_notifier_Stride_enabled = 1 + else: + settings_notifier_Stride_enabled = 0 + settings_notifier_Stride_url = request.forms.get('settings_notifier_Stride_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Stride'", (settings_notifier_Stride_enabled, settings_notifier_Stride_url)) + + settings_notifier_Windows_enabled = request.forms.get('settings_notifier_Windows_enabled') + if settings_notifier_Windows_enabled == 'on': + settings_notifier_Windows_enabled = 1 + else: + settings_notifier_Windows_enabled = 0 + settings_notifier_Windows_url = request.forms.get('settings_notifier_Windows_url') + c.execute("UPDATE table_settings_notifier SET enabled = ?, url = ? WHERE name = 'Windows'", (settings_notifier_Windows_enabled, settings_notifier_Windows_url)) + + conn.commit() + c.close() + + sonarr_full_update() + radarr_full_update() + + logging.info('Settings saved succesfully.') + + # reschedule full update task according to settings + sonarr_full_update() + + redirect(ref) + +@route(base_url + 'check_update') +@custom_auth_basic(check_credentials) +def check_update(): + authorize() + ref = request.environ['HTTP_REFERER'] + + check_and_apply_update() + + redirect(ref) + +@route(base_url + 'system') +@custom_auth_basic(check_credentials) +def system(): + authorize() + def get_time_from_interval(interval): + interval_clean = interval.split('[') + interval_clean = interval_clean[1][:-1] + interval_split = interval_clean.split(':') + + hour = interval_split[0] + minute = interval_split[1].lstrip("0") + second = interval_split[2].lstrip("0") + + text = "every " + if hour != "0": + text = text + hour + if hour == "1": + text = text + " hour" + else: + text = text + " hours" + + if minute != "" and second != "": + text = text + ", " + elif minute == "" and second != "": + text = text + " and " + elif minute != "" and second == "": + text = text + " and " + if minute != "": + text = text + minute + if minute == "1": + text = text + " minute" + else: + text = text + " minutes" + + if second != "": + text = text + " and " + if second != "": + text = text + second + if second == "1": + text = text + " second" + else: + text = text + " seconds" + + return text + + def get_time_from_cron(cron): + text = "at " + hour = str(cron[5]) + minute = str(cron[6]) + second = str(cron[7]) + + if hour != "0" and hour != "*": + text = text + hour + if hour == "0" or hour == "1": + text = text + " hour" + else: + text = text + " hours" + + if minute != "*" and second != "0": + text = text + ", " + elif minute == "*" and second != "0": + text = text + " and " + elif minute != "0" and minute != "*" and second == "0": + text = text + " and " + if minute != "0" and minute != "*": + text = text + minute + if minute == "0" or minute == "1": + text = text + " minute" + else: + text = text + " minutes" + + if second != "0" and second != "*": + text = text + " and " + if second != "0" and second != "*": + text = text + second + if second == "0" or second == "1": + text = text + " second" + else: + text = text + " seconds" + + return text + + + task_list = [] + for job in scheduler.get_jobs(): + if job.next_run_time is not None: + next_run = pretty.date(job.next_run_time.replace(tzinfo=None)) + else: + next_run = "Never" + + if job.trigger.__str__().startswith('interval'): + task_list.append([job.name, get_time_from_interval(str(job.trigger)), next_run, job.id]) + elif job.trigger.__str__().startswith('cron'): + task_list.append([job.name, get_time_from_cron(job.trigger.fields), next_run, job.id]) + + i = 0 + with open(os.path.join(config_dir, 'log/bazarr.log')) as f: + for i, l in enumerate(f, 1): + pass + row_count = i + page_size = int(get_general_settings()[21]) + max_page = int(math.ceil(row_count / (page_size + 0.0))) + + releases = [] + url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases' + try: + r = requests.get(url_releases, timeout=15) + r.raise_for_status() + except requests.exceptions.HTTPError as errh: + logging.exception("Error trying to get releases from Github. Http error.") + except requests.exceptions.ConnectionError as errc: + logging.exception("Error trying to get releases from Github. Connection Error.") + except requests.exceptions.Timeout as errt: + logging.exception("Error trying to get releases from Github. Timeout Error.") + except requests.exceptions.RequestException as err: + logging.exception("Error trying to get releases from Github.") + else: + for release in r.json(): + releases.append([release['name'],release['body']]) + + return template('system', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url, task_list=task_list, row_count=row_count, max_page=max_page, page_size=page_size, releases=releases) + +@route(base_url + 'logs/') +@custom_auth_basic(check_credentials) +def get_logs(page): + authorize() + page_size = int(get_general_settings()[21]) + begin = (page * page_size) - page_size + end = (page * page_size) - 1 + logs_complete = [] + for line in reversed(open(os.path.join(config_dir, 'log/bazarr.log')).readlines()): + logs_complete.append(line.rstrip()) + logs = logs_complete[begin:end] + + return template('logs', logs=logs, base_url=base_url) + +@route(base_url + 'execute/') +@custom_auth_basic(check_credentials) +def execute_task(taskid): + authorize() + ref = request.environ['HTTP_REFERER'] + + execute_now(taskid) + + redirect(ref) + + +@route(base_url + 'remove_subtitles', method='POST') +@custom_auth_basic(check_credentials) +def remove_subtitles(): + authorize() + episodePath = request.forms.get('episodePath') + language = request.forms.get('language') + subtitlesPath = request.forms.get('subtitlesPath') + sonarrSeriesId = request.forms.get('sonarrSeriesId') + sonarrEpisodeId = request.forms.get('sonarrEpisodeId') + + try: + os.remove(subtitlesPath) + result = language_from_alpha3(language) + " subtitles deleted from disk." + history_log(0, sonarrSeriesId, sonarrEpisodeId, result) + except OSError: + pass + store_subtitles(unicode(episodePath)) + list_missing_subtitles(sonarrSeriesId) + + +@route(base_url + 'remove_subtitles_movie', method='POST') +@custom_auth_basic(check_credentials) +def remove_subtitles_movie(): + authorize() + moviePath = request.forms.get('moviePath') + language = request.forms.get('language') + subtitlesPath = request.forms.get('subtitlesPath') + radarrId = request.forms.get('radarrId') + + try: + os.remove(subtitlesPath) + result = language_from_alpha3(language) + " subtitles deleted from disk." + history_log_movie(0, radarrId, result) + except OSError: + pass + store_subtitles_movie(unicode(moviePath)) + list_missing_subtitles_movies(radarrId) + + +@route(base_url + 'get_subtitle', method='POST') +@custom_auth_basic(check_credentials) +def get_subtitle(): + authorize() + ref = request.environ['HTTP_REFERER'] + + episodePath = request.forms.get('episodePath') + sceneName = request.forms.get('sceneName') + language = request.forms.get('language') + hi = request.forms.get('hi') + sonarrSeriesId = request.forms.get('sonarrSeriesId') + sonarrEpisodeId = request.forms.get('sonarrEpisodeId') + # tvdbid = request.forms.get('tvdbid') + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") + enabled_providers = c.fetchall() + c.close() + + providers_list = [] + providers_auth = {} + if len(enabled_providers) > 0: + for provider in enabled_providers: + providers_list.append(provider[0]) + try: + if provider[2] is not '' and provider[3] is not '': + provider_auth = providers_auth.append(provider[0]) + provider_auth.update({'username':providers[2], 'password':providers[3]}) + else: + providers_auth = None + except: + providers_auth = None + else: + providers_list = None + providers_auth = None + + try: + result = download_subtitle(episodePath, language, hi, providers_list, providers_auth, sceneName, 'series') + if result is not None: + history_log(1, sonarrSeriesId, sonarrEpisodeId, result) + send_notifications(sonarrSeriesId, sonarrEpisodeId, result) + store_subtitles(unicode(episodePath)) + list_missing_subtitles(sonarrSeriesId) + redirect(ref) + except OSError: + pass + +@route(base_url + 'manual_search', method='POST') +@custom_auth_basic(check_credentials) +def manual_search_json(): + authorize() + ref = request.environ['HTTP_REFERER'] + + episodePath = request.forms.get('episodePath') + sceneName = request.forms.get('sceneName') + language = request.forms.get('language') + hi = request.forms.get('hi') + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") + enabled_providers = c.fetchall() + c.close() + + providers_list = [] + providers_auth = {} + if len(enabled_providers) > 0: + for provider in enabled_providers: + providers_list.append(provider[0]) + try: + if provider[2] is not '' and provider[3] is not '': + provider_auth = providers_auth.append(provider[0]) + provider_auth.update({'username':providers[2], 'password':providers[3]}) + else: + providers_auth = None + except: + providers_auth = None + else: + providers_list = None + providers_auth = None + + data = manual_search(episodePath, language, hi, providers_list, providers_auth, sceneName, 'series') + return dict(data=data) + +@route(base_url + 'manual_get_subtitle', method='POST') +@custom_auth_basic(check_credentials) +def manual_get_subtitle(): + authorize() + ref = request.environ['HTTP_REFERER'] + + episodePath = request.forms.get('episodePath') + sceneName = request.forms.get('sceneName') + language = request.forms.get('language') + hi = request.forms.get('hi') + selected_provider = request.forms.get('provider') + subtitle = request.forms.get('subtitle') + sonarrSeriesId = request.forms.get('sonarrSeriesId') + sonarrEpisodeId = request.forms.get('sonarrEpisodeId') + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + provider = c.execute("SELECT * FROM table_settings_providers WHERE name = ?",(selected_provider,)).fetchone() + c.close() + providers_auth = {} + try: + if provider[2] is not '' and provider[3] is not '': + provider_auth = providers_auth.append(provider[0]) + provider_auth.update({'username':providers[2], 'password':providers[3]}) + else: + providers_auth = None + except: + providers_auth = None + + try: + result = manual_download_subtitle(episodePath, language, hi, subtitle, selected_provider, providers_auth, sceneName, 'series') + if result is not None: + history_log(1, sonarrSeriesId, sonarrEpisodeId, result) + send_notifications(sonarrSeriesId, sonarrEpisodeId, result) + store_subtitles(unicode(episodePath)) + list_missing_subtitles(sonarrSeriesId) + redirect(ref) + except OSError: + pass + +@route(base_url + 'get_subtitle_movie', method='POST') +@custom_auth_basic(check_credentials) +def get_subtitle_movie(): + authorize() + ref = request.environ['HTTP_REFERER'] + + moviePath = request.forms.get('moviePath') + sceneName = request.forms.get('sceneName') + language = request.forms.get('language') + hi = request.forms.get('hi') + radarrId = request.forms.get('radarrId') + # tmdbid = request.forms.get('tmdbid') + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") + enabled_providers = c.fetchall() + c.close() + + providers_list = [] + providers_auth = {} + if len(enabled_providers) > 0: + for provider in enabled_providers: + providers_list.append(provider[0]) + try: + if provider[2] is not '' and provider[3] is not '': + provider_auth = providers_auth.append(provider[0]) + provider_auth.update({'username':providers[2], 'password':providers[3]}) + else: + providers_auth = None + except: + providers_auth = None + else: + providers_list = None + providers_auth = None + + try: + result = download_subtitle(moviePath, language, hi, providers_list, providers_auth, sceneName, 'movie') + if result is not None: + history_log_movie(1, radarrId, result) + send_notifications_movie(radarrId, result) + store_subtitles_movie(unicode(moviePath)) + list_missing_subtitles_movies(radarrId) + redirect(ref) + except OSError: + pass + +@route(base_url + 'manual_search_movie', method='POST') +@custom_auth_basic(check_credentials) +def manual_search_movie_json(): + authorize() + ref = request.environ['HTTP_REFERER'] + + moviePath = request.forms.get('moviePath') + sceneName = request.forms.get('sceneName') + language = request.forms.get('language') + hi = request.forms.get('hi') + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + c.execute("SELECT * FROM table_settings_providers WHERE enabled = 1") + enabled_providers = c.fetchall() + c.close() + + providers_list = [] + providers_auth = {} + if len(enabled_providers) > 0: + for provider in enabled_providers: + providers_list.append(provider[0]) + try: + if provider[2] is not '' and provider[3] is not '': + provider_auth = providers_auth.append(provider[0]) + provider_auth.update({'username':providers[2], 'password':providers[3]}) + else: + providers_auth = None + except: + providers_auth = None + else: + providers_list = None + providers_auth = None + + data = manual_search(moviePath, language, hi, providers_list, providers_auth, sceneName, 'movie') + return dict(data=data) + +@route(base_url + 'manual_get_subtitle_movie', method='POST') +@custom_auth_basic(check_credentials) +def manual_get_subtitle_movie(): + authorize() + ref = request.environ['HTTP_REFERER'] + + moviePath = request.forms.get('moviePath') + sceneName = request.forms.get('sceneName') + language = request.forms.get('language') + hi = request.forms.get('hi') + selected_provider = request.forms.get('provider') + subtitle = request.forms.get('subtitle') + radarrId = request.forms.get('radarrId') + + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + provider = c.execute("SELECT * FROM table_settings_providers WHERE name = ?",(selected_provider,)).fetchone() + c.close() + providers_auth = {} + try: + if provider[2] is not '' and provider[3] is not '': + provider_auth = providers_auth.append(provider[0]) + provider_auth.update({'username':providers[2], 'password':providers[3]}) + else: + providers_auth = None + except: + providers_auth = None + + try: + result = manual_download_subtitle(moviePath, language, hi, subtitle, selected_provider, providers_auth, sceneName, 'movie') + if result is not None: + history_log_movie(1, radarrId, result) + send_notifications_movie(radarrId, result) + store_subtitles_movie(unicode(moviePath)) + list_missing_subtitles_movies(radarrId) + redirect(ref) + except OSError: + pass + +def configured(): + conn = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = conn.cursor() + c.execute("UPDATE system SET configured = 1") + conn.commit() + c.close() + +@route(base_url + 'api/wanted') +def api_wanted(): + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + data = c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]' ORDER BY table_episodes._rowid_ DESC").fetchall() + c.close() + return dict(subtitles=data) + +@route(base_url + 'api/history') +def api_history(): + db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30) + c = db.cursor() + data = c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, strftime('%Y-%m-%d', datetime(table_history.timestamp, 'unixepoch')), table_history.description FROM table_history INNER JOIN table_shows on table_shows.sonarrSeriesId = table_history.sonarrSeriesId INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = table_history.sonarrEpisodeId WHERE table_history.action = '1' ORDER BY id DESC").fetchall() + c.close() + return dict(subtitles=data) + +@route(base_url + 'test_url/', method='GET') +@custom_auth_basic(check_credentials) +def test_url(url): + try: + result = requests.get(url).json()['version'] + except: + return dict(status=False, version=result) + else: + return dict(status=True, version=result) + + +logging.info('Bazarr is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url)) +run(host=ip, port=port, server='waitress', app=app) +logging.info('Bazarr has been stopped.') diff --git a/get_settings.py b/get_settings.py index 09c9d0eb9..36c5d303d 100644 --- a/get_settings.py +++ b/get_settings.py @@ -60,7 +60,7 @@ def get_general_settings(): if cfg.has_option('general', 'minimum_score'): minimum_score = cfg.get('general', 'minimum_score') else: - minimum_score = '100' + minimum_score = '90' if cfg.has_option('general', 'use_scenename'): use_scenename = cfg.getboolean('general', 'use_scenename') @@ -130,7 +130,7 @@ def get_general_settings(): if cfg.has_option('general', 'minimum_score_movie'): minimum_score_movie = cfg.get('general', 'minimum_score_movie') else: - minimum_score_movie = '100' + minimum_score_movie = '70' if cfg.has_option('general', 'use_embedded_subs'): use_embedded_subs = cfg.getboolean('general', 'use_embedded_subs') @@ -156,7 +156,7 @@ def get_general_settings(): branch = 'master' auto_update = True single_language = False - minimum_score = '100' + minimum_score = '90' use_scenename = False use_postprocessing = False postprocessing_cmd = False @@ -170,7 +170,7 @@ def get_general_settings(): movie_default_language = [] movie_default_hi = False page_size = '25' - minimum_score_movie = '100' + minimum_score_movie = '70' use_embedded_subs = False only_monitored = False adaptive_searching = False @@ -187,10 +187,10 @@ def get_auth_settings(): pass if cfg.has_section('auth'): - if cfg.has_option('auth', 'enabled'): - enabled = cfg.getboolean('auth', 'enabled') + if cfg.has_option('auth', 'type'): + type = cfg.get('auth', 'type') else: - enabled = False + type = None if cfg.has_option('auth', 'username'): username = cfg.get('auth', 'username') @@ -202,11 +202,11 @@ def get_auth_settings(): else: password = '' else: - enabled = False + type = None username = '' password = '' - return [enabled, username, password] + return [type, username, password] def get_sonarr_settings(): diff --git a/init.py b/init.py index 54eb8b0bf..254a6087b 100644 --- a/init.py +++ b/init.py @@ -27,7 +27,7 @@ if os.path.exists(os.path.join(config_dir, 'log')) is False: config_file = os.path.normpath(os.path.join(config_dir, 'config/config.ini')) -# if os.path.exists(os.path.join(config_dir, 'db/bazarr.db')) is True and os.path.exists(config_file) is False: +cfg = ConfigParser() try: # Open database connection db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'), timeout=30) @@ -51,8 +51,6 @@ try: # Close database connection db.close() - cfg = ConfigParser() - section = 'general' if not cfg.has_section(section): @@ -164,7 +162,7 @@ except sqlite3.OperationalError: if not cfg.has_section(section): cfg.add_section(section) - cfg.set(section, 'enabled', "False") + cfg.set(section, 'type', "None") cfg.set(section, 'username', "") cfg.set(section, 'password', "") @@ -218,3 +216,32 @@ try: logging.info('Database created successfully') except: pass + +# Remove unused settings +try: + with open(config_file, 'r') as f: + cfg.read_file(f) +except Exception: + pass +cfg.remove_option('auth', 'enabled') +with open(config_file, 'w+') as configfile: + cfg.write(configfile) + +from cork import Cork +import time +if os.path.exists(os.path.normpath(os.path.join(config_dir, 'config/users.json'))) is False: + cork = Cork(os.path.normpath(os.path.join(config_dir, 'config')), initialize=True) + + cork._store.roles[''] = 100 + cork._store.save_roles() + + tstamp = str(time.time()) + username = password = '' + cork._store.users[username] = { + 'role': '', + 'hash': cork._hash(username, password), + 'email_addr': username, + 'desc': username, + 'creation_date': tstamp + } + cork._store.save_users() diff --git a/libs/cork/__init__.py b/libs/cork/__init__.py new file mode 100644 index 000000000..b77bd8967 --- /dev/null +++ b/libs/cork/__init__.py @@ -0,0 +1,7 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt +# +# Backends API - used to make backends available for importing +# +from .cork import Cork, JsonBackend, AAAException, AuthException, Mailer, FlaskCork, Redirect diff --git a/libs/cork/backends.py b/libs/cork/backends.py new file mode 100644 index 000000000..d0d533a58 --- /dev/null +++ b/libs/cork/backends.py @@ -0,0 +1,13 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt + +""" +.. module:: backends + :synopsis: Backends API - used to make backends available for importing +""" + +from .json_backend import JsonBackend +from .mongodb_backend import MongoDBBackend +from .sqlalchemy_backend import SqlAlchemyBackend +from .sqlite_backend import SQLiteBackend diff --git a/libs/cork/base_backend.py b/libs/cork/base_backend.py new file mode 100644 index 000000000..7f8690899 --- /dev/null +++ b/libs/cork/base_backend.py @@ -0,0 +1,31 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt + +""" +.. module:: backend.py + :synopsis: Base Backend. +""" + +class BackendIOException(Exception): + """Generic Backend I/O Exception""" + pass + +def ni(*args, **kwargs): + raise NotImplementedError + +class Backend(object): + """Base Backend class - to be subclassed by real backends.""" + save_users = ni + save_roles = ni + save_pending_registrations = ni + +class Table(object): + """Base Table class - to be subclassed by real backends.""" + __len__ = ni + __contains__ = ni + __setitem__ = ni + __getitem__ = ni + __iter__ = ni + iteritems = ni + diff --git a/libs/cork/cork.py b/libs/cork/cork.py new file mode 100644 index 000000000..c6f665188 --- /dev/null +++ b/libs/cork/cork.py @@ -0,0 +1,975 @@ +#!/usr/bin/env python +# +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from base64 import b64encode, b64decode +from datetime import datetime, timedelta +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from logging import getLogger +from smtplib import SMTP, SMTP_SSL +from threading import Thread +from time import time +import bottle +import hashlib +import os +import re +import sys +import uuid + +try: + import scrypt + scrypt_available = True +except ImportError: # pragma: no cover + scrypt_available = False + +try: + basestring +except NameError: + basestring = str + +from .backends import JsonBackend + +is_py3 = (sys.version_info.major == 3) + +log = getLogger(__name__) + + +class AAAException(Exception): + """Generic Authentication/Authorization Exception""" + pass + + +class AuthException(AAAException): + """Authentication Exception: incorrect username/password pair""" + pass + + +class BaseCork(object): + """Abstract class""" + + def __init__(self, directory=None, backend=None, email_sender=None, + initialize=False, session_domain=None, smtp_server=None, + smtp_url='localhost', session_key_name=None): + """Auth/Authorization/Accounting class + + :param directory: configuration directory + :type directory: str. + :param users_fname: users filename (without .json), defaults to 'users' + :type users_fname: str. + :param roles_fname: roles filename (without .json), defaults to 'roles' + :type roles_fname: str. + """ + if smtp_server: + smtp_url = smtp_server + self.mailer = Mailer(email_sender, smtp_url) + self.password_reset_timeout = 3600 * 24 + self.session_domain = session_domain + self.session_key_name = session_key_name or 'beaker.session' + self.preferred_hashing_algorithm = 'PBKDF2' + + # Setup JsonBackend by default for backward compatibility. + if backend is None: + self._store = JsonBackend(directory, users_fname='users', + roles_fname='roles', pending_reg_fname='register', + initialize=initialize) + + else: + self._store = backend + + def login(self, username, password, success_redirect=None, + fail_redirect=None): + """Check login credentials for an existing user. + Optionally redirect the user to another page (typically /login) + + :param username: username + :type username: str or unicode. + :param password: cleartext password + :type password: str.or unicode + :param success_redirect: redirect authorized users (optional) + :type success_redirect: str. + :param fail_redirect: redirect unauthorized users (optional) + :type fail_redirect: str. + :returns: True for successful logins, else False + """ + #assert isinstance(username, type(u'')), "the username must be a string" + #assert isinstance(password, type(u'')), "the password must be a string" + + if username in self._store.users: + salted_hash = self._store.users[username]['hash'] + if hasattr(salted_hash, 'encode'): + salted_hash = salted_hash.encode('ascii') + authenticated = self._verify_password( + username, + password, + salted_hash, + ) + if authenticated: + # Setup session data + self._setup_cookie(username) + self._store.users[username]['last_login'] = str(datetime.utcnow()) + self._store.save_users() + if success_redirect: + self._redirect(success_redirect) + return True + + if fail_redirect: + self._redirect(fail_redirect) + + return False + + def logout(self, success_redirect='/login', fail_redirect='/login'): + """Log the user out, remove cookie + + :param success_redirect: redirect the user after logging out + :type success_redirect: str. + :param fail_redirect: redirect the user if it is not logged in + :type fail_redirect: str. + """ + try: + session = self._beaker_session + session.delete() + except Exception as e: + log.debug("Exception %s while logging out." % repr(e)) + self._redirect(fail_redirect) + + self._redirect(success_redirect) + + def require(self, username=None, role=None, fixed_role=False, + fail_redirect=None): + """Ensure the user is logged in has the required role (or higher). + Optionally redirect the user to another page (typically /login) + If both `username` and `role` are specified, both conditions need to be + satisfied. + If none is specified, any authenticated user will be authorized. + By default, any role with higher level than `role` will be authorized; + set fixed_role=True to prevent this. + + :param username: username (optional) + :type username: str. + :param role: role + :type role: str. + :param fixed_role: require user role to match `role` strictly + :type fixed_role: bool. + :param redirect: redirect unauthorized users (optional) + :type redirect: str. + """ + # Parameter validation + if username is not None: + if username not in self._store.users: + raise AAAException("Nonexistent user") + + if fixed_role and role is None: + raise AAAException( + """A role must be specified if fixed_role has been set""") + + if role is not None and role not in self._store.roles: + raise AAAException("Role not found") + + # Authentication + try: + cu = self.current_user + except AAAException: + if fail_redirect is None: + raise AuthException("Unauthenticated user") + else: + self._redirect(fail_redirect) + + # Authorization + if cu.role not in self._store.roles: + raise AAAException("Role not found for the current user") + + if username is not None: + # A specific user is required + if username == self.current_user.username: + return + + if fail_redirect is None: + raise AuthException("Unauthorized access: incorrect" + " username") + + self._redirect(fail_redirect) + + if fixed_role: + # A specific role is required + if role == self.current_user.role: + return + + if fail_redirect is None: + raise AuthException("Unauthorized access: incorrect role") + + self._redirect(fail_redirect) + + if role is not None: + # Any role with higher level is allowed + current_lvl = self._store.roles[self.current_user.role] + threshold_lvl = self._store.roles[role] + if current_lvl >= threshold_lvl: + return + + if fail_redirect is None: + raise AuthException("Unauthorized access: ") + + self._redirect(fail_redirect) + + return # success + + def create_role(self, role, level): + """Create a new role. + + :param role: role name + :type role: str. + :param level: role level (0=lowest, 100=admin) + :type level: int. + :raises: AuthException on errors + """ + if self.current_user.level < 100: + raise AuthException("The current user is not authorized to ") + if role in self._store.roles: + raise AAAException("The role is already existing") + try: + int(level) + except ValueError: + raise AAAException("The level must be numeric.") + self._store.roles[role] = level + self._store.save_roles() + + def delete_role(self, role): + """Deleta a role. + + :param role: role name + :type role: str. + :raises: AuthException on errors + """ + if self.current_user.level < 100: + raise AuthException("The current user is not authorized to ") + if role not in self._store.roles: + raise AAAException("Nonexistent role.") + self._store.roles.pop(role) + self._store.save_roles() + + def list_roles(self): + """List roles. + + :returns: (role, role_level) generator (sorted by role) + """ + for role in sorted(self._store.roles): + yield (role, self._store.roles[role]) + + def create_user(self, username, role, password, email_addr=None, + description=None): + """Create a new user account. + This method is available to users with level>=100 + + :param username: username + :type username: str. + :param role: role + :type role: str. + :param password: cleartext password + :type password: str. + :param email_addr: email address (optional) + :type email_addr: str. + :param description: description (free form) + :type description: str. + :raises: AuthException on errors + """ + assert username, "Username must be provided." + if self.current_user.level < 100: + raise AuthException("The current user is not authorized" + " to create users.") + + if username in self._store.users: + raise AAAException("User is already existing.") + if role not in self._store.roles: + raise AAAException("Nonexistent user role.") + tstamp = str(datetime.utcnow()) + h = self._hash(username, password) + h = h.decode('ascii') + self._store.users[username] = { + 'role': role, + 'hash': h, + 'email_addr': email_addr, + 'desc': description, + 'creation_date': tstamp, + 'last_login': tstamp + } + self._store.save_users() + + def delete_user(self, username): + """Delete a user account. + This method is available to users with level>=100 + + :param username: username + :type username: str. + :raises: Exceptions on errors + """ + if self.current_user.level < 100: + raise AuthException("The current user is not authorized to ") + if username not in self._store.users: + raise AAAException("Nonexistent user.") + self.user(username).delete() + + def list_users(self): + """List users. + + :return: (username, role, email_addr, description) generator (sorted by + username) + """ + for un in sorted(self._store.users): + d = self._store.users[un] + yield (un, d['role'], d['email_addr'], d['desc']) + + @property + def current_user(self): + """Current autenticated user + + :returns: User() instance, if authenticated + :raises: AuthException otherwise + """ + session = self._beaker_session + username = session.get('username', None) + if username is None: + raise AuthException("Unauthenticated user") + if username is not None and username in self._store.users: + return User(username, self, session=session) + raise AuthException("Unknown user: %s" % username) + + @property + def user_is_anonymous(self): + """Check if the current user is anonymous. + + :returns: True if the user is anonymous, False otherwise + :raises: AuthException if the session username is unknown + """ + try: + username = self._beaker_session['username'] + except KeyError: + return True + + if username not in self._store.users: + raise AuthException("Unknown user: %s" % username) + + return False + + def user(self, username): + """Existing user + + :returns: User() instance if the user exist, None otherwise + """ + if username is not None and username in self._store.users: + return User(username, self) + return None + + def register(self, username, password, email_addr, role='user', + max_level=50, subject="Signup confirmation", + email_template='views/registration_email.tpl', + description=None, **kwargs): + """Register a new user account. An email with a registration validation + is sent to the user. + WARNING: this method is available to unauthenticated users + + :param username: username + :type username: str. + :param password: cleartext password + :type password: str. + :param role: role (optional), defaults to 'user' + :type role: str. + :param max_level: maximum role level (optional), defaults to 50 + :type max_level: int. + :param email_addr: email address + :type email_addr: str. + :param subject: email subject + :type subject: str. + :param email_template: email template filename + :type email_template: str. + :param description: description (free form) + :type description: str. + :raises: AssertError or AAAException on errors + """ + assert username, "Username must be provided." + assert password, "A password must be provided." + assert email_addr, "An email address must be provided." + if username in self._store.users: + raise AAAException("User is already existing.") + if role not in self._store.roles: + raise AAAException("Nonexistent role") + if self._store.roles[role] > max_level: + raise AAAException("Unauthorized role") + + registration_code = uuid.uuid4().hex + creation_date = str(datetime.utcnow()) + + # send registration email + email_text = bottle.template( + email_template, + username=username, + email_addr=email_addr, + role=role, + creation_date=creation_date, + registration_code=registration_code, + **kwargs + ) + self.mailer.send_email(email_addr, subject, email_text) + + # store pending registration + h = self._hash(username, password) + h = h.decode('ascii') + self._store.pending_registrations[registration_code] = { + 'username': username, + 'role': role, + 'hash': h, + 'email_addr': email_addr, + 'desc': description, + 'creation_date': creation_date, + } + self._store.save_pending_registrations() + + def validate_registration(self, registration_code): + """Validate pending account registration, create a new account if + successful. + + :param registration_code: registration code + :type registration_code: str. + """ + try: + data = self._store.pending_registrations.pop(registration_code) + except KeyError: + raise AuthException("Invalid registration code.") + + username = data['username'] + if username in self._store.users: + raise AAAException("User is already existing.") + + # the user data is moved from pending_registrations to _users + self._store.users[username] = { + 'role': data['role'], + 'hash': data['hash'], + 'email_addr': data['email_addr'], + 'desc': data['desc'], + 'creation_date': data['creation_date'], + 'last_login': str(datetime.utcnow()) + } + self._store.save_users() + + def send_password_reset_email(self, username=None, email_addr=None, + subject="Password reset confirmation", + email_template='views/password_reset_email', + **kwargs): + """Email the user with a link to reset his/her password + If only one parameter is passed, fetch the other from the users + database. If both are passed they will be matched against the users + database as a security check. + + :param username: username + :type username: str. + :param email_addr: email address + :type email_addr: str. + :param subject: email subject + :type subject: str. + :param email_template: email template filename + :type email_template: str. + :raises: AAAException on missing username or email_addr, + AuthException on incorrect username/email_addr pair + """ + if username is None: + if email_addr is None: + raise AAAException("At least `username` or `email_addr` must" + " be specified.") + + # only email_addr is specified: fetch the username + for k, v in self._store.users.iteritems(): + if v['email_addr'] == email_addr: + username = k + break + else: + raise AAAException("Email address not found.") + + else: # username is provided + if username not in self._store.users: + raise AAAException("Nonexistent user.") + if email_addr is None: + email_addr = self._store.users[username].get('email_addr', None) + if not email_addr: + raise AAAException("Email address not available.") + else: + # both username and email_addr are provided: check them + stored_email_addr = self._store.users[username]['email_addr'] + if email_addr != stored_email_addr: + raise AuthException("Username/email address pair not found.") + + # generate a reset_code token + reset_code = self._reset_code(username, email_addr) + + # send reset email + email_text = bottle.template( + email_template, + username=username, + email_addr=email_addr, + reset_code=reset_code, + **kwargs + ) + self.mailer.send_email(email_addr, subject, email_text) + + def reset_password(self, reset_code, password): + """Validate reset_code and update the account password + The username is extracted from the reset_code token + + :param reset_code: reset token + :type reset_code: str. + :param password: new password + :type password: str. + :raises: AuthException for invalid reset tokens, AAAException + """ + try: + reset_code = b64decode(reset_code).decode() + username, email_addr, tstamp, h = reset_code.split(':', 3) + tstamp = int(tstamp) + assert isinstance(username, type(u'')) + assert isinstance(email_addr, type(u'')) + if not isinstance(h, type(b'')): + h = h.encode('utf-8') + except (TypeError, ValueError): + raise AuthException("Invalid reset code.") + + if time() - tstamp > self.password_reset_timeout: + raise AuthException("Expired reset code.") + + assert isinstance(h, type(b'')) + if not self._verify_password(username, email_addr, h): + raise AuthException("Invalid reset code.") + user = self.user(username) + if user is None: + raise AAAException("Nonexistent user.") + user.update(pwd=password) + + def make_auth_decorator(self, username=None, role=None, fixed_role=False, fail_redirect='/login'): + ''' + Create a decorator to be used for authentication and authorization + + :param username: A resource can be protected for a specific user + :param role: Minimum role level required for authorization + :param fixed_role: Only this role gets authorized + :param fail_redirect: The URL to redirect to if a login is required. + ''' + session_manager = self + def auth_require(username=username, role=role, fixed_role=fixed_role, + fail_redirect=fail_redirect): + def decorator(func): + import functools + @functools.wraps(func) + def wrapper(*a, **ka): + session_manager.require(username=username, role=role, fixed_role=fixed_role, + fail_redirect=fail_redirect) + return func(*a, **ka) + return wrapper + return decorator + return(auth_require) + + + ## Private methods + + def _setup_cookie(self, username): + """Setup cookie for a user that just logged in""" + session = self._beaker_session + session['username'] = username + if self.session_domain is not None: + session.domain = self.session_domain + + self._save_session() + + def _hash(self, username, pwd, salt=None, algo=None): + """Hash username and password, generating salt value if required + """ + if algo is None: + algo = self.preferred_hashing_algorithm + + if algo == 'PBKDF2': + return self._hash_pbkdf2(username, pwd, salt=salt) + + if algo == 'scrypt': + return self._hash_scrypt(username, pwd, salt=salt) + + raise RuntimeError("Unknown hashing algorithm requested: %s" % algo) + + @staticmethod + def _hash_scrypt(username, pwd, salt=None): + """Hash username and password, generating salt value if required + Use scrypt. + + :returns: base-64 encoded str. + """ + if not scrypt_available: + raise Exception("scrypt.hash required." + " Please install the scrypt library.") + + if salt is None: + salt = os.urandom(32) + + assert len(salt) == 32, "Incorrect salt length" + + cleartext = "%s\0%s" % (username, pwd) + h = scrypt.hash(cleartext, salt) + + # 's' for scrypt + hashed = b's' + salt + h + return b64encode(hashed) + + @staticmethod + def _hash_pbkdf2(username, pwd, salt=None): + """Hash username and password, generating salt value if required + Use PBKDF2 from Beaker + + :returns: base-64 encoded str. + """ + if salt is None: + salt = os.urandom(32) + + assert isinstance(salt, bytes) + assert len(salt) == 32, "Incorrect salt length" + + username = username.encode('utf-8') + assert isinstance(username, bytes) + + pwd = pwd.encode('utf-8') + assert isinstance(pwd, bytes) + + cleartext = username + b'\0' + pwd + h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32) + + # 'p' for PBKDF2 + hashed = b'p' + salt + h + return b64encode(hashed) + + def _verify_password(self, username, pwd, salted_hash): + """Verity username/password pair against a salted hash + + :returns: bool + """ + assert isinstance(salted_hash, type(b'')) + decoded = b64decode(salted_hash) + hash_type = decoded[0] + if isinstance(hash_type, int): + hash_type = chr(hash_type) + + salt = decoded[1:33] + + if hash_type == 'p': # PBKDF2 + h = self._hash_pbkdf2(username, pwd, salt) + return salted_hash == h + + if hash_type == 's': # scrypt + h = self._hash_scrypt(username, pwd, salt) + return salted_hash == h + + raise RuntimeError("Unknown hashing algorithm in hash: %r" % decoded) + + def _purge_expired_registrations(self, exp_time=96): + """Purge expired registration requests. + + :param exp_time: expiration time (hours) + :type exp_time: float. + """ + pending = self._store.pending_registrations.items() + if is_py3: + pending = list(pending) + + for uuid_code, data in pending: + creation = datetime.strptime(data['creation_date'], + "%Y-%m-%d %H:%M:%S.%f") + now = datetime.utcnow() + maxdelta = timedelta(hours=exp_time) + if now - creation > maxdelta: + self._store.pending_registrations.pop(uuid_code) + + def _reset_code(self, username, email_addr): + """generate a reset_code token + + :param username: username + :type username: str. + :param email_addr: email address + :type email_addr: str. + :returns: Base-64 encoded token + """ + h = self._hash(username, email_addr) + t = "%d" % time() + t = t.encode('utf-8') + reset_code = b':'.join((username.encode('utf-8'), email_addr.encode('utf-8'), t, h)) + return b64encode(reset_code) + + +class User(object): + + def __init__(self, username, cork_obj, session=None): + """Represent an authenticated user, exposing useful attributes: + username, role, level, description, email_addr, session_creation_time, + session_accessed_time, session_id. The session-related attributes are + available for the current user only. + + :param username: username + :type username: str. + :param cork_obj: instance of :class:`Cork` + """ + self._cork = cork_obj + assert username in self._cork._store.users, "Unknown user" + self.username = username + user_data = self._cork._store.users[username] + self.role = user_data['role'] + self.description = user_data['desc'] + self.email_addr = user_data['email_addr'] + self.level = self._cork._store.roles[self.role] + + if session is not None: + try: + self.session_creation_time = session['_creation_time'] + self.session_accessed_time = session['_accessed_time'] + self.session_id = session['_id'] + except: + pass + + def update(self, role=None, pwd=None, email_addr=None): + """Update an user account data + + :param role: change user role, if specified + :type role: str. + :param pwd: change user password, if specified + :type pwd: str. + :param email_addr: change user email address, if specified + :type email_addr: str. + :raises: AAAException on nonexistent user or role. + """ + username = self.username + if username not in self._cork._store.users: + raise AAAException("User does not exist.") + + if role is not None: + if role not in self._cork._store.roles: + raise AAAException("Nonexistent role.") + + self._cork._store.users[username]['role'] = role + + if pwd is not None: + self._cork._store.users[username]['hash'] = self._cork._hash( + username, pwd) + + if email_addr is not None: + self._cork._store.users[username]['email_addr'] = email_addr + + self._cork._store.save_users() + + def delete(self): + """Delete user account + + :raises: AAAException on nonexistent user. + """ + try: + self._cork._store.users.pop(self.username) + except KeyError: + raise AAAException("Nonexistent user.") + self._cork._store.save_users() + + +class Redirect(Exception): + pass + + +def raise_redirect(path): + raise Redirect(path) + + +class Cork(BaseCork): + @staticmethod + def _redirect(location): + bottle.redirect(location) + + @property + def _beaker_session(self): + """Get session""" + return bottle.request.environ.get(self.session_key_name) + + def _save_session(self): + self._beaker_session.save() + + +class FlaskCork(BaseCork): + @staticmethod + def _redirect(location): + raise_redirect(location) + + @property + def _beaker_session(self): + """Get session""" + import flask + return flask.session + + def _save_session(self): + pass + + +class Mailer(object): + + def __init__(self, sender, smtp_url, join_timeout=5, use_threads=True): + """Send emails asyncronously + + :param sender: Sender email address + :type sender: str. + :param smtp_server: SMTP server + :type smtp_server: str. + """ + self.sender = sender + self.join_timeout = join_timeout + self.use_threads = use_threads + self._threads = [] + self._conf = self._parse_smtp_url(smtp_url) + + def _parse_smtp_url(self, url): + """Parse SMTP URL""" + match = re.match(r""" + ( # Optional protocol + (?Psmtp|starttls|ssl) # Protocol name + :// + )? + ( # Optional user:pass@ + (?P[^:]*) # Match every char except ':' + (: (?P.*) )? @ # Optional :pass + )? + (?P # Required FQDN on IP address + ()| # Empty string + ( # FQDN + [a-zA-Z_\-] # First character cannot be a number + [a-zA-Z0-9_\-\.]{,254} + ) + |( # IPv4 + ([0-9]{1,3}\.){3} + [0-9]{1,3} + ) + |( # IPv6 + \[ # Square brackets + ([0-9a-f]{,4}:){1,8} + [0-9a-f]{,4} + \] + ) + ) + ( # Optional :port + : + (?P[0-9]{,5}) # Up to 5-digits port + )? + [/]? + $ + """, url, re.VERBOSE) + + if not match: + raise RuntimeError("SMTP URL seems incorrect") + + d = match.groupdict() + if d['proto'] is None: + d['proto'] = 'smtp' + + if d['port'] is None: + d['port'] = 25 + else: + d['port'] = int(d['port']) + + if not 0 < d['port'] < 65536: + raise RuntimeError("Incorrect SMTP port") + + return d + + def send_email(self, email_addr, subject, email_text): + """Send an email + + :param email_addr: email address + :type email_addr: str. + :param subject: subject + :type subject: str. + :param email_text: email text + :type email_text: str. + :raises: AAAException if smtp_server and/or sender are not set + """ + if not (self._conf['fqdn'] and self.sender): + raise AAAException("SMTP server or sender not set") + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = self.sender + msg['To'] = email_addr + if isinstance(email_text, bytes): + email_text = email_text.encode('utf-8') + + part = MIMEText(email_text, 'html') + msg.attach(part) + msg = msg.as_string() + + log.debug("Sending email using %s" % self._conf['fqdn']) + + if self.use_threads: + thread = Thread(target=self._send, args=(email_addr, msg)) + thread.start() + self._threads.append(thread) + + else: + self._send(email_addr, msg) + + def _send(self, email_addr, msg): + """Deliver an email using SMTP + + :param email_addr: recipient + :type email_addr: str. + :param msg: email text + :type msg: str. + """ + proto = self._conf['proto'] + assert proto in ('smtp', 'starttls', 'ssl'), \ + "Incorrect protocol: %s" % proto + + try: + if proto == 'ssl': + log.debug("Setting up SSL") + session = SMTP_SSL(self._conf['fqdn'], self._conf['port']) + else: + session = SMTP(self._conf['fqdn'], self._conf['port']) + + if proto == 'starttls': + log.debug('Sending EHLO and STARTTLS') + session.ehlo() + session.starttls() + session.ehlo() + + if self._conf['user'] is not None: + log.debug('Performing login') + session.login(self._conf['user'], self._conf['pass']) + + log.debug('Sending') + session.sendmail(self.sender, email_addr, msg) + session.quit() + log.info('Email sent') + + except Exception as e: # pragma: no cover + log.error("Error sending email: %s" % e, exc_info=True) + + def join(self): + """Flush email queue by waiting the completion of the existing threads + + :returns: None + """ + return [t.join(self.join_timeout) for t in self._threads] + + def __del__(self): + """Class destructor: wait for threads to terminate within a timeout""" + try: + self.join() + except TypeError: + pass diff --git a/libs/cork/json_backend.py b/libs/cork/json_backend.py new file mode 100644 index 000000000..cfd38e737 --- /dev/null +++ b/libs/cork/json_backend.py @@ -0,0 +1,134 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt + +""" +.. module:: json_backend + :synopsis: JSON file-based storage backend. +""" + +from logging import getLogger +import os +import shutil +import sys + +try: + import json +except ImportError: # pragma: no cover + import simplejson as json + +from .base_backend import BackendIOException + +is_py3 = (sys.version_info.major == 3) + +log = getLogger(__name__) + +try: + dict.iteritems + py23dict = dict +except AttributeError: + class py23dict(dict): + iteritems = dict.items + +class BytesEncoder(json.JSONEncoder): + def default(self, obj): + if is_py3 and isinstance(obj, bytes): + return obj.decode() + + return json.JSONEncoder.default(self, obj) + + +class JsonBackend(object): + """JSON file-based storage backend.""" + + def __init__(self, directory, users_fname='users', + roles_fname='roles', pending_reg_fname='register', initialize=False): + """Data storage class. Handles JSON files + + :param users_fname: users file name (without .json) + :type users_fname: str. + :param roles_fname: roles file name (without .json) + :type roles_fname: str. + :param pending_reg_fname: pending registrations file name (without .json) + :type pending_reg_fname: str. + :param initialize: create empty JSON files (defaults to False) + :type initialize: bool. + """ + assert directory, "Directory name must be valid" + self._directory = directory + self.users = py23dict() + self._users_fname = users_fname + self.roles = py23dict() + self._roles_fname = roles_fname + self._mtimes = py23dict() + self._pending_reg_fname = pending_reg_fname + self.pending_registrations = py23dict() + if initialize: + self._initialize_storage() + self._refresh() # load users and roles + + def _initialize_storage(self): + """Create empty JSON files""" + self._savejson(self._users_fname, {}) + self._savejson(self._roles_fname, {}) + self._savejson(self._pending_reg_fname, {}) + + def _refresh(self): + """Load users and roles from JSON files, if needed""" + self._loadjson(self._users_fname, self.users) + self._loadjson(self._roles_fname, self.roles) + self._loadjson(self._pending_reg_fname, self.pending_registrations) + + def _loadjson(self, fname, dest): + """Load JSON file located under self._directory, if needed + + :param fname: short file name (without path and .json) + :type fname: str. + :param dest: destination + :type dest: dict + """ + try: + fname = "%s/%s.json" % (self._directory, fname) + mtime = os.stat(fname).st_mtime + + if self._mtimes.get(fname, 0) == mtime: + # no need to reload the file: the mtime has not been changed + return + + with open(fname) as f: + json_data = f.read() + except Exception as e: + raise BackendIOException("Unable to read json file %s: %s" % (fname, e)) + + try: + json_obj = json.loads(json_data) + dest.clear() + dest.update(json_obj) + self._mtimes[fname] = os.stat(fname).st_mtime + except Exception as e: + raise BackendIOException("Unable to parse JSON data from %s: %s" \ + % (fname, e)) + + def _savejson(self, fname, obj): + """Save obj in JSON format in a file in self._directory""" + fname = "%s/%s.json" % (self._directory, fname) + try: + with open("%s.tmp" % fname, 'w') as f: + json.dump(obj, f, cls=BytesEncoder) + f.flush() + shutil.move("%s.tmp" % fname, fname) + except Exception as e: + raise BackendIOException("Unable to save JSON file %s: %s" \ + % (fname, e)) + + def save_users(self): + """Save users in a JSON file""" + self._savejson(self._users_fname, self.users) + + def save_roles(self): + """Save roles in a JSON file""" + self._savejson(self._roles_fname, self.roles) + + def save_pending_registrations(self): + """Save pending registrations in a JSON file""" + self._savejson(self._pending_reg_fname, self.pending_registrations) diff --git a/libs/cork/mongodb_backend.py b/libs/cork/mongodb_backend.py new file mode 100644 index 000000000..564ed1ff4 --- /dev/null +++ b/libs/cork/mongodb_backend.py @@ -0,0 +1,180 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt + +""" +.. module:: mongodb_backend + :synopsis: MongoDB storage backend. +""" +from logging import getLogger +log = getLogger(__name__) + +from .base_backend import Backend, Table + +try: + import pymongo + is_pymongo_2 = (pymongo.version_tuple[0] == 2) +except ImportError: # pragma: no cover + pass + + +class MongoTable(Table): + """Abstract MongoDB Table. + Allow dictionary-like access. + """ + def __init__(self, name, key_name, collection): + self._name = name + self._key_name = key_name + self._coll = collection + + def create_index(self): + """Create collection index.""" + self._coll.create_index( + self._key_name, + drop_dups=True, + unique=True, + ) + + def __len__(self): + return self._coll.count() + + def __contains__(self, value): + r = self._coll.find_one({self._key_name: value}) + return r is not None + + def __iter__(self): + """Iter on dictionary keys""" + if is_pymongo_2: + r = self._coll.find(fields=[self._key_name,]) + else: + r = self._coll.find(projection=[self._key_name,]) + + return (i[self._key_name] for i in r) + + def iteritems(self): + """Iter on dictionary items. + + :returns: generator of (key, value) tuples + """ + r = self._coll.find() + for i in r: + d = i.copy() + d.pop(self._key_name) + d.pop('_id') + yield (i[self._key_name], d) + + def pop(self, key_val): + """Remove a dictionary item""" + r = self[key_val] + self._coll.remove({self._key_name: key_val}, w=1) + return r + + +class MongoSingleValueTable(MongoTable): + """MongoDB table accessible as a simple key -> value dictionary. + Used to store roles. + """ + # Values are stored in a MongoDB "column" named "val" + def __init__(self, *args, **kw): + super(MongoSingleValueTable, self).__init__(*args, **kw) + + def __setitem__(self, key_val, data): + assert not isinstance(data, dict) + spec = {self._key_name: key_val} + data = {self._key_name: key_val, 'val': data} + if is_pymongo_2: + self._coll.update(spec, {'$set': data}, upsert=True, w=1) + else: + self._coll.update_one(spec, {'$set': data}, upsert=True) + + def __getitem__(self, key_val): + r = self._coll.find_one({self._key_name: key_val}) + if r is None: + raise KeyError(key_val) + + return r['val'] + +class MongoMutableDict(dict): + """Represent an item from a Table. Acts as a dictionary. + """ + def __init__(self, parent, root_key, d): + """Create a MongoMutableDict instance. + :param parent: Table instance + :type parent: :class:`MongoTable` + """ + super(MongoMutableDict, self).__init__(d) + self._parent = parent + self._root_key = root_key + + def __setitem__(self, k, v): + super(MongoMutableDict, self).__setitem__(k, v) + spec = {self._parent._key_name: self._root_key} + if is_pymongo_2: + r = self._parent._coll.update(spec, {'$set': {k: v}}, upsert=True) + else: + r = self._parent._coll.update_one(spec, {'$set': {k: v}}, upsert=True) + + + +class MongoMultiValueTable(MongoTable): + """MongoDB table accessible as a dictionary. + """ + def __init__(self, *args, **kw): + super(MongoMultiValueTable, self).__init__(*args, **kw) + + def __setitem__(self, key_val, data): + assert isinstance(data, dict) + key_name = self._key_name + if key_name in data: + assert data[key_name] == key_val + else: + data[key_name] = key_val + + spec = {key_name: key_val} + if u'_id' in data: + del(data[u'_id']) + + if is_pymongo_2: + self._coll.update(spec, {'$set': data}, upsert=True, w=1) + else: + self._coll.update_one(spec, {'$set': data}, upsert=True) + + def __getitem__(self, key_val): + r = self._coll.find_one({self._key_name: key_val}) + if r is None: + raise KeyError(key_val) + + return MongoMutableDict(self, key_val, r) + + +class MongoDBBackend(Backend): + def __init__(self, db_name='cork', hostname='localhost', port=27017, initialize=False, username=None, password=None): + """Initialize MongoDB Backend""" + connection = pymongo.MongoClient(host=hostname, port=port) + db = connection[db_name] + if username and password: + db.authenticate(username, password) + self.users = MongoMultiValueTable('users', 'login', db.users) + self.pending_registrations = MongoMultiValueTable( + 'pending_registrations', + 'pending_registration', + db.pending_registrations + ) + self.roles = MongoSingleValueTable('roles', 'role', db.roles) + + if initialize: + self._initialize_storage() + + def _initialize_storage(self): + """Create MongoDB indexes.""" + for c in (self.users, self.roles, self.pending_registrations): + c.create_index() + + def save_users(self): + pass + + def save_roles(self): + pass + + def save_pending_registrations(self): + pass diff --git a/libs/cork/sessions.py b/libs/cork/sessions.py new file mode 100644 index 000000000..7e5391257 --- /dev/null +++ b/libs/cork/sessions.py @@ -0,0 +1,23 @@ +import json +import base64 +import hmac +from Crypto.Cipher import AES + +def _strcmp(a, b): + """Compares two strings while preventing timing attacks. Execution time + is not affected by lenghth of common prefix on strings of the same length""" + return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) + +class SecureSession(object): + + def __init__(self): + + json() + + + + + + + base64.b64encode(hmac.new(tob(key), msg).digest())): + return pickle.loads(base64.b64decode(msg)) diff --git a/libs/cork/sqlalchemy_backend.py b/libs/cork/sqlalchemy_backend.py new file mode 100644 index 000000000..60d49448a --- /dev/null +++ b/libs/cork/sqlalchemy_backend.py @@ -0,0 +1,204 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt + +""" +.. module:: sqlalchemy_backend + :synopsis: SQLAlchemy storage backend. +""" + +import sys +from logging import getLogger + +from . import base_backend + +log = getLogger(__name__) +is_py3 = (sys.version_info.major == 3) + +try: + from sqlalchemy import create_engine, delete, select, \ + Column, ForeignKey, Integer, MetaData, String, Table, Unicode + sqlalchemy_available = True +except ImportError: # pragma: no cover + sqlalchemy_available = False + + +class SqlRowProxy(dict): + def __init__(self, sql_dict, key, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.sql_dict = sql_dict + self.key = key + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + if self.sql_dict is not None: + self.sql_dict[self.key] = {key: value} + + +class SqlTable(base_backend.Table): + """Provides dictionary-like access to an SQL table.""" + + def __init__(self, engine, table, key_col_name): + self._engine = engine + self._table = table + self._key_col = table.c[key_col_name] + + def _row_to_value(self, row): + row_key = row[self._key_col] + row_value = SqlRowProxy(self, row_key, + ((k, row[k]) for k in row.keys() if k != self._key_col.name)) + return row_key, row_value + + def __len__(self): + query = self._table.count() + c = self._engine.execute(query).scalar() + return int(c) + + def __contains__(self, key): + query = select([self._key_col], self._key_col == key) + row = self._engine.execute(query).fetchone() + return row is not None + + def __setitem__(self, key, value): + if key in self: + values = value + query = self._table.update().where(self._key_col == key) + + else: + values = {self._key_col.name: key} + values.update(value) + query = self._table.insert() + + self._engine.execute(query.values(**values)) + + def __getitem__(self, key): + query = select([self._table], self._key_col == key) + row = self._engine.execute(query).fetchone() + if row is None: + raise KeyError(key) + return self._row_to_value(row)[1] + + def __iter__(self): + """Iterate over table index key values""" + query = select([self._key_col]) + result = self._engine.execute(query) + for row in result: + key = row[0] + yield key + + def iteritems(self): + """Iterate over table rows""" + query = select([self._table]) + result = self._engine.execute(query) + for row in result: + key = row[0] + d = self._row_to_value(row)[1] + yield (key, d) + + def pop(self, key): + query = select([self._table], self._key_col == key) + row = self._engine.execute(query).fetchone() + if row is None: + raise KeyError + + query = delete(self._table, self._key_col == key) + self._engine.execute(query) + return row + + def insert(self, d): + query = self._table.insert(d) + self._engine.execute(query) + log.debug("%s inserted" % repr(d)) + + def empty_table(self): + query = self._table.delete() + self._engine.execute(query) + log.info("Table purged") + + +class SqlSingleValueTable(SqlTable): + def __init__(self, engine, table, key_col_name, col_name): + SqlTable.__init__(self, engine, table, key_col_name) + self._col_name = col_name + + def _row_to_value(self, row): + return row[self._key_col], row[self._col_name] + + def __setitem__(self, key, value): + SqlTable.__setitem__(self, key, {self._col_name: value}) + + + +class SqlAlchemyBackend(base_backend.Backend): + + def __init__(self, db_full_url, users_tname='users', roles_tname='roles', + pending_reg_tname='register', initialize=False): + + if not sqlalchemy_available: + raise RuntimeError("The SQLAlchemy library is not available.") + + self._metadata = MetaData() + if initialize: + # Create new database if needed. + db_url, db_name = db_full_url.rsplit('/', 1) + if is_py3 and db_url.startswith('mysql'): + print("WARNING: MySQL is not supported under Python3") + + self._engine = create_engine(db_url, encoding='utf-8') + try: + self._engine.execute("CREATE DATABASE %s" % db_name) + except Exception as e: + log.info("Failed DB creation: %s" % e) + + # SQLite in-memory database URL: "sqlite://:memory:" + if db_name != ':memory:' and not db_url.startswith('postgresql'): + self._engine.execute("USE %s" % db_name) + + else: + self._engine = create_engine(db_full_url, encoding='utf-8') + + + self._users = Table(users_tname, self._metadata, + Column('username', Unicode(128), primary_key=True), + Column('role', ForeignKey(roles_tname + '.role')), + Column('hash', String(256), nullable=False), + Column('email_addr', String(128)), + Column('desc', String(128)), + Column('creation_date', String(128), nullable=False), + Column('last_login', String(128), nullable=False) + + ) + self._roles = Table(roles_tname, self._metadata, + Column('role', String(128), primary_key=True), + Column('level', Integer, nullable=False) + ) + self._pending_reg = Table(pending_reg_tname, self._metadata, + Column('code', String(128), primary_key=True), + Column('username', Unicode(128), nullable=False), + Column('role', ForeignKey(roles_tname + '.role')), + Column('hash', String(256), nullable=False), + Column('email_addr', String(128)), + Column('desc', String(128)), + Column('creation_date', String(128), nullable=False) + ) + + self.users = SqlTable(self._engine, self._users, 'username') + self.roles = SqlSingleValueTable(self._engine, self._roles, 'role', 'level') + self.pending_registrations = SqlTable(self._engine, self._pending_reg, 'code') + + if initialize: + self._initialize_storage(db_name) + log.debug("Tables created") + + + def _initialize_storage(self, db_name): + self._metadata.create_all(self._engine) + + def _drop_all_tables(self): + for table in reversed(self._metadata.sorted_tables): + log.info("Dropping table %s" % repr(table.name)) + self._engine.execute(table.delete()) + + def save_users(self): pass + def save_roles(self): pass + def save_pending_registrations(self): pass diff --git a/libs/cork/sqlite_backend.py b/libs/cork/sqlite_backend.py new file mode 100644 index 000000000..a15b33283 --- /dev/null +++ b/libs/cork/sqlite_backend.py @@ -0,0 +1,242 @@ +# Cork - Authentication module for the Bottle web framework +# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. +# Released under LGPLv3+ license, see LICENSE.txt + +""" +.. module:: sqlite_backend + :synopsis: SQLite storage backend. +""" + +from . import base_backend +from logging import getLogger +log = getLogger(__name__) + + +class SqlRowProxy(dict): + def __init__(self, table, key, row): + li = ((k, v) for (k, ktype), v in zip(table._columns[1:], row[1:])) + dict.__init__(self, li) + self._table = table + self._key = key + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + self._table[self._key] = self + + +class Table(base_backend.Table): + """Provides dictionary-like access to an SQL table.""" + + def __init__(self, backend, table_name): + self._backend = backend + self._engine = backend.connection + self._table_name = table_name + self._column_names = [n for n, t in self._columns] + self._key_col_num = 0 + self._key_col_name = self._column_names[self._key_col_num] + self._key_col = self._column_names[self._key_col_num] + + def _row_to_value(self, key, row): + assert isinstance(row, tuple) + row_key = row[self._key_col_num] + row_value = SqlRowProxy(self, key, row) + return row_key, row_value + + def __len__(self): + query = "SELECT count() FROM %s" % self._table_name + ret = self._backend.run_query(query) + return ret.fetchone()[0] + + def __contains__(self, key): + #FIXME: count() + query = "SELECT * FROM %s WHERE %s='%s'" % \ + (self._table_name, self._key_col, key) + row = self._backend.fetch_one(query) + return row is not None + + def __setitem__(self, key, value): + """Create or update a row""" + assert isinstance(value, dict) + v, cn = set(value), set(self._column_names[1:]) + assert not v - cn, repr(v - cn) + assert not cn - v, repr(cn - v) + + assert set(value) == set(self._column_names[1:]), "%s %s" % \ + (repr(set(value)), repr(set(self._column_names[1:]))) + + col_values = [key] + [value[k] for k in self._column_names[1:]] + + col_names = ', '.join(self._column_names) + question_marks = ', '.join('?' for x in col_values) + query = "INSERT OR REPLACE INTO %s (%s) VALUES (%s)" % \ + (self._table_name, col_names, question_marks) + + ret = self._backend.run_query_using_conversion(query, col_values) + + + def __getitem__(self, key): + query = "SELECT * FROM %s WHERE %s='%s'" % \ + (self._table_name, self._key_col, key) + row = self._backend.fetch_one(query) + if row is None: + raise KeyError(key) + + return self._row_to_value(key, row)[1] + #return dict(zip(self._column_names, row)) + + def __iter__(self): + """Iterate over table index key values""" + query = "SELECT %s FROM %s" % (self._key_col, self._table_name) + result = self._backend.run_query(query) + for row in result: + yield row[0] + + def iteritems(self): + """Iterate over table rows""" + query = "SELECT * FROM %s" % self._table_name + result = self._backend.run_query(query) + for row in result: + d = dict(zip(self._column_names, row)) + d.pop(self._key_col) + + yield (self._key_col, d) + + def pop(self, key): + d = self.__getitem__(key) + query = "DELETE FROM %s WHERE %s='%s'" % \ + (self._table_name, self._key_col, key) + self._backend.fetch_one(query) + #FIXME: check deletion + return d + + def insert(self, d): + raise NotImplementedError + + def empty_table(self): + raise NotImplementedError + + def create_table(self): + """Issue table creation""" + cc = [] + for col_name, col_type in self._columns: + if col_type == int: + col_type = 'INTEGER' + elif col_type == str: + col_type = 'TEXT' + + if col_name == self._key_col: + extras = 'PRIMARY KEY ASC' + else: + extras = '' + + cc.append("%s %s %s" % (col_name, col_type, extras)) + + cc = ','.join(cc) + query = "CREATE TABLE %s (%s)" % (self._table_name, cc) + self._backend.run_query(query) + + +class SingleValueTable(Table): + def __init__(self, *args): + super(SingleValueTable, self).__init__(*args) + self._value_col = self._column_names[1] + + def __setitem__(self, key, value): + """Create or update a row""" + assert not isinstance(value, dict) + query = "INSERT OR REPLACE INTO %s (%s, %s) VALUES (?, ?)" % \ + (self._table_name, self._key_col, self._value_col) + + col_values = (key, value) + ret = self._backend.run_query_using_conversion(query, col_values) + + def __getitem__(self, key): + query = "SELECT %s FROM %s WHERE %s='%s'" % \ + (self._value_col, self._table_name, self._key_col, key) + row = self._backend.fetch_one(query) + if row is None: + raise KeyError(key) + + return row[0] + +class UsersTable(Table): + def __init__(self, *args, **kwargs): + self._columns = ( + ('username', str), + ('role', str), + ('hash', str), + ('email_addr', str), + ('desc', str), + ('creation_date', str), + ('last_login', str) + ) + super(UsersTable, self).__init__(*args, **kwargs) + +class RolesTable(SingleValueTable): + def __init__(self, *args, **kwargs): + self._columns = ( + ('role', str), + ('level', int) + ) + super(RolesTable, self).__init__(*args, **kwargs) + +class PendingRegistrationsTable(Table): + def __init__(self, *args, **kwargs): + self._columns = ( + ('code', str), + ('username', str), + ('role', str), + ('hash', str), + ('email_addr', str), + ('desc', str), + ('creation_date', str) + ) + super(PendingRegistrationsTable, self).__init__(*args, **kwargs) + + + + +class SQLiteBackend(base_backend.Backend): + + def __init__(self, filename, users_tname='users', roles_tname='roles', + pending_reg_tname='register', initialize=False): + + self._filename = filename + + self.users = UsersTable(self, users_tname) + self.roles = RolesTable(self, roles_tname) + self.pending_registrations = PendingRegistrationsTable(self, pending_reg_tname) + + if initialize: + self.users.create_table() + self.roles.create_table() + self.pending_registrations.create_table() + log.debug("Tables created") + + @property + def connection(self): + try: + return self._connection + except AttributeError: + import sqlite3 + self._connection = sqlite3.connect(self._filename) + return self._connection + + def run_query(self, query): + return self._connection.execute(query) + + def run_query_using_conversion(self, query, args): + return self._connection.execute(query, args) + + def fetch_one(self, query): + return self._connection.execute(query).fetchone() + + def _initialize_storage(self, db_name): + raise NotImplementedError + + def _drop_all_tables(self): + raise NotImplementedError + + def save_users(self): pass + def save_roles(self): pass + def save_pending_registrations(self): pass diff --git a/requirements.txt b/requirements.txt index 0d30ee934..8bd5f6f6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ urllib3<1.23,>=1.21.1 waitress>=1.1.0 configparser>=3.5.0 backports.functools-lru-cache>=1.5 +beaker>=1.10.0 #Subliminal requirements click>=6.7 diff --git a/views/login.tpl b/views/login.tpl new file mode 100644 index 000000000..c900c5f23 --- /dev/null +++ b/views/login.tpl @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + Login - Bazarr + + + + + +
+
+

+ +

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ + % if msg == 'fail': + + % end +
+
+
+ + + + + + \ No newline at end of file diff --git a/views/settings.tpl b/views/settings.tpl index c6c19f57a..2cf51b433 100644 --- a/views/settings.tpl +++ b/views/settings.tpl @@ -42,7 +42,7 @@ % include('menu.tpl')
-
+

Some fields are in error and you can't save settings until you have corrected them. Be sure to check in every tabs.

@@ -191,14 +191,17 @@
- +
-
-
- +
+
-
+