mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-08 06:00:51 -07:00
Add option to upload or browse for a database file to import
This commit is contained in:
parent
a869859491
commit
980c4f7618
6 changed files with 316 additions and 79 deletions
|
@ -5,6 +5,7 @@
|
|||
<h4 class="modal-title">Import ${app} Database</h4>
|
||||
</div>
|
||||
<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()}" />
|
||||
% if app in ('PlexWatch', 'Plexivity'):
|
||||
<p class="help-block">
|
||||
|
@ -19,13 +20,33 @@
|
|||
</p>
|
||||
% endif
|
||||
<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="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>
|
||||
<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>
|
||||
% if app == 'Tautulli':
|
||||
<div class="form-group">
|
||||
|
@ -34,7 +55,7 @@
|
|||
<div class="col-xs-4">
|
||||
<select class="form-control" id="import_method" name="import_method">
|
||||
<option value="merge">Merge</option>
|
||||
<option value="overwrite">Oerwrite</option>
|
||||
<option value="overwrite">Overwrite</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
% endif
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div>
|
||||
|
@ -84,29 +106,61 @@
|
|||
</div>
|
||||
</div>
|
||||
<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() {
|
||||
var import_app = $("#import_app").val();
|
||||
var database_path = $("#import_database_path").val();
|
||||
var import_method = $("#import_method").val();
|
||||
var import_backup_db = $("#import_backup_db").is(':checked');
|
||||
var import_table_name = $("#import_table_name").val();
|
||||
var import_ignore_interval = $("#import_ignore_interval").val();
|
||||
var database_file = false;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('app', $("#import_app").val());
|
||||
formData.append('database_path', $("#import_database_path").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({
|
||||
url: 'import_database',
|
||||
data: {
|
||||
app: import_app,
|
||||
database_path: database_path,
|
||||
method: import_method,
|
||||
backup: import_backup_db,
|
||||
table_name: import_table_name,
|
||||
import_ignore_interval: import_ignore_interval
|
||||
},
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
cache: false,
|
||||
async: true,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function(data) {
|
||||
$("#status-message").html(data);
|
||||
$("#import_database_path").val('')
|
||||
$("#import_database_file").val(null);
|
||||
$("#import_database_path").val('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4297,3 +4297,7 @@ a[data-tab-destination] {
|
|||
margin-top: 0;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
#browse-path-list > li > span > i.fa {
|
||||
color: #999;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
$.ajax(url);
|
||||
}
|
||||
|
|
|
@ -1882,6 +1882,38 @@ Rating: {rating}/10 --> Rating: /10
|
|||
</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="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 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> " + 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() {
|
||||
|
||||
// Javascript to enable link to tab
|
||||
|
|
|
@ -42,6 +42,7 @@ import os
|
|||
import re
|
||||
import shlex
|
||||
import socket
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
|
@ -1197,3 +1198,64 @@ def user_page(user_id=None, user=None):
|
|||
params['user'] = user
|
||||
|
||||
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
|
||||
|
|
|
@ -21,6 +21,7 @@ from future.builtins import object
|
|||
from future.builtins import str
|
||||
|
||||
from io import open
|
||||
import base64
|
||||
import json
|
||||
import linecache
|
||||
import os
|
||||
|
@ -3740,13 +3741,15 @@ class WebInterface(object):
|
|||
@cherrypy.expose
|
||||
@requireAuth(member_of("admin"))
|
||||
@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):
|
||||
""" Import a Tautulli, PlexWatch, or Plexivity database into Tautulli.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
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
|
||||
method (str): For Tautulli only, "merge" or "overwrite"
|
||||
table_name (str): For PlexWatch or Plexivity only, "processed" or "grouped"
|
||||
|
@ -3765,6 +3768,20 @@ class WebInterface(object):
|
|||
if not app:
|
||||
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':
|
||||
db_check_msg = database.validate_database(database=database_path)
|
||||
if db_check_msg == 'success':
|
||||
|
@ -3816,6 +3833,21 @@ class WebInterface(object):
|
|||
logger.warn("No app specified for import.")
|
||||
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.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue