Add option to upload or browse for a database file to import

This commit is contained in:
JonnyWong16 2020-05-03 14:33:25 -07:00
parent a869859491
commit 980c4f7618
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
6 changed files with 316 additions and 79 deletions

View file

@ -5,75 +5,97 @@
<h4 class="modal-title">Import ${app} Database</h4> <h4 class="modal-title">Import ${app} Database</h4>
</div> </div>
<div class="modal-body" id="modal-text"> <div class="modal-body" id="modal-text">
<input type="hidden" id="import_app" name="import_app" value="${app.lower()}" /> <form id="import_database" enctype="multipart/form-data" method="post" name="import_database">
% if app in ('PlexWatch', 'Plexivity'): <input type="hidden" id="import_app" name="import_app" value="${app.lower()}" />
<p class="help-block"> % if app in ('PlexWatch', 'Plexivity'):
<% <p class="help-block">
v = '' <%
if app == 'PlexWatch': v = ''
v = '0.3.2' if app == 'PlexWatch':
elif app == 'Plexivity': v = '0.3.2'
v = '0.9.8' elif app == 'Plexivity':
%> v = '0.9.8'
<strong>Please ensure your ${app} database is at version ${v} or higher.</strong> %>
</p> <strong>Please ensure your ${app} database is at version ${v} or higher.</strong>
% endif </p>
<div class="form-group"> % endif
<label for="import_database_path">Database Location</label> <div class="form-group">
<div class="row"> <label for="import_database_file">Option 1: Upload a Database File</label>
<div class="col-xs-12"> <div class="row">
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" required> <div class="col-xs-12">
<div class="input-group">
<label for="import_database_file" class="input-group-btn">
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_database_file" name="import_database_file" required>
</label>
<input type="text" class="form-control" disabled>
</div>
</div>
</div> </div>
<p class="help-block">Upload the ${app} database you wish to import.</p>
</div> </div>
<p class="help-block">Enter the full path to the ${app} database you wish to import.</p> <div class="form-group">
</div> <label for="import_database_path">Option 2: Browse for a Database File</label>
% if app == 'Tautulli': <div class="row">
<div class="form-group"> <div class="col-xs-12">
<label for="table_name">Import Method</label> <div class="input-group">
<div class="row"> <span class="input-group-btn">
<div class="col-xs-4"> <button class="btn btn-form" type="button" id="import_database_path_browse">Browse</button>
<select class="form-control" id="import_method" name="import_method"> </span>
<option value="merge">Merge</option> <input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" required disabled>
<option value="overwrite">Oerwrite</option> </div>
</select> </div>
</div> </div>
<p class="help-block">Browse for the ${app} database you wish to import.</p>
</div> </div>
<p class="help-block">Select how you would like to import the Tautulli history.</p> % if app == 'Tautulli':
<ul class="help-block" style="padding-inline-start: 15px;"> <div class="form-group">
<li><strong>Merge</strong> will add all history and remove any duplicates from the imported database into the current database.</li> <label for="table_name">Import Method</label>
<li><strong>Overwrite</strong> will replace all history in the current database with the imported database.</li> <div class="row">
</ul> <div class="col-xs-4">
<p class="help-block">Note: Libraries, users, notification agents, newsletter agents, and registered mobile devices will also be imported</p> <select class="form-control" id="import_method" name="import_method">
</div> <option value="merge">Merge</option>
<div class="checkbox"> <option value="overwrite">Overwrite</option>
<label> </select>
<input type="checkbox" name="import_backup_db" id="import_backup_db" value="1" checked> Backup Current Database </div>
</label>
<p class="help-block">Automatically create a backup of the current database before importing.</p>
</div>
% else:
<div class="form-group">
<label for="import_table_name">Table Name</label>
<div class="row">
<div class="col-xs-4">
<select class="form-control" id="import_table_name" name="import_table_name">
<option value="processed">Processed</option>
<option value="grouped">Grouped</option>
</select>
</div> </div>
<p class="help-block">Select how you would like to import the Tautulli history.</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li><strong>Merge</strong> will add all history and remove any duplicates from the imported database into the current database.</li>
<li><strong>Overwrite</strong> will replace all history in the current database with the imported database.</li>
</ul>
<p class="help-block">Note: Libraries, users, notification agents, newsletter agents, and registered mobile devices will also be imported</p>
</div> </div>
<p class="help-block">Select the table name from which you wish to import. Only import one of these, importing both will result in duplicated data.</p> <div class="checkbox">
</div> <label>
<div class="form-group"> <input type="checkbox" name="import_backup_db" id="import_backup_db" value="1" checked> Backup Current Database
<label for="import_ignore_interval">Ignore Interval</label> </label>
<div class="row"> <p class="help-block">Automatically create a backup of the current database before importing.</p>
<div class="col-xs-2"> </div>
<input type="text" class="form-control" id="import_ignore_interval" name="import_ignore_interval" value="120" required> % else:
<div class="form-group">
<label for="import_table_name">Table Name</label>
<div class="row">
<div class="col-xs-4">
<select class="form-control" id="import_table_name" name="import_table_name">
<option value="processed">Processed</option>
<option value="grouped">Grouped</option>
</select>
</div>
</div> </div>
<p class="help-block">Select the table name from which you wish to import. Only import one of these, importing both will result in duplicated data.</p>
</div> </div>
<p class="help-block">Enter the minimum duration (in seconds) an item must have been active for. Set to 0 to import all.</p> <div class="form-group">
</div> <label for="import_ignore_interval">Ignore Interval</label>
% endif <div class="row">
<div class="col-xs-2">
<input type="text" class="form-control" id="import_ignore_interval" name="import_ignore_interval" value="120" required>
</div>
</div>
<p class="help-block">Enter the minimum duration (in seconds) an item must have been active for. Set to 0 to import all.</p>
</div>
% endif
</form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div> <div>
@ -84,29 +106,61 @@
</div> </div>
</div> </div>
<script> <script>
// Send database path to import script $('#import_database_path_browse').click(function () {
$('#browse-path-type').text('Databse File');
$('#browse-path-modal').modal('show');
browsePath(null, null, '.db');
});
$('#select-browse-file').click(function () {
$('#browse-path-modal').modal('hide');
$("#import_database_path").val($('#browse-path').val());
});
$('#import_database_file').change(function() {
if ($(this)[0].files[0]) {
var input = $(this).parents('.input-group').find(':text')
input.val($(this)[0].files[0].name);
}
});
$("#import_db").click(function() { $("#import_db").click(function() {
var import_app = $("#import_app").val(); var database_file = false;
var database_path = $("#import_database_path").val();
var import_method = $("#import_method").val(); var formData = new FormData();
var import_backup_db = $("#import_backup_db").is(':checked'); formData.append('app', $("#import_app").val());
var import_table_name = $("#import_table_name").val(); formData.append('database_path', $("#import_database_path").val());
var import_ignore_interval = $("#import_ignore_interval").val(); if ($('#import_database_file')[0].files[0]) {
database_file = true;
formData.append('database_file', $('#import_database_file')[0].files[0]);
}
if ($("#import_method").length) {
formData.append('method', $("#import_method").val());
}
if ($("#import_backup_db").length) {
formData.append('backup', $("#import_backup_db").is(':checked'));
}
if ($("#import_table_name").length) {
formData.append('table_name', $("#import_table_name").val());
}
if ($("#import_ignore_interval").length) {
formData.append('ignore_interval', $("#import_ignore_interval").val());
}
if (database_file) {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i> Uploading database file...');
}
$.ajax({ $.ajax({
url: 'import_database', url: 'import_database',
data: { type: 'POST',
app: import_app, data: formData,
database_path: database_path,
method: import_method,
backup: import_backup_db,
table_name: import_table_name,
import_ignore_interval: import_ignore_interval
},
cache: false, cache: false,
async: true, async: true,
contentType: false,
processData: false,
success: function(data) { success: function(data) {
$("#status-message").html(data); $("#status-message").html(data);
$("#import_database_path").val('') $("#import_database_file").val(null);
$("#import_database_path").val('');
} }
}); });
}); });

