from datetime import datetime import exceptions import util import plexapp import json # Search Types - Plex uses these to filter specific media types when searching. SEARCHTYPES = { 'movie': 1, 'show': 2, 'season': 3, 'episode': 4, 'artist': 8, 'album': 9, 'track': 10 } LIBRARY_TYPES = {} def registerLibType(cls): LIBRARY_TYPES[cls.TYPE] = cls return cls def registerLibFactory(ftype): def wrap(func): LIBRARY_TYPES[ftype] = func return func return wrap class PlexValue(unicode): def __new__(cls, value, parent=None): self = super(PlexValue, cls).__new__(cls, value) self.parent = parent self.NA = False return self def __call__(self, default): return not self.NA and self or PlexValue(default, self.parent) def asBool(self): return self == '1' def asInt(self, default=0): return int(self or default) def asFloat(self, default=0): return float(self or default) def asDatetime(self, format_=None): if not self: return None if self.isdigit(): dt = datetime.fromtimestamp(int(self)) else: dt = datetime.strptime(self, '%Y-%m-%d') if not format_: return dt return dt.strftime(format_) def asURL(self): return self.parent.server.url(self) def asTranscodedImageURL(self, w, h, **extras): return self.parent.server.getImageTranscodeURL(self, w, h, **extras) class JEncoder(json.JSONEncoder): def default(self, o): try: return json.JSONEncoder.default(self, o) except: return None def asFullObject(func): def wrap(self, *args, **kwargs): if not self.isFullObject(): self.reload() return func(self, *args, **kwargs) return wrap class Checks: def isLibraryItem(self): return "/library/metadata" in self.get('key', '') or ("/playlists/" in self.get('key', '') and self.get("type", "") == "playlist") def isVideoItem(self): return False def isMusicItem(self): return False def isOnlineItem(self): return self.isChannelItem() or self.isMyPlexItem() or self.isVevoItem() or self.isIvaItem() def isMyPlexItem(self): return self.container.server.TYPE == 'MYPLEXSERVER' or self.container.identifier == 'com.plexapp.plugins.myplex' def isChannelItem(self): identifier = self.getIdentifier() or "com.plexapp.plugins.library" return not self.isLibraryItem() and not self.isMyPlexItem() and identifier != "com.plexapp.plugins.library" def isVevoItem(self): return 'vevo://' in self.get('guid') def isIvaItem(self): return 'iva://' in self.get('guid') def isGracenoteCollection(self): return False def isIPhoto(self): return (self.title == "iPhoto" or self.container.title == "iPhoto" or (self.mediaType == "Image" or self.mediaType == "Movie")) def isDirectory(self): return self.name == "Directory" or self.name == "Playlist" def isPhotoOrDirectoryItem(self): return self.type == "photoalbum" # or self.isPhotoItem() def isMusicOrDirectoryItem(self): return self.type in ('artist', 'album', 'track') def isVideoOrDirectoryItem(self): return self.type in ('movie', 'show', 'episode') def isSettings(self): return False class PlexObject(object, Checks): def __init__(self, data, initpath=None, server=None, container=None): self.initpath = initpath self.key = None self.server = server self.container = container self.mediaChoice = None self.titleSort = PlexValue('') self.deleted = False self._reloaded = False if data is None: return self._setData(data) self.init(data) def _setData(self, data): if data is False: return self.name = data.tag for k, v in data.attrib.items(): setattr(self, k, PlexValue(v, self)) def __getattr__(self, attr): a = PlexValue('', self) a.NA = True try: setattr(self, attr, a) except AttributeError: util.LOG('Failed to set attribute: {0} ({1})'.format(attr, self.__class__)) return a def exists(self): # Used for media items - for others we just return True return True def get(self, attr, default=''): ret = self.__dict__.get(attr) return ret is not None and ret or PlexValue(default, self) def set(self, attr, value): setattr(self, attr, PlexValue(unicode(value), self)) def init(self, data): pass def isFullObject(self): return self.initpath is None or self.key is None or self.initpath == self.key def getAddress(self): return self.server.activeConnection.address @property def defaultTitle(self): return self.get('title') @property def defaultThumb(self): return self.__dict__.get('thumb') and self.thumb or PlexValue('', self) @property def defaultArt(self): return self.__dict__.get('art') and self.art or PlexValue('', self) def refresh(self): import requests self.server.query('%s/refresh' % self.key, method=requests.put) def reload(self, _soft=False, **kwargs): """ Reload the data for this object from PlexServer XML. """ if _soft and self._reloaded: return self try: if self.get('ratingKey'): data = self.server.query('/library/metadata/{0}'.format(self.ratingKey), params=kwargs) else: data = self.server.query(self.key, params=kwargs) self._reloaded = True except Exception, e: import traceback traceback.print_exc() util.ERROR(err=e) self.initpath = self.key return self self.initpath = self.key try: self._setData(data[0]) except IndexError: util.DEBUG_LOG('No data on reload: {0}'.format(self)) return self return self def softReload(self, **kwargs): return self.reload(_soft=True, **kwargs) def getLibrarySectionId(self): ID = self.get('librarySectionID') if not ID: ID = self.container.get("librarySectionID", '') return ID def getLibrarySectionTitle(self): title = self.get('librarySectionTitle') if not title: title = self.container.get("librarySectionTitle", '') if not title: lsid = self.getLibrarySectionId() if lsid: data = self.server.query('/library/sections/{0}'.format(lsid)) title = data.attrib.get('title1') if title: self.librarySectionTitle = title return title def getLibrarySectionType(self): type_ = self.get('librarySectionType') if not type_: type_ = self.container.get("librarySectionType", '') if not type_: lsid = self.getLibrarySectionId() if lsid: data = self.server.query('/library/sections/{0}'.format(lsid)) type_ = data.attrib.get('type') if type_: self.librarySectionTitle = type_ return type_ def getLibrarySectionUuid(self): uuid = self.get("uuid") or self.get("librarySectionUUID") if not uuid: uuid = self.container.get("librarySectionUUID", "") return uuid def _findLocation(self, data): elem = data.find('Location') if elem is not None: return elem.attrib.get('path') return None def _findPlayer(self, data): elem = data.find('Player') if elem is not None: from plexapi.client import Client return Client(self.server, elem) return None def _findTranscodeSession(self, data): elem = data.find('TranscodeSession') if elem is not None: from plexapi import media return media.TranscodeSession(self.server, elem) return None def _findUser(self, data): elem = data.find('User') if elem is not None: from plexapi.myplex import MyPlexUser return MyPlexUser(elem, self.initpath) return None def getAbsolutePath(self, attr): path = getattr(self, attr, None) if path is None: return None else: return self.container._getAbsolutePath(path) def _getAbsolutePath(self, path): if path.startswith('/'): return path elif "://" in path: return path else: return self.getAddress() + "/" + path def getParentPath(self, key): # Some containers have /children on its key while others (such as playlists) use /items path = self.getAbsolutePath(key) if path is None: return "" for suffix in ("/children", "/items"): path = path.replace(suffix, "") return path def getServer(self): return self.server def getTranscodeServer(self, localServerRequired=False, transcodeType=None): server = self.server # If the server is myPlex, try to use a different PMS for transcoding import myplexserver if server == myplexserver.MyPlexServer: fallbackServer = plexapp.SERVERMANAGER.getChannelServer() if fallbackServer: server = fallbackServer elif localServerRequired: return None return server @classmethod def deSerialize(cls, jstring): import plexserver obj = json.loads(jstring) server = plexserver.PlexServer.deSerialize(obj['server']) server.identifier = None ad = util.AttributeDict() ad.attrib = obj['obj'] ad.find = lambda x: None po = buildItem(server, ad, ad.initpath, container=server) return po def serialize(self, full=False): import json odict = {} if full: for k, v in self.__dict__.items(): if k not in ('server', 'container', 'media', 'initpath', '_data') and v: odict[k] = v else: odict['key'] = self.key odict['type'] = self.type odict['initpath'] = '/none' obj = {'obj': odict, 'server': self.server.serialize(full=full)} return json.dumps(obj, cls=JEncoder) class PlexContainer(PlexObject): def __init__(self, data, initpath=None, server=None, address=None): PlexObject.__init__(self, data, initpath, server) self.setAddress(address) def setAddress(self, address): if address != "/" and address.endswith("/"): self.address = address[:-1] else: self.address = address # TODO(schuyler): Do we need to make sure that we only hang onto the path here and not a full URL? if not self.address.startswith("/") and "node.plexapp.com" not in self.address: util.FATAL("Container address is not an expected path: {0}".format(address)) def getAbsolutePath(self, path): if path.startswith('/'): return path elif "://" in path: return path else: return self.address + "/" + path class PlexServerContainer(PlexContainer): def __init__(self, data, initpath=None, server=None, address=None): PlexContainer.__init__(self, data, initpath, server, address) import plexserver self.resources = [plexserver.PlexServer(elem) for elem in data] def __getitem__(self, idx): return self.resources[idx] def __iter__(self): for i in self.resources: yield i def __len__(self): return len(self.resources) class PlexItemList(object): def __init__(self, data, item_cls, tag, server=None, container=None): self._data = data self._itemClass = item_cls self._itemTag = tag self._server = server self._container = container self._items = None def __iter__(self): for i in self.items: yield i def __getitem__(self, idx): return self.items[idx] @property def items(self): if self._items is None: if self._data is not None: if self._server: self._items = [self._itemClass(elem, server=self._server, container=self._container) for elem in self._data if elem.tag == self._itemTag] else: self._items = [self._itemClass(elem) for elem in self._data if elem.tag == self._itemTag] else: self._items = [] return self._items def __call__(self, *args): return self.items def __len__(self): return len(self.items) def append(self, item): self.items.append(item) class PlexMediaItemList(PlexItemList): def __init__(self, data, item_cls, tag, initpath=None, server=None, media=None): PlexItemList.__init__(self, data, item_cls, tag, server) self._initpath = initpath self._media = media self._items = None @property def items(self): if self._items is None: if self._data is not None: self._items = [self._itemClass(elem, self._initpath, self._server, self._media) for elem in self._data if elem.tag == self._itemTag] else: self._items = [] return self._items def findItem(server, path, title): for elem in server.query(path): if elem.attrib.get('title').lower() == title.lower(): return buildItem(server, elem, path) raise exceptions.NotFound('Unable to find item: {0}'.format(title)) def buildItem(server, elem, initpath, bytag=False, container=None, tag_fallback=False): libtype = elem.tag if bytag else elem.attrib.get('type') if not libtype and tag_fallback: libtype = elem.tag if libtype in LIBRARY_TYPES: cls = LIBRARY_TYPES[libtype] return cls(elem, initpath=initpath, server=server, container=container) raise exceptions.UnknownType('Unknown library type: {0}'.format(libtype)) class ItemContainer(list): def __getattr__(self, attr): return getattr(self.container, attr) def init(self, container): self.container = container return self def listItems(server, path, libtype=None, watched=None, bytag=False, data=None, container=None): data = data if data is not None else server.query(path) container = container or PlexContainer(data, path, server, path) items = ItemContainer().init(container) for elem in data: if libtype and elem.attrib.get('type') != libtype: continue if watched is True and elem.attrib.get('viewCount', 0) == 0: continue if watched is False and elem.attrib.get('viewCount', 0) >= 1: continue try: items.append(buildItem(server, elem, path, bytag, container)) except exceptions.UnknownType: pass return items def searchType(libtype): searchtypesstrs = [str(k) for k in SEARCHTYPES.keys()] if libtype in SEARCHTYPES + searchtypesstrs: return libtype stype = SEARCHTYPES.get(libtype.lower()) if not stype: raise exceptions.NotFound('Unknown libtype: %s' % libtype) return stype