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 = '