Skip to content

Commit ae59620

Browse files
authored
Merge pull request pushingkarmaorg#601 from JonnyWong16/server_browse
Add ability to browse and walk the Plex server system file directories
2 parents b623b43 + 33f7aa4 commit ae59620

File tree

5 files changed

+125
-1
lines changed

5 files changed

+125
-1
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ lib/
2626
pip-selfcheck.json
2727
pyvenv.cfg
2828
MANIFEST
29+
30+
31+
# path for the test lib.
32+
tools/plex

plexapi/library.py

+51
Original file line numberDiff line numberDiff line change
@@ -1570,3 +1570,54 @@ def setArt(self, art):
15701570

15711571
# def edit(self, **kwargs):
15721572
# TODO
1573+
1574+
1575+
@utils.registerPlexObject
1576+
class Path(PlexObject):
1577+
""" Represents a single directory Path.
1578+
1579+
Attributes:
1580+
TAG (str): 'Path'
1581+
1582+
home (bool): True if the path is the home directory
1583+
key (str): API URL (/services/browse/<base64path>)
1584+
network (bool): True if path is a network location
1585+
path (str): Full path to folder
1586+
title (str): Folder name
1587+
"""
1588+
TAG = 'Path'
1589+
1590+
def _loadData(self, data):
1591+
self.home = utils.cast(bool, data.attrib.get('home'))
1592+
self.key = data.attrib.get('key')
1593+
self.network = utils.cast(bool, data.attrib.get('network'))
1594+
self.path = data.attrib.get('path')
1595+
self.title = data.attrib.get('title')
1596+
1597+
def browse(self, includeFiles=True):
1598+
""" Alias for :func:`~plexapi.server.PlexServer.browse`. """
1599+
return self._server.browse(self, includeFiles)
1600+
1601+
def walk(self):
1602+
""" Alias for :func:`~plexapi.server.PlexServer.walk`. """
1603+
for path, paths, files in self._server.walk(self):
1604+
yield path, paths, files
1605+
1606+
1607+
@utils.registerPlexObject
1608+
class File(PlexObject):
1609+
""" Represents a single File.
1610+
1611+
Attributes:
1612+
TAG (str): 'File'
1613+
1614+
key (str): API URL (/services/browse/<base64path>)
1615+
path (str): Full path to file
1616+
title (str): File name
1617+
"""
1618+
TAG = 'File'
1619+
1620+
def _loadData(self, data):
1621+
self.key = data.attrib.get('key')
1622+
self.path = data.attrib.get('path')
1623+
self.title = data.attrib.get('title')

plexapi/server.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from plexapi.base import PlexObject
1616
from plexapi.client import PlexClient
1717
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
18-
from plexapi.library import Hub, Library
18+
from plexapi.library import Hub, Library, Path, File
1919
from plexapi.media import Conversion, Optimized
2020
from plexapi.playlist import Playlist
2121
from plexapi.playqueue import PlayQueue
@@ -247,6 +247,53 @@ def _myPlexClientPorts(self):
247247
log.warning('Unable to fetch client ports from myPlex: %s', err)
248248
return ports
249249

250+
def browse(self, path=None, includeFiles=True):
251+
""" Browse the system file path using the Plex API.
252+
Returns list of :class:`~plexapi.library.Path` and :class:`~plexapi.library.File` objects.
253+
254+
Parameters:
255+
path (:class:`~plexapi.library.Path` or str, optional): Full path to browse.
256+
includeFiles (bool): True to include files when browsing (Default).
257+
False to only return folders.
258+
"""
259+
if isinstance(path, Path):
260+
key = path.key
261+
elif path is not None:
262+
base64path = utils.base64str(path)
263+
key = '/services/browse/%s' % base64path
264+
else:
265+
key = '/services/browse'
266+
if includeFiles:
267+
key += '?includeFiles=1'
268+
return self.fetchItems(key)
269+
270+
def walk(self, path=None):
271+
""" Walk the system file tree using the Plex API similar to `os.walk`.
272+
Yields a 3-tuple `(path, paths, files)` where
273+
`path` is a string of the directory path,
274+
`paths` is a list of :class:`~plexapi.library.Path` objects, and
275+
`files` is a list of :class:`~plexapi.library.File` objects.
276+
277+
Parameters:
278+
path (:class:`~plexapi.library.Path` or str, optional): Full path to walk.
279+
"""
280+
paths = []
281+
files = []
282+
for item in self.browse(path):
283+
if isinstance(item, Path):
284+
paths.append(item)
285+
elif isinstance(item, File):
286+
files.append(item)
287+
288+
if isinstance(path, Path):
289+
path = path.path
290+
291+
yield path or '', paths, files
292+
293+
for _path in paths:
294+
for path, paths, files in self.walk(_path):
295+
yield path, paths, files
296+
250297
def clients(self):
251298
""" Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """
252299
items = []

plexapi/utils.py

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: utf-8 -*-
2+
import base64
23
import logging
34
import os
45
import re
@@ -411,3 +412,7 @@ def getAgentIdentifier(section, agent):
411412
agents += identifiers
412413
raise NotFound('Couldnt find "%s" in agents list (%s)' %
413414
(agent, ', '.join(agents)))
415+
416+
417+
def base64str(text):
418+
return base64.b64encode(text.encode('utf-8')).decode('utf-8')

tests/test_server.py

+17
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,23 @@ def test_server_downloadDatabases(tmpdir, plex):
287287
assert len(tmpdir.listdir()) > 1
288288

289289

290+
def test_server_browse(plex, movies):
291+
movies_path = movies.locations[0]
292+
# browse root
293+
paths = plex.browse()
294+
assert len(paths)
295+
# browse the path of the movie library
296+
paths = plex.browse(movies_path)
297+
assert len(paths)
298+
# browse the path of the movie library without files
299+
paths = plex.browse(movies_path, includeFiles=False)
300+
assert not len([f for f in paths if f.TAG == 'File'])
301+
# walk the path of the movie library
302+
for path, paths, files in plex.walk(movies_path):
303+
assert path.startswith(movies_path)
304+
assert len(paths) or len(files)
305+
306+
290307
def test_server_allowMediaDeletion(account):
291308
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken)
292309
# Check server current allowMediaDeletion setting

0 commit comments

Comments
 (0)