diff --git a/plexpy/exporter.py b/plexpy/exporter.py new file mode 100644 index 00000000..8055b19b --- /dev/null +++ b/plexpy/exporter.py @@ -0,0 +1,916 @@ +# -*- coding: utf-8 -*- + +# This file is part of Tautulli. +# +# Tautulli is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Tautulli is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Tautulli. If not, see . + +from __future__ import unicode_literals + +import csv +import json +import os + +from functools import partial +from io import open +from multiprocessing.dummy import Pool as ThreadPool + +import plexpy +if plexpy.PYTHON2: + import helpers + import logger + from plex import Plex +else: + from plexpy import helpers + from plexpy import logger + from plexpy.plex import Plex + + +MOVIE_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'audienceRating': None, + 'audienceRatingImage': None, + 'chapters': { + 'id': None, + 'tag': None, + 'index': None, + 'start': None, + 'end': None, + 'thumb': None + }, + 'chapterSource': None, + 'collections': { + 'id': None, + 'tag': None + }, + 'contentRating': None, + 'countries': { + 'id': None, + 'tag': None + }, + 'directors': { + 'id': None, + 'tag': None + }, + 'duration': None, + 'fields': { + 'name': None, + 'locked': None + }, + 'genres': { + 'id': None, + 'tag': None + }, + 'guid': None, + 'key': None, + 'labels': { + 'id': None, + 'tag': None + }, + 'lastViewedAt': helpers.datetime_to_iso, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'locations': None, + 'media': { + 'aspectRatio': None, + 'audioChannels': None, + 'audioCodec': None, + 'audioProfile': None, + 'bitrate': None, + 'container': None, + 'duration': None, + 'height': None, + 'id': None, + 'has64bitOffsets': None, + 'optimizedForStreaming': None, + 'optimizedVersion': None, + 'target': None, + 'title': None, + 'videoCodec': None, + 'videoFrameRate': None, + 'videoProfile': None, + 'videoResolution': None, + 'width': None, + 'parts': { + 'accessible': None, + 'audioProfile': None, + 'container': None, + 'deepAnalysisVersion': None, + 'duration': None, + 'exists': None, + 'file': None, + 'has64bitOffsets': None, + 'id': None, + 'indexes': None, + 'key': None, + 'size': None, + 'optimizedForStreaming': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'syncItemId': None, + 'syncState': None, + 'videoProfile': None, + 'videoStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'language': None, + 'languageCode': None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'bitDepth': None, + 'bitrate': None, + 'cabac': None, + 'chromaLocation': None, + 'chromaSubsampling': None, + 'colorPrimaries': None, + 'colorRange': None, + 'colorSpace': None, + 'colorTrc': None, + 'duration': None, + 'frameRate': None, + 'frameRateMode': None, + 'hasScalingMatrix': None, + 'height': None, + 'level': None, + 'profile': None, + 'refFrames': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'scanType': None, + 'streamIdentifier': None, + 'width': None + }, + 'audioStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'language': None, + 'languageCode': None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'audioChannelLayout': None, + 'bitDepth': None, + 'bitrate': None, + 'bitrateMode': None, + 'channels': None, + 'dialogNorm': None, + 'duration': None, + 'profile': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'samplingRate': None + }, + 'subtitleStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'language': None, + 'languageCode': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'forced': None, + 'format': None, + 'key': None + } + } + }, + 'originallyAvailableAt': partial(helpers.datetime_to_iso, to_date=True), + 'originalTitle': None, + 'producers': { + 'id': None, + 'tag': None + }, + 'rating': None, + 'ratingImage': None, + 'ratingKey': None, + 'roles': { + 'id': None, + 'tag': None, + 'role': None, + 'thumb': None + }, + 'studio': None, + 'summary': None, + 'tagline': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'writers': { + 'id': None, + 'tag': None + }, + 'year': None +} + +SHOW_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'banner': None, + 'childCount': None, + 'collections': { + 'id': None, + 'tag': None + }, + 'contentRating': None, + 'duration': None, + 'fields': { + 'name': None, + 'locked': None + }, + 'genres': { + 'id': None, + 'tag': None + }, + 'guid': None, + 'index': None, + 'key': None, + 'labels': { + 'id': None, + 'tag': None + }, + 'lastViewedAt': helpers.datetime_to_iso, + 'leafCount': None, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'locations': None, + 'originallyAvailableAt': partial(helpers.datetime_to_iso, to_date=True), + 'rating': None, + 'ratingKey': None, + 'roles': { + 'id': None, + 'tag': None, + 'role': None, + 'thumb': None + }, + 'studio': None, + 'summary': None, + 'theme': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'viewedLeafCount': None, + 'year': None, + 'seasons': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]) +} + +SEASON_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'fields': { + 'name': None, + 'locked': None + }, + 'guid': None, + 'index': None, + 'key': None, + 'lastViewedAt': helpers.datetime_to_iso, + 'leafCount': None, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'parentGuid': None, + 'parentIndex': None, + 'parentKey': None, + 'parentRatingKey': None, + 'parentTheme': None, + 'parentThumb': None, + 'parentTitle': None, + 'ratingKey': None, + 'summary': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'viewedLeafCount': None, + 'episodes': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]) +} + +EPISODE_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'chapterSource': None, + 'contentRating': None, + 'directors': { + 'id': None, + 'tag': None + }, + 'duration': None, + 'fields': { + 'name': None, + 'locked': None + }, + 'grandparentArt': None, + 'grandparentGuid': None, + 'grandparentKey': None, + 'grandparentRatingKey': None, + 'grandparentTheme': None, + 'grandparentThumb': None, + 'grandparentTitle': None, + 'guid': None, + 'index': None, + 'key': None, + 'lastViewedAt': helpers.datetime_to_iso, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'locations': None, + 'media': { + 'aspectRatio': None, + 'audioChannels': None, + 'audioCodec': None, + 'audioProfile': None, + 'bitrate': None, + 'container': None, + 'duration': None, + 'height': None, + 'id': None, + 'has64bitOffsets': None, + 'optimizedForStreaming': None, + 'optimizedVersion': None, + 'target': None, + 'title': None, + 'videoCodec': None, + 'videoFrameRate': None, + 'videoProfile': None, + 'videoResolution': None, + 'width': None, + 'parts': { + 'accessible': None, + 'audioProfile': None, + 'container': None, + 'deepAnalysisVersion': None, + 'duration': None, + 'exists': None, + 'file': None, + 'has64bitOffsets': None, + 'id': None, + 'indexes': None, + 'key': None, + 'size': None, + 'optimizedForStreaming': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'syncItemId': None, + 'syncState': None, + 'videoProfile': None, + 'videoStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'language': None, + 'languageCode': None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'bitDepth': None, + 'bitrate': None, + 'cabac': None, + 'chromaLocation': None, + 'chromaSubsampling': None, + 'colorPrimaries': None, + 'colorRange': None, + 'colorSpace': None, + 'colorTrc': None, + 'duration': None, + 'frameRate': None, + 'frameRateMode': None, + 'hasScalingMatrix': None, + 'height': None, + 'level': None, + 'profile': None, + 'refFrames': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'scanType': None, + 'streamIdentifier': None, + 'width': None + }, + 'audioStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'language': None, + 'languageCode': None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'audioChannelLayout': None, + 'bitDepth': None, + 'bitrate': None, + 'bitrateMode': None, + 'channels': None, + 'dialogNorm': None, + 'duration': None, + 'profile': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'samplingRate': None + }, + 'subtitleStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'language': None, + 'languageCode': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'forced': None, + 'format': None, + 'key': None + } + } + }, + 'originallyAvailableAt': partial(helpers.datetime_to_iso, to_date=True), + 'parentGuid': None, + 'parentIndex': None, + 'parentKey': None, + 'parentRatingKey': None, + 'parentThumb': None, + 'parentTitle': None, + 'rating': None, + 'ratingKey': None, + 'summary': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'writers': { + 'id': None, + 'tag': None + }, + 'year': None +} + +ARTIST_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'collections': { + 'id': None, + 'tag': None + }, + 'countries': { + 'id': None, + 'tag': None + }, + 'fields': { + 'name': None, + 'locked': None + }, + 'genres': { + 'id': None, + 'tag': None + }, + 'guid': None, + 'index': None, + 'key': None, + 'lastViewedAt': helpers.datetime_to_iso, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'locations': None, + 'moods': { + 'id': None, + 'tag': None + }, + 'rating': None, + 'ratingKey': None, + 'styles': { + 'id': None, + 'tag': None + }, + 'summary': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'albums': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]) +} + +ALBUM_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'collections': { + 'id': None, + 'tag': None + }, + 'fields': { + 'name': None, + 'locked': None + }, + 'genres': { + 'id': None, + 'tag': None + }, + 'guid': None, + 'index': None, + 'key': None, + 'labels': { + 'id': None, + 'tag': None + }, + 'lastViewedAt': helpers.datetime_to_iso, + 'leafCount': None, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'loudnessAnalysisVersion': None, + 'moods': { + 'id': None, + 'tag': None + }, + 'originallyAvailableAt': partial(helpers.datetime_to_iso, to_date=True), + 'parentGuid': None, + 'parentKey': None, + 'parentRatingKey': None, + 'parentThumb': None, + 'parentTitle': None, + 'rating': None, + 'ratingKey': None, + 'styles': { + 'id': None, + 'tag': None + }, + 'summary': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'viewedLeafCount': None, + 'tracks': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]) +} + +TRACK_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'duration': None, + 'grandparentArt': None, + 'grandparentGuid': None, + 'grandparentKey': None, + 'grandparentRatingKey': None, + 'grandparentThumb': None, + 'grandparentTitle': None, + 'guid': None, + 'index': None, + 'key': None, + 'lastViewedAt': helpers.datetime_to_iso, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'media': { + 'audioChannels': None, + 'audioCodec': None, + 'audioProfile': None, + 'bitrate': None, + 'container': None, + 'duration': None, + 'id': None, + 'title': None, + 'parts': { + 'accessible': None, + 'audioProfile': None, + 'container': None, + 'deepAnalysisVersion': None, + 'duration': None, + 'exists': None, + 'file': None, + 'hasThumbnail': None, + 'id': None, + 'key': None, + 'size': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'syncItemId': None, + 'syncState': None, + 'audioStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'selected': None, + 'streamType': None, + 'title': None, + 'type': None, + 'albumGain': None, + 'albumPeak': None, + 'albumRange': None, + 'audioChannelLayout': None, + 'bitrate': None, + 'channels': None, + 'duration': None, + 'endRamp': None, + 'gain': None, + 'loudness': None, + 'lra': None, + 'peak': None, + 'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None, + 'samplingRate': None, + 'startRamp': None, + }, + 'lyricStreams': { + 'codec': None, + 'codecID': None, + 'displayTitle': None, + 'extendedDisplayTitle': None, + 'id': None, + 'index': None, + 'minLines': None, + 'provider': None, + 'streamType': None, + 'timed': None, + 'title': None, + 'type': None, + 'format': None, + 'key': None + } + } + }, + 'moods': { + 'id': None, + 'tag': None + }, + 'originalTitle': None, + 'parentGuid': None, + 'parentIndex': None, + 'parentKey': None, + 'parentRatingKey': None, + 'parentThumb': None, + 'parentTitle': None, + 'ratingCount': None, + 'ratingKey': None, + 'summary': None, + 'thumb': None, + 'title': None, + 'titleSort': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'userRating': None, + 'viewCount': None, + 'year': None, +} + +PHOTO_ALBUM_ATTRS = { + # For some reason photos needs to be first, + # otherwise the photo album ratingKey gets + # clobbered by the first photo's ratingKey + 'photos': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]), + 'addedAt': helpers.datetime_to_iso, + 'art': None, + 'composite': None, + 'guid': None, + 'index': None, + 'key': None, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'ratingKey': None, + 'summary': None, + 'thumb': None, + 'title': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso +} + +PHOTO_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'createdAtAccuracy': None, + 'createdAtTZOffset': None, + 'guid': None, + 'index': None, + 'key': None, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'originallyAvailableAt': partial(helpers.datetime_to_iso, to_date=True), + 'parentGuid': None, + 'parentIndex': None, + 'parentKey': None, + 'parentRatingKey': None, + 'parentThumb': None, + 'parentTitle': None, + 'ratingKey': None, + 'summary': None, + 'thumb': None, + 'title': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'year': None, + 'media': { + 'aperture': None, + 'aspectRatio': None, + 'container': None, + 'height': None, + 'id': None, + 'iso': None, + 'lens': None, + 'make': None, + 'model': None, + 'width': None, + 'parts': { + 'accessible': None, + 'container': None, + 'exists': None, + 'file': None, + 'id': None, + 'key': None, + 'size': None + } + }, + 'tag': { + 'id': None, + 'tag': None, + 'title': None + } +} + +COLLECTION_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'childCount': None, + 'collectionMode': None, + 'collectionSort': None, + 'contentRating': None, + 'fields': { + 'name': None, + 'locked': None + }, + 'guid': None, + 'index': None, + 'key': None, + 'librarySectionID': None, + 'librarySectionKey': None, + 'librarySectionTitle': None, + 'maxYear': None, + 'minYear': None, + 'ratingKey': None, + 'subtype': None, + 'summary': None, + 'thumb': None, + 'title': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'children': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]) +} + +PLAYLIST_ATTRS = { + 'addedAt': helpers.datetime_to_iso, + 'composite': None, + 'duration': None, + 'guid': None, + 'key': None, + 'leafCount': None, + 'playlistType': None, + 'ratingKey': None, + 'smart': None, + 'summary': None, + 'title': None, + 'type': None, + 'updatedAt': helpers.datetime_to_iso, + 'items': lambda e: helpers.get_attrs_to_dict(e, MEDIA_TYPES[e.type]) +} + +MEDIA_TYPES = { + 'movie': MOVIE_ATTRS, + 'show': SHOW_ATTRS, + 'season': SEASON_ATTRS, + 'episode': EPISODE_ATTRS, + 'artist': ARTIST_ATTRS, + 'album': ALBUM_ATTRS, + 'track': TRACK_ATTRS, + 'photo album': PHOTO_ALBUM_ATTRS, + 'photo': PHOTO_ATTRS, + 'collection': COLLECTION_ATTRS, + 'playlist': PLAYLIST_ATTRS +} + + +def export(section_id=None, rating_key=None, output_format='json'): + timestamp = helpers.timestamp() + + if not section_id and not rating_key: + logger.error("Tautulli Exporter :: Export called but no section_id or rating_key provided.") + return + elif section_id and not str(section_id).isdigit(): + logger.error("Tautulli Exporter :: Export called with invalid section_id '%s'.", section_id) + return + elif rating_key and not str(rating_key).isdigit(): + logger.error("Tautulli Exporter :: Export called with invalid rating_key '%s'.", rating_key) + return + elif output_format not in ('json', 'csv'): + logger.error("Tautulli Exporter :: Export called but invalid output_format '%s' provided.", output_format) + return + + plex = Plex(plexpy.CONFIG.PMS_URL, plexpy.CONFIG.PMS_TOKEN) + + if section_id: + logger.debug("Tautulli Exporter :: Exporting called with section_id %s", section_id) + + library = plex.get_library(section_id) + media_type = library.type + library_title = library.title + filename = 'Library - {} [{}].{}.{}'.format( + library_title, section_id, helpers.timestamp_to_YMDHMS(timestamp), output_format) + items = library.all() + + elif rating_key: + logger.debug("Tautulli Exporter :: Exporting called with rating_key %s", rating_key) + + item = plex.get_item(helpers.cast_to_int(rating_key)) + media_type = item.type + + if media_type in ('season', 'episode', 'album', 'track'): + item_title = item._defaultSyncTitle() + else: + item_title = item.title + + if media_type == 'photo' and item.TAG == 'Directory': + media_type = 'photo album' + + filename = '{} - {} [{}].{}.{}'.format( + media_type.title(), item_title, rating_key, helpers.timestamp_to_YMDHMS(timestamp), output_format) + + items = [item] + + else: + return + + filename = helpers.clean_filename(filename) + filepath = os.path.join(plexpy.CONFIG.CACHE_DIR, filename) + logger.info("Tautulli Exporter :: Starting export for '%s'...", filename) + + attrs = MEDIA_TYPES[media_type] + part = partial(helpers.get_attrs_to_dict, attrs=attrs) + + with ThreadPool(processes=4) as pool: + result = pool.map(part, items) + + if output_format == 'json': + with open(filepath, 'w', encoding='utf-8') as outfile: + json.dump(result, outfile, indent=4, ensure_ascii=False, sort_keys=True) + + elif output_format == 'csv': + flatten_result = helpers.flatten_dict(result) + flatten_attrs = helpers.flatten_dict(attrs) + with open(filepath, 'w', encoding='utf-8', newline='') as outfile: + writer = csv.DictWriter(outfile, sorted(flatten_attrs[0].keys())) + writer.writeheader() + writer.writerows(flatten_result) + + logger.info("Tautulli Exporter :: Successfully exported to '%s'", filepath) diff --git a/plexpy/plex.py b/plexpy/plex.py new file mode 100644 index 00000000..b46f0722 --- /dev/null +++ b/plexpy/plex.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# This file is part of Tautulli. +# +# Tautulli is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Tautulli is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Tautulli. If not, see . + +from __future__ import unicode_literals +from future.builtins import object +from future.builtins import str + +from plexapi.server import PlexServer + +import plexpy +if plexpy.PYTHON2: + import logger +else: + from plexpy import logger + + +class Plex(object): + def __init__(self, url, token): + self.plex = PlexServer(url, token) + + def get_library(self, section_id): + return self.plex.library.sectionByID(str(section_id)) + + def get_library_items(self, section_id): + return self.get_library(str(section_id)).all() + + def get_item(self, rating_key): + return self.plex.fetchItem(rating_key) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 47bb7912..aa439417 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -48,6 +48,7 @@ if plexpy.PYTHON2: import config import database import datafactory + import exporter import graphs import helpers import http_handler @@ -81,6 +82,7 @@ else: from plexpy import config from plexpy import database from plexpy import datafactory + from plexpy import exporter from plexpy import graphs from plexpy import helpers from plexpy import http_handler @@ -6414,3 +6416,15 @@ class WebInterface(object): status['message'] = 'Database not ok' return status + + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def export_metadata(self, section_id=None, rating_key=None, output_format='json', **kwargs): + threading.Thread(target=exporter.export, + kwargs={'section_id': section_id, + 'rating_key': rating_key, + 'output_format': output_format}).start() + return {'result': 'success', + 'message': 'Metadata export has started. Check the logs to monitor any problems.'}