mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-11 07:46:07 -07:00
Add table to list exported items
This commit is contained in:
parent
c102020698
commit
5468676811
6 changed files with 357 additions and 0 deletions
|
@ -473,6 +473,21 @@ fieldset[disabled] .btn-bright.active {
|
||||||
background-color: #ac2925;
|
background-color: #ac2925;
|
||||||
border-color: #761c19;
|
border-color: #761c19;
|
||||||
}
|
}
|
||||||
|
.btn-dark.btn-download:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #449d44;
|
||||||
|
border-color: #398439;
|
||||||
|
}
|
||||||
|
.btn-dark.btn-download.active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #449d44;
|
||||||
|
border-color: #398439;
|
||||||
|
}
|
||||||
|
.btn-dark.btn-download.active:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #398439;
|
||||||
|
border-color: #255625;
|
||||||
|
}
|
||||||
.btn-group select {
|
.btn-group select {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
100
data/interfaces/default/js/tables/export_table.js
Normal file
100
data/interfaces/default/js/tables/export_table.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
var date_format = 'YYYY-MM-DD';
|
||||||
|
var time_format = 'hh:mm a';
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_date_formats',
|
||||||
|
type: 'GET',
|
||||||
|
success: function (data) {
|
||||||
|
date_format = data.date_format;
|
||||||
|
time_format = data.time_format;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export_table_options = {
|
||||||
|
"destroy": true,
|
||||||
|
"language": {
|
||||||
|
"search": "Search: ",
|
||||||
|
"lengthMenu": "Show _MENU_ entries per page",
|
||||||
|
"info": "Showing _START_ to _END_ of _TOTAL_ library items",
|
||||||
|
"infoEmpty": "Showing 0 to 0 of 0 entries",
|
||||||
|
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
|
||||||
|
"emptyTable": "No data in table",
|
||||||
|
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
|
||||||
|
},
|
||||||
|
"pagingType": "full_numbers",
|
||||||
|
"stateSave": true,
|
||||||
|
"stateDuration": 0,
|
||||||
|
"processing": false,
|
||||||
|
"serverSide": true,
|
||||||
|
"pageLength": 25,
|
||||||
|
"order": [0, 'desc'],
|
||||||
|
"autoWidth": false,
|
||||||
|
"scrollX": true,
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"targets": [0],
|
||||||
|
"data": "timestamp",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(moment(cellData, "X").format(date_format + ' ' + time_format));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%",
|
||||||
|
"className": "no-wrap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [1],
|
||||||
|
"data": "media_type_title",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%",
|
||||||
|
"className": "no-wrap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [2],
|
||||||
|
"data": "rating_key",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%",
|
||||||
|
"className": "no-wrap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [3],
|
||||||
|
"data": "filename",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "60%",
|
||||||
|
"className": "no-wrap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [4],
|
||||||
|
"data": "complete",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData === 1) {
|
||||||
|
$(td).html('<button class="btn btn-xs btn-dark btn-download" data-id="' + rowData['row_id'] + '"><i class="fa fa-file-download fa-fw"></i> Download</button>');
|
||||||
|
} else {
|
||||||
|
$(td).html('<button class="btn btn-xs btn-dark" data-id="' + rowData['row_id'] + '" disabled><i class="fa fa-spinner fa-spin fa-fw"></i> Processing</button>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"drawCallback": function (settings) {
|
||||||
|
// Jump to top of page
|
||||||
|
//$('html,body').scrollTop(0);
|
||||||
|
$('#ajaxMsg').fadeOut();
|
||||||
|
},
|
||||||
|
"preDrawCallback": function(settings) {
|
||||||
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
|
showMsg(msg, false, false, 0)
|
||||||
|
}
|
||||||
|
};
|
|
@ -93,6 +93,7 @@ DOCUMENTATION :: END
|
||||||
% if _session['user_group'] == 'admin':
|
% if _session['user_group'] == 'admin':
|
||||||
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
% if data['section_id'] != LIVE_TV_SECTION_ID:
|
||||||
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
|
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
|
||||||
|
<li><a id="export-tab-btn" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -305,6 +306,47 @@ DOCUMENTATION :: END
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="tabs-export">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-file-export"></i> Exports for <strong>
|
||||||
|
<span class="set-username">${data['section_name']}</span>
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="button-bar">
|
||||||
|
% if _session['user_group'] == 'admin':
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-dark refresh-export-table-button" id="refresh-export-table">
|
||||||
|
<i class="fa fa-refresh"></i> Refresh exports
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
<div class="btn-group colvis-button-bar" id="button-bar-export"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-card-back">
|
||||||
|
<table class="display export_table" id="export_table-SID-${data['section_id']}" width="100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left" id="timestamp">Exported At</th>
|
||||||
|
<th align="left" id="media_type_title">Media Type</th>
|
||||||
|
<th align="left" id="rating_key">Rating Key</th>
|
||||||
|
<th align="left" id="filename">Filename</th>
|
||||||
|
<th align="left" id="complete">Download</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -387,6 +429,7 @@ DOCUMENTATION :: END
|
||||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||||
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
|
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
|
||||||
|
<script src="${http_root}js/tables/export_table.js${cache_param}"></script>
|
||||||
<script>
|
<script>
|
||||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||||
|
@ -461,6 +504,36 @@ DOCUMENTATION :: END
|
||||||
refresh_table = false;
|
refresh_table = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function loadExportTable() {
|
||||||
|
// Build export table
|
||||||
|
export_table_options.ajax = {
|
||||||
|
url: 'get_library_export',
|
||||||
|
type: 'POST',
|
||||||
|
data: function ( d ) {
|
||||||
|
return {
|
||||||
|
json_data: JSON.stringify( d ),
|
||||||
|
section_id: section_id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export_table = $('#export_table-SID-${data["section_id"]}').DataTable(export_table_options);
|
||||||
|
|
||||||
|
var colvis = new $.fn.dataTable.ColVis(export_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||||
|
$(colvis.button()).appendTo('#button-bar-export-table');
|
||||||
|
|
||||||
|
clearSearchButton('export_table-SID-${data["section_id"]}', export_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('a[href="#tabs-export"]').on('shown.bs.tab', function() {
|
||||||
|
if (typeof(export_table) === 'undefined') {
|
||||||
|
loadExportTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#refresh-export-table").click(function () {
|
||||||
|
export_table.draw();
|
||||||
|
});
|
||||||
|
|
||||||
$("#edit-library-tooltip").tooltip();
|
$("#edit-library-tooltip").tooltip();
|
||||||
|
|
||||||
// Load edit library modal
|
// Load edit library modal
|
||||||
|
|
|
@ -788,6 +788,13 @@ def dbcheck():
|
||||||
'img_hash TEXT, cloudinary_title TEXT, cloudinary_url TEXT)'
|
'img_hash TEXT, cloudinary_title TEXT, cloudinary_url TEXT)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# exports table :: This table keeps record of the exported files
|
||||||
|
c_db.execute(
|
||||||
|
'CREATE TABLE IF NOT EXISTS exports (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
|
'timestamp INTEGER, section_id INTEGER, rating_key INTEGER, media_type TEXT, '
|
||||||
|
'filename TEXT, complete INTEGER DEFAULT 0)'
|
||||||
|
)
|
||||||
|
|
||||||
# Upgrade sessions table from earlier versions
|
# Upgrade sessions table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT started FROM sessions')
|
c_db.execute('SELECT started FROM sessions')
|
||||||
|
|
|
@ -27,10 +27,14 @@ from multiprocessing.dummy import Pool as ThreadPool
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
if plexpy.PYTHON2:
|
if plexpy.PYTHON2:
|
||||||
|
import database
|
||||||
|
import datatables
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
from plex import Plex
|
from plex import Plex
|
||||||
else:
|
else:
|
||||||
|
from plexpy import database
|
||||||
|
from plexpy import datatables
|
||||||
from plexpy import helpers
|
from plexpy import helpers
|
||||||
from plexpy import logger
|
from plexpy import logger
|
||||||
from plexpy.plex import Plex
|
from plexpy.plex import Plex
|
||||||
|
@ -874,6 +878,7 @@ def export(section_id=None, rating_key=None, output_format='json'):
|
||||||
|
|
||||||
item = plex.get_item(helpers.cast_to_int(rating_key))
|
item = plex.get_item(helpers.cast_to_int(rating_key))
|
||||||
media_type = item.type
|
media_type = item.type
|
||||||
|
section_id = item.librarySectionID
|
||||||
|
|
||||||
if media_type in ('season', 'episode', 'album', 'track'):
|
if media_type in ('season', 'episode', 'album', 'track'):
|
||||||
item_title = item._defaultSyncTitle()
|
item_title = item._defaultSyncTitle()
|
||||||
|
@ -895,6 +900,15 @@ def export(section_id=None, rating_key=None, output_format='json'):
|
||||||
filepath = os.path.join(plexpy.CONFIG.CACHE_DIR, filename)
|
filepath = os.path.join(plexpy.CONFIG.CACHE_DIR, filename)
|
||||||
logger.info("Tautulli Exporter :: Starting export for '%s'...", filename)
|
logger.info("Tautulli Exporter :: Starting export for '%s'...", filename)
|
||||||
|
|
||||||
|
export_id = set_export_state(timestamp=timestamp,
|
||||||
|
section_id=section_id,
|
||||||
|
rating_key=rating_key,
|
||||||
|
media_type=media_type,
|
||||||
|
filename=filename)
|
||||||
|
if not export_id:
|
||||||
|
logger.error("Tautulli Exporter :: Failed to export '%s'", filename)
|
||||||
|
return
|
||||||
|
|
||||||
attrs = MEDIA_TYPES[media_type]
|
attrs = MEDIA_TYPES[media_type]
|
||||||
part = partial(helpers.get_attrs_to_dict, attrs=attrs)
|
part = partial(helpers.get_attrs_to_dict, attrs=attrs)
|
||||||
|
|
||||||
|
@ -913,4 +927,93 @@ def export(section_id=None, rating_key=None, output_format='json'):
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(flatten_result)
|
writer.writerows(flatten_result)
|
||||||
|
|
||||||
|
set_export_complete(export_id=export_id)
|
||||||
logger.info("Tautulli Exporter :: Successfully exported to '%s'", filepath)
|
logger.info("Tautulli Exporter :: Successfully exported to '%s'", filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def set_export_state(timestamp, section_id, rating_key, media_type, filename):
|
||||||
|
keys = {'timestamp': timestamp,
|
||||||
|
'section_id': section_id,
|
||||||
|
'rating_key': rating_key,
|
||||||
|
'media_type': media_type}
|
||||||
|
|
||||||
|
values = {'filename': filename}
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
try:
|
||||||
|
db.upsert(table_name='exports', key_dict=keys, value_dict=values)
|
||||||
|
return db.last_insert_id()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Tautulli Exporter :: Unable to save export to database: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def set_export_complete(export_id):
|
||||||
|
keys = {'id': export_id}
|
||||||
|
values = {'complete': 1}
|
||||||
|
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
db.upsert(table_name='exports', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
|
|
||||||
|
def get_export_datatable(section_id=None, rating_key=None, kwargs=None):
|
||||||
|
default_return = {'recordsFiltered': 0,
|
||||||
|
'recordsTotal': 0,
|
||||||
|
'draw': 0,
|
||||||
|
'data': 'null',
|
||||||
|
'error': 'Unable to execute database query.'}
|
||||||
|
|
||||||
|
data_tables = datatables.DataTables()
|
||||||
|
|
||||||
|
custom_where = []
|
||||||
|
if section_id:
|
||||||
|
custom_where.append(['exports.section_id', section_id])
|
||||||
|
if rating_key:
|
||||||
|
custom_where.append(['exports.rating_key', rating_key])
|
||||||
|
|
||||||
|
columns = ['exports.id AS row_id',
|
||||||
|
'exports.timestamp',
|
||||||
|
'exports.section_id',
|
||||||
|
'exports.rating_key',
|
||||||
|
'exports.media_type',
|
||||||
|
'exports.filename',
|
||||||
|
'exports.complete'
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
query = data_tables.ssp_query(table_name='exports',
|
||||||
|
columns=columns,
|
||||||
|
custom_where=custom_where,
|
||||||
|
group_by=[],
|
||||||
|
join_types=[],
|
||||||
|
join_tables=[],
|
||||||
|
join_evals=[],
|
||||||
|
kwargs=kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn("Tautulli Exporter :: Unable to execute database query for get_export_datatable: %s." % e)
|
||||||
|
return default_return
|
||||||
|
|
||||||
|
result = query['result']
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for item in result:
|
||||||
|
media_type_title = item['media_type'].title()
|
||||||
|
|
||||||
|
row = {'row_id': item['row_id'],
|
||||||
|
'timestamp': item['timestamp'],
|
||||||
|
'section_id': item['section_id'],
|
||||||
|
'rating_key': item['rating_key'],
|
||||||
|
'media_type': item['media_type'],
|
||||||
|
'media_type_title': media_type_title,
|
||||||
|
'filename': item['filename'],
|
||||||
|
'complete': item['complete']
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
result = {'recordsFiltered': query['filteredCount'],
|
||||||
|
'recordsTotal': query['totalCount'],
|
||||||
|
'data': rows,
|
||||||
|
'draw': query['draw']
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
|
@ -870,6 +870,65 @@ class WebInterface(object):
|
||||||
|
|
||||||
return {'success': result}
|
return {'success': result}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
@requireAuth(member_of("admin"))
|
||||||
|
@addtoapi()
|
||||||
|
def get_library_export(self, section_id=None, rating_key=None, **kwargs):
|
||||||
|
""" Get the data on the Tautulli export tables.
|
||||||
|
|
||||||
|
```
|
||||||
|
Required parameters:
|
||||||
|
section_id (str): The id of the Plex library section, OR
|
||||||
|
rating_key (str): The rating key of the exported item
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
order_column (str): "added_at", "sort_title", "container", "bitrate", "video_codec",
|
||||||
|
"video_resolution", "video_framerate", "audio_codec", "audio_channels",
|
||||||
|
"file_size", "last_played", "play_count"
|
||||||
|
order_dir (str): "desc" or "asc"
|
||||||
|
start (int): Row to start from, 0
|
||||||
|
length (int): Number of items to return, 25
|
||||||
|
search (str): A string to search for, "Thrones"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
json:
|
||||||
|
{"draw": 1,
|
||||||
|
"recordsTotal": 10,
|
||||||
|
"recordsFiltered": 3,
|
||||||
|
"data":
|
||||||
|
[{"row_id": 2,
|
||||||
|
"timestamp": 1596484600,
|
||||||
|
"section_id": 1,
|
||||||
|
"rating_key": 270716,
|
||||||
|
"media_type": "movie",
|
||||||
|
"media_type_title": "Movie",
|
||||||
|
"filename": "Movie - Frozen II [270716].20200803125640.json",
|
||||||
|
"complete": 1
|
||||||
|
},
|
||||||
|
{...},
|
||||||
|
{...}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# Check if datatables json_data was received.
|
||||||
|
# If not, then build the minimal amount of json data for a query
|
||||||
|
if not kwargs.get('json_data'):
|
||||||
|
# TODO: Find some one way to automatically get the columns
|
||||||
|
dt_columns = [("timestamp", True, False),
|
||||||
|
("media_type_title", True, True),
|
||||||
|
("rating_key", True, True),
|
||||||
|
("filename", True, True),
|
||||||
|
("complete", True, False)]
|
||||||
|
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "timestamp")
|
||||||
|
|
||||||
|
result = exporter.get_export_datatable(section_id=section_id,
|
||||||
|
rating_key=rating_key,
|
||||||
|
kwargs=kwargs)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue