def stop(self): """ Stop the AlertListener thread. Once the notifier is stopped, it cannot be directly started again. You must call :func:`~plexapi.server.PlexServer.startAlertListener` from a PlexServer instance. """ log.info('Stopping AlertListener.') self._ws.close()
def run(self): # create the websocket connection url = self._server.url(self.key, includeToken=True).replace('http', 'ws') log.info('Starting AlertListener: %s', url) self._ws = websocket.WebSocketApp(url, on_message=self._onMessage, on_error=self._onError) self._ws.run_forever()
def connect(self, ssl=None): # Only check non-local connections unless we own the resource connections = sorted(self.connections, key=lambda c: c.local, reverse=True) if not self.owned: connections = [c for c in connections if c.local is False] # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. threads, results = [], [] for testssl, attr in self.SSLTESTS: if ssl in [None, testssl]: for i in range(len(connections)): uri = getattr(connections[i], attr) args = (uri, results, len(results)) results.append(None) threads.append(Thread(target=self._connect, args=args)) threads[-1].start() for thread in threads: thread.join() # At this point we have a list of result tuples containing (uri, PlexServer) # or (uri, None) in the case a connection could not be established. for uri, result in results: log.info('Testing connection: %s %s', uri, 'OK' if result else 'ERR') results = list(filter(None, [r[1] for r in results if r])) if not results: raise NotFound('Unable to connect to resource: %s' % self.name) log.info('Connecting to server: %s', results[0]) return results[0]
def connect(self, ssl=None, timeout=None): """ Returns a new :class:`~server.PlexServer` or :class:`~client.PlexClient` object. Often times there is more than one address specified for a server or client. This function will prioritize local connections before remote and HTTPS before HTTP. After trying to connect to all available addresses for this resource and assuming at least one connection was successful, the PlexServer object is built and returned. Parameters: ssl (optional): Set True to only connect to HTTPS connections. Set False to only connect to HTTP connections. Set None (default) to connect to any HTTP or HTTPS connection. Raises: :class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource. """ # Sort connections from (https, local) to (http, remote) # Only check non-local connections unless we own the resource connections = sorted(self.connections, key=lambda c: c.local, reverse=True) owned_or_unowned_non_local = lambda x: self.owned or (not self.owned and not x.local) https = [c.uri for c in connections if owned_or_unowned_non_local(c)] http = [c.httpuri for c in connections if owned_or_unowned_non_local(c)] cls = PlexServer if 'server' in self.provides else PlexClient # Force ssl, no ssl, or any (default) if ssl is True: connections = https elif ssl is False: connections = http else: connections = https + http # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. listargs = [[cls, url, self.accessToken, timeout] for url in connections] log.info('Testing %s resource connections..', len(listargs)) results = utils.threaded(_connect, listargs) return _chooseConnection('Resource', self.name, results)
def stop(self): """ Stop the AlertListener thread. Once the notifier is stopped, it cannot be diractly started again. You must call :func:`plexapi.server.PlexServer.startAlertListener()` from a PlexServer instance. """ log.info('Stopping AlertListener.') self._ws.close()
def connect(self): """ Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than one address specified for a server or client. After trying to connect to all available addresses for this client and assuming at least one connection was successful, the PlexClient object is built and returned. Raises: :class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device. """ # Try connecting to all known clients in parellel, but # only return the first server (in order) that provides a response. listargs = [[c] for c in self.connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) # or (url, token, None) in the case a connection could not be established. for url, token, result in results: okerr = 'OK' if result else 'ERR' log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr) results = [r[2] for r in results if r and r[2] is not None] if not results: raise NotFound('Unable to connect to client: %s' % self.name) log.info('Connecting to client: %s?X-Plex-Token=%s', results[0]._baseurl, results[0]._token) return results[0]
def query(self, path, method=None, headers=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. Parameters: path (str): Relative path to query on the server api (ex: '/search?query=HELLO') method (func): requests.method to use for this query (request.get or requests.put; defaults to get) headers (dict): Optionally include additional headers for this request. **kwargs (dict): Optionally include additional kwargs for in the specified reuqest method. These kwargs are simply passed through to method(). Raises: :class:`~plexapi.exceptions.BadRequest`: Raised when response is not in (200, 201). """ url = self.url(path) method = method or self.session.get log.info('%s %s', method.__name__.upper(), url) h = self.headers().copy() if headers: h.update(headers) response = method(url, headers=h, timeout=TIMEOUT, **kwargs) #print(response.url) if response.status_code not in [200, 201]: # pragma: no cover codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data else None
def setWebhooks(self, urls): log.info('Setting webhooks: %s' % urls) data = self.query(self.WEBHOOKS, self._session.post, data={'urls[]': urls}) self._webhooks = self.listAttrs(data, 'url', etag='webhook') return self._webhooks
def fetch_servers(cls, token): headers = plexapi.BASE_HEADERS headers['X-Plex-Token'] = token log.info('GET %s?X-Plex-Token=%s', cls.SERVERS, token) response = requests.get(cls.SERVERS, headers=headers, timeout=TIMEOUT) data = ElementTree.fromstring(response.text.encode('utf8')) return [MyPlexServer(elem) for elem in data]
def connect(self, ssl=None): # Only check non-local connections unless we own the resource connections = sorted(self.connections, key=lambda c:c.local, reverse=True) if not self.owned: connections = [c for c in connections if c.local is False] # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. threads, results = [], [] for testssl, attr in self.SSLTESTS: if ssl in [None, testssl]: for i in range(len(connections)): uri = getattr(connections[i], attr) args = (uri, results, len(results)) results.append(None) threads.append(Thread(target=self._connect, args=args)) threads[-1].start() for thread in threads: thread.join() # At this point we have a list of result tuples containing (uri, PlexServer) # or (uri, None) in the case a connection could not be established. for uri, result in results: log.info('Testing connection: %s %s', uri, 'OK' if result else 'ERR') results = list(filter(None, [r[1] for r in results if r])) if not results: raise NotFound('Unable to connect to resource: %s' % self.name) log.info('Connecting to server: %s', results[0]) return results[0]
def fetch_resources(cls, token): headers = plexapi.BASE_HEADERS headers['X-Plex-Token'] = token log.info('GET %s?X-Plex-Token=%s', cls.DEVICES, token) response = requests.get(cls.DEVICES, headers=headers, timeout=TIMEOUT) data = ElementTree.fromstring(response.text.encode('utf8')) return [MyPlexDevice(elem) for elem in data]
def _listItems(url, token, cls): headers = plexapi.BASE_HEADERS headers['X-Plex-Token'] = token log.info('GET %s?X-Plex-Token=%s', url, token) response = requests.get(url, headers=headers, timeout=TIMEOUT) data = ElementTree.fromstring(response.text.encode('utf8')) return [cls(elem) for elem in data]
def query(self, path, method=None, headers=None, **kwargs): """Used to fetch relative paths to pms. Args: path (str): Relative path method (None, optional): requests.post etc headers (None, optional): Set headers manually **kwargs (TYPE): Passord to the http request used for filter, sorting. Returns: Element Raises: BadRequest: Http error and code """ url = self.url(path) method = method or self.session.get log.info('%s %s', method.__name__.upper(), url) headers = dict(self.headers(), **(headers or {})) # remove hack response = method(url, headers=headers, timeout=TIMEOUT, **kwargs) if response.status_code not in [200, 201]: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data else None
def run(self): # create the websocket connection url = self._server.url(self.key).replace('http', 'ws') log.info('Starting AlertListener: %s', url) self._ws = websocket.WebSocketApp(url, on_message=self._onMessage, on_error=self._onError) self._ws.run_forever()
def _listItems(url, token, cls): """ Builds list of classes from a XML response. """ headers = plexapi.BASE_HEADERS headers['X-Plex-Token'] = token log.info('GET %s?X-Plex-Token=%s', url, token) response = requests.get(url, headers=headers, timeout=TIMEOUT) data = ElementTree.fromstring(response.text.encode('utf8')) return [cls(elem) for elem in data]
def sendClientCommand(self, command, args=None): url = '%s%s' % (self.url(command), utils.joinArgs(args)) log.info('GET %s', url) response = requests.get(url, timeout=TIMEOUT) if response.status_code != requests.codes.ok: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data else None
def query(self, path, method=None, **kwargs): url = self.url(path) method = method or self.session.get log.info('%s %s', method.__name__.upper(), url) response = method(url, headers=self.headers(), timeout=TIMEOUT, **kwargs) if response.status_code not in [200, 201]: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data else None
def query(self, path, method=requests.get): global TOTAL_QUERIES TOTAL_QUERIES += 1 url = self.url(path) log.info('%s %s', method.__name__.upper(), url) response = method(url, headers=self.headers(), timeout=TIMEOUT) if response.status_code not in [200, 201]: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') return ElementTree.fromstring(data) if data else None
def _chooseConnection(ctype, name, results): """ Chooses the first (best) connection from the given _connect results. """ # At this point we have a list of result tuples containing (url, token, PlexServer, runtime) # or (url, token, None, runtime) in the case a connection could not be established. for url, token, result, runtime in results: okerr = 'OK' if result else 'ERR' log.info('%s connection %s (%ss): %s?X-Plex-Token=%s', ctype, okerr, runtime, url, token) results = [r[2] for r in results if r and r[2] is not None] if results: log.info('Connecting to %s: %s?X-Plex-Token=%s', ctype, results[0]._baseurl, results[0]._token) return results[0] raise NotFound('Unable to connect to %s: %s' % (ctype.lower(), name))
def _signin(self, username, password): auth = (username, password) log.info('POST %s', self.SIGNIN) response = requests.post(self.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT) if response.status_code != requests.codes.created: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) self.response = response data = response.text.encode('utf8') return ElementTree.fromstring(data)
def signin(cls, username, password): if 'X-Plex-Token' in plexapi.BASE_HEADERS: del plexapi.BASE_HEADERS['X-Plex-Token'] auth = (username, password) log.info('POST %s', cls.SIGNIN) response = requests.post(cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT) if response.status_code != requests.codes.created: codename = codes.get(response.status_code)[0] if response.status_code == 401: raise Unauthorized('(%s) %s' % (response.status_code, codename)) raise BadRequest('(%s) %s' % (response.status_code, codename)) data = ElementTree.fromstring(response.text.encode('utf8')) return cls(data, cls.SIGNIN)
def connect(self, timeout=None): """ Returns a new :class:`~plexapi.client.PlexClient` or :class:`~plexapi.server.PlexServer` Sometimes there is more than one address specified for a server or client. After trying to connect to all available addresses for this client and assuming at least one connection was successful, the PlexClient object is built and returned. Raises: :class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device. """ cls = PlexServer if 'server' in self.provides else PlexClient listargs = [[cls, url, self.token, timeout] for url in self.connections] log.info('Testing %s device connections..', len(listargs)) results = utils.threaded(_connect, listargs) return _chooseConnection('Device', self.name, results)
def run(self): try: import websocket except ImportError: log.warning("Can't use the AlertListener without websocket") return # create the websocket connection url = self._server.url(self.key, includeToken=True).replace('http', 'ws') log.info('Starting AlertListener: %s', url) self._ws = websocket.WebSocketApp(url, on_message=self._onMessage, on_error=self._onError) self._ws.run_forever()
def connect(self, timeout=None): """ Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than one address specified for a server or client. After trying to connect to all available addresses for this client and assuming at least one connection was successful, the PlexClient object is built and returned. Raises: :class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device. """ listargs = [[PlexClient, url, self.token, timeout] for url in self.connections] log.info('Testing %s device connections..', len(listargs)) results = utils.threaded(_connect, listargs) _chooseConnection('Device', self.name, results)
def connect(self, ssl=None): # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. listargs = [[c] for c in self.connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) # or (url, token, None) in the case a connection could not be established. for url, token, result in results: okerr = 'OK' if result else 'ERR' log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr) results = list(filter(None, [r[2] for r in results if r])) if not results: raise NotFound('Unable to connect to resource: %s' % self.name) log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token) return results[0]
def save(self): """ Save any outstanding settnig changes to the :class:`~plexapi.server.PlexServer`. This performs a full reload() of Settings after complete. """ params = {} for setting in self.all(): if setting._setValue: log.info('Saving PlexServer setting %s = %s' % (setting.id, setting._setValue)) params[setting.id] = quote(setting._setValue) if not params: raise BadRequest('No setting have been modified.') querystr = '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) url = '%s?%s' % (self.key, querystr) self._server.query(url, self._server._session.put) self.reload()
def sendCommand(self, command, args=None): url = '%s%s' % (self.url(command), utils.joinArgs(args)) log.info('GET %s', url) headers = plexapi.BASE_HEADERS headers['X-Plex-Target-Client-Identifier'] = self.clientIdentifier response = requests.get(url, headers=headers, timeout=TIMEOUT) if response.status_code != requests.codes.ok: codename = codes.get(response.status_code)[0] raise BadRequest('(%s) %s' % (response.status_code, codename)) data = response.text.encode('utf8') if data: try: return ElementTree.fromstring(data) except: pass return None
def _listItems(url, token, cls): """Helper that builds list of classes from a XML response. Args: url (str): Description token (str): Description cls (class): Class to initate Returns: List: of classes """ headers = plexapi.BASE_HEADERS headers['X-Plex-Token'] = token log.info('GET %s?X-Plex-Token=%s', url, token) response = requests.get(url, headers=headers, timeout=TIMEOUT) data = ElementTree.fromstring(response.text.encode('utf8')) return [cls(elem) for elem in data]
def connect(self, ssl=None): """ Returns a new :class:`~server.PlexServer` object. Often times there is more than one address specified for a server or client. This function will prioritize local connections before remote and HTTPS before HTTP. After trying to connect to all available addresses for this resource and assuming at least one connection was successful, the PlexServer object is built and returned. Parameters: ssl (optional): Set True to only connect to HTTPS connections. Set False to only connect to HTTP connections. Set None (default) to connect to any HTTP or HTTPS connection. Raises: :class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource. """ # Sort connections from (https, local) to (http, remote) # Only check non-local connections unless we own the resource forcelocal = lambda c: self.owned or c.local connections = sorted(self.connections, key=lambda c: c.local, reverse=True) https = [c.uri for c in self.connections if forcelocal(c)] http = [c.httpuri for c in self.connections if forcelocal(c)] # Force ssl, no ssl, or any (default) if ssl is True: connections = https elif ssl is False: connections = http else: connections = https + http # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. listargs = [[c] for c in connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) # or (url, token, None) in the case a connection could not be # established. for url, token, result in results: okerr = 'OK' if result else 'ERR' log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr) results = [r[2] for r in results if r and r[2] is not None] if not results: raise NotFound('Unable to connect to resource: %s' % self.name) log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token) return results[0]
def connect(self): # Try connecting to all known addresses in parellel to save time, but # only return the first server (in order) that provides a response. addresses = [self.address] if self.owned: addresses = self.localAddresses + [self.address] threads = [None] * len(addresses) results = [None] * len(addresses) for i in range(len(addresses)): args = (addresses[i], results, i) threads[i] = Thread(target=self._connect, args=args) threads[i].start() for thread in threads: thread.join() results = list(filter(None, results)) if results: log.info('Connecting to server: %s', results[0]) return results[0] raise NotFound('Unable to connect to server: %s' % self.name)
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.info(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 connect(self, ssl=None): """Connect. Args: ssl (None, optional): Use ssl. Returns: class: Plexserver Raises: NotFound: Unable to connect to resource: name """ # Sort connections from (https, local) to (http, remote) # Only check non-local connections unless we own the resource forcelocal = lambda c: self.owned or c.local connections = sorted(self.connections, key=lambda c: c.local, reverse=True) https = [c.uri for c in self.connections if forcelocal(c)] http = [c.httpuri for c in self.connections if forcelocal(c)] connections = https + http # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. listargs = [[c] for c in connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) # or (url, token, None) in the case a connection could not be # established. for url, token, result in results: okerr = 'OK' if result else 'ERR' log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr) results = [r[2] for r in results if r and r is not None] if not results: raise NotFound('Unable to connect to resource: %s' % self.name) log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token) return results[0]
def toDatetime(value, format=None): """ Returns a datetime object from the specified value. Parameters: value (str): value to return as a datetime format (str): Format to pass strftime (optional; if value is a str). """ if value and value is not None: if format: try: value = datetime.strptime(value, format) except ValueError: log.info('Failed to parse %s to datetime, defaulting to None', value) return None else: # https://bugs.python.org/issue30684 # And platform support for before epoch seems to be flaky. # TODO check for others errors too. if int(value) <= 0: value = 86400 value = datetime.fromtimestamp(int(value)) return value
def connect(self, ssl=None): # Sort connections from (https, local) to (http, remote) # Only check non-local connections unless we own the resource forcelocal = lambda c: self.owned or c.local connections = sorted(self.connections, key=lambda c:c.local, reverse=True) https = [c.uri for c in self.connections if forcelocal(c)] http = [c.httpuri for c in self.connections if forcelocal(c)] connections = https + http # Try connecting to all known resource connections in parellel, but # only return the first server (in order) that provides a response. listargs = [[c] for c in connections] results = utils.threaded(self._connect, listargs) # At this point we have a list of result tuples containing (url, token, PlexServer) # or (url, token, None) in the case a connection could not be established. for url, token, result in results: okerr = 'OK' if result else 'ERR' log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr) results = list(filter(None, [r[2] for r in results if r])) if not results: raise NotFound('Unable to connect to resource: %s' % self.name) log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token) return results[0]
def _findResource(resources, search, port=32400): """ Searches server.name """ search = search.lower() log.info('Looking for server: %s', search) for server in resources: if search == server.name.lower(): log.info('Server found: %s', server) return server log.info('Unable to find server: %s', search) raise NotFound('Unable to find server: %s' % search)
def _findServer(servers, search, port=32400): """ Searches server.name, server.sourceTitle and server.host:server.port """ search = search.lower() ipaddr = addrToIP(search) log.info('Looking for server: %s (host: %s:%s)', search, ipaddr, port) for server in servers: serverName = server.name.lower() if server.name else 'NA' sourceTitle = server.sourceTitle.lower() if server.sourceTitle else 'NA' if (search in [serverName, sourceTitle]) or (server.host == ipaddr and server.port == port): log.info('Server found: %s', server) return server log.info('Unable to find server: %s (host: %s:%s)', search, ipaddr, port) raise NotFound('Unable to find server: %s (host: %s:%s)' % (search, ipaddr, port))
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