diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css index 3dd6cef2..cceb9758 100644 --- a/data/interfaces/default/css/tautulli.css +++ b/data/interfaces/default/css/tautulli.css @@ -3491,6 +3491,9 @@ pre::-webkit-scrollbar-thumb { .selectize-input input[type='text'] { height: 20px; } +.selectize-input.disabled, .selectize-input.disabled * { + cursor: not-allowed !important; +} .small-muted { font-size: small; color: #777; diff --git a/data/interfaces/default/export_modal.html b/data/interfaces/default/export_modal.html index 37d7fc36..72553834 100644 --- a/data/interfaces/default/export_modal.html +++ b/data/interfaces/default/export_modal.html @@ -79,10 +79,10 @@ DOCUMENTATION :: END

Add additional fields to the selected media info export level.

- +
- % for format in file_formats: % endfor @@ -184,13 +184,31 @@ DOCUMENTATION :: END } getExportFields(); + $('#export_file_format').on('change', function() { + if ($(this).val() === 'm3u8') { + $('#metadata_export_level_select').prop('disabled', true); + $('#media_info_export_level_select').prop('disabled', true); + $("#export_include_thumb").prop('disabled', true); + $("#export_include_art").prop('disabled', true); + export_custom_metadata_fields.disable(); + export_custom_media_info_fields.disable(); + } else { + $('#metadata_export_level_select').prop('disabled', false); + $('#media_info_export_level_select').prop('disabled', false); + $("#export_include_thumb").prop('disabled', false); + $("#export_include_art").prop('disabled', false); + export_custom_metadata_fields.enable(); + export_custom_media_info_fields.enable(); + } + }) + $("#export_metadata").click(function() { var section_id = $('#export_section_id').val(); var user_id = $('#export_user_id').val(); var rating_key = $('#export_rating_key').val(); var metadata_export_level = $('#metadata_export_level_select option:selected').val(); var media_info_export_level = $('#media_info_export_level_select option:selected').val(); - var file_format = $('#file_format_select option:selected').val(); + var file_format = $('#export_file_format option:selected').val(); var include_thumb = $("#export_include_thumb").is(':checked') ? 1 : 0; var include_art = $("#export_include_art").is(':checked') ? 1 : 0; var custom_fields = [ diff --git a/data/interfaces/default/info.html b/data/interfaces/default/info.html index 0291c02b..d22258db 100644 --- a/data/interfaces/default/info.html +++ b/data/interfaces/default/info.html @@ -989,14 +989,14 @@ DOCUMENTATION :: END clearSearchButton('export_table-RK-${data["rating_key"]}', export_table); } + $('a[href="#tabs-export"]').on('shown.bs.tab', function() { + if (typeof(export_table) === 'undefined') { + loadExportTable(); + } + }); + $(document).ready(function () { - if ($('#tabs-history').length) { - $('a[href="#tabs-export"]').on('shown.bs.tab', function() { - if (typeof(export_table) === 'undefined') { - loadExportTable(); - } - }); - } else { + if (!($('#tabs-history').length)) { loadExportTable(); } }); diff --git a/plexpy/exporter.py b/plexpy/exporter.py index 98ac685c..449fa2d6 100644 --- a/plexpy/exporter.py +++ b/plexpy/exporter.py @@ -91,7 +91,7 @@ class Export(object): } METADATA_LEVELS = (0, 1, 2, 3, 9) MEDIA_INFO_LEVELS = (0, 1, 2, 3, 9) - FILE_FORMATS = ('csv', 'json', 'xml') + FILE_FORMATS = ('csv', 'json', 'xml', 'm3u8') EXPORT_TYPES = ('all', 'collection', 'playlist') def __init__(self, section_id=None, user_id=None, rating_key=None, file_format='csv', @@ -101,7 +101,7 @@ class Export(object): self.section_id = helpers.cast_to_int(section_id) or None self.user_id = helpers.cast_to_int(user_id) or None self.rating_key = helpers.cast_to_int(rating_key) or None - self.file_format = file_format + self.file_format = str(file_format).lower() self.metadata_level = helpers.cast_to_int(metadata_level) self.media_info_level = helpers.cast_to_int(media_info_level) self.include_thumb = include_thumb @@ -120,6 +120,14 @@ class Export(object): self.file_size = None self.success = False + # Reset export options for m3u8 + if self.file_format == 'm3u8': + self.metadata_level = 1 + self.media_info_level = 1 + self.include_thumb = False + self.include_art = False + self.custom_fields = '' + def return_attrs(self, media_type, flatten=False): # o: current object # e: element in object attribute value list @@ -1629,10 +1637,15 @@ class Export(object): outfile.write(json_data) elif self.file_format == 'xml': - xml_data = helpers.dict2xml({self.media_type: result}, root_node='export') + xml_data = helpers.dict_to_xml({self.media_type: result}, root_node='export') with open(filepath, 'w', encoding='utf-8') as outfile: outfile.write(xml_data) + elif self.file_format == 'm3u8': + m3u8_data = self.dict_to_m3u8(result) + with open(filepath, 'w', encoding='utf-8') as outfile: + outfile.write(m3u8_data) + self.file_size = os.path.getsize(filepath) if os.path.exists(images_folder): @@ -1773,6 +1786,41 @@ class Export(object): def is_media_info_attr(attr): return attr.startswith('media.') or attr == 'locations' + def dict_to_m3u8(self, data): + items = self._get_m3u8_items(data) + + m3u8 = '#EXTM3U\n' + m3u8 += '# Playlist: {}\n\n'.format(self.filename) + m3u8_item_template = '# ratingKey: {ratingKey}\n#EXTINF:{duration},{title}\n{location}\n' + m3u8_items = [] + + for item in items: + m3u8_items.append(m3u8_item_template.format(**item)) + + m3u8 = m3u8 + '\n'.join(m3u8_items) + + return m3u8 + + def _get_m3u8_items(self, data): + items = [] + + for d in data: + if 'locations' in d: + location = { + 'ratingKey': d['ratingKey'], + 'duration': d['duration'], + 'title': d['title'], + 'location': d['locations'][0] + } + items.append(location) + + child_media_type = self.CHILD_MEDIA_TYPES[d['type']] + if child_media_type: + child_locations = self._get_m3u8_items(d[self.PLURAL_MEDIA_TYPES[child_media_type]]) + items.extend(child_locations) + + return items + def get_export(export_id): db = database.MonitorDatabase() diff --git a/plexpy/helpers.py b/plexpy/helpers.py index eda2f820..2732b546 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -1385,7 +1385,7 @@ def escape_xml(value): # https://gist.github.com/reimund/5435343/ -def dict2xml(d, root_node=None, indent=4, level=0): +def dict_to_xml(d, root_node=None, indent=4, level=0): 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 @@ -1395,9 +1395,9 @@ def dict2xml(d, root_node=None, indent=4, level=0): if isinstance(d, dict): for key, value in sorted(d.items()): if isinstance(value, dict): - children.append(dict2xml(value, key, level=level + 1)) + children.append(dict_to_xml(value, key, level=level + 1)) elif isinstance(value, list): - children.append(dict2xml(value, key, level=level + 1)) + children.append(dict_to_xml(value, key, level=level + 1)) else: xml = '{} {}="{}"'.format(xml, key, escape_xml(value)) elif isinstance(d, list): @@ -1405,7 +1405,7 @@ def dict2xml(d, root_node=None, indent=4, level=0): # Custom tag replacement for collections/playlists if isinstance(value, dict) and root in ('children', 'items'): root_singular = value.get('type', root_singular) - children.append(dict2xml(value, root_singular, level=level)) + children.append(dict_to_xml(value, root_singular, level=level)) else: children.append(escape_xml(d)) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 2e2cea8a..7c0cc9b2 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -6707,6 +6707,9 @@ class WebInterface(object): elif result['file_format'] == 'xml': return serve_file(filepath, name=result['filename'], content_type='application/xml;charset=UTF-8') + elif result['file_format'] == 'm3u8': + return serve_file(filepath, name=result['filename'], content_type='text/plain;charset=UTF-8') + else: if result and result.get('complete') == 0: msg = 'Export is still being processed.'