Merge branch 'development'

pull/26/head
Louis Vézina 7 years ago
commit 245d480931

@ -1,9 +1,12 @@
bazarr_version = '0.1.1' # coding: utf-8
from __future__ import unicode_literals
bazarr_version = '0.1.4'
from bottle import route, run, template, static_file, request, redirect from bottle import route, run, template, static_file, request, redirect
import bottle import bottle
#bottle.debug(True) bottle.debug(True)
#bottle.TEMPLATES.clear() bottle.TEMPLATES.clear()
import os import os
bottle.TEMPLATE_PATH.insert(0,os.path.join(os.path.dirname(__file__), 'views/')) bottle.TEMPLATE_PATH.insert(0,os.path.join(os.path.dirname(__file__), 'views/'))
@ -292,16 +295,6 @@ def check_update():
@route(base_url + 'system') @route(base_url + 'system')
def system(): def system():
db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
c = db.cursor()
c.execute("SELECT * FROM table_scheduler")
tasks = c.fetchall()
c.close()
logs = []
for line in reversed(open(os.path.join(os.path.dirname(__file__), 'data/log/bazarr.log')).readlines()):
logs.append(line.rstrip())
def get_time_from_interval(interval): def get_time_from_interval(interval):
interval_clean = interval.split('[') interval_clean = interval.split('[')
interval_clean = interval_clean[1][:-1] interval_clean = interval_clean[1][:-1]
@ -387,8 +380,26 @@ def system():
task_list.append([job.name, get_time_from_interval(str(job.trigger)), pretty.date(job.next_run_time.replace(tzinfo=None)), job.id]) task_list.append([job.name, get_time_from_interval(str(job.trigger)), pretty.date(job.next_run_time.replace(tzinfo=None)), job.id])
elif job.trigger.__str__().startswith('cron'): elif job.trigger.__str__().startswith('cron'):
task_list.append([job.name, get_time_from_cron(job.trigger.fields), pretty.date(job.next_run_time.replace(tzinfo=None)), job.id]) task_list.append([job.name, get_time_from_cron(job.trigger.fields), pretty.date(job.next_run_time.replace(tzinfo=None)), job.id])
with open(os.path.join(os.path.dirname(__file__), 'data/log/bazarr.log')) as f:
for i, l in enumerate(f, 1):
pass
row_count = i
max_page = (row_count / 50) + 1
return template('system', tasks=tasks, logs=logs, base_url=base_url, task_list=task_list, bazarr_version=bazarr_version) return template('system', base_url=base_url, task_list=task_list, row_count=row_count, max_page=max_page, bazarr_version=bazarr_version)
@route(base_url + 'logs/<page:int>')
def get_logs(page):
page_size = 50
begin = (page * page_size) - page_size
end = (page * page_size) - 1
logs_complete = []
for line in reversed(open(os.path.join(os.path.dirname(__file__), 'data/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/<taskid>') @route(base_url + 'execute/<taskid>')
def execute_task(taskid): def execute_task(taskid):
@ -414,7 +425,7 @@ def remove_subtitles():
except OSError: except OSError:
pass pass
store_subtitles(episodePath) store_subtitles(episodePath)
list_missing_subtitles(tvdbid) list_missing_subtitles(sonarrSeriesId)
@route(base_url + 'get_subtitle', method='POST') @route(base_url + 'get_subtitle', method='POST')
def get_subtitle(): def get_subtitle():
@ -442,7 +453,7 @@ def get_subtitle():
if result is not None: if result is not None:
history_log(1, sonarrSeriesId, sonarrEpisodeId, result) history_log(1, sonarrSeriesId, sonarrEpisodeId, result)
store_subtitles(episodePath) store_subtitles(episodePath)
list_missing_subtitles(tvdbid) list_missing_subtitles(sonarrSeriesId)
redirect(ref) redirect(ref)
except OSError: except OSError:
pass pass

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
from get_general_settings import * from get_general_settings import *
import os import os
@ -25,7 +28,25 @@ def check_and_apply_update(repo=local_repo, remote_name='origin'):
repo.head.set_target(remote_id) repo.head.set_target(remote_id)
result = 'Bazarr updated to latest version and restarting.' result = 'Bazarr updated to latest version and restarting.'
os.execlp('python', 'python', os.path.join(os.path.dirname(__file__), 'bazarr.py')) os.execlp('python', 'python', os.path.join(os.path.dirname(__file__), 'bazarr.py'))
else: # We can just do it normally
raise AssertionError('Unknown merge analysis result') elif merge_result & pygit2.GIT_MERGE_ANALYSIS_NORMAL:
repo.merge(remote_id)
print repo.index.conflicts
assert repo.index.conflicts is None, 'Conflicts, ahhhh!'
user = repo.default_signature
tree = repo.index.write_tree()
commit = repo.create_commit('HEAD',
user,
user,
'Merge!',
tree,
[repo.head.target, remote_id])
repo.state_cleanup()
result = 'Conflict detected when trying to update.'
os.execlp('python', 'python', os.path.join(os.path.dirname(__file__), 'bazarr.py'))
# We can't do it
else:
result = 'Bazarr cannot be updated: Unknown merge analysis result'
return result return result

@ -41,11 +41,6 @@ CREATE TABLE "table_settings_general" (
`auto_update` INTEGER `auto_update` INTEGER
); );
INSERT INTO `table_settings_general` (ip,port,base_url,path_mapping,log_level, branch, auto_update) VALUES ('0.0.0.0',6767,'/',Null,'INFO','master','True'); INSERT INTO `table_settings_general` (ip,port,base_url,path_mapping,log_level, branch, auto_update) VALUES ('0.0.0.0',6767,'/',Null,'INFO','master','True');
CREATE TABLE `table_scheduler` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`name` TEXT NOT NULL,
`frequency` TEXT NOT NULL
);
CREATE TABLE "table_history" ( CREATE TABLE "table_history" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`action` INTEGER NOT NULL, `action` INTEGER NOT NULL,

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import os import os
import sqlite3 import sqlite3
import requests import requests
@ -36,7 +39,7 @@ def update_all_episodes():
url_sonarr_api_episode = protocol_sonarr + "://" + config_sonarr[0] + ":" + str(config_sonarr[1]) + base_url_sonarr + "/api/episode?seriesId=" + str(seriesId[0]) + "&apikey=" + apikey_sonarr url_sonarr_api_episode = protocol_sonarr + "://" + config_sonarr[0] + ":" + str(config_sonarr[1]) + base_url_sonarr + "/api/episode?seriesId=" + str(seriesId[0]) + "&apikey=" + apikey_sonarr
r = requests.get(url_sonarr_api_episode) r = requests.get(url_sonarr_api_episode)
for episode in r.json(): for episode in r.json():
if episode['hasFile']: if episode['hasFile'] and episode['episodeFile']['size'] > 20480:
# Add shows in Sonarr to current shows list # Add shows in Sonarr to current shows list
current_episodes_sonarr.append(episode['id']) current_episodes_sonarr.append(episode['id'])
@ -60,15 +63,6 @@ def update_all_episodes():
# Close database connection # Close database connection
c.close() c.close()
#Cleanup variables to free memory
del current_episodes_db
del current_episodes_db_list
del seriesIdList
del r
del current_episodes_sonarr
del deleted_items
del c
# Store substitles for all episodes # Store substitles for all episodes
full_scan_subtitles() full_scan_subtitles()
@ -110,7 +104,7 @@ def add_new_episodes():
url_sonarr_api_episode = protocol_sonarr + "://" + config_sonarr[0] + ":" + str(config_sonarr[1]) + base_url_sonarr + "/api/episode?seriesId=" + str(seriesId[0]) + "&apikey=" + apikey_sonarr url_sonarr_api_episode = protocol_sonarr + "://" + config_sonarr[0] + ":" + str(config_sonarr[1]) + base_url_sonarr + "/api/episode?seriesId=" + str(seriesId[0]) + "&apikey=" + apikey_sonarr
r = requests.get(url_sonarr_api_episode) r = requests.get(url_sonarr_api_episode)
for episode in r.json(): for episode in r.json():
if episode['hasFile']: if episode['hasFile'] and episode['episodeFile']['size'] > 20480:
# Add shows in Sonarr to current shows list # Add shows in Sonarr to current shows list
current_episodes_sonarr.append(episode['id']) current_episodes_sonarr.append(episode['id'])
@ -132,15 +126,6 @@ def add_new_episodes():
# Close database connection # Close database connection
c.close() c.close()
#Cleanup variables to free memory
del current_episodes_db
del current_episodes_db_list
del seriesIdList
del r
del current_episodes_sonarr
del deleted_items
del c
# Store substitles from episodes we've just added # Store substitles from episodes we've just added
new_scan_subtitles() new_scan_subtitles()

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import sqlite3 import sqlite3
import os import os
import ast import ast
@ -27,27 +30,23 @@ branch = general_settings[5]
automatic = general_settings[6] automatic = general_settings[6]
def path_replace(path): def path_replace(path):
for path_mapping in path_mappings: for path_mapping in path_mappings:
path = path.replace(path_mapping[0], path_mapping[1], 1) if path_mapping[0] in path:
path = path.replace(path_mapping[0], path_mapping[1])
if '\\' in path: if path.startswith('\\\\'):
path = path.replace('/', '\\') path = path.replace('/', '\\')
elif path.startswith('/'):
return path path = path.replace('\\', '/')
break
return path
def path_replace_reverse(path): def path_replace_reverse(path):
for path_mapping in path_mappings: for path_mapping in path_mappings:
if path.startswith('\\\\\\\\'): if path_mapping[1] in path:
if '\\\\' in path: path = path.replace(path_mapping[1], path_mapping[0])
path = path.replace('\\\\', '\\') if path.startswith('\\\\'):
elif '\\' in path: path = path.replace('/', '\\')
path = path.replace('\\\\', '\\') elif path.startswith('/'):
path = path.replace('\\', '/')
path = path.replace(path_mapping[1], path_mapping[0], 1) break
elif path.startswith('\\\\'): return path
path = path.replace(path_mapping[1], path_mapping[0], 1)
if '\\' in path:
path = path.replace('\\', '/')
return path

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import sqlite3 import sqlite3
import pycountry import pycountry
import os import os

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import sqlite3 import sqlite3
import os import os
from subliminal import * from subliminal import *

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import os import os
import sqlite3 import sqlite3
import requests import requests

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import sqlite3 import sqlite3
import os import os
import ast import ast

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import os import os
import sqlite3 import sqlite3
import ast import ast
@ -26,20 +29,10 @@ def download_subtitle(path, language, hi, providers):
except: except:
return None return None
del video
del best_subtitles
try:
del result
del downloaded_provider
del downloaded_language
del message
except:
pass
def series_download_subtitles(no): def series_download_subtitles(no):
conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db')) conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
c_db = conn_db.cursor() c_db = conn_db.cursor()
episodes_details = c_db.execute("SELECT path, missing_subtitles, sonarrEpisodeId FROM table_episodes WHERE path = ?", (no,)).fetchall() episodes_details = c_db.execute("SELECT path, missing_subtitles, sonarrEpisodeId FROM table_episodes WHERE sonarrSeriesId = ?", (no,)).fetchall()
series_details = c_db.execute("SELECT hearing_impaired FROM table_shows WHERE sonarrSeriesId = ?", (no,)).fetchone() series_details = c_db.execute("SELECT hearing_impaired FROM table_shows WHERE sonarrSeriesId = ?", (no,)).fetchone()
enabled_providers = c_db.execute("SELECT name FROM table_settings_providers WHERE enabled = 1").fetchall() enabled_providers = c_db.execute("SELECT name FROM table_settings_providers WHERE enabled = 1").fetchall()
c_db.close() c_db.close()
@ -75,16 +68,6 @@ def wanted_download_subtitles(path):
list_missing_subtitles(episode[3]) list_missing_subtitles(episode[3])
history_log(1, episode[3], episode[2], message) history_log(1, episode[3], episode[2], message)
del conn_db
del c_db
del episodes_details
del enabled_providers
del providers_list
try:
del message
except:
pass
def wanted_search_missing_subtitles(): def wanted_search_missing_subtitles():
db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db')) db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
db.create_function("path_substitution", 1, path_replace) db.create_function("path_substitution", 1, path_replace)
@ -96,7 +79,3 @@ def wanted_search_missing_subtitles():
for episode in data: for episode in data:
wanted_download_subtitles(episode[0]) wanted_download_subtitles(episode[0])
del db
del c
del data

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import os import os
import sqlite3 import sqlite3

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import os import os
import enzyme import enzyme
import babelfish import babelfish
@ -49,18 +52,17 @@ def store_subtitles(file):
except: except:
pass pass
conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
c_db = conn_db.cursor()
subtitles = core.search_external_subtitles(file) subtitles = core.search_external_subtitles(file)
for subtitle, language in subtitles.iteritems(): for subtitle, language in subtitles.iteritems():
actual_subtitles.append([str(language), path_replace_reverse(os.path.join(os.path.dirname(file), subtitle))]) actual_subtitles.append([str(language), path_replace_reverse(os.path.join(os.path.dirname(file), subtitle))])
try:
c_db.execute("UPDATE table_episodes SET subtitles = ? WHERE path = ?", (str(actual_subtitles), path_replace_reverse(file))) conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
conn_db.commit() c_db = conn_db.cursor()
except:
pass c_db.execute("UPDATE table_episodes SET subtitles = ? WHERE path = ?", (str(actual_subtitles), path_replace_reverse(file)))
conn_db.commit()
c_db.close() c_db.close()
return actual_subtitles return actual_subtitles
@ -68,7 +70,7 @@ def store_subtitles(file):
def list_missing_subtitles(*no): def list_missing_subtitles(*no):
query_string = '' query_string = ''
try: try:
query_string = " WHERE table_shows.tvdbId = " + str(no[0]) query_string = " WHERE table_shows.sonarrSeriesId = " + str(no[0])
except: except:
pass pass
conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db')) conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
@ -107,7 +109,7 @@ def full_scan_subtitles():
c_db.close() c_db.close()
for episode in episodes: for episode in episodes:
store_subtitles(path_replace(episode[0].encode('utf-8'))) store_subtitles(path_replace(episode[0]))
def series_scan_subtitles(no): def series_scan_subtitles(no):
conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db')) conn_db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'))
@ -116,7 +118,7 @@ def series_scan_subtitles(no):
c_db.close() c_db.close()
for episode in episodes: for episode in episodes:
store_subtitles(path_replace(episode[0].encode('utf-8'))) store_subtitles(path_replace(episode[0]))
list_missing_subtitles(no) list_missing_subtitles(no)
@ -127,4 +129,4 @@ def new_scan_subtitles():
c_db.close() c_db.close()
for episode in episodes: for episode in episodes:
store_subtitles(path_replace(episode[0].encode('utf-8'))) store_subtitles(path_replace(episode[0]))

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
from get_general_settings import * from get_general_settings import *
from get_series import * from get_series import *
from get_episodes import * from get_episodes import *

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
from get_general_settings import * from get_general_settings import *
import git import git

