diff --git a/API.md b/API.md index 4c37d009..c1ecb6ff 100644 --- a/API.md +++ b/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 diff --git a/data/interfaces/default/export_modal.html b/data/interfaces/default/export_modal.html index 7a06466a..5830d5e3 100644 --- a/data/interfaces/default/export_modal.html +++ b/data/interfaces/default/export_modal.html @@ -81,8 +81,9 @@ DOCUMENTATION :: END
diff --git a/plexpy/exporter.py b/plexpy/exporter.py index cc2ef9d2..da554100 100644 --- a/plexpy/exporter.py +++ b/plexpy/exporter.py @@ -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) diff --git a/plexpy/helpers.py b/plexpy/helpers.py index ceeb6e2c..e839d6e8 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -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' diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 70dc9aeb..e0d3fd8d 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -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 = '
' + \ ''.join(reader.fieldnames) + \ @@ -6605,6 +6606,13 @@ class WebInterface(object): 'th, td {padding: 3px; white-space: nowrap;}' \ '' return '{style}
{table}
'.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.'