mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11:15 -07:00
Add XML export format
This commit is contained in:
parent
ad8dee3c47
commit
454235dd9a
5 changed files with 87 additions and 21 deletions
2
API.md
2
API.md
|
@ -416,7 +416,7 @@ Required parameters:
|
||||||
rating_key (int): The rating key of the media item to export
|
rating_key (int): The rating key of the media item to export
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
file_format (str): 'json' (default) or 'csv'
|
file_format (str): csv (default), json, or xml
|
||||||
metadata_level (int): The level of metadata to export (default 1)
|
metadata_level (int): The level of metadata to export (default 1)
|
||||||
media_info_level (int): The level of media info to export (default 1)
|
media_info_level (int): The level of media info to export (default 1)
|
||||||
include_thumb (bool): True to export poster/cover images
|
include_thumb (bool): True to export poster/cover images
|
||||||
|
|
|
@ -81,8 +81,9 @@ DOCUMENTATION :: END
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<select class="form-control" id="file_format_select" name="file_format_select">
|
<select class="form-control" id="file_format_select" name="file_format_select">
|
||||||
<option value="csv">CSV</option>
|
% for format in file_formats:
|
||||||
<option value="json">JSON</option>
|
<option value="${format}">${format.upper()}</option>
|
||||||
|
% endfor
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -89,8 +89,9 @@ class Export(object):
|
||||||
}
|
}
|
||||||
METADATA_LEVELS = (0, 1, 2, 3, 9)
|
METADATA_LEVELS = (0, 1, 2, 3, 9)
|
||||||
MEDIA_INFO_LEVELS = (0, 1, 2, 3, 9)
|
MEDIA_INFO_LEVELS = (0, 1, 2, 3, 9)
|
||||||
|
FILE_FORMATS = ('csv', 'json', 'xml')
|
||||||
|
|
||||||
def __init__(self, section_id=None, rating_key=None, file_format='json',
|
def __init__(self, section_id=None, rating_key=None, file_format='csv',
|
||||||
metadata_level=1, media_info_level=1,
|
metadata_level=1, media_info_level=1,
|
||||||
include_thumb=False, include_art=False,
|
include_thumb=False, include_art=False,
|
||||||
custom_fields=''):
|
custom_fields=''):
|
||||||
|
@ -1456,7 +1457,7 @@ class Export(object):
|
||||||
msg = "Export called with invalid metadata_level '{}'.".format(self.metadata_level)
|
msg = "Export called with invalid metadata_level '{}'.".format(self.metadata_level)
|
||||||
elif self.media_info_level not in self.MEDIA_INFO_LEVELS:
|
elif self.media_info_level not in self.MEDIA_INFO_LEVELS:
|
||||||
msg = "Export called with invalid media_info_level '{}'.".format(self.media_info_level)
|
msg = "Export called with invalid media_info_level '{}'.".format(self.media_info_level)
|
||||||
elif self.file_format not in ('json', 'csv'):
|
elif self.file_format not in self.FILE_FORMATS:
|
||||||
msg = "Export called with invalid file_format '{}'.".format(self.file_format)
|
msg = "Export called with invalid file_format '{}'.".format(self.file_format)
|
||||||
|
|
||||||
if msg:
|
if msg:
|
||||||
|
@ -1583,18 +1584,23 @@ class Export(object):
|
||||||
try:
|
try:
|
||||||
result = pool.map(self._export_obj, items)
|
result = pool.map(self._export_obj, items)
|
||||||
|
|
||||||
if self.file_format == 'json':
|
if self.file_format == 'csv':
|
||||||
|
csv_data = helpers.flatten_dict(result)
|
||||||
|
csv_headers = set().union(*csv_data)
|
||||||
|
with open(filepath, 'w', encoding='utf-8', newline='') as outfile:
|
||||||
|
writer = csv.DictWriter(outfile, sorted(csv_headers))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(csv_data)
|
||||||
|
|
||||||
|
elif self.file_format == 'json':
|
||||||
json_data = json.dumps(result, indent=4, ensure_ascii=False, sort_keys=True)
|
json_data = json.dumps(result, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
outfile.write(json_data)
|
outfile.write(json_data)
|
||||||
|
|
||||||
elif self.file_format == 'csv':
|
elif self.file_format == 'xml':
|
||||||
flatten_result = helpers.flatten_dict(result)
|
xml_data = helpers.dict2xml(result, root_node=self.media_type)
|
||||||
flatten_attrs = set().union(*flatten_result)
|
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||||
with open(filepath, 'w', encoding='utf-8', newline='') as outfile:
|
outfile.write(xml_data)
|
||||||
writer = csv.DictWriter(outfile, sorted(flatten_attrs))
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(flatten_result)
|
|
||||||
|
|
||||||
self.file_size = os.path.getsize(filepath)
|
self.file_size = os.path.getsize(filepath)
|
||||||
|
|
||||||
|
|
|
@ -1312,6 +1312,57 @@ def dict_update(*dict_args):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/28703510
|
||||||
|
def escape_xml(value):
|
||||||
|
if value is None:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
value = str(value) \
|
||||||
|
.replace("&", "&") \
|
||||||
|
.replace("<", "<") \
|
||||||
|
.replace(">", ">") \
|
||||||
|
.replace('"', """) \
|
||||||
|
.replace("'", "'")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# https://gist.github.com/reimund/5435343/
|
||||||
|
def dict2xml(d, root_node=None):
|
||||||
|
wrap = not bool(root_node is None or isinstance(d, list))
|
||||||
|
root = root_node or 'objects'
|
||||||
|
root_singular = root[:-1] if root.endswith('s') and isinstance(d, list) else root
|
||||||
|
xml = ''
|
||||||
|
children = []
|
||||||
|
|
||||||
|
if isinstance(d, dict):
|
||||||
|
for key, value in sorted(d.items()):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
children.append(dict2xml(value, key))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
children.append(dict2xml(value, key))
|
||||||
|
else:
|
||||||
|
xml = '{} {}="{}"'.format(xml, key, escape_xml(value))
|
||||||
|
elif isinstance(d, list):
|
||||||
|
for value in d:
|
||||||
|
children.append(dict2xml(value, root_singular))
|
||||||
|
else:
|
||||||
|
children.append(escape_xml(d))
|
||||||
|
|
||||||
|
end_tag = '>' if len(children) > 0 else '/>'
|
||||||
|
|
||||||
|
if wrap or isinstance(d, dict):
|
||||||
|
xml = '<{}{}{}'.format(root, xml, end_tag)
|
||||||
|
|
||||||
|
if len(children) > 0:
|
||||||
|
for child in children:
|
||||||
|
xml = '{}{}'.format(xml, child)
|
||||||
|
|
||||||
|
if wrap or isinstance(d, dict):
|
||||||
|
xml = '{}</{}>'.format(xml, root)
|
||||||
|
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
def is_hdr(bit_depth, color_space):
|
def is_hdr(bit_depth, color_space):
|
||||||
bit_depth = cast_to_int(bit_depth)
|
bit_depth = cast_to_int(bit_depth)
|
||||||
return bit_depth > 8 and color_space == 'bt2020nc'
|
return bit_depth > 8 and color_space == 'bt2020nc'
|
||||||
|
|
|
@ -6484,10 +6484,12 @@ class WebInterface(object):
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
def export_metadata_modal(self, section_id=None, rating_key=None,
|
def export_metadata_modal(self, section_id=None, rating_key=None,
|
||||||
media_type=None, sub_media_type=None, **kwargs):
|
media_type=None, sub_media_type=None, **kwargs):
|
||||||
|
file_formats = exporter.Export.FILE_FORMATS
|
||||||
|
|
||||||
return serve_template(templatename="export_modal.html", title="Export Metadata",
|
return serve_template(templatename="export_modal.html", title="Export Metadata",
|
||||||
section_id=section_id, rating_key=rating_key,
|
section_id=section_id, rating_key=rating_key,
|
||||||
media_type=media_type, sub_media_type=sub_media_type)
|
media_type=media_type, sub_media_type=sub_media_type,
|
||||||
|
file_formats=file_formats)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
|
@ -6525,7 +6527,7 @@ class WebInterface(object):
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@requireAuth(member_of("admin"))
|
@requireAuth(member_of("admin"))
|
||||||
@addtoapi()
|
@addtoapi()
|
||||||
def export_metadata(self, section_id=None, rating_key=None, file_format='json',
|
def export_metadata(self, section_id=None, rating_key=None, file_format='csv',
|
||||||
metadata_level=1, media_info_level=1,
|
metadata_level=1, media_info_level=1,
|
||||||
include_thumb=False, include_art=False,
|
include_thumb=False, include_art=False,
|
||||||
custom_fields='', **kwargs):
|
custom_fields='', **kwargs):
|
||||||
|
@ -6537,7 +6539,7 @@ class WebInterface(object):
|
||||||
rating_key (int): The rating key of the media item to export
|
rating_key (int): The rating key of the media item to export
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
file_format (str): 'json' (default) or 'csv'
|
file_format (str): csv (default), json, or xml
|
||||||
metadata_level (int): The level of metadata to export (default 1)
|
metadata_level (int): The level of metadata to export (default 1)
|
||||||
media_info_level (int): The level of media info to export (default 1)
|
media_info_level (int): The level of media info to export (default 1)
|
||||||
include_thumb (bool): True to export poster/cover images
|
include_thumb (bool): True to export poster/cover images
|
||||||
|
@ -6585,11 +6587,10 @@ 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']:
|
||||||
if result['file_format'] == 'json':
|
filepath = exporter.get_export_filepath(result['filename'])
|
||||||
return serve_file(exporter.get_export_filepath(result['filename']), name=result['filename'],
|
|
||||||
content_type='application/json')
|
if result['file_format'] == 'csv':
|
||||||
elif result['file_format'] == 'csv':
|
with open(filepath, 'r', encoding='utf-8') as infile:
|
||||||
with open(exporter.get_export_filepath(result['filename']), 'r', encoding='utf-8') as infile:
|
|
||||||
reader = csv.DictReader(infile)
|
reader = csv.DictReader(infile)
|
||||||
table = '<table><tr><th>' + \
|
table = '<table><tr><th>' + \
|
||||||
'</th><th>'.join(reader.fieldnames) + \
|
'</th><th>'.join(reader.fieldnames) + \
|
||||||
|
@ -6605,6 +6606,13 @@ class WebInterface(object):
|
||||||
'th, td {padding: 3px; white-space: nowrap;}' \
|
'th, td {padding: 3px; white-space: nowrap;}' \
|
||||||
'</style>'
|
'</style>'
|
||||||
return '{style}<pre>{table}</pre>'.format(style=style, table=table)
|
return '{style}<pre>{table}</pre>'.format(style=style, table=table)
|
||||||
|
|
||||||
|
elif result['file_format'] == 'json':
|
||||||
|
return serve_file(filepath, name=result['filename'], content_type='application/json;charset=UTF-8')
|
||||||
|
|
||||||
|
elif result['file_format'] == 'xml':
|
||||||
|
return serve_file(filepath, name=result['filename'], content_type='application/xml;charset=UTF-8')
|
||||||
|
|
||||||
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