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,6 +5,7 @@
<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">
<form id="import_database" enctype="multipart/form-data" method="post" name="import_database">
<input type="hidden" id="import_app" name="import_app" value="${app.lower()}" /> <input type="hidden" id="import_app" name="import_app" value="${app.lower()}" />
% if app in ('PlexWatch', 'Plexivity'): % if app in ('PlexWatch', 'Plexivity'):
<p class="help-block"> <p class="help-block">
@ -19,13 +20,33 @@
</p> </p>
% endif % endif
<div class="form-group"> <div class="form-group">
<label for="import_database_path">Database Location</label> <label for="import_database_file">Option 1: Upload a Database File</label>
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" required> <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">Enter the full path to the ${app} database you wish to import.</p> </div>
<p class="help-block">Upload the ${app} database you wish to import.</p>
</div>
<div class="form-group">
<label for="import_database_path">Option 2: Browse for a Database File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="import_database_path_browse">Browse</button>
</span>
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the ${app} database you wish to import.</p>
</div> </div>
% if app == 'Tautulli': % if app == 'Tautulli':
<div class="form-group"> <div class="form-group">
@ -34,7 +55,7 @@
<div class="col-xs-4"> <div class="col-xs-4">
<select class="form-control" id="import_method" name="import_method"> <select class="form-control" id="import_method" name="import_method">
<option value="merge">Merge</option> <option value="merge">Merge</option>
<option value="overwrite">Oerwrite</option> <option value="overwrite">Overwrite</option>
</select> </select>
</div> </div>
</div> </div>
@ -74,6 +95,7 @@
<p class="help-block">Enter the minimum duration (in seconds) an item must have been active for. Set to 0 to import all.</p> <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> </div>
% endif % 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"))