mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-07 13:41:15 -07:00
Rework exporter to allow exporting individual files from library
This commit is contained in:
parent
034ad05383
commit
f3fa9601c0
6 changed files with 428 additions and 247 deletions
|
@ -56,6 +56,14 @@ DOCUMENTATION :: END
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Select the export data file format.</p>
|
<p class="help-block">Select the export data file format.</p>
|
||||||
</div>
|
</div>
|
||||||
|
% if not rating_key:
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="export_individual_files" name="export_individual_files" value="1"> Export Individual Files
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Enable to export one file for each ${media_type}, otherwise only export a single file containing all ${media_type}s.</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="export_metadata_level">Metadata Export Level</label>
|
<label for="export_metadata_level">Metadata Export Level</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -249,6 +257,7 @@ DOCUMENTATION :: END
|
||||||
$('#export_custom_media_info_fields').val()
|
$('#export_custom_media_info_fields').val()
|
||||||
].filter(Boolean).join(',');
|
].filter(Boolean).join(',');
|
||||||
var export_type = $('#export_export_type').val()
|
var export_type = $('#export_export_type').val()
|
||||||
|
var individual_files = $('#export_individual_files').is(':checked')
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'export_metadata',
|
url: 'export_metadata',
|
||||||
|
@ -262,7 +271,8 @@ DOCUMENTATION :: END
|
||||||
thumb_level: thumb_level,
|
thumb_level: thumb_level,
|
||||||
art_level: art_level,
|
art_level: art_level,
|
||||||
custom_fields: custom_fields,
|
custom_fields: custom_fields,
|
||||||
export_type: export_type
|
export_type: export_type,
|
||||||
|
individual_files: individual_files
|
||||||
},
|
},
|
||||||
async: true,
|
async: true,
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
|
|
|
@ -40,7 +40,8 @@ export_table_options = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "8%",
|
"width": "8%",
|
||||||
"className": "no-wrap"
|
"className": "no-wrap",
|
||||||
|
"searchable": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [1],
|
"targets": [1],
|
||||||
|
@ -66,13 +67,23 @@ export_table_options = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [3],
|
"targets": [3],
|
||||||
"data": "filename",
|
"data": "title",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== '') {
|
if (cellData !== '') {
|
||||||
if (rowData['complete'] === 1 && rowData['exists']) {
|
var tooltip;
|
||||||
$(td).html('<a href="view_export?export_id=' + rowData['export_id'] + '" target="_blank">' + cellData + '</a>');
|
var filename;
|
||||||
|
if (!rowData['individual_files']) {
|
||||||
|
tooltip = '<span data-toggle="tooltip" title="Single File"><i class="fa fa-file-alt fa-fw"></i></span>';
|
||||||
|
filename = cellData + '.' + rowData['file_format']
|
||||||
} else {
|
} else {
|
||||||
$(td).html(cellData);
|
tooltip = '<span data-toggle="tooltip" title="Multiple Files"><i class="fa fa-folder fa-fw"></i></span>';
|
||||||
|
filename = cellData
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowData['complete'] === 1 && rowData['exists'] && !rowData['individual_files']) {
|
||||||
|
$(td).html('<a href="view_export?export_id=' + rowData['export_id'] + '" target="_blank">' + tooltip + ' ' + filename + '</a>');
|
||||||
|
} else {
|
||||||
|
$(td).html(tooltip + ' ' + filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -136,14 +147,25 @@ export_table_options = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "6%",
|
"width": "6%",
|
||||||
"className": "no-wrap"
|
"className": "no-wrap",
|
||||||
|
"searchable": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [9],
|
"targets": [9],
|
||||||
"data": "complete",
|
"data": "complete",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData === 1 && rowData['exists']) {
|
if (cellData === 1 && rowData['exists']) {
|
||||||
$(td).html('<button class="btn btn-xs btn-success pull-left" data-id="' + rowData['export_id'] + '"><i class="fa fa-file-download fa-fw"></i> Download</button>');
|
var tooltip_title = '';
|
||||||
|
var icon = '';
|
||||||
|
if (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) {
|
||||||
|
tooltip_title = 'Zip Archive';
|
||||||
|
icon = 'fa-file-archive';
|
||||||
|
} else {
|
||||||
|
tooltip_title = rowData['file_format'].toUpperCase() + ' File';
|
||||||
|
icon = 'fa-file-download';
|
||||||
|
}
|
||||||
|
var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download';
|
||||||
|
$(td).html('<button class="btn btn-xs btn-success pull-left" data-id="' + rowData['export_id'] + '"><span data-toggle="tooltip" data-placement="left" title="' + tooltip_title + '"><i class="fa ' + icon + ' fa-fw"></i> Download</span></button>');
|
||||||
} else if (cellData === 0) {
|
} else if (cellData === 0) {
|
||||||
$(td).html('<span class="btn btn-xs btn-dark pull-left export-processing" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-spinner fa-spin fa-fw"></i> Processing</span>');
|
$(td).html('<span class="btn btn-xs btn-dark pull-left export-processing" data-id="' + rowData['export_id'] + '" disabled><i class="fa fa-spinner fa-spin fa-fw"></i> Processing</span>');
|
||||||
} else if (cellData === -1) {
|
} else if (cellData === -1) {
|
||||||
|
@ -153,7 +175,8 @@ export_table_options = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "7%",
|
"width": "7%",
|
||||||
"className": "export_download"
|
"className": "export_download",
|
||||||
|
"searchable": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [10],
|
"targets": [10],
|
||||||
|
@ -166,7 +189,8 @@ export_table_options = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "7%",
|
"width": "7%",
|
||||||
"className": "export_delete"
|
"className": "export_delete",
|
||||||
|
"searchable": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"drawCallback": function (settings) {
|
"drawCallback": function (settings) {
|
||||||
|
@ -174,6 +198,12 @@ export_table_options = {
|
||||||
//$('html,body').scrollTop(0);
|
//$('html,body').scrollTop(0);
|
||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
|
// Create the tooltips.
|
||||||
|
$('body').tooltip({
|
||||||
|
selector: '[data-toggle="tooltip"]',
|
||||||
|
container: 'body'
|
||||||
|
});
|
||||||
|
|
||||||
if (export_processing_timer) {
|
if (export_processing_timer) {
|
||||||
clearTimeout(export_processing_timer);
|
clearTimeout(export_processing_timer);
|
||||||
}
|
}
|
||||||
|
@ -208,7 +238,7 @@ $('.export_table').on('click', '> tbody > tr > td.export_delete > button', funct
|
||||||
var row = export_table.row(tr);
|
var row = export_table.row(tr);
|
||||||
var rowData = row.data();
|
var rowData = row.data();
|
||||||
|
|
||||||
var msg = 'Are you sure you want to delete the following export?<br /><br /><strong>' + rowData['filename'] + '</strong>';
|
var msg = 'Are you sure you want to delete the following export?<br /><br /><strong>' + rowData['title'] + '</strong>';
|
||||||
var url = 'delete_export?export_id=' + rowData['export_id'];
|
var url = 'delete_export?export_id=' + rowData['export_id'];
|
||||||
confirmAjaxCall(url, msg, null, null, redrawExportTable);
|
confirmAjaxCall(url, msg, null, null, redrawExportTable);
|
||||||
});
|
});
|
||||||
|
|
|
@ -798,10 +798,10 @@ def dbcheck():
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS exports (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS exports (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
'timestamp INTEGER, section_id INTEGER, user_id INTEGER, rating_key INTEGER, media_type TEXT, '
|
'timestamp INTEGER, section_id INTEGER, user_id INTEGER, rating_key INTEGER, media_type TEXT, '
|
||||||
'filename TEXT, file_format TEXT, '
|
'title TEXT, file_format TEXT, '
|
||||||
'metadata_level INTEGER, media_info_level INTEGER, '
|
'metadata_level INTEGER, media_info_level INTEGER, '
|
||||||
'thumb_level INTEGER DEFAULT 0, art_level INTEGER DEFAULT 0, '
|
'thumb_level INTEGER DEFAULT 0, art_level INTEGER DEFAULT 0, '
|
||||||
'custom_fields TEXT, '
|
'custom_fields TEXT, individual_files INTEGER DEFAULT 0, '
|
||||||
'file_size INTEGER DEFAULT 0, complete INTEGER DEFAULT 0)'
|
'file_size INTEGER DEFAULT 0, complete INTEGER DEFAULT 0)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2179,6 +2179,18 @@ def dbcheck():
|
||||||
'UPDATE exports SET art_level = 9 WHERE include_art = 1'
|
'UPDATE exports SET art_level = 9 WHERE include_art = 1'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade exports table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT title FROM exports')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug("Altering database. Updating database table exports.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE exports ADD COLUMN title TEXT'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE exports ADD COLUMN individual_files INTEGER DEFAULT 0'
|
||||||
|
)
|
||||||
|
|
||||||
# Add "Local" user to database as default unauthenticated user.
|
# Add "Local" user to database as default unauthenticated user.
|
||||||
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
|
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
|
||||||
if not result.fetchone():
|
if not result.fetchone():
|
||||||
|
|
|
@ -101,7 +101,7 @@ class Export(object):
|
||||||
def __init__(self, section_id=None, user_id=None, rating_key=None, file_format='csv',
|
def __init__(self, section_id=None, user_id=None, rating_key=None, file_format='csv',
|
||||||
metadata_level=1, media_info_level=1,
|
metadata_level=1, media_info_level=1,
|
||||||
thumb_level=0, art_level=0,
|
thumb_level=0, art_level=0,
|
||||||
custom_fields='', export_type=None):
|
custom_fields='', export_type=None, individual_files=False):
|
||||||
self.section_id = helpers.cast_to_int(section_id) or None
|
self.section_id = helpers.cast_to_int(section_id) or None
|
||||||
self.user_id = helpers.cast_to_int(user_id) or None
|
self.user_id = helpers.cast_to_int(user_id) or None
|
||||||
self.rating_key = helpers.cast_to_int(rating_key) or None
|
self.rating_key = helpers.cast_to_int(rating_key) or None
|
||||||
|
@ -112,17 +112,22 @@ class Export(object):
|
||||||
self.art_level = helpers.cast_to_int(art_level)
|
self.art_level = helpers.cast_to_int(art_level)
|
||||||
self.custom_fields = custom_fields.replace(' ', '')
|
self.custom_fields = custom_fields.replace(' ', '')
|
||||||
self._custom_fields = {}
|
self._custom_fields = {}
|
||||||
self.export_type = export_type or 'all'
|
self.export_type = str(export_type).lower() or 'all'
|
||||||
|
self.individual_files = individual_files
|
||||||
|
|
||||||
self.timestamp = helpers.timestamp()
|
self.timestamp = helpers.timestamp()
|
||||||
|
|
||||||
self.media_type = None
|
self.media_type = None
|
||||||
self.obj = None
|
self.obj = None
|
||||||
self.title = ''
|
self.obj_title = ''
|
||||||
|
|
||||||
|
self.directory = None
|
||||||
self.filename = None
|
self.filename = None
|
||||||
|
self.title = None
|
||||||
self.export_id = None
|
self.export_id = None
|
||||||
self.file_size = None
|
self.file_size = 0
|
||||||
|
self.exported_thumb = False
|
||||||
|
self.exported_art = False
|
||||||
self.success = False
|
self.success = False
|
||||||
|
|
||||||
# Reset export options for m3u8
|
# Reset export options for m3u8
|
||||||
|
@ -416,7 +421,7 @@ class Export(object):
|
||||||
'viewCount': None,
|
'viewCount': None,
|
||||||
'viewedLeafCount': None,
|
'viewedLeafCount': None,
|
||||||
'year': None,
|
'year': None,
|
||||||
'seasons': lambda e: self._export_obj(e)
|
'seasons': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _show_attrs
|
return _show_attrs
|
||||||
|
|
||||||
|
@ -455,7 +460,7 @@ class Export(object):
|
||||||
'userRating': None,
|
'userRating': None,
|
||||||
'viewCount': None,
|
'viewCount': None,
|
||||||
'viewedLeafCount': None,
|
'viewedLeafCount': None,
|
||||||
'episodes': lambda e: self._export_obj(e)
|
'episodes': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _season_attrs
|
return _season_attrs
|
||||||
|
|
||||||
|
@ -700,7 +705,7 @@ class Export(object):
|
||||||
'updatedAt': helpers.datetime_to_iso,
|
'updatedAt': helpers.datetime_to_iso,
|
||||||
'userRating': None,
|
'userRating': None,
|
||||||
'viewCount': None,
|
'viewCount': None,
|
||||||
'albums': lambda e: self._export_obj(e)
|
'albums': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _artist_attrs
|
return _artist_attrs
|
||||||
|
|
||||||
|
@ -760,7 +765,7 @@ class Export(object):
|
||||||
'userRating': None,
|
'userRating': None,
|
||||||
'viewCount': None,
|
'viewCount': None,
|
||||||
'viewedLeafCount': None,
|
'viewedLeafCount': None,
|
||||||
'tracks': lambda e: self._export_obj(e)
|
'tracks': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _album_attrs
|
return _album_attrs
|
||||||
|
|
||||||
|
@ -907,9 +912,9 @@ class Export(object):
|
||||||
'titleSort': None,
|
'titleSort': None,
|
||||||
'type': lambda e: 'photoalbum' if e == 'photo' else e,
|
'type': lambda e: 'photoalbum' if e == 'photo' else e,
|
||||||
'updatedAt': helpers.datetime_to_iso,
|
'updatedAt': helpers.datetime_to_iso,
|
||||||
'photoalbums': lambda o: [self._export_obj(e) for e in getattr(o, 'albums')()],
|
'photoalbums': lambda o: [self.export_obj(e) for e in getattr(o, 'albums')()],
|
||||||
'photos': lambda e: self._export_obj(e),
|
'photos': lambda e: self.export_obj(e),
|
||||||
'clips': lambda e: self._export_obj(e)
|
'clips': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _photo_album_attrs
|
return _photo_album_attrs
|
||||||
|
|
||||||
|
@ -1007,7 +1012,7 @@ class Export(object):
|
||||||
'titleSort': None,
|
'titleSort': None,
|
||||||
'type': None,
|
'type': None,
|
||||||
'updatedAt': helpers.datetime_to_iso,
|
'updatedAt': helpers.datetime_to_iso,
|
||||||
'children': lambda e: self._export_obj(e)
|
'children': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _collection_attrs
|
return _collection_attrs
|
||||||
|
|
||||||
|
@ -1027,7 +1032,7 @@ class Export(object):
|
||||||
'title': None,
|
'title': None,
|
||||||
'type': None,
|
'type': None,
|
||||||
'updatedAt': helpers.datetime_to_iso,
|
'updatedAt': helpers.datetime_to_iso,
|
||||||
'items': lambda e: self._export_obj(e)
|
'items': lambda e: self.export_obj(e)
|
||||||
}
|
}
|
||||||
return _playlist_attrs
|
return _playlist_attrs
|
||||||
|
|
||||||
|
@ -1500,6 +1505,8 @@ class Export(object):
|
||||||
elif self.user_id and self.export_type != 'playlist':
|
elif self.user_id and self.export_type != 'playlist':
|
||||||
msg = "Export called with invalid export_type '{}'. " \
|
msg = "Export called with invalid export_type '{}'. " \
|
||||||
"Only export_type 'playlist' is allowed for user export."
|
"Only export_type 'playlist' is allowed for user export."
|
||||||
|
elif self.individual_files and self.rating_key:
|
||||||
|
msg = "Individual file export is only allowed for library or user export."
|
||||||
|
|
||||||
if msg:
|
if msg:
|
||||||
logger.error("Tautulli Exporter :: %s", msg)
|
logger.error("Tautulli Exporter :: %s", msg)
|
||||||
|
@ -1518,49 +1525,42 @@ class Export(object):
|
||||||
if self.rating_key:
|
if self.rating_key:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Tautulli Exporter :: Export called with rating_key %s, "
|
"Tautulli Exporter :: Export called with rating_key %s, "
|
||||||
"metadata_level %d, media_info_level %d, thumb_level %s, art_level %s",
|
"metadata_level %d, media_info_level %d, thumb_level %s, art_level %s, "
|
||||||
|
"file_format %s",
|
||||||
self.rating_key, self.metadata_level, self.media_info_level,
|
self.rating_key, self.metadata_level, self.media_info_level,
|
||||||
self.thumb_level, self.art_level)
|
self.thumb_level, self.art_level, self.file_format)
|
||||||
|
|
||||||
self.obj = plex.get_item(self.rating_key)
|
self.obj = plex.get_item(self.rating_key)
|
||||||
self.media_type = 'photoalbum' if self.is_photoalbum(self.obj) else self.obj.type
|
self.media_type = self._media_type(self.obj)
|
||||||
|
|
||||||
if self.media_type != 'playlist':
|
if self.media_type != 'playlist':
|
||||||
self.section_id = self.obj.librarySectionID
|
self.section_id = self.obj.librarySectionID
|
||||||
|
|
||||||
if self.media_type in ('season', 'episode', 'album', 'track'):
|
if self.media_type in ('season', 'episode', 'album', 'track'):
|
||||||
self.title = self.obj._defaultSyncTitle()
|
self.obj_title = self.obj._defaultSyncTitle()
|
||||||
else:
|
else:
|
||||||
self.title = self.obj.title
|
self.obj_title = self.obj.title
|
||||||
|
|
||||||
filename = '{} - {} [{}].{}'.format(
|
|
||||||
self.media_type.capitalize(), self.title, self.rating_key,
|
|
||||||
helpers.timestamp_to_YMDHMS(self.timestamp))
|
|
||||||
|
|
||||||
elif self.user_id:
|
elif self.user_id:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Tautulli Exporter :: Export called with user_id %s, "
|
"Tautulli Exporter :: Export called with user_id %s, "
|
||||||
"metadata_level %d, media_info_level %d, thumb_level %s, art_level %s, "
|
"metadata_level %d, media_info_level %d, thumb_level %s, art_level %s, "
|
||||||
"export_type %s",
|
"export_type %s, file_format %s",
|
||||||
self.user_id, self.metadata_level, self.media_info_level,
|
self.user_id, self.metadata_level, self.media_info_level,
|
||||||
self.thumb_level, self.art_level, self.export_type)
|
self.thumb_level, self.art_level, self.export_type, self.file_format)
|
||||||
|
|
||||||
self.obj = plex.plex
|
self.obj = plex.plex
|
||||||
self.media_type = self.export_type
|
self.media_type = self.export_type
|
||||||
|
|
||||||
self.title = user_info['username']
|
self.obj_title = user_info['username']
|
||||||
|
|
||||||
filename = 'User - {} - {} [{}].{}'.format(
|
|
||||||
self.title, self.export_type.capitalize(), self.user_id,
|
|
||||||
helpers.timestamp_to_YMDHMS(self.timestamp))
|
|
||||||
|
|
||||||
elif self.section_id:
|
elif self.section_id:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Tautulli Exporter :: Export called with section_id %s, "
|
"Tautulli Exporter :: Export called with section_id %s, "
|
||||||
"metadata_level %d, media_info_level %d, thumb_level %s, art_level %s, "
|
"metadata_level %d, media_info_level %d, thumb_level %s, art_level %s, "
|
||||||
"export_type %s",
|
"export_type %s, file_format %s",
|
||||||
self.section_id, self.metadata_level, self.media_info_level,
|
self.section_id, self.metadata_level, self.media_info_level,
|
||||||
self.thumb_level, self.art_level, self.export_type)
|
self.thumb_level, self.art_level, self.export_type, self.file_format)
|
||||||
|
|
||||||
self.obj = plex.get_library(str(self.section_id))
|
self.obj = plex.get_library(str(self.section_id))
|
||||||
if self.export_type == 'all':
|
if self.export_type == 'all':
|
||||||
|
@ -1568,11 +1568,7 @@ class Export(object):
|
||||||
else:
|
else:
|
||||||
self.media_type = self.export_type
|
self.media_type = self.export_type
|
||||||
|
|
||||||
self.title = self.obj.title
|
self.obj_title = self.obj.title
|
||||||
|
|
||||||
filename = 'Library - {} - {} [{}].{}'.format(
|
|
||||||
self.title, self.export_type.capitalize(), self.section_id,
|
|
||||||
helpers.timestamp_to_YMDHMS(self.timestamp))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = "Export called but no section_id, user_id, or rating_key provided."
|
msg = "Export called but no section_id, user_id, or rating_key provided."
|
||||||
|
@ -1591,10 +1587,13 @@ class Export(object):
|
||||||
|
|
||||||
self._process_custom_fields()
|
self._process_custom_fields()
|
||||||
|
|
||||||
self.filename = '{}.{}'.format(helpers.clean_filename(filename), self.file_format)
|
self.directory = self._filename(directory=True)
|
||||||
|
self.filename = self._filename()
|
||||||
|
self.title = self._filename(extension=False)
|
||||||
|
|
||||||
self.export_id = self.add_export()
|
self.export_id = self.add_export()
|
||||||
if not self.export_id:
|
if not self.export_id:
|
||||||
msg = "Failed to export '{}'.".format(self.filename)
|
msg = "Failed to export '{}'.".format(self.directory)
|
||||||
logger.error("Tautulli Exporter :: %s", msg)
|
logger.error("Tautulli Exporter :: %s", msg)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
@ -1603,19 +1602,24 @@ class Export(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def add_export(self):
|
def add_export(self):
|
||||||
keys = {'timestamp': self.timestamp,
|
keys = {
|
||||||
'section_id': self.section_id,
|
'timestamp': self.timestamp,
|
||||||
'user_id': self.user_id,
|
'section_id': self.section_id,
|
||||||
'rating_key': self.rating_key,
|
'user_id': self.user_id,
|
||||||
'media_type': self.media_type}
|
'rating_key': self.rating_key,
|
||||||
|
'media_type': self.media_type
|
||||||
|
}
|
||||||
|
|
||||||
values = {'file_format': self.file_format,
|
values = {
|
||||||
'filename': self.filename,
|
'title': self.title,
|
||||||
'metadata_level': self.metadata_level,
|
'file_format': self.file_format,
|
||||||
'media_info_level': self.media_info_level,
|
'metadata_level': self.metadata_level,
|
||||||
'thumb_level': self.thumb_level,
|
'media_info_level': self.media_info_level,
|
||||||
'art_level': self.art_level,
|
'thumb_level': self.thumb_level,
|
||||||
'custom_fields': self.custom_fields}
|
'art_level': self.art_level,
|
||||||
|
'custom_fields': self.custom_fields,
|
||||||
|
'individual_files': self.individual_files
|
||||||
|
}
|
||||||
|
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
try:
|
try:
|
||||||
|
@ -1631,20 +1635,21 @@ class Export(object):
|
||||||
else:
|
else:
|
||||||
complete = -1
|
complete = -1
|
||||||
|
|
||||||
keys = {'id': self.export_id}
|
keys = {
|
||||||
values = {'complete': complete,
|
'id': self.export_id
|
||||||
'file_size': self.file_size,
|
}
|
||||||
'thumb_level': self.thumb_level,
|
values = {
|
||||||
'art_level': self.art_level}
|
'thumb_level': self.thumb_level,
|
||||||
|
'art_level': self.art_level,
|
||||||
|
'complete': complete,
|
||||||
|
'file_size': self.file_size
|
||||||
|
}
|
||||||
|
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
db.upsert(table_name='exports', key_dict=keys, value_dict=values)
|
db.upsert(table_name='exports', key_dict=keys, value_dict=values)
|
||||||
|
|
||||||
def _real_export(self):
|
def _real_export(self):
|
||||||
logger.info("Tautulli Exporter :: Starting export for '%s'...", self.filename)
|
logger.info("Tautulli Exporter :: Starting export for '%s'...", self.title)
|
||||||
|
|
||||||
filepath = get_export_filepath(self.filename)
|
|
||||||
images_folder = get_export_filepath(self.filename, images=True)
|
|
||||||
|
|
||||||
if self.rating_key:
|
if self.rating_key:
|
||||||
items = [self.obj]
|
items = [self.obj]
|
||||||
|
@ -1656,71 +1661,120 @@ class Export(object):
|
||||||
items = method()
|
items = method()
|
||||||
|
|
||||||
pool = ThreadPool(processes=4)
|
pool = ThreadPool(processes=4)
|
||||||
|
items = [ExportObject(self, item) for item in items]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = pool.map(self._export_obj, items)
|
result = pool.map(self._export_obj, items)
|
||||||
|
|
||||||
if self.file_format == 'csv':
|
if self.individual_files:
|
||||||
csv_data = helpers.flatten_dict(result)
|
for item, item_result in zip(items, result):
|
||||||
csv_headers = sorted(set().union(*csv_data), key=helpers.sort_attrs)
|
self._save_file([item_result], item.filename)
|
||||||
with open(filepath, 'w', encoding='utf-8', newline='') as outfile:
|
self._exported_images(item.title)
|
||||||
writer = csv.DictWriter(outfile, csv_headers)
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(csv_data)
|
|
||||||
|
|
||||||
elif self.file_format == 'json':
|
else:
|
||||||
json_data = json.dumps(helpers.sort_obj(result),
|
self._save_file(result, self.filename)
|
||||||
indent=4, ensure_ascii=False)
|
self._exported_images(self.title)
|
||||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
|
||||||
outfile.write(json_data)
|
|
||||||
|
|
||||||
elif self.file_format == 'xml':
|
self.thumb_level = self.thumb_level or 10 if self.exported_thumb else 0
|
||||||
xml_data = helpers.dict_to_xml({self.media_type: helpers.sort_obj(result)},
|
self.art_level = self.art_level or 10 if self.exported_art else 0
|
||||||
root_node='export', indent=4)
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
|
||||||
outfile.write(xml_data)
|
|
||||||
|
|
||||||
elif self.file_format == 'm3u8':
|
self.file_size += sum(item.file_size for item in items)
|
||||||
m3u8_data = self.dict_to_m3u8(result)
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
|
||||||
outfile.write(m3u8_data)
|
|
||||||
|
|
||||||
self.file_size = os.path.getsize(filepath)
|
|
||||||
|
|
||||||
exported_thumb = exported_art = False
|
|
||||||
if os.path.exists(images_folder):
|
|
||||||
for f in os.listdir(images_folder):
|
|
||||||
if f.endswith('.thumb.jpg'):
|
|
||||||
exported_thumb = True
|
|
||||||
elif f.endswith('.art.jpg'):
|
|
||||||
exported_art = True
|
|
||||||
|
|
||||||
image_path = os.path.join(images_folder, f)
|
|
||||||
if os.path.isfile(image_path):
|
|
||||||
self.file_size += os.path.getsize(image_path)
|
|
||||||
|
|
||||||
self.thumb_level = self.thumb_level if exported_thumb else 0
|
|
||||||
self.art_level = self.art_level if exported_art else 0
|
|
||||||
|
|
||||||
self.success = True
|
self.success = True
|
||||||
logger.info("Tautulli Exporter :: Successfully exported to '%s'", filepath)
|
|
||||||
|
dirpath = get_export_dirpath(self.directory)
|
||||||
|
logger.info("Tautulli Exporter :: Successfully exported to '%s'", dirpath)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Tautulli Exporter :: Failed to export '%s': %s", self.filename, e)
|
logger.exception("Tautulli Exporter :: Failed to export '%s': %s", self.title, e)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
pool.close()
|
pool.close()
|
||||||
pool.join()
|
pool.join()
|
||||||
self.set_export_state()
|
self.set_export_state()
|
||||||
|
|
||||||
def _export_obj(self, obj):
|
@staticmethod
|
||||||
# Reload ~plexapi.base.PlexPartialObject
|
def _export_obj(export_obj):
|
||||||
if hasattr(obj, 'isPartialObject') and obj.isPartialObject():
|
return export_obj.export_obj(export_obj)
|
||||||
obj = obj.reload()
|
|
||||||
|
|
||||||
media_type = 'photoalbum' if self.is_photoalbum(obj) else obj.type
|
def _save_file(self, result, filename):
|
||||||
export_attrs = self._get_export_attrs(media_type)
|
dirpath = get_export_dirpath(self.directory)
|
||||||
return helpers.get_attrs_to_dict(obj, attrs=export_attrs)
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(dirpath):
|
||||||
|
os.makedirs(dirpath)
|
||||||
|
|
||||||
|
if self.file_format == 'csv':
|
||||||
|
csv_data = helpers.flatten_dict(result)
|
||||||
|
csv_headers = sorted(set().union(*csv_data), key=helpers.sort_attrs)
|
||||||
|
with open(filepath, 'w', encoding='utf-8', newline='') as outfile:
|
||||||
|
writer = csv.DictWriter(outfile, csv_headers)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(csv_data)
|
||||||
|
|
||||||
|
elif self.file_format == 'json':
|
||||||
|
json_data = json.dumps(helpers.sort_obj(result),
|
||||||
|
indent=4, ensure_ascii=False)
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
|
outfile.write(json_data)
|
||||||
|
|
||||||
|
elif self.file_format == 'xml':
|
||||||
|
xml_data = helpers.dict_to_xml({self.media_type: helpers.sort_obj(result)},
|
||||||
|
root_node='export', indent=4)
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
|
outfile.write(xml_data)
|
||||||
|
|
||||||
|
elif self.file_format == 'm3u8':
|
||||||
|
m3u8_data = self.dict_to_m3u8(result)
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
|
outfile.write(m3u8_data)
|
||||||
|
|
||||||
|
self.file_size += os.path.getsize(filepath)
|
||||||
|
|
||||||
|
def _exported_images(self, title):
|
||||||
|
images_dirpath = get_export_dirpath(self.directory, images_directory=title)
|
||||||
|
|
||||||
|
if os.path.exists(images_dirpath):
|
||||||
|
for f in os.listdir(images_dirpath):
|
||||||
|
if f.endswith('.thumb.jpg'):
|
||||||
|
self.exported_thumb = True
|
||||||
|
elif f.endswith('.art.jpg'):
|
||||||
|
self.exported_art = True
|
||||||
|
|
||||||
|
def _media_type(self, obj):
|
||||||
|
return 'photoalbum' if self.is_photoalbum(obj) else obj.type
|
||||||
|
|
||||||
|
def _filename(self, obj=None, directory=False, extension=True):
|
||||||
|
if obj:
|
||||||
|
media_type = self._media_type(obj)
|
||||||
|
if media_type in ('season', 'episode', 'album', 'track'):
|
||||||
|
title = obj._defaultSyncTitle()
|
||||||
|
else:
|
||||||
|
title = obj.title
|
||||||
|
filename = '{} - {} [{}]'.format(
|
||||||
|
media_type.capitalize(), title, obj.ratingKey)
|
||||||
|
|
||||||
|
elif self.rating_key:
|
||||||
|
filename = '{} - {} [{}]'.format(
|
||||||
|
self.media_type.capitalize(), self.obj_title, self.rating_key)
|
||||||
|
|
||||||
|
elif self.user_id:
|
||||||
|
filename = 'User - {} - {} [{}]'.format(
|
||||||
|
self.obj_title, self.export_type.capitalize(), self.user_id)
|
||||||
|
|
||||||
|
elif self.section_id:
|
||||||
|
filename = 'Library - {} - {} [{}]'.format(
|
||||||
|
self.obj_title, self.export_type.capitalize(), self.section_id)
|
||||||
|
|
||||||
|
else:
|
||||||
|
filename = 'Export - Unknown'
|
||||||
|
|
||||||
|
filename = helpers.clean_filename(filename)
|
||||||
|
if directory:
|
||||||
|
return format_export_directory(filename, self.timestamp)
|
||||||
|
elif extension:
|
||||||
|
return format_export_filename(filename, self.file_format)
|
||||||
|
return filename
|
||||||
|
|
||||||
def _process_custom_fields(self):
|
def _process_custom_fields(self):
|
||||||
if self.custom_fields:
|
if self.custom_fields:
|
||||||
|
@ -1798,61 +1852,6 @@ class Export(object):
|
||||||
|
|
||||||
return reduce(helpers.dict_merge, export_attrs_list, {})
|
return reduce(helpers.dict_merge, export_attrs_list, {})
|
||||||
|
|
||||||
def get_any_hdr(self, item, media_type):
|
|
||||||
root = self.return_attrs(media_type)['media']
|
|
||||||
attrs = helpers.get_dict_value_by_path(root, 'parts.videoStreams.hdr')
|
|
||||||
media = helpers.get_attrs_to_dict(item, attrs)
|
|
||||||
return any(vs.get('hdr') for p in media.get('parts', []) for vs in p.get('videoStreams', []))
|
|
||||||
|
|
||||||
def get_image(self, item, image):
|
|
||||||
media_type = item.type
|
|
||||||
rating_key = item.ratingKey
|
|
||||||
|
|
||||||
export_image = True
|
|
||||||
if self.thumb_level == 1 or self.art_level == 1:
|
|
||||||
posters = item.arts() if image == 'art' else item.posters()
|
|
||||||
export_image = any(poster.selected and poster.ratingKey.startswith('upload://')
|
|
||||||
for poster in posters)
|
|
||||||
elif self.thumb_level == 2 or self.art_level == 2:
|
|
||||||
export_image = any(field.locked and field.name == image
|
|
||||||
for field in item.fields)
|
|
||||||
elif self.thumb_level == 9 or self.art_level == 9:
|
|
||||||
export_image = True
|
|
||||||
|
|
||||||
if not export_image and image + 'File' in self._custom_fields.get(media_type, set()):
|
|
||||||
export_image = True
|
|
||||||
|
|
||||||
if not export_image:
|
|
||||||
return
|
|
||||||
|
|
||||||
image_url = None
|
|
||||||
if image == 'thumb':
|
|
||||||
image_url = item.thumbUrl
|
|
||||||
elif image == 'art':
|
|
||||||
image_url = item.artUrl
|
|
||||||
|
|
||||||
if not image_url:
|
|
||||||
return
|
|
||||||
|
|
||||||
if media_type in ('season', 'episode', 'album', 'track'):
|
|
||||||
item_title = item._defaultSyncTitle()
|
|
||||||
else:
|
|
||||||
item_title = item.title
|
|
||||||
|
|
||||||
folder = get_export_filepath(self.filename, images=True)
|
|
||||||
filename = helpers.clean_filename('{} [{}].{}.jpg'.format(item_title, rating_key, image))
|
|
||||||
filepath = os.path.join(folder, filename)
|
|
||||||
|
|
||||||
os.makedirs(folder, exist_ok=True)
|
|
||||||
|
|
||||||
r = requests.get(image_url, stream=True)
|
|
||||||
if r.status_code == 200:
|
|
||||||
with open(filepath, 'wb') as outfile:
|
|
||||||
for chunk in r:
|
|
||||||
outfile.write(chunk)
|
|
||||||
|
|
||||||
return os.path.join(os.path.basename(folder), filename)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_media_info_attr(attr):
|
def is_media_info_attr(attr):
|
||||||
return attr.startswith('media.') or attr == 'locations'
|
return attr.startswith('media.') or attr == 'locations'
|
||||||
|
@ -1863,10 +1862,8 @@ class Export(object):
|
||||||
|
|
||||||
def dict_to_m3u8(self, data):
|
def dict_to_m3u8(self, data):
|
||||||
items = self._get_m3u8_items(data)
|
items = self._get_m3u8_items(data)
|
||||||
m3u8_metadata = {
|
|
||||||
'filename': self.filename,
|
m3u8_metadata = {'type': self.media_type}
|
||||||
'type': self.media_type
|
|
||||||
}
|
|
||||||
if self.rating_key:
|
if self.rating_key:
|
||||||
m3u8_metadata['ratingKey'] = self.rating_key
|
m3u8_metadata['ratingKey'] = self.rating_key
|
||||||
if self.user_id:
|
if self.user_id:
|
||||||
|
@ -1916,63 +1913,156 @@ class Export(object):
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def export_obj(self, export_obj):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_any_hdr(self, item, media_type):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_image(self, item, image):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ExportObject(Export):
|
||||||
|
def __init__(self, export, obj):
|
||||||
|
super(ExportObject, self).__init__()
|
||||||
|
self.__dict__.update(export.__dict__)
|
||||||
|
|
||||||
|
self.obj = obj
|
||||||
|
self.filename = self._filename(obj=self.obj)
|
||||||
|
self.title = self._filename(obj=self.obj, extension=False)
|
||||||
|
|
||||||
|
def export_obj(self, export_obj):
|
||||||
|
if isinstance(export_obj, ExportObject):
|
||||||
|
obj = export_obj.obj
|
||||||
|
else:
|
||||||
|
obj = export_obj
|
||||||
|
|
||||||
|
# Reload ~plexapi.base.PlexPartialObject
|
||||||
|
if hasattr(obj, 'isPartialObject') and obj.isPartialObject():
|
||||||
|
obj = obj.reload()
|
||||||
|
|
||||||
|
media_type = self._media_type(obj)
|
||||||
|
export_attrs = self._get_export_attrs(media_type)
|
||||||
|
return helpers.get_attrs_to_dict(obj, attrs=export_attrs)
|
||||||
|
|
||||||
|
def get_any_hdr(self, item, media_type):
|
||||||
|
root = self.return_attrs(media_type)['media']
|
||||||
|
attrs = helpers.get_dict_value_by_path(root, 'parts.videoStreams.hdr')
|
||||||
|
media = helpers.get_attrs_to_dict(item, attrs)
|
||||||
|
return any(vs.get('hdr') for p in media.get('parts', []) for vs in p.get('videoStreams', []))
|
||||||
|
|
||||||
|
def get_image(self, item, image):
|
||||||
|
media_type = item.type
|
||||||
|
rating_key = item.ratingKey
|
||||||
|
|
||||||
|
export_image = True
|
||||||
|
if self.thumb_level == 1 or self.art_level == 1:
|
||||||
|
posters = item.arts() if image == 'art' else item.posters()
|
||||||
|
export_image = any(poster.selected and poster.ratingKey.startswith('upload://')
|
||||||
|
for poster in posters)
|
||||||
|
elif self.thumb_level == 2 or self.art_level == 2:
|
||||||
|
export_image = any(field.locked and field.name == image
|
||||||
|
for field in item.fields)
|
||||||
|
elif self.thumb_level == 9 or self.art_level == 9:
|
||||||
|
export_image = True
|
||||||
|
|
||||||
|
if not export_image and image + 'File' in self._custom_fields.get(media_type, set()):
|
||||||
|
export_image = True
|
||||||
|
|
||||||
|
if not export_image:
|
||||||
|
return
|
||||||
|
|
||||||
|
image_url = None
|
||||||
|
if image == 'thumb':
|
||||||
|
image_url = item.thumbUrl
|
||||||
|
elif image == 'art':
|
||||||
|
image_url = item.artUrl
|
||||||
|
|
||||||
|
if not image_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
r = requests.get(image_url, stream=True)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
if media_type in ('season', 'episode', 'album', 'track'):
|
||||||
|
item_title = item._defaultSyncTitle()
|
||||||
|
else:
|
||||||
|
item_title = item.title
|
||||||
|
|
||||||
|
dirpath = get_export_dirpath(self.directory, images_directory=self.title)
|
||||||
|
filename = helpers.clean_filename('{} [{}].{}.jpg'.format(item_title, rating_key, image))
|
||||||
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(dirpath):
|
||||||
|
os.makedirs(dirpath)
|
||||||
|
|
||||||
|
with open(filepath, 'wb') as outfile:
|
||||||
|
for chunk in r:
|
||||||
|
outfile.write(chunk)
|
||||||
|
|
||||||
|
self.file_size += os.path.getsize(filepath)
|
||||||
|
|
||||||
|
return os.path.join(os.path.basename(dirpath), filename)
|
||||||
|
|
||||||
|
|
||||||
def get_export(export_id):
|
def get_export(export_id):
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
result = db.select_single('SELECT filename, file_format, thumb_level, art_level, complete '
|
result = db.select_single('SELECT timestamp, title, file_format, thumb_level, art_level, '
|
||||||
|
'individual_files, complete '
|
||||||
'FROM exports WHERE id = ?',
|
'FROM exports WHERE id = ?',
|
||||||
[export_id])
|
[export_id])
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
result['exists'] = check_export_exists(result['filename'])
|
if result['individual_files']:
|
||||||
|
result['filename'] = None
|
||||||
|
result['exists'] = check_export_exists(result['title'], result['timestamp'])
|
||||||
|
else:
|
||||||
|
result['filename'] = '{}.{}'.format(result['title'], result['file_format'])
|
||||||
|
result['exists'] = check_export_exists(result['title'], result['timestamp'], result['filename'])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def delete_export(export_id):
|
def delete_export(export_id):
|
||||||
db = database.MonitorDatabase()
|
|
||||||
if str(export_id).isdigit():
|
if str(export_id).isdigit():
|
||||||
export_data = get_export(export_id=export_id)
|
deleted = True
|
||||||
|
|
||||||
logger.info("Tautulli Exporter :: Deleting export_id %s from the database.", export_id)
|
result = get_export(export_id=export_id)
|
||||||
result = db.action('DELETE FROM exports WHERE id = ?', args=[export_id])
|
if result and check_export_exists(result['title'], result['timestamp']): # Only check if folder exists
|
||||||
|
dirpath = get_export_dirpath(result['title'], result['timestamp'])
|
||||||
if export_data and export_data['exists']:
|
logger.info("Tautulli Exporter :: Deleting export '%s'.", dirpath)
|
||||||
filepath = get_export_filepath(export_data['filename'])
|
|
||||||
logger.info("Tautulli Exporter :: Deleting exported file from '%s'.", filepath)
|
|
||||||
try:
|
try:
|
||||||
os.remove(filepath)
|
shutil.rmtree(dirpath, ignore_errors=True)
|
||||||
images_folder = get_export_filepath(export_data['filename'], images=True)
|
|
||||||
shutil.rmtree(images_folder, ignore_errors=True)
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error("Tautulli Exporter :: Failed to delete exported file '%s': %s", filepath, e)
|
logger.error("Tautulli Exporter :: Failed to delete export '%s': %s", dirpath, e)
|
||||||
return True
|
deleted = False
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
logger.info("Tautulli Exporter :: Deleting export_id %s from the database.", export_id)
|
||||||
|
db = database.MonitorDatabase()
|
||||||
|
result = db.action('DELETE FROM exports WHERE id = ?', args=[export_id])
|
||||||
|
|
||||||
|
return deleted
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def delete_all_exports():
|
def delete_all_exports():
|
||||||
db = database.MonitorDatabase()
|
logger.info("Tautulli Exporter :: Deleting all exports from the export directory.")
|
||||||
result = db.select('SELECT filename FROM exports')
|
|
||||||
|
|
||||||
logger.info("Tautulli Exporter :: Deleting all exports from the database.")
|
export_dir = plexpy.CONFIG.EXPORT_DIR
|
||||||
|
try:
|
||||||
|
shutil.rmtree(export_dir, ignore_errors=True)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("Tautulli Exporter :: Failed to delete export directory '%s': %s", export_dir, e)
|
||||||
|
|
||||||
deleted_files = True
|
if not os.path.exists(export_dir):
|
||||||
for row in result:
|
os.makedirs(export_dir)
|
||||||
if check_export_exists(row['filename']):
|
|
||||||
filepath = get_export_filepath(row['filename'])
|
|
||||||
try:
|
|
||||||
os.remove(filepath)
|
|
||||||
images_folder = get_export_filepath(row['filename'], images=True)
|
|
||||||
shutil.rmtree(images_folder, ignore_errors=True)
|
|
||||||
except OSError as e:
|
|
||||||
logger.error("Tautulli Exporter :: Failed to delete exported file '%s': %s", filepath, e)
|
|
||||||
deleted_files = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if deleted_files:
|
database.delete_exports()
|
||||||
database.delete_exports()
|
return True
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_exports():
|
def cancel_exports():
|
||||||
|
@ -2003,13 +2093,17 @@ def get_export_datatable(section_id=None, user_id=None, rating_key=None, kwargs=
|
||||||
'exports.user_id',
|
'exports.user_id',
|
||||||
'exports.rating_key',
|
'exports.rating_key',
|
||||||
'exports.media_type',
|
'exports.media_type',
|
||||||
'exports.filename',
|
'CASE WHEN exports.media_type = "photoalbum" THEN "Photo Album" ELSE '
|
||||||
|
'UPPER(SUBSTR(exports.media_type, 1, 1)) || SUBSTR(exports.media_type, 2) END '
|
||||||
|
'AS media_type_title',
|
||||||
|
'exports.title',
|
||||||
'exports.file_format',
|
'exports.file_format',
|
||||||
'exports.metadata_level',
|
'exports.metadata_level',
|
||||||
'exports.media_info_level',
|
'exports.media_info_level',
|
||||||
'exports.thumb_level',
|
'exports.thumb_level',
|
||||||
'exports.art_level',
|
'exports.art_level',
|
||||||
'exports.custom_fields',
|
'exports.custom_fields',
|
||||||
|
'exports.individual_files',
|
||||||
'exports.file_size',
|
'exports.file_size',
|
||||||
'exports.complete'
|
'exports.complete'
|
||||||
]
|
]
|
||||||
|
@ -2030,8 +2124,12 @@ def get_export_datatable(section_id=None, user_id=None, rating_key=None, kwargs=
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for item in result:
|
for item in result:
|
||||||
media_type_title = item['media_type'].title()
|
if item['individual_files']:
|
||||||
exists = helpers.cast_to_int(check_export_exists(item['filename']))
|
filename = None
|
||||||
|
exists = check_export_exists(item['title'], item['timestamp'])
|
||||||
|
else:
|
||||||
|
filename = format_export_filename(item['title'], item['file_format'])
|
||||||
|
exists = check_export_exists(item['title'], item['timestamp'], filename)
|
||||||
|
|
||||||
row = {'export_id': item['export_id'],
|
row = {'export_id': item['export_id'],
|
||||||
'timestamp': item['timestamp'],
|
'timestamp': item['timestamp'],
|
||||||
|
@ -2039,14 +2137,16 @@ def get_export_datatable(section_id=None, user_id=None, rating_key=None, kwargs=
|
||||||
'user_id': item['user_id'],
|
'user_id': item['user_id'],
|
||||||
'rating_key': item['rating_key'],
|
'rating_key': item['rating_key'],
|
||||||
'media_type': item['media_type'],
|
'media_type': item['media_type'],
|
||||||
'media_type_title': media_type_title,
|
'media_type_title': item['media_type_title'],
|
||||||
'filename': item['filename'],
|
'title': item['title'],
|
||||||
|
'filename': filename,
|
||||||
'file_format': item['file_format'],
|
'file_format': item['file_format'],
|
||||||
'metadata_level': item['metadata_level'],
|
'metadata_level': item['metadata_level'],
|
||||||
'media_info_level': item['media_info_level'],
|
'media_info_level': item['media_info_level'],
|
||||||
'thumb_level': item['thumb_level'],
|
'thumb_level': item['thumb_level'],
|
||||||
'art_level': item['art_level'],
|
'art_level': item['art_level'],
|
||||||
'custom_fields': item['custom_fields'],
|
'custom_fields': item['custom_fields'],
|
||||||
|
'individual_files': item['individual_files'],
|
||||||
'file_size': item['file_size'],
|
'file_size': item['file_size'],
|
||||||
'complete': item['complete'],
|
'complete': item['complete'],
|
||||||
'exists': exists
|
'exists': exists
|
||||||
|
@ -2063,15 +2163,32 @@ def get_export_datatable(section_id=None, user_id=None, rating_key=None, kwargs=
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_export_filepath(filename, images=False):
|
def format_export_directory(title, timestamp):
|
||||||
if images:
|
return '{}.{}'.format(title, helpers.timestamp_to_YMDHMS(timestamp))
|
||||||
images_folder = '{}.images'.format(os.path.splitext(filename)[0])
|
|
||||||
return os.path.join(plexpy.CONFIG.EXPORT_DIR, images_folder)
|
|
||||||
return os.path.join(plexpy.CONFIG.EXPORT_DIR, filename)
|
|
||||||
|
|
||||||
|
|
||||||
def check_export_exists(filename):
|
def format_export_filename(title, file_format):
|
||||||
return os.path.isfile(get_export_filepath(filename))
|
return '{}.{}'.format(title, file_format)
|
||||||
|
|
||||||
|
|
||||||
|
def get_export_dirpath(title, timestamp=None, images_directory=None):
|
||||||
|
if timestamp:
|
||||||
|
title = format_export_directory(title, timestamp)
|
||||||
|
dirpath = os.path.join(plexpy.CONFIG.EXPORT_DIR, title)
|
||||||
|
if images_directory:
|
||||||
|
dirpath = os.path.join(dirpath, '{}.images'.format(images_directory))
|
||||||
|
return dirpath
|
||||||
|
|
||||||
|
|
||||||
|
def get_export_filepath(title, timestamp, filename):
|
||||||
|
dirpath = get_export_dirpath(title, timestamp)
|
||||||
|
return os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
|
||||||
|
def check_export_exists(title, timestamp=None, filename=None):
|
||||||
|
if filename:
|
||||||
|
return os.path.isfile(get_export_filepath(title, timestamp, filename))
|
||||||
|
return os.path.isdir(get_export_dirpath(title, timestamp))
|
||||||
|
|
||||||
|
|
||||||
def get_custom_fields(media_type, sub_media_type=None):
|
def get_custom_fields(media_type, sub_media_type=None):
|
||||||
|
|
|
@ -1474,6 +1474,16 @@ def version_to_tuple(version):
|
||||||
return tuple(cast_to_int(v) for v in version.strip('v').split('.'))
|
return tuple(cast_to_int(v) for v in version.strip('v').split('.'))
|
||||||
|
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/1855118
|
||||||
|
def zipdir(path, ziph):
|
||||||
|
# ziph is zipfile handle
|
||||||
|
for root, dirs, files in os.walk(path):
|
||||||
|
for file in files:
|
||||||
|
ziph.write(os.path.join(root, file),
|
||||||
|
arcname=os.path.relpath(os.path.join(root, file),
|
||||||
|
os.path.join(path, '.')))
|
||||||
|
|
||||||
|
|
||||||
def page(endpoint, *args, **kwargs):
|
def page(endpoint, *args, **kwargs):
|
||||||
endpoints = {
|
endpoints = {
|
||||||
'pms_image_proxy': pms_image_proxy,
|
'pms_image_proxy': pms_image_proxy,
|
||||||
|
|
|
@ -6578,8 +6578,12 @@ class WebInterface(object):
|
||||||
dt_columns = [("timestamp", True, False),
|
dt_columns = [("timestamp", True, False),
|
||||||
("media_type_title", True, True),
|
("media_type_title", True, True),
|
||||||
("rating_key", True, True),
|
("rating_key", True, True),
|
||||||
|
("title", True, True),
|
||||||
("file_format", True, True),
|
("file_format", True, True),
|
||||||
("filename", True, True),
|
("metadata_level", True, True),
|
||||||
|
("media_info_level", True, True),
|
||||||
|
("custom_fields", True, True),
|
||||||
|
("file_size", True, False),
|
||||||
("complete", True, False)]
|
("complete", True, False)]
|
||||||
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "timestamp")
|
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "timestamp")
|
||||||
|
|
||||||
|
@ -6646,7 +6650,7 @@ class WebInterface(object):
|
||||||
def export_metadata(self, section_id=None, user_id=None, rating_key=None, file_format='csv',
|
def export_metadata(self, section_id=None, user_id=None, rating_key=None, file_format='csv',
|
||||||
metadata_level=1, media_info_level=1,
|
metadata_level=1, media_info_level=1,
|
||||||
thumb_level=0, art_level=0,
|
thumb_level=0, art_level=0,
|
||||||
custom_fields='', export_type=None, **kwargs):
|
custom_fields='', export_type=None, individual_files=False, **kwargs):
|
||||||
""" Export library or media metadata to a file
|
""" Export library or media metadata to a file
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -6663,8 +6667,9 @@ class WebInterface(object):
|
||||||
art_level (int): The level of background artwork images to export (default 0)
|
art_level (int): The level of background artwork images to export (default 0)
|
||||||
custom_fields (str): Comma separated list of custom fields to export
|
custom_fields (str): Comma separated list of custom fields to export
|
||||||
in addition to the export level selected
|
in addition to the export level selected
|
||||||
export_type (str): collection or playlist for library/user export,
|
export_type (str): 'collection' or 'playlist' for library/user export,
|
||||||
otherwise default to all library items
|
otherwise default to all library items
|
||||||
|
individual_files (bool): Export each item as an individual file for library/user export.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
json:
|
json:
|
||||||
|
@ -6673,6 +6678,7 @@ class WebInterface(object):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
individual_files = helpers.bool_true(individual_files)
|
||||||
result = exporter.Export(section_id=section_id,
|
result = exporter.Export(section_id=section_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
rating_key=rating_key,
|
rating_key=rating_key,
|
||||||
|
@ -6682,7 +6688,8 @@ class WebInterface(object):
|
||||||
thumb_level=thumb_level,
|
thumb_level=thumb_level,
|
||||||
art_level=art_level,
|
art_level=art_level,
|
||||||
custom_fields=custom_fields,
|
custom_fields=custom_fields,
|
||||||
export_type=export_type).export()
|
export_type=export_type,
|
||||||
|
individual_files=individual_files).export()
|
||||||
|
|
||||||
if result is True:
|
if result is True:
|
||||||
return {'result': 'success', 'message': 'Metadata export has started.'}
|
return {'result': 'success', 'message': 'Metadata export has started.'}
|
||||||
|
@ -6707,8 +6714,8 @@ class WebInterface(object):
|
||||||
"""
|
"""
|
||||||
result = exporter.get_export(export_id=export_id)
|
result = exporter.get_export(export_id=export_id)
|
||||||
|
|
||||||
if result and result['complete'] == 1 and result['exists']:
|
if result and result['complete'] == 1 and result['exists'] and not result['individual_files']:
|
||||||
filepath = exporter.get_export_filepath(result['filename'])
|
filepath = exporter.get_export_filepath(result['title'], result['timestamp'], result['filename'])
|
||||||
|
|
||||||
if result['file_format'] == 'csv':
|
if result['file_format'] == 'csv':
|
||||||
with open(filepath, 'r', encoding='utf-8') as infile:
|
with open(filepath, 'r', encoding='utf-8') as infile:
|
||||||
|
@ -6769,28 +6776,23 @@ class WebInterface(object):
|
||||||
result = exporter.get_export(export_id=export_id)
|
result = exporter.get_export(export_id=export_id)
|
||||||
|
|
||||||
if result and result['complete'] == 1 and result['exists']:
|
if result and result['complete'] == 1 and result['exists']:
|
||||||
export_filepath = exporter.get_export_filepath(result['filename'])
|
if result['thumb_level'] or result['art_level'] or result['individual_files']:
|
||||||
|
directory = exporter.format_export_directory(result['title'], result['timestamp'])
|
||||||
|
dirpath = exporter.get_export_dirpath(directory)
|
||||||
|
zip_filename = '{}.zip'.format(directory)
|
||||||
|
|
||||||
if result['thumb_level'] or result['art_level']:
|
buffer = BytesIO()
|
||||||
zip_filename = '{}.zip'.format(os.path.splitext(result['filename'])[0])
|
temp_zip = zipfile.ZipFile(buffer, 'w')
|
||||||
images_folder = exporter.get_export_filepath(result['filename'], images=True)
|
helpers.zipdir(dirpath, temp_zip)
|
||||||
|
temp_zip.close()
|
||||||
|
|
||||||
if os.path.exists(images_folder):
|
return serve_fileobj(buffer.getvalue(), content_type='application/zip',
|
||||||
buffer = BytesIO()
|
disposition='attachment', name=zip_filename)
|
||||||
temp_zip = zipfile.ZipFile(buffer, 'w')
|
|
||||||
temp_zip.write(export_filepath, arcname=result['filename'])
|
|
||||||
|
|
||||||
_images_folder = os.path.basename(images_folder)
|
else:
|
||||||
|
filepath = exporter.get_export_filepath(result['title'], result['timestamp'], result['filename'])
|
||||||
|
return serve_download(filepath, name=result['filename'])
|
||||||
|
|
||||||
for f in os.listdir(images_folder):
|
|
||||||
image_path = os.path.join(images_folder, f)
|
|
||||||
temp_zip.write(image_path, arcname=os.path.join(_images_folder, f))
|
|
||||||
|
|
||||||
temp_zip.close()
|
|
||||||
return serve_fileobj(buffer.getvalue(), content_type='application/zip',
|
|
||||||
disposition='attachment', name=zip_filename)
|
|
||||||
|
|
||||||
return serve_download(exporter.get_export_filepath(result['filename']), name=result['filename'])
|
|
||||||
else:
|
else:
|
||||||
if result and result.get('complete') == 0:
|
if result and result.get('complete') == 0:
|
||||||
msg = 'Export is still being processed.'
|
msg = 'Export is still being processed.'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue