Skip to content

Commit

Permalink
Add properties to return the Plex Media Server data metadata paths (#…
Browse files Browse the repository at this point in the history
…1207)

* Add property to return the Plex Media Server data metadata path

* Add property to return the filepath for a resource

* Add tests for metadataDirectory and resourceFilepath
  • Loading branch information
JonnyWong16 authored Aug 27, 2023
1 parent 0679816 commit b98b605
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 1 deletion.
19 changes: 19 additions & 0 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os
from pathlib import Path
from urllib.parse import quote_plus

from plexapi import media, utils
Expand Down Expand Up @@ -240,6 +241,12 @@ def station(self):
key = f'{self.key}?includeStations=1'
return next(iter(self.fetchItems(key, cls=Playlist, rtag="Stations")), None)

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Artists' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Album(
Expand Down Expand Up @@ -359,6 +366,12 @@ def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return f'{self.parentTitle} - {self.title}'

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Albums' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Track(
Expand Down Expand Up @@ -470,6 +483,12 @@ def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey)

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.parentGuid)
return str(Path('Metadata') / 'Albums' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class TrackSession(PlexSession, Track):
Expand Down
7 changes: 7 additions & 0 deletions plexapi/collection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from pathlib import Path
from urllib.parse import quote_plus

from plexapi import media, utils
Expand Down Expand Up @@ -560,3 +561,9 @@ def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, clien
raise Unsupported('Unsupported collection content')

return myplex.sync(sync_item, client=client, clientId=clientId)

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Collections' / guid_hash[0] / f'{guid_hash[1:]}.bundle')
16 changes: 15 additions & 1 deletion plexapi/media.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

import xml
from pathlib import Path
from urllib.parse import quote_plus

from plexapi import log, settings, utils
Expand Down Expand Up @@ -1024,6 +1024,20 @@ def select(self):
except xml.etree.ElementTree.ParseError:
pass

@property
def resourceFilepath(self):
""" Returns the file path to the resource in the Plex Media Server data directory.
Note: Returns the URL if the resource is not stored locally.
"""
if self.ratingKey.startswith('media://'):
return str(Path('Media') / 'localhost' / self.ratingKey.split('://')[-1])
elif self.ratingKey.startswith('metadata://'):
return str(Path(self._parent().metadataDirectory) / 'Contents' / '_combined' / self.ratingKey.split('://')[-1])
elif self.ratingKey.startswith('upload://'):
return str(Path(self._parent().metadataDirectory) / 'Uploads' / self.ratingKey.split('://')[-1])
else:
return self.ratingKey


class Art(BaseResource):
""" Represents a single Art object. """
Expand Down
13 changes: 13 additions & 0 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os
from pathlib import Path
from urllib.parse import quote_plus

from plexapi import media, utils, video
Expand Down Expand Up @@ -139,6 +140,12 @@ def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.key, legacy=1)

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Photos' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Photo(
Expand Down Expand Up @@ -290,6 +297,12 @@ def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='details', key=self.parentKey, legacy=1)

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.parentGuid)
return str(Path('Metadata') / 'Photos' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class PhotoSession(PlexSession, Photo):
Expand Down
7 changes: 7 additions & 0 deletions plexapi/playlist.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import re
from pathlib import Path
from urllib.parse import quote_plus, unquote

from plexapi import media, utils
Expand Down Expand Up @@ -496,3 +497,9 @@ def sync(self, videoQuality=None, photoResolution=None, audioBitrate=None, clien
def _getWebURL(self, base=None):
""" Get the Plex Web URL with the correct parameters. """
return self._server._buildWebURL(base=base, endpoint='playlist', key=self.key)

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Playlists' / guid_hash[0] / f'{guid_hash[1:]}.bundle')
6 changes: 6 additions & 0 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from collections import deque
from datetime import datetime, timedelta
from getpass import getpass
from hashlib import sha1
from threading import Event, Thread
from urllib.parse import quote

Expand Down Expand Up @@ -650,3 +651,8 @@ def openOrRead(file):
return file.read()
with open(file, 'rb') as f:
return f.read()


def sha1hash(guid):
""" Return the SHA1 hash of a guid. """
return sha1(guid.encode('utf-8')).hexdigest()
31 changes: 31 additions & 0 deletions plexapi/video.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os
from pathlib import Path
from urllib.parse import quote_plus

from plexapi import media, utils
Expand Down Expand Up @@ -445,6 +446,12 @@ def removeFromContinueWatching(self):
self._server.query(key, params=params, method=self._server._session.put)
return self

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Movies' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Show(
Expand Down Expand Up @@ -655,6 +662,12 @@ def download(self, savepath=None, keep_original_name=False, subfolders=False, **
filepaths += episode.download(_savepath, keep_original_name, **kwargs)
return filepaths

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'TV Shows' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Season(
Expand Down Expand Up @@ -808,6 +821,12 @@ def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return f'{self.parentTitle} - {self.title}'

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.parentGuid)
return str(Path('Metadata') / 'TV Shows' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Episode(
Expand Down Expand Up @@ -1000,6 +1019,12 @@ def removeFromContinueWatching(self):
self._server.query(key, params=params, method=self._server._session.put)
return self

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.grandparentGuid)
return str(Path('Metadata') / 'TV Shows' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


@utils.registerPlexObject
class Clip(
Expand Down Expand Up @@ -1058,6 +1083,12 @@ def _prettyfilename(self):
""" Returns a filename for use in download. """
return self.title

@property
def metadataDirectory(self):
""" Returns the Plex Media Server data directory where the metadata is stored. """
guid_hash = utils.sha1hash(self.guid)
return str(Path('Metadata') / 'Movies' / guid_hash[0] / f'{guid_hash[1:]}.bundle')


class Extra(Clip):
""" Represents a single Extra (trailer, behindTheScenes, etc). """
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
STUB_MOVIE_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "video_stub.mp4")
STUB_MP3_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "audio_stub.mp3")
STUB_IMAGE_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "cute_cat.jpg")
# For the default Docker bootstrap test Plex Media Server data directory
BOOTSTRAP_DATA_PATH = os.path.join(BASE_DIR_PATH, "plex", "db", "Library", "Application Support", "Plex Media Server")


def pytest_addoption(parser):
Expand Down
12 changes: 12 additions & 0 deletions tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -1459,3 +1459,15 @@ def test_video_optimize(plex, movie, tvshows, show):
movie.optimize()
with pytest.raises(BadRequest):
movie.optimize(target="mobile", locationID=-100)


def test_video_Movie_matadataDirectory(movie):
assert os.path.exists(os.path.join(utils.BOOTSTRAP_DATA_PATH, movie.metadataDirectory))

for poster in movie.posters():
if not poster.ratingKey.startswith('http'):
assert os.path.exists(os.path.join(utils.BOOTSTRAP_DATA_PATH, poster.resourceFilepath))

for art in movie.arts():
if not art.ratingKey.startswith('http'):
assert os.path.exists(os.path.join(utils.BOOTSTRAP_DATA_PATH, art.resourceFilepath))

0 comments on commit b98b605

Please sign in to comment.