View file

@ -4297,3 +4297,7 @@ a[data-tab-destination] {
margin-top: 0; margin-top: 0;
color: #737373; color: #737373;
} }
#browse-path-list > li > span > i.fa {
color: #999;
}

View file

@ -237,6 +237,27 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
}); });
} }
getBrowsePath = function (key, path, filter_ext) {
var deferred = $.Deferred();
$.ajax({
url: 'browse_path',
type: 'GET',
data: {
key: key,
path: path,
filter_ext: filter_ext
},
success: function(data) {
deferred.resolve(data);
},
error: function() {
deferred.reject();
}
});
return deferred;
};
function doSimpleAjaxCall(url) { function doSimpleAjaxCall(url) {
$.ajax(url); $.ajax(url);
} }

View file

@ -1882,6 +1882,38 @@ Rating: {rating}/10 --> Rating: /10
</div> </div>
</div> </div>
<div id="mobile-device-config-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="mobile-device-config-modal"></div> <div id="mobile-device-config-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="mobile-device-config-modal"></div>
<div id="browse-path-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="browse-path-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">File Browser</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="browse-path">Select a <span id="browse-path-type"></span> Below</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="browse-path" name="browse-path" value="" size="30" disabled>
</div>
</div>
</div>
</div>
<div class="col-md-12" style="height: 400px; overflow: auto;">
<ul id="browse-path-list" class="stacked-configs list-unstyled">
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<span id="browse-path-status-message" style="padding-right: 25px;"></span>
<input type="button" id="select-browse-file" class="btn btn-bright" value="Select">
</div>
</div>
</div>
</div>
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
@ -1988,6 +2020,38 @@ Rating: {rating}/10 --> Rating: /10
}); });
} }
function browsePath(key, path, filter_ext) {
$("#browse-path-status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
getBrowsePath(key, path, filter_ext).then(function (data) {
if (data.result === 'error') {
$("#browse-path-status-message").html("<i class='fa fa-exclamation-triangle'></i> " + data.message);
} else {
$("#browse-path-status-message").html("");
$('#browse-path').val(data.path);
var browse_list = $('#browse-path-list');
browse_list.parent().animate({ scrollTop: 0 }, 0);
browse_list.empty();
$.each(data.data, function(i, item) {
var browse_item = $('<li/>')
.html("<span><i class='fa fa-fw fa-" + item.icon + "'></i>&nbsp; " + item.title + "</span>")
.addClass(item.type + ' pointer')
.data('key', item.key)
.data('path', item.path)
.appendTo(browse_list)
});
$('#browse-path-list li').click(function (){
$('#browse-path').val($(this).data('path'));
if ($(this).hasClass('folder')) {
browsePath($(this).data('key'), null, filter_ext)
}
});
}
});
}
$(document).ready(function() { $(document).ready(function() {
// Javascript to enable link to tab // Javascript to enable link to tab

View file

@ -42,6 +42,7 @@ import os
import re import re
import shlex import shlex
import socket import socket
import string
import sys import sys
import time import time
import unicodedata import unicodedata
@ -1197,3 +1198,64 @@ def user_page(user_id=None, user=None):
params['user'] = user params['user'] = user
return params return params
def browse_path(path=None, include_hidden=False, filter_ext=''):
output = []
if not os.path.isdir(path):
return output
if path != os.path.dirname(path):
parent_path = os.path.dirname(path)
out = {
'key': base64.b64encode(parent_path.encode('UTF-8')),
'path': parent_path,
'title': '..',
'type': 'folder',
'icon': 'level-up-alt'
}
output.append(out)
elif os.name == 'nt':
drives = ['%s:\\' % d for d in string.ascii_uppercase if os.path.exists('%s:' % d)]
for drive in drives:
out = {
'key': base64.b64encode(drive.encode('UTF-8')),
'path': drive,
'title': drive,
'type': 'folder',
'icon': 'level-up-alt'
}
output.append(out)
for root, dirs, files in os.walk(path):
for d in dirs:
if not include_hidden and d.startswith('.'):
continue
dir_path = os.path.join(root, d)
out = {
'key': base64.b64encode(dir_path.encode('UTF-8')),
'path': dir_path,
'title': d,
'type': 'folder',
'icon': 'folder'
}
output.append(out)
for f in files:
if not include_hidden and f.startswith('.'):
continue
if filter_ext and not f.endswith(filter_ext):
continue
file_path = os.path.join(root, f)
out = {
'key': base64.b64encode(file_path.encode('UTF-8')),
'path': file_path,
'title': f,
'type': 'file',
'icon': 'file'
}
output.append(out)
break
return output

View file

@ -21,6 +21,7 @@ from future.builtins import object
from future.builtins import str from future.builtins import str
from io import open from io import open
import base64
import json import json
import linecache import linecache
import os import os
@ -3740,13 +3741,15 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))
@addtoapi() @addtoapi()
def import_database(self, app=None, database_path=None, method=None, backup=True, def import_database(self, app=None, database_file=None, database_path=None, method=None, backup=True,
table_name=None, import_ignore_interval=0, **kwargs): table_name=None, import_ignore_interval=0, **kwargs):
""" Import a Tautulli, PlexWatch, or Plexivity database into Tautulli. """ Import a Tautulli, PlexWatch, or Plexivity database into Tautulli.
``` ```
Required parameters: Required parameters:
app (str): "tautulli" or "plexwatch" or "plexivity" app (str): "tautulli" or "plexwatch" or "plexivity"
database_file (file): The database file to import (multipart/form-data)
or
database_path (str): The full path to the plexwatch database file database_path (str): The full path to the plexwatch database file
method (str): For Tautulli only, "merge" or "overwrite" method (str): For Tautulli only, "merge" or "overwrite"
table_name (str): For PlexWatch or Plexivity only, "processed" or "grouped" table_name (str): For PlexWatch or Plexivity only, "processed" or "grouped"
@ -3765,6 +3768,20 @@ class WebInterface(object):
if not app: if not app:
return 'No app specified for import' return 'No app specified for import'
if database_file:
database_path = os.path.join(plexpy.CONFIG.CACHE_DIR, database_file.filename)
logger.info("Received database file '%s' for import. Saving to cache '%s'.",
database_file.filename, database_path)
with open(database_path, 'wb') as f:
while True:
data = database_file.file.read(8192)
if not data:
break
f.write(data)
if not database_path:
return 'No database specified for import'
if app.lower() == 'tautulli': if app.lower() == 'tautulli':
db_check_msg = database.validate_database(database=database_path) db_check_msg = database.validate_database(database=database_path)
if db_check_msg == 'success': if db_check_msg == 'success':
@ -3816,6 +3833,21 @@ class WebInterface(object):
logger.warn("No app specified for import.") logger.warn("No app specified for import.")
return return
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def browse_path(self, key=None, path=None, filter_ext=''):
if key:
path = base64.b64decode(key)
if not path:
path = plexpy.DATA_DIR
data = helpers.browse_path(path=path, filter_ext=filter_ext)
if data:
return {'result': 'success', 'path': path, 'data': data}
else:
return {'result': 'error', 'message': 'Invalid path.'}
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
@requireAuth(member_of("admin")) @requireAuth(member_of("admin"))