mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -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
|
||||
|
||||
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)
|
||||
media_info_level (int): The level of media info to export (default 1)
|
||||
include_thumb (bool): True to export poster/cover images
|
||||
|
|
|
@ -81,8 +81,9 @@ DOCUMENTATION :: END
|
|||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<select class="form-control" id="file_format_select" name="file_format_select">
|
||||
<option value="csv">CSV</option>
|
||||
<option value="json">JSON</option>
|
||||
% for format in file_formats:
|
||||
<option value="${format}">${format.upper()}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -89,8 +89,9 @@ class Export(object):
|
|||
}
|
||||
METADATA_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,
|
||||
include_thumb=False, include_art=False,
|
||||
custom_fields=''):
|
||||
|
@ -1456,7 +1457,7 @@ class Export(object):
|
|||
msg = "Export called with invalid metadata_level '{}'.".format(self.metadata_level)
|
||||
elif self.media_info_level not in self.MEDIA_INFO_LEVELS:
|
||||
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)
|
||||
|
||||
if msg:
|
||||
|
@ -1583,18 +1584,23 @@ class Export(object):
|
|||
try:
|
||||
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)
|
||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(json_data)
|
||||
|
||||
elif self.file_format == 'csv':
|
||||
flatten_result = helpers.flatten_dict(result)
|
||||
flatten_attrs = set().union(*flatten_result)
|
||||
with open(filepath, 'w', encoding='utf-8', newline='') as outfile:
|
||||
writer = csv.DictWriter(outfile, sorted(flatten_attrs))
|
||||
writer.writeheader()
|
||||
writer.writerows(flatten_result)
|
||||
elif self.file_format == 'xml':
|
||||
xml_data = helpers.dict2xml(result, root_node=self.media_type)
|
||||
with open(filepath, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(xml_data)
|
||||
|
||||
self.file_size = os.path.getsize(filepath)
|
||||
|
||||
|
|
|
@ -1312,6 +1312,57 @@ def dict_update(*dict_args):
|
|||
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):
|
||||
bit_depth = cast_to_int(bit_depth)
|
||||
return bit_depth > 8 and color_space == 'bt2020nc'
|
||||
|
|
|
@ -6484,10 +6484,12 @@ class WebInterface(object):
|
|||
@requireAuth(member_of("admin"))
|
||||
def export_metadata_modal(self, section_id=None, rating_key=None,
|
||||
media_type=None, sub_media_type=None, **kwargs):
|
||||
file_formats = exporter.Export.FILE_FORMATS
|
||||
|
||||
return serve_template(templatename="export_modal.html", title="Export Metadata",
|
||||
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.tools.json_out()
|
||||
|
@ -6525,7 +6527,7 @@ class WebInterface(object):
|
|||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@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,
|
||||
include_thumb=False, include_art=False,
|
||||
custom_fields='', **kwargs):
|
||||
|
@ -6537,7 +6539,7 @@ class WebInterface(object):
|
|||
rating_key (int): The rating key of the media item to export
|
||||
|
||||
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)
|
||||
media_info_level (int): The level of media info to export (default 1)
|
||||
include_thumb (bool): True to export poster/cover images
|
||||
|
@ -6585,11 +6587,10 @@ class WebInterface(object):
|
|||
result = exporter.get_export(export_id=export_id)
|
||||
|
||||
if result and result['complete'] == 1 and result['exists']:
|
||||
if result['file_format'] == 'json':
|
||||
return serve_file(exporter.get_export_filepath(result['filename']), name=result['filename'],
|
||||
content_type='application/json')
|
||||
elif result['file_format'] == 'csv':
|
||||
with open(exporter.get_export_filepath(result['filename']), 'r', encoding='utf-8') as infile:
|
||||
filepath = exporter.get_export_filepath(result['filename'])
|
||||
|
||||
if result['file_format'] == 'csv':
|
||||
with open(filepath, 'r', encoding='utf-8') as infile:
|
||||
reader = csv.DictReader(infile)
|
||||
table = '<table><tr><th>' + \
|
||||
'</th><th>'.join(reader.fieldnames) + \
|
||||
|
@ -6605,6 +6606,13 @@ class WebInterface(object):
|
|||
'th, td {padding: 3px; white-space: nowrap;}' \
|
||||
'</style>'
|
||||
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:
|
||||
if result and result.get('complete') == 0:
|
||||
msg = 'Export is still being processed.'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue