diff --git a/data/interfaces/default/export_modal.html b/data/interfaces/default/export_modal.html index 5ac09450..d4d9de0d 100644 --- a/data/interfaces/default/export_modal.html +++ b/data/interfaces/default/export_modal.html @@ -27,22 +27,34 @@ DOCUMENTATION :: END
- +
- - - -
-

Select the export level.

+

Select the metadata export level.

+
+
+ +
+
+ +
+
+

Select the media info export level.

@@ -69,7 +81,8 @@ DOCUMENTATION :: END $("#export_metadata").click(function() { var section_id = $('#section_id').val(); var rating_key = $('#rating_key').val(); - var export_level = $('#export_level_select option:selected').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(); $.ajax({ @@ -77,7 +90,8 @@ DOCUMENTATION :: END data: { section_id: section_id, rating_key: rating_key, - export_level: export_level, + metadata_level: metadata_export_level, + media_info_level: media_info_export_level, file_format: file_format }, async: true, diff --git a/plexpy/exporter.py b/plexpy/exporter.py index 7fcaf844..3bf5b4ff 100644 --- a/plexpy/exporter.py +++ b/plexpy/exporter.py @@ -23,6 +23,7 @@ import json import os import threading +from copy import deepcopy from functools import partial, reduce from io import open from multiprocessing.dummy import Pool as ThreadPool @@ -859,57 +860,61 @@ PLAYLIST_ATTRS = { 'items': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type][0]) } -MOVIE_LEVELS = { - 1: [ - 'ratingKey', 'title', 'titleSort', 'originalTitle', 'originallyAvailableAt', 'year', - 'rating', 'ratingImage', 'audienceRating', 'audienceRatingImage', 'userRating', 'contentRating', - 'studio', 'tagline', 'summary', 'guid', 'duration', 'durationHuman', 'type' - ], - 2: [ - 'directors.tag', 'writers.tag', 'producers.tag', 'roles.tag', 'roles.role', - 'countries.tag', 'genres.tag', 'collections.tag', 'labels.tag', 'fields.name', 'fields.locked' - ], - 3: [ - 'art', 'thumb', 'key', 'chapterSource', - 'chapters.tag', 'chapters.index', 'chapters.start', 'chapters.end', 'chapters.thumb', - 'updatedAt', 'lastViewedAt', 'viewCount' - ], - 4: [ - 'locations', 'media.aspectRatio', 'media.audioChannels', 'media.audioCodec', 'media.audioProfile', - 'media.bitrate', 'media.container', 'media.duration', 'media.height', 'media.width', - 'media.videoCodec', 'media.videoFrameRate', 'media.videoProfile', 'media.videoResolution', - 'media.optimizedVersion', 'media.hdr' - ], - 5: [ - 'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration', - 'media.parts.container', 'media.parts.indexes', 'media.parts.size', 'media.parts.sizeHuman', - 'media.parts.audioProfile', 'media.parts.videoProfile', - 'media.parts.optimizedForStreaming', 'media.parts.deepAnalysisVersion' - ], - 6: [ - 'media.parts.videoStreams.codec', 'media.parts.videoStreams.bitrate', - 'media.parts.videoStreams.language', 'media.parts.videoStreams.languageCode', - 'media.parts.videoStreams.title', 'media.parts.videoStreams.displayTitle', - 'media.parts.videoStreams.extendedDisplayTitle', 'media.parts.videoStreams.hdr', - 'media.parts.videoStreams.bitDepth', 'media.parts.videoStreams.colorSpace', - 'media.parts.videoStreams.frameRate', 'media.parts.videoStreams.level', - 'media.parts.videoStreams.profile', 'media.parts.videoStreams.refFrames', - 'media.parts.videoStreams.scanType', 'media.parts.videoStreams.default', - 'media.parts.videoStreams.height', 'media.parts.videoStreams.width', - 'media.parts.audioStreams.codec', 'media.parts.audioStreams.bitrate', - 'media.parts.audioStreams.language', 'media.parts.audioStreams.languageCode', - 'media.parts.audioStreams.title', 'media.parts.audioStreams.displayTitle', - 'media.parts.audioStreams.extendedDisplayTitle', 'media.parts.audioStreams.bitDepth', - 'media.parts.audioStreams.channels', 'media.parts.audioStreams.audioChannelLayout', - 'media.parts.audioStreams.profile', 'media.parts.audioStreams.samplingRate', - 'media.parts.audioStreams.default', - 'media.parts.subtitleStreams.codec', 'media.parts.subtitleStreams.format', - 'media.parts.subtitleStreams.language', 'media.parts.subtitleStreams.languageCode', - 'media.parts.subtitleStreams.title', 'media.parts.subtitleStreams.displayTitle', - 'media.parts.subtitleStreams.extendedDisplayTitle', 'media.parts.subtitleStreams.forced', - 'media.parts.subtitleStreams.default' - ] -} +MOVIE_LEVELS = [ + { + 1: [ + 'ratingKey', 'title', 'titleSort', 'originalTitle', 'originallyAvailableAt', 'year', + 'rating', 'ratingImage', 'audienceRating', 'audienceRatingImage', 'userRating', 'contentRating', + 'studio', 'tagline', 'summary', 'guid', 'duration', 'durationHuman', 'type' + ], + 2: [ + 'directors.tag', 'writers.tag', 'producers.tag', 'roles.tag', 'roles.role', + 'countries.tag', 'genres.tag', 'collections.tag', 'labels.tag', 'fields.name', 'fields.locked' + ], + 3: [ + 'art', 'thumb', 'key', 'chapterSource', + 'chapters.tag', 'chapters.index', 'chapters.start', 'chapters.end', 'chapters.thumb', + 'updatedAt', 'lastViewedAt', 'viewCount' + ] + }, + { + 1: [ + 'locations', 'media.aspectRatio', 'media.audioChannels', 'media.audioCodec', 'media.audioProfile', + 'media.bitrate', 'media.container', 'media.duration', 'media.height', 'media.width', + 'media.videoCodec', 'media.videoFrameRate', 'media.videoProfile', 'media.videoResolution', + 'media.optimizedVersion', 'media.hdr' + ], + 2: [ + 'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration', + 'media.parts.container', 'media.parts.indexes', 'media.parts.size', 'media.parts.sizeHuman', + 'media.parts.audioProfile', 'media.parts.videoProfile', + 'media.parts.optimizedForStreaming', 'media.parts.deepAnalysisVersion' + ], + 3: [ + 'media.parts.videoStreams.codec', 'media.parts.videoStreams.bitrate', + 'media.parts.videoStreams.language', 'media.parts.videoStreams.languageCode', + 'media.parts.videoStreams.title', 'media.parts.videoStreams.displayTitle', + 'media.parts.videoStreams.extendedDisplayTitle', 'media.parts.videoStreams.hdr', + 'media.parts.videoStreams.bitDepth', 'media.parts.videoStreams.colorSpace', + 'media.parts.videoStreams.frameRate', 'media.parts.videoStreams.level', + 'media.parts.videoStreams.profile', 'media.parts.videoStreams.refFrames', + 'media.parts.videoStreams.scanType', 'media.parts.videoStreams.default', + 'media.parts.videoStreams.height', 'media.parts.videoStreams.width', + 'media.parts.audioStreams.codec', 'media.parts.audioStreams.bitrate', + 'media.parts.audioStreams.language', 'media.parts.audioStreams.languageCode', + 'media.parts.audioStreams.title', 'media.parts.audioStreams.displayTitle', + 'media.parts.audioStreams.extendedDisplayTitle', 'media.parts.audioStreams.bitDepth', + 'media.parts.audioStreams.channels', 'media.parts.audioStreams.audioChannelLayout', + 'media.parts.audioStreams.profile', 'media.parts.audioStreams.samplingRate', + 'media.parts.audioStreams.default', + 'media.parts.subtitleStreams.codec', 'media.parts.subtitleStreams.format', + 'media.parts.subtitleStreams.language', 'media.parts.subtitleStreams.languageCode', + 'media.parts.subtitleStreams.title', 'media.parts.subtitleStreams.displayTitle', + 'media.parts.subtitleStreams.extendedDisplayTitle', 'media.parts.subtitleStreams.forced', + 'media.parts.subtitleStreams.default' + ] + } +] SHOW_LEVELS = {} @@ -952,9 +957,11 @@ def get_any_hdr(obj, root): return any(vs.get('hdr') for p in media.get('parts', []) for vs in p.get('videoStreams', [])) +def export(section_id=None, rating_key=None, file_format='json', metadata_level=1, media_info_level=1): timestamp = helpers.timestamp() - level = helpers.cast_to_int(level) + metadata_level = helpers.cast_to_int(metadata_level) + media_info_level = helpers.cast_to_int(media_info_level) if not section_id and not rating_key: logger.error("Tautulli Exporter :: Export called but no section_id or rating_key provided.") @@ -965,8 +972,11 @@ def get_any_hdr(obj, root): elif section_id and not str(section_id).isdigit(): logger.error("Tautulli Exporter :: Export called with invalid section_id '%s'.", section_id) return - elif not level: - logger.error("Tautulli Exporter :: Export called with invalid level '%s'.", level) + elif not metadata_level: + logger.error("Tautulli Exporter :: Export called with invalid metadata_level '%s'.", metadata_level) + return + elif not media_info_level: + logger.error("Tautulli Exporter :: Export called with invalid media_info_level '%s'.", media_info_level) return elif file_format not in ('json', 'csv'): logger.error("Tautulli Exporter :: Export called but invalid file_format '%s' provided.", file_format) @@ -975,7 +985,8 @@ def get_any_hdr(obj, root): plex = Plex(plexpy.CONFIG.PMS_URL, plexpy.CONFIG.PMS_TOKEN) if rating_key: - logger.debug("Tautulli Exporter :: Export called with rating_key %s, level %d", rating_key, level) + logger.debug("Tautulli Exporter :: Export called with rating_key %s, metadata_level %d, media_info_level %d", + rating_key, metadata_level, media_info_level) item = plex.get_item(helpers.cast_to_int(rating_key)) media_type = item.type @@ -997,7 +1008,8 @@ def get_any_hdr(obj, root): items = [item] elif section_id: - logger.debug("Tautulli Exporter :: Export called with section_id %s, level %d", section_id, level) + logger.debug("Tautulli Exporter :: Export called with section_id %s, metadata_level %d, media_info_level %d", + rating_key, metadata_level, media_info_level) library = plex.get_library(section_id) media_type = library.type @@ -1013,31 +1025,45 @@ def get_any_hdr(obj, root): logger.error("Tautulli Exporter :: Cannot export media type '%s'", media_type) return - media_attrs, level_attrs = MEDIA_TYPES[media_type] + media_attrs, (metadata_level_attrs, media_info_level_attrs) = MEDIA_TYPES[media_type] - if level == 9: - export_attrs = media_attrs + if metadata_level != 9 and metadata_level not in metadata_level_attrs: + logger.error("Tautulli Exporter :: Export called with invalid metadata_level '%s'.", metadata_level) + return + elif media_info_level != 9 and media_info_level not in media_info_level_attrs: + logger.error("Tautulli Exporter :: Export called with invalid media_info_level '%s'.", media_info_level) + return + + export_attrs_list = [] + export_attrs_set = set() + + if metadata_level == 9: + metadata_export_attrs = deepcopy(media_attrs) + del metadata_export_attrs['media'] + export_attrs_list.append(metadata_export_attrs) else: - if level not in level_attrs: - logger.error("Tautulli Exporter :: Export called with invalid level '%s'.", level) - return + _metadata_levels = sorted(metadata_level_attrs.keys()) + for _metadata_level in _metadata_levels[:_metadata_levels.index(metadata_level) + 1]: + export_attrs_set.update(metadata_level_attrs[_metadata_level]) - export_attrs_set = set() - _levels = sorted(level_attrs.keys()) - for _level in _levels[:_levels.index(level) + 1]: - export_attrs_set.update(level_attrs[_level]) + if media_info_level == 9: + media_info_export_attrs = {'media': deepcopy(media_attrs['media'])} + export_attrs_list.append(media_info_export_attrs) + else: + _media_info_levels = sorted(media_info_level_attrs.keys()) + for _media_info_level in _media_info_levels[:_media_info_levels.index(media_info_level) + 1]: + export_attrs_set.update(media_info_level_attrs[_media_info_level]) - export_attrs_list = [] - for attr in export_attrs_set: - try: - value = helpers.get_dict_value_by_path(media_attrs, attr) - except KeyError: - logger.warn("Tautulli Exporter :: Unknown export attribute '%s', skipping...", attr) - continue + for attr in export_attrs_set: + try: + value = helpers.get_dict_value_by_path(media_attrs, attr) + except KeyError: + logger.warn("Tautulli Exporter :: Unknown export attribute '%s', skipping...", attr) + continue - export_attrs_list.append(value) + export_attrs_list.append(value) - export_attrs = reduce(helpers.dict_merge, export_attrs_list) + export_attrs = reduce(helpers.dict_merge, export_attrs_list) filename = helpers.clean_filename(filename) @@ -1091,6 +1117,8 @@ def _real_export(export_id, items, attrs, file_format, filename): except Exception as e: set_export_state(export_id=export_id, success=False) logger.error("Tautulli Exporter :: Failed to export '%s': %s", filename, e) + import traceback + traceback.print_exc() success = False finally: diff --git a/plexpy/webserve.py b/plexpy/webserve.py index c19b92bd..c2f85e20 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -6491,17 +6491,18 @@ class WebInterface(object): @requireAuth(member_of("admin")) @addtoapi() def export_metadata(self, section_id=None, rating_key=None, file_format='json', - export_level=1, **kwargs): + metadata_level=1, media_info_level=1, **kwargs): """ Export library or media metadata to a file ``` Required parameters: - section_id (int): The section id of the library to export, OR - rating_key (int): The rating key of the media item to export + section_id (int): The section id of the library to export, OR + rating_key (int): The rating key of the media item to export Optional parameters: - file_format (str): 'json' (default) or 'csv' - export_level (int): The level of metadata to export + file_format (str): 'json' (default) or 'csv' + metadata_level (int): The level of metadata to export (default 1) + media_info_level (int): The level of media info to export (default 1) Returns: json: @@ -6513,7 +6514,8 @@ class WebInterface(object): result = exporter.export(section_id=section_id, rating_key=rating_key, file_format=file_format, - level=export_level) + metadata_level=metadata_level, + media_info_level=media_info_level) if result: return {'result': 'success', 'message': 'Metadata export has started.'}