Separate metadata and media export levels

This commit is contained in:
JonnyWong16 2020-09-20 13:02:02 -07:00
parent 35cdef1340
commit ca06154805
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
3 changed files with 133 additions and 89 deletions

View file

@ -27,22 +27,34 @@ DOCUMENTATION :: END
<input type="hidden" id="section_id" name="section_id" value="${section_id or ''}" /> <input type="hidden" id="section_id" name="section_id" value="${section_id or ''}" />
<input type="hidden" id="rating_key" name="rating_key" value="${rating_key or ''}" /> <input type="hidden" id="rating_key" name="rating_key" value="${rating_key or ''}" />
<div class="form-group"> <div class="form-group">
<label for="export_level_select">Export Level</label> <label for="metadata_export_level_select">Metadata Export Level</label>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<select class="form-control" id="export_level_select" name="export_level_select"> <select class="form-control" id="metadata_export_level_select" name="metadata_export_level_select">
<option value="1">Level 1 - Basic Metadata</option> <option value="1">Level 1 - Basic Metadata</option>
<option value="2">Level 2 - Extended Metadata</option> <option value="2">Level 2 - Extended Metadata</option>
<option value="3">Level 3 - All Metadata</option> <option value="3">Level 3 - All Metadata</option>
<option value="4">Level 4 - Basic Media Info</option>
<option value="5">Level 5 - Extended Media Info</option>
<option value="6">Level 6 - All Media Info</option>
<option value="9">Level 9 - Everything</option> <option value="9">Level 9 - Everything</option>
<option value="-999">Custom</option> <option value="-999">Custom</option>
</select> </select>
</div> </div>
</div> </div>
<p class="help-block">Select the export level.</p> <p class="help-block">Select the metadata export level.</p>
</div>
<div class="form-group">
<label for="media_info_export_level_select">Media Info Export Level</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="media_info_export_level_select" name="media_info_export_level_select">
<option value="1">Level 1 - Basic Media Info</option>
<option value="2">Level 2 - Extended Media Info</option>
<option value="3">Level 3 - All Media Info</option>
<option value="9">Level 9 - Everything</option>
<option value="-999">Custom</option>
</select>
</div>
</div>
<p class="help-block">Select the media info export level.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="file_format_select">File Format</label> <label for="file_format_select">File Format</label>
@ -69,7 +81,8 @@ DOCUMENTATION :: END
$("#export_metadata").click(function() { $("#export_metadata").click(function() {
var section_id = $('#section_id').val(); var section_id = $('#section_id').val();
var rating_key = $('#rating_key').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(); var file_format = $('#file_format_select option:selected').val();
$.ajax({ $.ajax({
@ -77,7 +90,8 @@ DOCUMENTATION :: END
data: { data: {
section_id: section_id, section_id: section_id,
rating_key: rating_key, rating_key: rating_key,
export_level: export_level, metadata_level: metadata_export_level,
media_info_level: media_info_export_level,
file_format: file_format file_format: file_format
}, },
async: true, async: true,

View file

@ -23,6 +23,7 @@ import json
import os import os
import threading import threading
from copy import deepcopy
from functools import partial, reduce from functools import partial, reduce
from io import open from io import open
from multiprocessing.dummy import Pool as ThreadPool 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]) 'items': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type][0])
} }
MOVIE_LEVELS = { MOVIE_LEVELS = [
1: [ {
'ratingKey', 'title', 'titleSort', 'originalTitle', 'originallyAvailableAt', 'year', 1: [
'rating', 'ratingImage', 'audienceRating', 'audienceRatingImage', 'userRating', 'contentRating', 'ratingKey', 'title', 'titleSort', 'originalTitle', 'originallyAvailableAt', 'year',
'studio', 'tagline', 'summary', 'guid', 'duration', 'durationHuman', 'type' 'rating', 'ratingImage', 'audienceRating', 'audienceRatingImage', 'userRating', 'contentRating',
], 'studio', 'tagline', 'summary', 'guid', 'duration', 'durationHuman', 'type'
2: [ ],
'directors.tag', 'writers.tag', 'producers.tag', 'roles.tag', 'roles.role', 2: [
'countries.tag', 'genres.tag', 'collections.tag', 'labels.tag', 'fields.name', 'fields.locked' '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', 3: [
'chapters.tag', 'chapters.index', 'chapters.start', 'chapters.end', 'chapters.thumb', 'art', 'thumb', 'key', 'chapterSource',
'updatedAt', 'lastViewedAt', 'viewCount' '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', 1: [
'media.optimizedVersion', 'media.hdr' 'locations', 'media.aspectRatio', 'media.audioChannels', 'media.audioCodec', 'media.audioProfile',
], 'media.bitrate', 'media.container', 'media.duration', 'media.height', 'media.width',
5: [ 'media.videoCodec', 'media.videoFrameRate', 'media.videoProfile', 'media.videoResolution',
'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration', 'media.optimizedVersion', 'media.hdr'
'media.parts.container', 'media.parts.indexes', 'media.parts.size', 'media.parts.sizeHuman', ],
'media.parts.audioProfile', 'media.parts.videoProfile', 2: [
'media.parts.optimizedForStreaming', 'media.parts.deepAnalysisVersion' 'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration',
], 'media.parts.container', 'media.parts.indexes', 'media.parts.size', 'media.parts.sizeHuman',
6: [ 'media.parts.audioProfile', 'media.parts.videoProfile',
'media.parts.videoStreams.codec', 'media.parts.videoStreams.bitrate', 'media.parts.optimizedForStreaming', 'media.parts.deepAnalysisVersion'
'media.parts.videoStreams.language', 'media.parts.videoStreams.languageCode', ],
'media.parts.videoStreams.title', 'media.parts.videoStreams.displayTitle', 3: [
'media.parts.videoStreams.extendedDisplayTitle', 'media.parts.videoStreams.hdr', 'media.parts.videoStreams.codec', 'media.parts.videoStreams.bitrate',
'media.parts.videoStreams.bitDepth', 'media.parts.videoStreams.colorSpace', 'media.parts.videoStreams.language', 'media.parts.videoStreams.languageCode',
'media.parts.videoStreams.frameRate', 'media.parts.videoStreams.level', 'media.parts.videoStreams.title', 'media.parts.videoStreams.displayTitle',
'media.parts.videoStreams.profile', 'media.parts.videoStreams.refFrames', 'media.parts.videoStreams.extendedDisplayTitle', 'media.parts.videoStreams.hdr',
'media.parts.videoStreams.scanType', 'media.parts.videoStreams.default', 'media.parts.videoStreams.bitDepth', 'media.parts.videoStreams.colorSpace',
'media.parts.videoStreams.height', 'media.parts.videoStreams.width', 'media.parts.videoStreams.frameRate', 'media.parts.videoStreams.level',
'media.parts.audioStreams.codec', 'media.parts.audioStreams.bitrate', 'media.parts.videoStreams.profile', 'media.parts.videoStreams.refFrames',
'media.parts.audioStreams.language', 'media.parts.audioStreams.languageCode', 'media.parts.videoStreams.scanType', 'media.parts.videoStreams.default',
'media.parts.audioStreams.title', 'media.parts.audioStreams.displayTitle', 'media.parts.videoStreams.height', 'media.parts.videoStreams.width',
'media.parts.audioStreams.extendedDisplayTitle', 'media.parts.audioStreams.bitDepth', 'media.parts.audioStreams.codec', 'media.parts.audioStreams.bitrate',
'media.parts.audioStreams.channels', 'media.parts.audioStreams.audioChannelLayout', 'media.parts.audioStreams.language', 'media.parts.audioStreams.languageCode',
'media.parts.audioStreams.profile', 'media.parts.audioStreams.samplingRate', 'media.parts.audioStreams.title', 'media.parts.audioStreams.displayTitle',
'media.parts.audioStreams.default', 'media.parts.audioStreams.extendedDisplayTitle', 'media.parts.audioStreams.bitDepth',
'media.parts.subtitleStreams.codec', 'media.parts.subtitleStreams.format', 'media.parts.audioStreams.channels', 'media.parts.audioStreams.audioChannelLayout',
'media.parts.subtitleStreams.language', 'media.parts.subtitleStreams.languageCode', 'media.parts.audioStreams.profile', 'media.parts.audioStreams.samplingRate',
'media.parts.subtitleStreams.title', 'media.parts.subtitleStreams.displayTitle', 'media.parts.audioStreams.default',
'media.parts.subtitleStreams.extendedDisplayTitle', 'media.parts.subtitleStreams.forced', 'media.parts.subtitleStreams.codec', 'media.parts.subtitleStreams.format',
'media.parts.subtitleStreams.default' '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 = {} 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', [])) 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() 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: if not section_id and not rating_key:
logger.error("Tautulli Exporter :: Export called but no section_id or rating_key provided.") 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(): elif section_id and not str(section_id).isdigit():
logger.error("Tautulli Exporter :: Export called with invalid section_id '%s'.", section_id) logger.error("Tautulli Exporter :: Export called with invalid section_id '%s'.", section_id)
return return
elif not level: elif not metadata_level:
logger.error("Tautulli Exporter :: Export called with invalid level '%s'.", 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 return
elif file_format not in ('json', 'csv'): elif file_format not in ('json', 'csv'):
logger.error("Tautulli Exporter :: Export called but invalid file_format '%s' provided.", file_format) 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) plex = Plex(plexpy.CONFIG.PMS_URL, plexpy.CONFIG.PMS_TOKEN)
if rating_key: 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)) item = plex.get_item(helpers.cast_to_int(rating_key))
media_type = item.type media_type = item.type
@ -997,7 +1008,8 @@ def get_any_hdr(obj, root):
items = [item] items = [item]
elif section_id: 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) library = plex.get_library(section_id)
media_type = library.type 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) logger.error("Tautulli Exporter :: Cannot export media type '%s'", media_type)
return 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: if metadata_level != 9 and metadata_level not in metadata_level_attrs:
export_attrs = media_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: else:
if level not in level_attrs: _metadata_levels = sorted(metadata_level_attrs.keys())
logger.error("Tautulli Exporter :: Export called with invalid level '%s'.", level) for _metadata_level in _metadata_levels[:_metadata_levels.index(metadata_level) + 1]:
return export_attrs_set.update(metadata_level_attrs[_metadata_level])
export_attrs_set = set() if media_info_level == 9:
_levels = sorted(level_attrs.keys()) media_info_export_attrs = {'media': deepcopy(media_attrs['media'])}
for _level in _levels[:_levels.index(level) + 1]: export_attrs_list.append(media_info_export_attrs)
export_attrs_set.update(level_attrs[_level]) 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:
for attr in export_attrs_set: try:
try: value = helpers.get_dict_value_by_path(media_attrs, attr)
value = helpers.get_dict_value_by_path(media_attrs, attr) except KeyError:
except KeyError: logger.warn("Tautulli Exporter :: Unknown export attribute '%s', skipping...", attr)
logger.warn("Tautulli Exporter :: Unknown export attribute '%s', skipping...", attr) continue
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) filename = helpers.clean_filename(filename)
@ -1091,6 +1117,8 @@ def _real_export(export_id, items, attrs, file_format, filename):
except Exception as e: except Exception as e:
set_export_state(export_id=export_id, success=False) set_export_state(export_id=export_id, success=False)
logger.error("Tautulli Exporter :: Failed to export '%s': %s", filename, e) logger.error("Tautulli Exporter :: Failed to export '%s': %s", filename, e)
import traceback
traceback.print_exc()
success = False success = False
finally: finally:

View file

@ -6491,17 +6491,18 @@ class WebInterface(object):
@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='json',
export_level=1, **kwargs): metadata_level=1, media_info_level=1, **kwargs):
""" Export library or media metadata to a file """ Export library or media metadata to a file
``` ```
Required parameters: Required parameters:
section_id (int): The section id of the library to export, OR section_id (int): The section id of the library to export, OR
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): 'json' (default) or 'csv'
export_level (int): The level of metadata to export 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: Returns:
json: json:
@ -6513,7 +6514,8 @@ class WebInterface(object):
result = exporter.export(section_id=section_id, result = exporter.export(section_id=section_id,
rating_key=rating_key, rating_key=rating_key,
file_format=file_format, file_format=file_format,
level=export_level) metadata_level=metadata_level,
media_info_level=media_info_level)
if result: if result:
return {'result': 'success', 'message': 'Metadata export has started.'} return {'result': 'success', 'message': 'Metadata export has started.'}