diff --git a/.gitignore b/.gitignore index 6b7703a0..3f426050 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ lib/ pip-selfcheck.json pyvenv.cfg MANIFEST + + +# path for the test lib. +tools/plex \ No newline at end of file diff --git a/plexapi/library.py b/plexapi/library.py index d77b47a3..0de9a604 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -1570,3 +1570,54 @@ class Collections(PlexPartialObject): # def edit(self, **kwargs): # TODO + + +@utils.registerPlexObject +class Path(PlexObject): + """ Represents a single directory Path. + + Attributes: + TAG (str): 'Path' + + home (bool): True if the path is the home directory + key (str): API URL (/services/browse/) + network (bool): True if path is a network location + path (str): Full path to folder + title (str): Folder name + """ + TAG = 'Path' + + def _loadData(self, data): + self.home = utils.cast(bool, data.attrib.get('home')) + self.key = data.attrib.get('key') + self.network = utils.cast(bool, data.attrib.get('network')) + self.path = data.attrib.get('path') + self.title = data.attrib.get('title') + + def browse(self, includeFiles=True): + """ Alias for :func:`~plexapi.server.PlexServer.browse`. """ + return self._server.browse(self, includeFiles) + + def walk(self): + """ Alias for :func:`~plexapi.server.PlexServer.walk`. """ + for path, paths, files in self._server.walk(self): + yield path, paths, files + + +@utils.registerPlexObject +class File(PlexObject): + """ Represents a single File. + + Attributes: + TAG (str): 'File' + + key (str): API URL (/services/browse/) + path (str): Full path to file + title (str): File name + """ + TAG = 'File' + + def _loadData(self, data): + self.key = data.attrib.get('key') + self.path = data.attrib.get('path') + self.title = data.attrib.get('title') diff --git a/plexapi/server.py b/plexapi/server.py index a9439bda..cfe3bb31 100644 --- a/plexapi/server.py +++ b/plexapi/server.py @@ -15,7 +15,7 @@ from plexapi.alert import AlertListener from plexapi.base import PlexObject from plexapi.client import PlexClient from plexapi.exceptions import BadRequest, NotFound, Unauthorized -from plexapi.library import Hub, Library +from plexapi.library import Hub, Library, Path, File from plexapi.media import Conversion, Optimized from plexapi.playlist import Playlist from plexapi.playqueue import PlayQueue @@ -247,6 +247,53 @@ class PlexServer(PlexObject): log.warning('Unable to fetch client ports from myPlex: %s', err) return ports + def browse(self, path=None, includeFiles=True): + """ Browse the system file path using the Plex API. + Returns list of :class:`~plexapi.library.Path` and :class:`~plexapi.library.File` objects. + + Parameters: + path (:class:`~plexapi.library.Path` or str, optional): Full path to browse. + includeFiles (bool): True to include files when browsing (Default). + False to only return folders. + """ + if isinstance(path, Path): + key = path.key + elif path is not None: + base64path = utils.base64str(path) + key = '/services/browse/%s' % base64path + else: + key = '/services/browse' + if includeFiles: + key += '?includeFiles=1' + return self.fetchItems(key) + + def walk(self, path=None): + """ Walk the system file tree using the Plex API similar to `os.walk`. + Yields a 3-tuple `(path, paths, files)` where + `path` is a string of the directory path, + `paths` is a list of :class:`~plexapi.library.Path` objects, and + `files` is a list of :class:`~plexapi.library.File` objects. + + Parameters: + path (:class:`~plexapi.library.Path` or str, optional): Full path to walk. + """ + paths = [] + files = [] + for item in self.browse(path): + if isinstance(item, Path): + paths.append(item) + elif isinstance(item, File): + files.append(item) + + if isinstance(path, Path): + path = path.path + + yield path or '', paths, files + + for _path in paths: + for path, paths, files in self.walk(_path): + yield path, paths, files + def clients(self): """ Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """ items = [] diff --git a/plexapi/utils.py b/plexapi/utils.py index 6512b7f3..08000d58 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import base64 import logging import os import re @@ -411,3 +412,7 @@ def getAgentIdentifier(section, agent): agents += identifiers raise NotFound('Couldnt find "%s" in agents list (%s)' % (agent, ', '.join(agents))) + + +def base64str(text): + return base64.b64encode(text.encode('utf-8')).decode('utf-8') diff --git a/tests/test_server.py b/tests/test_server.py index 73b094b6..03d35b21 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -287,6 +287,23 @@ def test_server_downloadDatabases(tmpdir, plex): assert len(tmpdir.listdir()) > 1 +def test_server_browse(plex, movies): + movies_path = movies.locations[0] + # browse root + paths = plex.browse() + assert len(paths) + # browse the path of the movie library + paths = plex.browse(movies_path) + assert len(paths) + # browse the path of the movie library without files + paths = plex.browse(movies_path, includeFiles=False) + assert not len([f for f in paths if f.TAG == 'File']) + # walk the path of the movie library + for path, paths, files in plex.walk(movies_path): + assert path.startswith(movies_path) + assert len(paths) or len(files) + + def test_server_allowMediaDeletion(account): plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken) # Check server current allowMediaDeletion setting