@ -1,3 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
import os import os
import sqlite3 import sqlite3
import time import time

@ -40,6 +40,8 @@
.fast.backward, .backward, .forward, .fast.forward { .fast.backward, .backward, .forward, .fast.forward {
cursor: pointer; cursor: pointer;
} }
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
</style> </style>
</head> </head>
<body> <body>

@ -0,0 +1,87 @@
<html>
<head>
<!DOCTYPE html>
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
<script src="{{base_url}}static/semantic/semantic.min.js"></script>
<script src="{{base_url}}static/jquery/tablesort.js"></script>
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.min.css">
<style>
body {
background-color: #272727;
}
</style>
</head>
<body>
<div id='logs_loader' class="ui page dimmer">
<div class="ui indeterminate text loader">Loading...</div>
</div>
<div class="content">
<table class="ui very basic selectable table">
<thead>
<tr>
<th class="collapsing"></th>
<th>Message</th>
<th class="collapsing">Time</th>
</tr>
</thead>
<tbody>
%import time
%import datetime
%import pretty
%for log in logs:
%line = []
%line = log.split('|')
<tr class='log' data-message='{{line[2]}}' data-exception='{{line[3].replace("\\n", "<br />")}}'>
<td class="collapsing"><i class="\\
%if line[1] == 'INFO':
blue info circle \\
%elif line[1] == 'WARNING':
yellow warning circle \\
%elif line[1] == 'ERROR':
red bug \\
%end
icon"></i></td>
<td>{{line[2]}}</td>
<td title='{{line[0]}}' class="collapsing">{{pretty.date(int(time.mktime(datetime.datetime.strptime(line[0], "%d/%m/%Y %H:%M:%S").timetuple())))}}</td>
</tr>
%end
</tbody>
</table>
</div>
<div class="ui small modal">
<i class="close icon"></i>
<div class="header">
<div>Details</div>
</div>
<div class="content">
Message
<div id='message' class="ui segment">
<p></p>
</div>
Exception
<div id='exception' class="ui segment">
<p></p>
</div>
</div>
<div class="actions">
<button class="ui cancel button" >Close</button>
</div>
</div>
</body>
</html>
<script>
$('.modal')
.modal({
autofocus: false
})
;
$('.log').click(function(){
$("#message").html($(this).data("message"));
$("#exception").html($(this).data("exception"));
$('.small.modal').modal('show');
})
</script>

