Add XML export format

This commit is contained in:
JonnyWong16 2020-09-30 00:04:27 -07:00
parent ad8dee3c47
commit 454235dd9a
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
5 changed files with 87 additions and 21 deletions

2
API.md
View file

@ -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

View file

@ -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>

View file

@ -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)

View file

@ -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("&", "&amp;") \
.replace("<", "&lt;") \
.replace(">", "&gt;") \
.replace('"', "&quot;") \
.replace("'", "&apos;")
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'

View file

@ -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.'