def sendCommand(self, command, proxy=None, **params): """ Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily send simple commands to the client. Returns an ElementTree object containing the response. Parameters: command (str): Command to be sent in for format '<controller>/<command>'. proxy (bool): Set True to proxy this command through the PlexServer. **params (dict): Additional GET parameters to include with the command. Raises: :class:`~plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability. """ command = command.strip('/') controller = command.split('/')[0] if controller not in self.protocolCapabilities: log.debug('Client %s doesnt support %s controller.' 'What your trying might not work' % (self.title, controller)) params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} proxy = self._proxyThroughServer if proxy is None else proxy if proxy: return self._server.query(key, headers=headers) return self.query(key, headers=headers)
def clients(self): """ Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """ items = [] cache_resource = None from plexapi.myplex import MyPlexResource for elem in self.query('/clients'): # Some shitty clients dont include a port.. port = elem.attrib.get('port') if port is None: log.debug( "%s didn't provide a port. Checking https://plex.tv/devices.xml" % elem.attrib.get('name')) data = cache_resource or self._server._session.get( 'https://plex.tv/devices.xml?X-Plex-Token=%s' % self.token) # noqa cache_resource = data resources = MyPlexResource(self, data) for resource in resources: if resource.clientIdentifier == elem.attrib.get( 'machineIdentifier'): for conn in resource.connection: if conn.local is True: port = conn.port break baseurl = 'http://%s:%s' % (elem.attrib['host'], port) items.append( PlexClient(baseurl=baseurl, server=self, data=elem, connect=False)) return items
def query(self, key, method=None, headers=None, timeout=None, **kwargs): """ Main method used to handle HTTPS requests to the Plex server. This method helps by encoding the response to utf-8 and parsing the returned XML into and ElementTree object. Returns None if no data exists in the response. """ url = self.url(key) method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s', method.__name__.upper(), url) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201, 204): codename = codes.get(response.status_code)[0] errtext = response.text.replace('\n', ' ') message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext) if response.status_code == 401: raise Unauthorized(message) elif response.status_code == 404: raise NotFound(message) else: raise BadRequest(message) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def download(url, filename=None, savepath=None, session=None, chunksize=4024, unpack=False, mocked=False): """ Helper to download a thumb, videofile or other media item. Returns the local path to the downloaded file. Parameters: url (str): URL where the content be reached. filename (str): Filename of the downloaded file, default None. savepath (str): Defaults to current working dir. chunksize (int): What chunksize read/write at the time. mocked (bool): Helper to do evertything except write the file. unpack (bool): Unpack the zip file Example: >>> download(a_episode.getStreamURL(), a_episode.location) /path/to/file """ from plexapi import log # fetch the data to be saved session = session or requests.Session() response = session.get(url, stream=True) # make sure the savepath directory exists savepath = savepath or os.getcwd() compat.makedirs(savepath, exist_ok=True) # try getting filename from header if not specified in arguments (used for logs, db) if not filename and response.headers.get('Content-Disposition'): filename = re.findall(r'filename=\"(.+)\"', response.headers.get('Content-Disposition')) filename = filename[0] if filename[0] else None filename = os.path.basename(filename) fullpath = os.path.join(savepath, filename) # append file.ext from content-type if not already there extension = os.path.splitext(fullpath)[-1] if not extension: contenttype = response.headers.get('content-type') if contenttype and 'image' in contenttype: fullpath += contenttype.split('/')[1] # check this is a mocked download (testing) if mocked: log.debug('Mocked download %s', fullpath) return fullpath # save the file to disk log.info('Downloading: %s', fullpath) with open(fullpath, 'wb') as handle: for chunk in response.iter_content(chunk_size=chunksize): handle.write(chunk) # check we want to unzip the contents if fullpath.endswith('zip') and unpack: with zipfile.ZipFile(fullpath, 'r') as handle: handle.extractall(savepath) # finished; return fillpath return fullpath
def _onMessage(self, ws, message): """ Called when websocket message is recieved. """ try: data = json.loads(message)['NotificationContainer'] log.debug('Alert: %s %s %s', *data) if self._callback: self._callback(data) except Exception as err: # pragma: no cover log.error('AlertListener Msg Error: %s', err)
def query(self, url, method=None, headers=None, timeout=None, **kwargs): method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s %s', method.__name__.upper(), url, kwargs.get('json', '')) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201, 204): # pragma: no cover codename = codes.get(response.status_code)[0] errtext = response.text.replace('\n', ' ') raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def sendCommand(self, command, proxy=None, **params): """ Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily send simple commands to the client. Returns an ElementTree object containing the response. Parameters: command (str): Command to be sent in for format '<controller>/<command>'. proxy (bool): Set True to proxy this command through the PlexServer. **params (dict): Additional GET parameters to include with the command. Raises: :class:`plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability. """ command = command.strip('/') controller = command.split('/')[0] headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} if controller not in self.protocolCapabilities: log.debug('Client %s doesnt support %s controller.' 'What your trying might not work' % (self.title, controller)) proxy = self._proxyThroughServer if proxy is None else proxy query = self._server.query if proxy else self.query # Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244 t = time.time() if command == 'timeline/poll': self._last_call = t elif t - self._last_call >= 80 and self.product in ( 'ptp', 'Plex Media Player'): self._last_call = t self.sendCommand(ClientTimeline.key, wait=0) params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) log.debug('Plexapi %s command %s %s ' ' key sent: %s' % (self.product, self.title, controller, key)) try: return query(key, headers=headers) except ElementTree.ParseError: # Workaround for players which don't return valid XML on successful commands # - Plexamp, Plex for Android: `b'OK'` # - Plex for Samsung: `b'<?xml version="1.0"?><Response code="200" status="OK">'` if self.product in ( 'Plexamp', 'Plex for Android (TV)', 'Plex for Android (Mobile)', 'Plex for Samsung', ): return raise
def query(self, url, method=None, headers=None, timeout=None, **kwargs): method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s %s', method.__name__.upper(), url, kwargs.get('json', '')) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201, 204): # pragma: no cover codename = codes.get(response.status_code)[0] errtext = response.text.replace('\n', ' ') log.warning('BadRequest (%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) raise BadRequest('(%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def _allowMediaDeletion(self, toggle=False): """ Toggle allowMediaDeletion. Parameters: toggle (bool): True enables Media Deletion False or None disable Media Deletion (Default) """ if self.allowMediaDeletion and toggle is False: log.debug( 'Plex is currently allowed to delete media. Toggling off.') elif self.allowMediaDeletion and toggle is True: log.debug( 'Plex is currently allowed to delete media. Toggle set to allow, exiting.' ) raise BadRequest( 'Plex is currently allowed to delete media. Toggle set to allow, exiting.' ) elif self.allowMediaDeletion is None and toggle is True: log.debug( 'Plex is currently not allowed to delete media. Toggle set to allow.' ) else: log.debug( 'Plex is currently not allowed to delete media. Toggle set to not allow, exiting.' ) raise BadRequest( 'Plex is currently not allowed to delete media. Toggle set to not allow, exiting.' ) value = 1 if toggle is True else 0 return self.query('/:/prefs?allowMediaDeletion=%s' % value, self._session.put)
def __getattribute__(self, attr): # Dragons inside.. :-/ value = super(PlexPartialObject, self).__getattribute__(attr) # Check a few cases where we dont want to reload if attr == 'key' or attr.startswith('_'): return value if value not in (None, []): return value if self.isFullObject(): return value # Log the reload. clsname = self.__class__.__name__ title = self.__dict__.get('title', self.__dict__.get('name')) objname = "%s '%s'" % (clsname, title) if title else clsname log.debug("Reloading %s for attr '%s'" % (objname, attr)) # Reload and return the value self.reload() return super(PlexPartialObject, self).__getattribute__(attr)
def _onMessage(self, *args): """ Called when websocket message is received. In earlier releases, websocket-client returned a tuple of two parameters: a websocket.app.WebSocketApp object and the message as a STR. Current releases appear to only return the message. We are assuming the last argument in the tuple is the message. This is to support compatibility with current and previous releases of websocket-client. """ message = args[-1] try: data = json.loads(message)['NotificationContainer'] log.debug('Alert: %s %s %s', *data) if self._callback: self._callback(data) except Exception as err: # pragma: no cover log.error('AlertListener Msg Error: %s', err)
def query(self, url, method=None, headers=None, **kwargs): method = method or self._session.get delim = '&' if '?' in url else '?' url = '%s%sX-Plex-Token=%s' % (url, delim, self._token) log.debug('%s %s', method.__name__.upper(), url) allheaders = BASE_HEADERS.copy() allheaders.update(headers or {}) response = method(url, headers=allheaders, timeout=TIMEOUT, **kwargs) if response.status_code not in (200, 201): codename = codes.get(response.status_code)[0] log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def query(self, path, method=None, headers=None, timeout=None, **kwargs): """ Main method used to handle HTTPS requests to the Plex client. This method helps by encoding the response to utf-8 and parsing the returned XML into and ElementTree object. Returns None if no data exists in the response. """ url = self.url(path) method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s', method.__name__.upper(), url) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201): codename = codes.get(response.status_code)[0] log.warn('BadRequest (%s) %s %s' % (response.status_code, codename, response.url)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def query(self, key, method=None, headers=None, timeout=None, **kwargs): """ Main method used to handle HTTPS requests to the Plex server. This method helps by encoding the response to utf-8 and parsing the returned XML into and ElementTree object. Returns None if no data exists in the response. """ url = self.url(key) method = method or self._session.get timeout = timeout or TIMEOUT log.debug('%s %s', method.__name__.upper(), url) headers = self._headers(**headers or {}) response = method(url, headers=headers, timeout=timeout, **kwargs) if response.status_code not in (200, 201): codename = codes.get(response.status_code)[0] errtext = response.text.replace('\n', ' ') log.warning('BadRequest (%s) %s %s; %s' % (response.status_code, codename, response.url, errtext)) raise BadRequest('(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data.strip() else None
def __getattribute__(self, attr): # Dragons inside.. :-/ value = super(PlexPartialObject, self).__getattribute__(attr) # Check a few cases where we dont want to reload if attr in _DONT_RELOAD_FOR_KEYS: return value if attr in _DONT_OVERWRITE_SESSION_KEYS: return value if attr in USER_DONT_RELOAD_FOR_KEYS: return value if attr.startswith('_'): return value if value not in (None, []): return value if self.isFullObject(): return value # Log the reload. clsname = self.__class__.__name__ title = self.__dict__.get('title', self.__dict__.get('name')) objname = "%s '%s'" % (clsname, title) if title else clsname log.debug("Reloading %s for attr '%s'", objname, attr) # Reload and return the value self._reload(_autoReload=True) return super(PlexPartialObject, self).__getattribute__(attr)
def _getSectionIds(self, server, sections): """ Converts a list of section objects or names to sectionIds needed for library sharing. """ if not sections: return [] # Get a list of all section ids for looking up each section. allSectionIds = {} machineIdentifier = server.machineIdentifier if isinstance(server, PlexServer) else server url = self.PLEXSERVERS.replace('{machineId}', machineIdentifier) data = self.query(url, self._session.get) for elem in data[0]: allSectionIds[elem.attrib.get('id', '').lower()] = elem.attrib.get('id') allSectionIds[elem.attrib.get('title', '').lower()] = elem.attrib.get('id') allSectionIds[elem.attrib.get('key', '').lower()] = elem.attrib.get('id') log.debug(allSectionIds) # Convert passed in section items to section ids from above lookup sectionIds = [] for section in sections: sectionKey = section.key if isinstance(section, LibrarySection) else section sectionIds.append(allSectionIds[sectionKey.lower()]) return sectionIds
def sendCommand(self, command, proxy=None, **params): """ Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily send simple commands to the client. Returns an ElementTree object containing the response. Parameters: command (str): Command to be sent in for format '<controller>/<command>'. proxy (bool): Set True to proxy this command through the PlexServer. **params (dict): Additional GET parameters to include with the command. Raises: :class:`plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability. """ command = command.strip('/') controller = command.split('/')[0] headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} if controller not in self.protocolCapabilities: log.debug('Client %s doesnt support %s controller.' 'What your trying might not work' % (self.title, controller)) # Workaround for ptp. See https://github.com/pkkid/python-plexapi/issues/244 t = time.time() if t - self._last_call >= 80 and self.product in ('ptp', 'Plex Media Player'): url = '/player/timeline/poll?wait=0&commandID=%s' % self._nextCommandId( ) if proxy: self._server.query(url, headers=headers) else: self.query(url, headers=headers) self._last_call = t params['commandID'] = self._nextCommandId() key = '/player/%s%s' % (command, utils.joinArgs(params)) proxy = self._proxyThroughServer if proxy is None else proxy if proxy: return self._server.query(key, headers=headers) return self.query(key, headers=headers)
def download(url, token, filename=None, savepath=None, session=None, chunksize=4024, unpack=False, mocked=False, showstatus=False): """ Helper to download a thumb, videofile or other media item. Returns the local path to the downloaded file. Parameters: url (str): URL where the content be reached. token (str): Plex auth token to include in headers. filename (str): Filename of the downloaded file, default None. savepath (str): Defaults to current working dir. chunksize (int): What chunksize read/write at the time. mocked (bool): Helper to do evertything except write the file. unpack (bool): Unpack the zip file. showstatus(bool): Display a progressbar. Example: >>> download(a_episode.getStreamURL(), a_episode.location) /path/to/file """ from plexapi import log # fetch the data to be saved session = session or requests.Session() headers = {'X-Plex-Token': token} response = session.get(url, headers=headers, stream=True) # make sure the savepath directory exists savepath = savepath or os.getcwd() compat.makedirs(savepath, exist_ok=True) # try getting filename from header if not specified in arguments (used for logs, db) if not filename and response.headers.get('Content-Disposition'): filename = re.findall(r'filename=\"(.+)\"', response.headers.get('Content-Disposition')) filename = filename[0] if filename[0] else None filename = os.path.basename(filename) fullpath = os.path.join(savepath, filename) # append file.ext from content-type if not already there extension = os.path.splitext(fullpath)[-1] if not extension: contenttype = response.headers.get('content-type') if contenttype and 'image' in contenttype: fullpath += contenttype.split('/')[1] # check this is a mocked download (testing) if mocked: log.debug('Mocked download %s', fullpath) return fullpath # save the file to disk log.info('Downloading: %s', fullpath) if showstatus: # pragma: no cover total = int(response.headers.get('content-length', 0)) bar = tqdm(unit='B', unit_scale=True, total=total, desc=filename) with open(fullpath, 'wb') as handle: for chunk in response.iter_content(chunk_size=chunksize): handle.write(chunk) if showstatus: bar.update(len(chunk)) if showstatus: # pragma: no cover bar.close() # check we want to unzip the contents if fullpath.endswith('zip') and unpack: with zipfile.ZipFile(fullpath, 'r') as handle: handle.extractall(savepath) return fullpath