@ -37,6 +37,11 @@
margin-bottom: 3em; margin-bottom: 3em;
padding: 1em; padding: 1em;
} }
.fast.backward, .backward, .forward, .fast.forward {
cursor: pointer;
}
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
</style> </style>
</head> </head>
<body> <body>
@ -107,86 +112,78 @@
</div> </div>
<div class="ui bottom attached tab segment" data-tab="logs"> <div class="ui bottom attached tab segment" data-tab="logs">
<div class="content"> <div class="content">
<table class="ui very basic selectable table"> <div id="logs"></div>
<thead>
<tr> <div class="ui grid">
<th class="collapsing"></th> <div class="three column row">
<th>Message</th> <div class="column"></div>
<th class="collapsing">Time</th> <div class="center aligned column">
</tr> <i class="fast backward icon"></i>
</thead> <i class="backward icon"></i>
<tbody> <span id="page"></span> / {{max_page}}
%import time <i class="forward icon"></i>
%import datetime <i class="fast forward icon"></i>
%import pretty </div>
%for log in logs: <div class="right floated right aligned column">Total records: {{row_count}}</div>
%line = [] </div>
%line = log.split('|') </div>
<tr class='log' data-message='{{line[2]}}' data-exception='{{line[3].replace("\\n", "<br />")}}'>
<td class="collapsing"><i class="\\
%if line[1] == 'INFO':
blue info circle \\
%elif line[1] == 'WARNING':
yellow warning circle \\
%elif line[1] == 'ERROR':
red bug \\
%end
icon"></i></td>
<td>{{line[2]}}</td>
<td title='{{line[0]}}' class="collapsing">{{pretty.date(int(time.mktime(datetime.datetime.strptime(line[0], "%d/%m/%Y %H:%M:%S").timetuple())))}}</td>
</tr>
%end
</tbody>
</table>
</div> </div>
</div> </div>
<div class="ui bottom attached tab segment" data-tab="about"> <div class="ui bottom attached tab segment" data-tab="about">
Bazarr version: {{bazarr_version}} Bazarr version: {{bazarr_version}}
</div> </div>
</div> </div>
<div class="ui small modal">
<i class="close icon"></i>
<div class="header">
<div>Details</div>
</div>
<div class="content">
Message
<div id='message' class="ui segment">
<p></p>
</div>
Exception
<div id='exception' class="ui segment">
<p></p>
</div>
</div>
<div class="actions">
<button class="ui cancel button" >Close</button>
</div>
</div>
</body> </body>
</html> </html>
<script> <script>
$('.modal')
.modal({
autofocus: false
})
;
$('.menu .item') $('.menu .item')
.tab() .tab()
; ;
$('.execute').click(function(){ function loadURL(page) {
window.location = '{{base_url}}execute/' + $(this).data("taskid"); $.ajax({
url: "{{base_url}}logs/" + page,
beforeSend: function() { $('#loader').addClass('active'); },
complete: function() { $('#loader').removeClass('active'); },
cache: false
}).done(function(data) {
$("#logs").html(data);
});
current_page = page;
$("#page").text(current_page);
if (current_page == 1) {
$(".backward, .fast.backward").addClass("disabled");
}
if (current_page == {{int(max_page)}}) {
$(".forward, .fast.forward").addClass("disabled");
}
if (current_page > 1 && current_page < {{int(max_page)}}) {
$(".backward, .fast.backward").removeClass("disabled");
$(".forward, .fast.forward").removeClass("disabled");
}
}
loadURL(1);
$('.backward').click(function(){
loadURL(current_page - 1);
})
$('.fast.backward').click(function(){
loadURL(1);
})
$('.forward').click(function(){
loadURL(current_page + 1);
})
$('.fast.forward').click(function(){
loadURL({{int(max_page)}});
}) })
$('.log').click(function(){ $('.execute').click(function(){
$("#message").html($(this).data("message")); window.location = '{{base_url}}execute/' + $(this).data("taskid");
$("#exception").html($(this).data("exception"));
$('.small.modal').modal('show');
}) })
$('a:not(.tabs), button:not(.cancel)').click(function(){ $('a:not(.tabs), button:not(.cancel)').click(function(){

@ -43,6 +43,8 @@
.fast.backward, .backward, .forward, .fast.forward { .fast.backward, .backward, .forward, .fast.forward {
cursor: pointer; cursor: pointer;
} }
.fast.backward, .backward, .forward, .fast.forward { pointer-events: auto; }
.fast.backward.disabled, .backward.disabled, .forward.disabled, .fast.forward.disabled { pointer-events: none; }
</style> </style>
</head> </head>
<body> <body>

Loading…
Cancel
Save