mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-10 15:32:38 -07:00
1830 lines
75 KiB
Python
1830 lines
75 KiB
Python
# -*- 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 <http://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import unicode_literals
|
|
from future.builtins import str
|
|
from backports import csv
|
|
|
|
import json
|
|
import os
|
|
import requests
|
|
import shutil
|
|
import threading
|
|
|
|
from functools import partial, reduce
|
|
from io import open
|
|
from multiprocessing.dummy import Pool as ThreadPool
|
|
|
|
import plexpy
|
|
if plexpy.PYTHON2:
|
|
import database
|
|
import datatables
|
|
import helpers
|
|
import logger
|
|
from plex import Plex
|
|
else:
|
|
from plexpy import database
|
|
from plexpy import datatables
|
|
from plexpy import helpers
|
|
from plexpy import logger
|
|
from plexpy.plex import Plex
|
|
|
|
|
|
class Export(object):
|
|
MEDIA_TYPES = (
|
|
'movie',
|
|
'show', 'season', 'episode',
|
|
'artist', 'album', 'track',
|
|
'photo album', 'photo',
|
|
'collection',
|
|
'playlist'
|
|
)
|
|
CHILD_MEDIA_TYPES = {
|
|
'show': 'season',
|
|
'season': 'episode',
|
|
'artist': 'album',
|
|
'album': 'track',
|
|
'photo album': 'photo',
|
|
'collection': 'children'
|
|
}
|
|
CHILD_ATTR_KEY = {
|
|
'show': 'seasons.',
|
|
'season': 'episodes.',
|
|
'artist': 'albums.',
|
|
'album': 'tracks.',
|
|
'photo album': 'photos.',
|
|
'collection': 'children.'
|
|
}
|
|
LEVELS = (1, 2, 3, 9)
|
|
|
|
def __init__(self, section_id=None, rating_key=None, file_format='json',
|
|
metadata_level=1, media_info_level=1, include_images=False):
|
|
self.section_id = helpers.cast_to_int(section_id)
|
|
self.rating_key = helpers.cast_to_int(rating_key)
|
|
self.file_format = file_format
|
|
self.metadata_level = helpers.cast_to_int(metadata_level)
|
|
self.media_info_level = helpers.cast_to_int(media_info_level)
|
|
self.include_images = include_images
|
|
|
|
self.timestamp = helpers.timestamp()
|
|
|
|
self.media_type = None
|
|
self.sub_media_type = None
|
|
self.items = []
|
|
|
|
self.filename = None
|
|
self.export_id = None
|
|
self.file_size = None
|
|
self.success = False
|
|
|
|
def _get_all_metadata_attr(self, media_type):
|
|
exclude_attrs = ('media', 'artFile', 'thumbFile')
|
|
all_attrs = self.return_attrs(media_type)
|
|
return [attr for attr in all_attrs if attr not in exclude_attrs]
|
|
|
|
def return_attrs(self, media_type):
|
|
def movie_attrs():
|
|
_movie_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'artFile': lambda i: get_image(i, 'art', self.filename),
|
|
'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,
|
|
'durationHuman': lambda i: helpers.human_duration(getattr(i, 'duration', 0), sig='dhm'),
|
|
'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,
|
|
'hdr': lambda i: get_any_hdr(i, self.return_attrs('movie')['media']),
|
|
'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,
|
|
'sizeHuman': lambda i: helpers.human_file_size(getattr(i, 'size', 0)),
|
|
'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,
|
|
'default': 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,
|
|
'hdr': lambda i: helpers.is_hdr(getattr(i, 'bitDepth', 0), getattr(i, 'colorSpace', '')),
|
|
'height': None,
|
|
'level': None,
|
|
'pixelAspectRatio': None,
|
|
'pixelFormat': 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,
|
|
'default': 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,
|
|
'default': 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,
|
|
'headerCompression': 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,
|
|
'thumbFile': lambda i: get_image(i, 'thumb', self.filename),
|
|
'title': None,
|
|
'titleSort': None,
|
|
'type': None,
|
|
'updatedAt': helpers.datetime_to_iso,
|
|
'userRating': None,
|
|
'viewCount': None,
|
|
'writers': {
|
|
'id': None,
|
|
'tag': None
|
|
},
|
|
'year': None
|
|
}
|
|
return _movie_attrs
|
|
|
|
def show_attrs():
|
|
_show_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'artFile': lambda i: get_image(i, 'art', self.filename),
|
|
'banner': None,
|
|
'childCount': None,
|
|
'collections': {
|
|
'id': None,
|
|
'tag': None
|
|
},
|
|
'contentRating': None,
|
|
'duration': None,
|
|
'durationHuman': lambda i: helpers.human_duration(getattr(i, 'duration', 0), sig='dhm'),
|
|
'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,
|
|
'thumbFile': lambda i: get_image(i, 'thumb', self.filename),
|
|
'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.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type)[0])
|
|
}
|
|
return _show_attrs
|
|
|
|
def season_attrs():
|
|
_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,
|
|
'thumbFile': lambda i: get_image(i, 'thumb', self.filename),
|
|
'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.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type)[0])
|
|
}
|
|
return _season_attrs
|
|
|
|
def episode_attrs():
|
|
_episode_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'chapterSource': None,
|
|
'contentRating': None,
|
|
'directors': {
|
|
'id': None,
|
|
'tag': None
|
|
},
|
|
'duration': None,
|
|
'durationHuman': lambda i: helpers.human_duration(getattr(i, 'duration', 0), sig='dhm'),
|
|
'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,
|
|
'hdr': lambda i: get_any_hdr(i, self.return_attrs('episode')['media']),
|
|
'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,
|
|
'sizeHuman': lambda i: helpers.human_file_size(getattr(i, 'size', 0)),
|
|
'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,
|
|
'default': 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,
|
|
'hdr': lambda i: helpers.is_hdr(getattr(i, 'bitDepth', 0), getattr(i, 'colorSpace', '')),
|
|
'height': None,
|
|
'level': None,
|
|
'pixelAspectRatio': None,
|
|
'pixelFormat': 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,
|
|
'default': 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,
|
|
'default': 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,
|
|
'headerCompression': 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
|
|
}
|
|
return _episode_attrs
|
|
|
|
def artist_attrs():
|
|
_artist_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'artFile': lambda i: get_image(i, 'art', self.filename),
|
|
'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,
|
|
'thumbFile': lambda i: get_image(i, 'thumb', self.filename),
|
|
'title': None,
|
|
'titleSort': None,
|
|
'type': None,
|
|
'updatedAt': helpers.datetime_to_iso,
|
|
'userRating': None,
|
|
'viewCount': None,
|
|
'albums': lambda e: helpers.get_attrs_to_dict(e.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type))
|
|
}
|
|
return _artist_attrs
|
|
|
|
def album_attrs():
|
|
_album_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'artFile': lambda i: get_image(i, 'art', self.filename),
|
|
'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,
|
|
'thumbFile': lambda i: get_image(i, 'thumb', self.filename),
|
|
'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.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type))
|
|
}
|
|
return _album_attrs
|
|
|
|
def track_attrs():
|
|
_track_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'duration': None,
|
|
'durationHuman': lambda i: helpers.human_duration(getattr(i, 'duration', 0), sig='dhm'),
|
|
'fields': {
|
|
'name': None,
|
|
'locked': 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,
|
|
'locations': 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,
|
|
'sizeHuman': lambda i: helpers.human_file_size(getattr(i, 'size', 0)),
|
|
'requiredBandwidths': lambda e: [int(b) for b in e.split(',')] if e else None,
|
|
'syncItemId': None,
|
|
'syncState': None,
|
|
'audioStreams': {
|
|
'codec': None,
|
|
'codecID': None,
|
|
'default': 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,
|
|
'default': 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,
|
|
}
|
|
return _track_attrs
|
|
|
|
def photo_album_attrs():
|
|
_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.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type)),
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'composite': None,
|
|
'fields': {
|
|
'name': None,
|
|
'locked': None
|
|
},
|
|
'guid': None,
|
|
'index': None,
|
|
'key': None,
|
|
'librarySectionID': None,
|
|
'librarySectionKey': None,
|
|
'librarySectionTitle': None,
|
|
'ratingKey': None,
|
|
'summary': None,
|
|
'thumb': None,
|
|
'title': None,
|
|
'titleSort': None,
|
|
'type': None,
|
|
'updatedAt': helpers.datetime_to_iso
|
|
}
|
|
return _photo_album_attrs
|
|
|
|
def photo_attrs():
|
|
_photo_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'createdAtAccuracy': None,
|
|
'createdAtTZOffset': None,
|
|
'fields': {
|
|
'name': None,
|
|
'locked': 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,
|
|
'titleSort': 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,
|
|
'sizeHuman': lambda i: helpers.human_file_size(getattr(i, 'size', 0)),
|
|
}
|
|
},
|
|
'tag': {
|
|
'id': None,
|
|
'tag': None,
|
|
'title': None
|
|
}
|
|
}
|
|
return _photo_attrs
|
|
|
|
def collection_attrs():
|
|
_collection_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'art': None,
|
|
'artFile': lambda i: get_image(i, 'art', self.filename),
|
|
'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,
|
|
'thumbFile': lambda i: get_image(i, 'thumb', self.filename),
|
|
'title': None,
|
|
'titleSort': None,
|
|
'type': None,
|
|
'updatedAt': helpers.datetime_to_iso,
|
|
'children': lambda e: helpers.get_attrs_to_dict(e.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type))
|
|
}
|
|
return _collection_attrs
|
|
|
|
def playlist_attrs():
|
|
_playlist_attrs = {
|
|
'addedAt': helpers.datetime_to_iso,
|
|
'composite': None,
|
|
'duration': None,
|
|
'durationHuman': lambda i: helpers.human_duration(getattr(i, 'duration', 0), sig='dhm'),
|
|
'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.reload() if e.isPartialObject() else e,
|
|
self.return_attrs(e.type))
|
|
}
|
|
return _playlist_attrs
|
|
|
|
_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,
|
|
}
|
|
|
|
return _media_types[media_type]()
|
|
|
|
def return_levels(self, media_type):
|
|
|
|
def movie_levels():
|
|
_media_type = 'movie'
|
|
|
|
_movie_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'originalTitle', 'originallyAvailableAt', 'year', 'addedAt',
|
|
'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'
|
|
],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
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'
|
|
],
|
|
9: [
|
|
'locations', 'media'
|
|
]
|
|
}
|
|
]
|
|
return _movie_levels
|
|
|
|
def show_levels():
|
|
_media_type = 'show'
|
|
_child_type = self.CHILD_MEDIA_TYPES[_media_type]
|
|
_child_attr = self.CHILD_ATTR_KEY[_media_type]
|
|
_child_levels = self.return_levels(_child_type)
|
|
|
|
_show_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'originallyAvailableAt', 'year', 'addedAt',
|
|
'rating', 'userRating', 'contentRating',
|
|
'studio', 'summary', 'guid', 'duration', 'durationHuman', 'type', 'childCount'
|
|
] + [_child_attr + attr for attr in _child_levels[0][1]],
|
|
2: [
|
|
'roles.tag', 'roles.role',
|
|
'genres.tag', 'collections.tag', 'labels.tag',
|
|
'fields.name', 'fields.locked'
|
|
] + [_child_attr + attr for attr in _child_levels[0][2]],
|
|
3: [
|
|
'art', 'thumb', 'banner', 'theme', 'key',
|
|
'updatedAt', 'lastViewedAt', 'viewCount'
|
|
] + [_child_attr + attr for attr in _child_levels[0][3]],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
l: [_child_attr + attr for attr in _child_levels[1][l]] for l in self.LEVELS
|
|
}
|
|
]
|
|
return _show_levels
|
|
|
|
def season_levels():
|
|
_media_type = 'season'
|
|
_child_type = self.CHILD_MEDIA_TYPES[_media_type]
|
|
_child_attr = self.CHILD_ATTR_KEY[_media_type]
|
|
_child_levels = self.return_levels(_child_type)
|
|
|
|
_season_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'addedAt',
|
|
'userRating',
|
|
'summary', 'guid', 'type', 'index',
|
|
'parentTitle', 'parentRatingKey', 'parentGuid'
|
|
] + [_child_attr + attr for attr in _child_levels[0][1]],
|
|
2: [
|
|
'fields.name', 'fields.locked'
|
|
] + [_child_attr + attr for attr in _child_levels[0][2]],
|
|
3: [
|
|
'art', 'thumb', 'key',
|
|
'updatedAt', 'lastViewedAt', 'viewCount',
|
|
'parentKey', 'parentTheme', 'parentThumb'
|
|
] + [_child_attr + attr for attr in _child_levels[0][3]],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
l: [_child_attr + attr for attr in _child_levels[1][l]] for l in self.LEVELS
|
|
}
|
|
]
|
|
return _season_levels
|
|
|
|
def episode_levels():
|
|
_media_type = 'episode'
|
|
|
|
_episode_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'originallyAvailableAt', 'year', 'addedAt',
|
|
'rating', 'userRating', 'contentRating',
|
|
'summary', 'guid', 'duration', 'durationHuman', 'type', 'index',
|
|
'parentTitle', 'parentRatingKey', 'parentGuid', 'parentIndex',
|
|
'grandparentTitle', 'grandparentRatingKey', 'grandparentGuid'
|
|
],
|
|
2: [
|
|
'directors.tag', 'writers.tag',
|
|
'fields.name', 'fields.locked'
|
|
],
|
|
3: [
|
|
'art', 'thumb', 'key', 'chapterSource',
|
|
'updatedAt', 'lastViewedAt', 'viewCount',
|
|
'parentThumb', 'parentKey',
|
|
'grandparentArt', 'grandparentThumb', 'grandparentTheme', 'grandparentKey'
|
|
],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
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'
|
|
],
|
|
9: [
|
|
'locations', 'media'
|
|
]
|
|
}
|
|
]
|
|
return _episode_levels
|
|
|
|
def artist_levels():
|
|
_media_type = 'artist'
|
|
_child_type = self.CHILD_MEDIA_TYPES[_media_type]
|
|
_child_attr = self.CHILD_ATTR_KEY[_media_type]
|
|
_child_levels = self.return_levels(_child_type)
|
|
|
|
_artist_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'addedAt',
|
|
'rating', 'userRating',
|
|
'summary', 'guid', 'type',
|
|
] + [_child_attr + attr for attr in _child_levels[0][1]],
|
|
2: [
|
|
'collections.tag', 'genres.tag', 'countries.tag', 'moods.tag', 'styles.tag',
|
|
'fields.name', 'fields.locked'
|
|
] + [_child_attr + attr for attr in _child_levels[0][2]],
|
|
3: [
|
|
'art', 'thumb', 'key',
|
|
'updatedAt', 'lastViewedAt', 'viewCount'
|
|
] + [_child_attr + attr for attr in _child_levels[0][3]],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
l: [_child_attr + attr for attr in _child_levels[1][l]] for l in self.LEVELS
|
|
}
|
|
]
|
|
return _artist_levels
|
|
|
|
def album_levels():
|
|
_media_type = 'album'
|
|
_child_type = self.CHILD_MEDIA_TYPES[_media_type]
|
|
_child_attr = self.CHILD_ATTR_KEY[_media_type]
|
|
_child_levels = self.return_levels(_child_type)
|
|
|
|
_album_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'originallyAvailableAt', 'addedAt',
|
|
'rating', 'userRating',
|
|
'summary', 'guid', 'type', 'index',
|
|
'parentTitle', 'parentRatingKey', 'parentGuid'
|
|
] + [_child_attr + attr for attr in _child_levels[0][1]],
|
|
2: [
|
|
'collections.tag', 'genres.tag', 'labels.tag', 'moods.tag', 'styles.tag',
|
|
'fields.name', 'fields.locked'
|
|
] + [_child_attr + attr for attr in _child_levels[0][2]],
|
|
3: [
|
|
'art', 'thumb', 'key',
|
|
'updatedAt', 'lastViewedAt', 'viewCount',
|
|
'parentKey', 'parentThumb'
|
|
] + [_child_attr + attr for attr in _child_levels[0][3]],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
l: [_child_attr + attr for attr in _child_levels[1][l]] for l in self.LEVELS
|
|
}
|
|
]
|
|
return _album_levels
|
|
|
|
def track_levels():
|
|
_media_type = 'track'
|
|
|
|
_track_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'originalTitle', 'year', 'addedAt',
|
|
'userRating', 'ratingCount',
|
|
'summary', 'guid', 'duration', 'durationHuman', 'type', 'index',
|
|
'parentTitle', 'parentRatingKey', 'parentGuid', 'parentIndex',
|
|
'grandparentTitle', 'grandparentRatingKey', 'grandparentGuid'
|
|
],
|
|
2: [
|
|
'moods.tag', 'writers.tag',
|
|
'fields.name', 'fields.locked'
|
|
],
|
|
3: [
|
|
'art', 'thumb', 'key',
|
|
'updatedAt', 'lastViewedAt', 'viewCount',
|
|
'parentThumb', 'parentKey',
|
|
'grandparentArt', 'grandparentThumb', 'grandparentKey'
|
|
],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
1: [
|
|
'locations', 'media.audioChannels', 'media.audioCodec',
|
|
'media.audioProfile',
|
|
'media.bitrate', 'media.container', 'media.duration'
|
|
],
|
|
2: [
|
|
'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration',
|
|
'media.parts.container', 'media.parts.size', 'media.parts.sizeHuman',
|
|
'media.parts.audioProfile',
|
|
'media.parts.deepAnalysisVersion', 'media.parts.hasThumbnail'
|
|
],
|
|
3: [
|
|
'media.parts.audioStreams.codec', 'media.parts.audioStreams.bitrate',
|
|
'media.parts.audioStreams.title', 'media.parts.audioStreams.displayTitle',
|
|
'media.parts.audioStreams.extendedDisplayTitle',
|
|
'media.parts.audioStreams.channels', 'media.parts.audioStreams.audioChannelLayout',
|
|
'media.parts.audioStreams.samplingRate',
|
|
'media.parts.audioStreams.default',
|
|
'media.parts.audioStreams.albumGain', 'media.parts.audioStreams.albumPeak',
|
|
'media.parts.audioStreams.albumRange',
|
|
'media.parts.audioStreams.loudness', 'media.parts.audioStreams.gain',
|
|
'media.parts.audioStreams.lra', 'media.parts.audioStreams.peak',
|
|
'media.parts.audioStreams.startRamp', 'media.parts.audioStreams.endRamp',
|
|
'media.parts.lyricStreams.codec', 'media.parts.lyricStreams.format',
|
|
'media.parts.lyricStreams.title', 'media.parts.lyricStreams.displayTitle',
|
|
'media.parts.lyricStreams.extendedDisplayTitle',
|
|
'media.parts.lyricStreams.default', 'media.parts.lyricStreams.minLines',
|
|
'media.parts.lyricStreams.provider', 'media.parts.lyricStreams.timed',
|
|
],
|
|
9: [
|
|
'locations', 'media'
|
|
]
|
|
}
|
|
]
|
|
return _track_levels
|
|
|
|
def photo_album_levels():
|
|
_media_type = 'photo album'
|
|
_child_type = self.CHILD_MEDIA_TYPES[_media_type]
|
|
_child_attr = self.CHILD_ATTR_KEY[_media_type]
|
|
_child_levels = self.return_levels(_child_type)
|
|
|
|
_photo_album_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'addedAt',
|
|
'summary', 'guid', 'type', 'index',
|
|
] + [_child_attr + attr for attr in _child_levels[0][1]],
|
|
2: [
|
|
'fields.name', 'fields.locked'
|
|
] + [_child_attr + attr for attr in _child_levels[0][2]],
|
|
3: [
|
|
'art', 'thumb', 'key',
|
|
'updatedAt'
|
|
] + [_child_attr + attr for attr in _child_levels[0][3]],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
l: [_child_attr + attr for attr in _child_levels[1][l]] for l in self.LEVELS
|
|
}
|
|
]
|
|
return _photo_album_levels
|
|
|
|
def photo_levels():
|
|
_media_type = 'photo'
|
|
|
|
_photo_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'year', 'originallyAvailableAt', 'addedAt',
|
|
'summary', 'guid', 'type', 'index',
|
|
'parentTitle', 'parentRatingKey', 'parentGuid', 'parentIndex',
|
|
'createdAtAccuracy', 'createdAtTZOffset'
|
|
],
|
|
2: [
|
|
'tag.tag', 'tag.title'
|
|
],
|
|
3: [
|
|
'thumb', 'key',
|
|
'updatedAt',
|
|
'parentThumb', 'parentKey'
|
|
],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
1: [
|
|
'media.aspectRatio', 'media.aperture',
|
|
'media.container', 'media.height', 'media.width',
|
|
'media.iso', 'media.lens', 'media.make', 'media.model'
|
|
],
|
|
2: [
|
|
'media.parts.accessible', 'media.parts.exists', 'media.parts.file',
|
|
'media.parts.container', 'media.parts.size', 'media.parts.sizeHuman'
|
|
],
|
|
3: [
|
|
],
|
|
9: [
|
|
'media'
|
|
]
|
|
}
|
|
]
|
|
return _photo_levels
|
|
|
|
def collection_levels():
|
|
_media_type = 'collection'
|
|
_child_type = self.CHILD_MEDIA_TYPES[_media_type]
|
|
_child_attr = self.CHILD_ATTR_KEY[_media_type]
|
|
_child_levels = self.return_levels(self.sub_media_type)
|
|
|
|
_collection_levels = [
|
|
{
|
|
1: [
|
|
'ratingKey', 'title', 'titleSort', 'minYear', 'maxYear', 'addedAt',
|
|
'contentRating',
|
|
'summary', 'guid', 'type', 'subtype', 'childCount',
|
|
'collectionMode', 'collectionSort'
|
|
] + [_child_attr + attr for attr in _child_levels[0][1]],
|
|
2: [
|
|
'fields.name', 'fields.locked'
|
|
] + [_child_attr + attr for attr in _child_levels[0][2]],
|
|
3: [
|
|
'art', 'thumb', 'key',
|
|
'updatedAt'
|
|
] + [_child_attr + attr for attr in _child_levels[0][3]],
|
|
9: self._get_all_metadata_attr(_media_type)
|
|
},
|
|
{
|
|
l: [_child_attr + attr for attr in _child_levels[1][l]] for l in self.LEVELS
|
|
}
|
|
]
|
|
return _collection_levels
|
|
|
|
def playlist_levels():
|
|
_playlist_levels = []
|
|
return _playlist_levels
|
|
|
|
_media_types = {
|
|
'movie': movie_levels,
|
|
'show': show_levels,
|
|
'season': season_levels,
|
|
'episode': episode_levels,
|
|
'artist': artist_levels,
|
|
'album': album_levels,
|
|
'track': track_levels,
|
|
'photo album': photo_album_levels,
|
|
'photo': photo_levels,
|
|
'collection': collection_levels,
|
|
'playlist': playlist_levels
|
|
}
|
|
|
|
return _media_types[media_type]()
|
|
|
|
def export(self):
|
|
if not self.section_id and not self.rating_key:
|
|
logger.error("Tautulli Exporter :: Export called but no section_id or rating_key provided.")
|
|
return
|
|
elif self.rating_key and not str(self.rating_key).isdigit():
|
|
logger.error("Tautulli Exporter :: Export called with invalid rating_key '%s'.", self.rating_key)
|
|
return
|
|
elif self.section_id and not str(self.section_id).isdigit():
|
|
logger.error("Tautulli Exporter :: Export called with invalid section_id '%s'.", self.section_id)
|
|
return
|
|
elif not self.metadata_level:
|
|
logger.error("Tautulli Exporter :: Export called with invalid metadata_level '%s'.", self.metadata_level)
|
|
return
|
|
elif not self.media_info_level:
|
|
logger.error("Tautulli Exporter :: Export called with invalid media_info_level '%s'.", self.media_info_level)
|
|
return
|
|
elif self.file_format not in ('json', 'csv'):
|
|
logger.error("Tautulli Exporter :: Export called but invalid file_format '%s' provided.", self.file_format)
|
|
return
|
|
|
|
plex = Plex(plexpy.CONFIG.PMS_URL, plexpy.CONFIG.PMS_TOKEN)
|
|
|
|
if self.rating_key:
|
|
logger.debug(
|
|
"Tautulli Exporter :: Export called with rating_key %s, "
|
|
"metadata_level %d, media_info_level %d, include_images %s",
|
|
self.rating_key, self.metadata_level, self.media_info_level, self.include_images)
|
|
|
|
item = plex.get_item(self.rating_key)
|
|
self.media_type = item.type
|
|
|
|
if self.media_type == 'collection':
|
|
self.sub_media_type = item.subtype
|
|
|
|
if self.media_type != 'playlist':
|
|
self.section_id = item.librarySectionID
|
|
|
|
if self.media_type in ('season', 'episode', 'album', 'track'):
|
|
item_title = item._defaultSyncTitle()
|
|
else:
|
|
item_title = item.title
|
|
|
|
if self.media_type == 'photo' and item.TAG == 'Directory':
|
|
self.media_type = 'photo album'
|
|
|
|
filename = '{} - {} [{}].{}'.format(
|
|
self.media_type.title(), item_title, self.rating_key,
|
|
helpers.timestamp_to_YMDHMS(self.timestamp))
|
|
|
|
self.items = [item]
|
|
|
|
elif self.section_id:
|
|
logger.debug(
|
|
"Tautulli Exporter :: Export called with section_id %s, metadata_level %d, media_info_level %d",
|
|
self.section_id, self.metadata_level, self.media_info_level)
|
|
|
|
library = plex.get_library(str(self.section_id))
|
|
self.media_type = library.type
|
|
library_title = library.title
|
|
|
|
filename = 'Library - {} [{}].{}'.format(
|
|
library_title, self.section_id,
|
|
helpers.timestamp_to_YMDHMS(self.timestamp))
|
|
|
|
self.items = library.all()
|
|
|
|
else:
|
|
return
|
|
|
|
if self.media_type not in self.MEDIA_TYPES:
|
|
logger.error("Tautulli Exporter :: Cannot export media type '%s'", self.media_type)
|
|
return
|
|
|
|
media_attrs = self.return_attrs(self.media_type)
|
|
metadata_level_attrs, media_info_level_attrs = self.return_levels(self.media_type)
|
|
|
|
if self.metadata_level not in metadata_level_attrs:
|
|
logger.error("Tautulli Exporter :: Export called with invalid metadata_level '%s'.", self.metadata_level)
|
|
return
|
|
elif self.media_info_level not in media_info_level_attrs:
|
|
logger.error("Tautulli Exporter :: Export called with invalid media_info_level '%s'.", self.media_info_level)
|
|
return
|
|
|
|
export_attrs_list = []
|
|
export_attrs_set = set()
|
|
|
|
for level, attrs in metadata_level_attrs.items():
|
|
if level <= self.metadata_level:
|
|
export_attrs_set.update(attrs)
|
|
|
|
for level, attrs in media_info_level_attrs.items():
|
|
if level <= self.media_info_level:
|
|
export_attrs_set.update(attrs)
|
|
|
|
if self.include_images:
|
|
for image_attr in ('artFile', 'thumbFile'):
|
|
if image_attr in media_attrs:
|
|
export_attrs_set.add(image_attr)
|
|
if self.media_type in ('show', 'artist', 'collection'):
|
|
child_media_type = self.CHILD_MEDIA_TYPES[self.media_type]
|
|
child_attr_key = self.CHILD_ATTR_KEY[self.media_type]
|
|
child_media_attrs = self.return_attrs(self.sub_media_type or child_media_type)
|
|
for image_attr in ('artFile', 'thumbFile'):
|
|
if image_attr in child_media_attrs:
|
|
export_attrs_set.add(child_attr_key + image_attr)
|
|
|
|
for attr in export_attrs_set:
|
|
value = self._get_attr_value(media_attrs, attr)
|
|
if not value:
|
|
continue
|
|
export_attrs_list.append(value)
|
|
|
|
export_attrs = reduce(helpers.dict_merge, export_attrs_list)
|
|
|
|
self.filename = '{}.{}'.format(helpers.clean_filename(filename), self.file_format)
|
|
|
|
self.export_id = self.add_export()
|
|
|
|
if not self.export_id:
|
|
logger.error("Tautulli Exporter :: Failed to export '%s'", self.filename)
|
|
return
|
|
|
|
threading.Thread(target=self._real_export,
|
|
kwargs={'attrs': export_attrs}).start()
|
|
|
|
return True
|
|
|
|
def _real_export(self, attrs):
|
|
logger.info("Tautulli Exporter :: Starting export for '%s'...", self.filename)
|
|
|
|
filepath = get_export_filepath(self.filename)
|
|
|
|
part = partial(helpers.get_attrs_to_dict, attrs=attrs)
|
|
pool = ThreadPool(processes=4)
|
|
|
|
try:
|
|
result = pool.map(part, self.items)
|
|
|
|
if 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)
|
|
|
|
self.file_size = os.path.getsize(filepath)
|
|
|
|
if self.include_images:
|
|
images_folder = get_export_filepath(self.filename, images=True)
|
|
if os.path.exists(images_folder):
|
|
for f in os.listdir(images_folder):
|
|
image_path = os.path.join(images_folder, f)
|
|
if os.path.isfile(image_path):
|
|
self.file_size += os.path.getsize(image_path)
|
|
|
|
self.success = True
|
|
logger.info("Tautulli Exporter :: Successfully exported to '%s'", filepath)
|
|
|
|
except Exception as e:
|
|
logger.exception("Tautulli Exporter :: Failed to export '%s': %s", self.filename, e)
|
|
|
|
finally:
|
|
pool.close()
|
|
pool.join()
|
|
self.set_export_state()
|
|
|
|
def add_export(self):
|
|
keys = {'timestamp': self.timestamp,
|
|
'section_id': self.section_id,
|
|
'rating_key': self.rating_key,
|
|
'media_type': self.media_type}
|
|
|
|
values = {'file_format': self.file_format,
|
|
'filename': self.filename,
|
|
'include_images': self.include_images}
|
|
|
|
db = database.MonitorDatabase()
|
|
try:
|
|
db.upsert(table_name='exports', key_dict=keys, value_dict=values)
|
|
return db.last_insert_id()
|
|
except Exception as e:
|
|
logger.error("Tautulli Exporter :: Unable to save export to database: %s", e)
|
|
return False
|
|
|
|
def set_export_state(self):
|
|
if self.success:
|
|
complete = 1
|
|
else:
|
|
complete = -1
|
|
|
|
keys = {'id': self.export_id}
|
|
values = {'complete': complete,
|
|
'file_size': self.file_size}
|
|
|
|
db = database.MonitorDatabase()
|
|
db.upsert(table_name='exports', key_dict=keys, value_dict=values)
|
|
|
|
def _get_attr_value(self, media_attrs, attr):
|
|
try:
|
|
return helpers.get_dict_value_by_path(media_attrs, attr)
|
|
except KeyError:
|
|
pass
|
|
except TypeError:
|
|
if '.' in attr:
|
|
sub_media_type, sub_attr = attr.split('.', maxsplit=1)
|
|
if sub_media_type == 'children':
|
|
_sub_media_type = self.sub_media_type
|
|
else:
|
|
_sub_media_type = sub_media_type[:-1]
|
|
|
|
if _sub_media_type in self.MEDIA_TYPES:
|
|
sub_media_attrs = self.return_attrs(_sub_media_type)
|
|
return {sub_media_type: self._get_attr_value(sub_media_attrs, sub_attr)}
|
|
|
|
logger.warn("Tautulli Exporter :: Unknown export attribute '%s', skipping...", attr)
|
|
|
|
|
|
def get_any_hdr(obj, root):
|
|
attrs = helpers.get_dict_value_by_path(root, 'parts.videoStreams.hdr')
|
|
media = helpers.get_attrs_to_dict(obj, attrs)
|
|
return any(vs.get('hdr') for p in media.get('parts', []) for vs in p.get('videoStreams', []))
|
|
|
|
|
|
def get_image(item, image, export_filename):
|
|
media_type = item.type
|
|
rating_key = item.ratingKey
|
|
|
|
if media_type in ('season', 'episode', 'album', 'track'):
|
|
item_title = item._defaultSyncTitle()
|
|
else:
|
|
item_title = item.title
|
|
|
|
folder = get_export_filepath(export_filename, images=True)
|
|
filename = helpers.clean_filename('{} [{}].{}.jpg'.format(item_title, rating_key, image))
|
|
filepath = os.path.join(folder, filename)
|
|
|
|
if not os.path.exists(folder):
|
|
os.makedirs(folder)
|
|
|
|
image_url = None
|
|
if image == 'art':
|
|
image_url = item.artUrl
|
|
elif image == 'thumb':
|
|
image_url = item.thumbUrl
|
|
|
|
if not image_url:
|
|
return
|
|
|
|
r = requests.get(image_url, stream=True)
|
|
if r.status_code == 200:
|
|
with open(filepath, 'wb') as outfile:
|
|
for chunk in r:
|
|
outfile.write(chunk)
|
|
|
|
return filepath
|
|
|
|
|
|
def get_export(export_id):
|
|
db = database.MonitorDatabase()
|
|
result = db.select_single('SELECT filename, file_format, include_images, complete '
|
|
'FROM exports WHERE id = ?',
|
|
[export_id])
|
|
|
|
if result:
|
|
result['exists'] = check_export_exists(result['filename'])
|
|
|
|
return result
|
|
|
|
|
|
def delete_export(export_id):
|
|
db = database.MonitorDatabase()
|
|
if str(export_id).isdigit():
|
|
export_data = get_export(export_id=export_id)
|
|
|
|
logger.info("Tautulli Exporter :: Deleting export_id %s from the database.", export_id)
|
|
result = db.action('DELETE FROM exports WHERE id = ?', args=[export_id])
|
|
|
|
if export_data and export_data['exists']:
|
|
filepath = get_export_filepath(export_data['filename'])
|
|
logger.info("Tautulli Exporter :: Deleting exported file from '%s'.", filepath)
|
|
try:
|
|
os.remove(filepath)
|
|
if export_data['include_images']:
|
|
images_folder = get_export_filepath(export_data['filename'], images=True)
|
|
if os.path.exists(images_folder):
|
|
shutil.rmtree(images_folder)
|
|
except OSError as e:
|
|
logger.error("Tautulli Exporter :: Failed to delete exported file '%s': %s", filepath, e)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def delete_all_exports():
|
|
db = database.MonitorDatabase()
|
|
result = db.select('SELECT filename, include_images FROM exports')
|
|
|
|
logger.info("Tautulli Exporter :: Deleting all exports from the database.")
|
|
|
|
deleted_files = True
|
|
for row in result:
|
|
if check_export_exists(row['filename']):
|
|
filepath = get_export_filepath(row['filename'])
|
|
try:
|
|
os.remove(filepath)
|
|
if row['include_images']:
|
|
images_folder = get_export_filepath(row['filename'], images=True)
|
|
if os.path.exists(images_folder):
|
|
shutil.rmtree(images_folder)
|
|
except OSError as e:
|
|
logger.error("Tautulli Exporter :: Failed to delete exported file '%s': %s", filepath, e)
|
|
deleted_files = False
|
|
break
|
|
|
|
if deleted_files:
|
|
database.delete_exports()
|
|
return True
|
|
|
|
|
|
def cancel_exports():
|
|
db = database.MonitorDatabase()
|
|
db.action('UPDATE exports SET complete = -1 WHERE complete = 0')
|
|
|
|
|
|
def get_export_datatable(section_id=None, rating_key=None, kwargs=None):
|
|
default_return = {'recordsFiltered': 0,
|
|
'recordsTotal': 0,
|
|
'draw': 0,
|
|
'data': 'null',
|
|
'error': 'Unable to execute database query.'}
|
|
|
|
data_tables = datatables.DataTables()
|
|
|
|
custom_where = []
|
|
if section_id:
|
|
custom_where.append(['exports.section_id', section_id])
|
|
if rating_key:
|
|
custom_where.append(['exports.rating_key', rating_key])
|
|
|
|
columns = ['exports.id AS export_id',
|
|
'exports.timestamp',
|
|
'exports.section_id',
|
|
'exports.rating_key',
|
|
'exports.media_type',
|
|
'exports.filename',
|
|
'exports.file_format',
|
|
'exports.include_images',
|
|
'exports.file_size',
|
|
'exports.complete'
|
|
]
|
|
try:
|
|
query = data_tables.ssp_query(table_name='exports',
|
|
columns=columns,
|
|
custom_where=custom_where,
|
|
group_by=[],
|
|
join_types=[],
|
|
join_tables=[],
|
|
join_evals=[],
|
|
kwargs=kwargs)
|
|
except Exception as e:
|
|
logger.warn("Tautulli Exporter :: Unable to execute database query for get_export_datatable: %s.", e)
|
|
return default_return
|
|
|
|
result = query['result']
|
|
|
|
rows = []
|
|
for item in result:
|
|
media_type_title = item['media_type'].title()
|
|
exists = helpers.cast_to_int(check_export_exists(item['filename']))
|
|
|
|
row = {'export_id': item['export_id'],
|
|
'timestamp': item['timestamp'],
|
|
'section_id': item['section_id'],
|
|
'rating_key': item['rating_key'],
|
|
'media_type': item['media_type'],
|
|
'media_type_title': media_type_title,
|
|
'filename': item['filename'],
|
|
'file_format': item['file_format'],
|
|
'include_images': item['include_images'],
|
|
'file_size': item['file_size'],
|
|
'complete': item['complete'],
|
|
'exists': exists
|
|
}
|
|
|
|
rows.append(row)
|
|
|
|
result = {'recordsFiltered': query['filteredCount'],
|
|
'recordsTotal': query['totalCount'],
|
|
'data': rows,
|
|
'draw': query['draw']
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
def get_export_filepath(filename, images=False):
|
|
if images:
|
|
images_folder = '{}.images'.format(os.path.splitext(filename)[0])
|
|
return os.path.join(plexpy.CONFIG.EXPORT_DIR, images_folder)
|
|
return os.path.join(plexpy.CONFIG.EXPORT_DIR, filename)
|
|
|
|
|
|
def check_export_exists(filename):
|
|
return os.path.isfile(get_export_filepath(filename))
|