python-plexapi/plexapi/photo.py

305 lines
14 KiB
Python
Raw Normal View History

2016-04-10 03:59:47 +00:00
# -*- coding: utf-8 -*-
import os
2020-05-12 21:15:16 +00:00
from urllib.parse import quote_plus
2020-12-24 04:48:58 +00:00
from plexapi import media, utils, video
from plexapi.base import Playable, PlexPartialObject, PlexSession
2020-12-24 06:00:00 +00:00
from plexapi.exceptions import BadRequest
from plexapi.mixins import (
RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin,
PhotoalbumEditMixins, PhotoEditMixins
)
2016-04-10 03:59:47 +00:00
@utils.registerPlexObject
class Photoalbum(
PlexPartialObject,
RatingMixin,
ArtMixin, PosterMixin,
PhotoalbumEditMixins
):
2020-12-24 06:24:46 +00:00
""" Represents a single Photoalbum (collection of photos).
Attributes:
TAG (str): 'Directory'
TYPE (str): 'photo'
2020-12-24 00:16:08 +00:00
addedAt (datetime): Datetime the photo album was added to the library.
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>)
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
guid (str): Plex GUID for the photo album (local://229674).
index (sting): Plex index number for the photo album.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo album was last rated.
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
2020-12-24 00:16:08 +00:00
librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key.
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
listType (str): Hardcoded as 'photo' (useful for search filters).
2020-12-24 00:16:08 +00:00
ratingKey (int): Unique key identifying the photo album.
summary (str): Summary of the photoalbum.
2020-12-24 00:16:08 +00:00
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
title (str): Name of the photo album. (Trip to Disney World)
titleSort (str): Title to use when sorting (defaults to title).
type (str): 'photo'
updatedAt (datetime): Datetime the photo album was updated.
userRating (float): Rating of the photo album (0.0 - 10.0) equaling (0 stars - 5 stars).
2017-01-02 21:06:40 +00:00
"""
TAG = 'Directory'
TYPE = 'photo'
_searchType = 'photoalbum'
2016-04-10 03:59:47 +00:00
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
2017-02-04 17:43:50 +00:00
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
self.art = data.attrib.get('art')
self.composite = data.attrib.get('composite')
2020-12-24 04:39:15 +00:00
self.fields = self.findItems(data, media.Field)
2017-02-04 17:43:50 +00:00
self.guid = data.attrib.get('guid')
self.index = utils.cast(int, data.attrib.get('index'))
2021-01-25 05:28:27 +00:00
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
2021-03-11 21:27:08 +00:00
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
2020-12-24 00:16:08 +00:00
self.librarySectionKey = data.attrib.get('librarySectionKey')
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.listType = 'photo'
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
2017-02-04 17:43:50 +00:00
self.summary = data.attrib.get('summary')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
2020-12-24 00:16:08 +00:00
self.titleSort = data.attrib.get('titleSort', self.title)
2017-02-04 17:43:50 +00:00
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.userRating = utils.cast(float, data.attrib.get('userRating'))
2017-01-02 21:06:40 +00:00
def album(self, title):
""" Returns the :class:`~plexapi.photo.Photoalbum` that matches the specified title.
Parameters:
title (str): Title of the photo album to return.
"""
key = f'{self.key}/children'
return self.fetchItem(key, Photoalbum, title__iexact=title)
def albums(self, **kwargs):
2020-12-24 00:16:08 +00:00
""" Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """
key = f'{self.key}/children'
2020-12-24 04:48:58 +00:00
return self.fetchItems(key, Photoalbum, **kwargs)
def photo(self, title):
""" Returns the :class:`~plexapi.photo.Photo` that matches the specified title.
Parameters:
title (str): Title of the photo to return.
"""
key = f'{self.key}/children'
return self.fetchItem(key, Photo, title__iexact=title)
2017-02-09 04:08:25 +00:00
def photos(self, **kwargs):
2020-12-24 00:16:08 +00:00
""" Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """
key = f'{self.key}/children'
2020-12-24 04:48:58 +00:00
return self.fetchItems(key, Photo, **kwargs)
2016-04-10 03:59:47 +00:00
def clip(self, title):
""" Returns the :class:`~plexapi.video.Clip` that matches the specified title.
Parameters:
title (str): Title of the clip to return.
"""
key = f'{self.key}/children'
return self.fetchItem(key, video.Clip, title__iexact=title)
2017-01-02 21:06:40 +00:00
2020-12-24 00:16:08 +00:00
def clips(self, **kwargs):
""" Returns a list of :class:`~plexapi.video.Clip` objects in the album. """
key = f'{self.key}/children'
2020-12-24 04:48:58 +00:00
return self.fetchItems(key, video.Clip, **kwargs)
2020-12-24 00:16:08 +00:00
def get(self, title):
""" Alias to :func:`~plexapi.photo.Photoalbum.photo`. """
return self.episode(title)
2020-12-24 00:16:08 +00:00
def download(self, savepath=None, keep_original_name=False, subfolders=False):
""" Download all photos and clips from the photo album. See :func:`~plexapi.base.Playable.download` for details.
Parameters:
savepath (str): Defaults to current working dir.
keep_original_name (bool): True to keep the original filename otherwise
a friendlier filename is generated.
subfolders (bool): True to separate photos/clips in to photo album folders.
"""
filepaths = []
for album in self.albums():
_savepath = os.path.join(savepath, album.title) if subfolders else savepath
filepaths += album.download(_savepath, keep_original_name)
for photo in self.photos() + self.clips():
filepaths += photo.download(savepath, keep_original_name)
return filepaths
2021-09-26 22:23:09 +00:00
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)
2016-04-10 03:59:47 +00:00
@utils.registerPlexObject
class Photo(
PlexPartialObject, Playable,
RatingMixin,
ArtUrlMixin, PosterUrlMixin,
PhotoEditMixins
):
2020-12-24 06:24:46 +00:00
""" Represents a single Photo.
Attributes:
TAG (str): 'Photo'
TYPE (str): 'photo'
2020-12-24 00:16:08 +00:00
addedAt (datetime): Datetime the photo was added to the library.
createdAtAccuracy (str): Unknown (local).
createdAtTZOffset (int): Unknown (-25200).
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
2020-12-24 00:16:08 +00:00
guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn).
index (sting): Plex index number for the photo.
key (str): API URL (/library/metadata/<ratingkey>).
lastRatedAt (datetime): Datetime the photo was last rated.
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
2020-12-24 00:16:08 +00:00
librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key.
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
listType (str): Hardcoded as 'photo' (useful for search filters).
2020-12-24 00:16:08 +00:00
media (List<:class:`~plexapi.media.Media`>): List of media objects.
originallyAvailableAt (datetime): Datetime the photo was added to Plex.
parentGuid (str): Plex GUID for the photo album (local://229674).
parentIndex (int): Plex index number for the photo album.
parentKey (str): API URL of the photo album (/library/metadata/<parentRatingKey>).
parentRatingKey (int): Unique key identifying the photo album.
parentThumb (str): URL to photo album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the photo album for the photo.
ratingKey (int): Unique key identifying the photo.
summary (str): Summary of the photo.
2021-01-24 22:32:02 +00:00
tags (List<:class:`~plexapi.media.Tag`>): List of tag objects.
2020-12-24 00:16:08 +00:00
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
title (str): Name of the photo.
titleSort (str): Title to use when sorting (defaults to title).
type (str): 'photo'
updatedAt (datetime): Datetime the photo was updated.
userRating (float): Rating of the photo (0.0 - 10.0) equaling (0 stars - 5 stars).
2020-12-24 00:16:08 +00:00
year (int): Year the photo was taken.
2017-01-02 21:06:40 +00:00
"""
TAG = 'Photo'
2016-04-10 03:59:47 +00:00
TYPE = 'photo'
2018-09-08 15:25:16 +00:00
METADATA_TYPE = 'photo'
2016-04-10 03:59:47 +00:00
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Playable._loadData(self, data)
2017-02-04 17:43:50 +00:00
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
2020-12-24 00:16:08 +00:00
self.createdAtAccuracy = data.attrib.get('createdAtAccuracy')
self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))
self.fields = self.findItems(data, media.Field)
self.guid = data.attrib.get('guid')
2017-02-04 17:43:50 +00:00
self.index = utils.cast(int, data.attrib.get('index'))
self.key = data.attrib.get('key', '')
self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))
2021-03-11 21:27:08 +00:00
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
2020-12-24 00:16:08 +00:00
self.librarySectionKey = data.attrib.get('librarySectionKey')
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.listType = 'photo'
self.media = self.findItems(data, media.Media)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
self.parentGuid = data.attrib.get('parentGuid')
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
2017-02-04 17:43:50 +00:00
self.parentKey = data.attrib.get('parentKey')
2020-12-24 00:16:08 +00:00
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle')
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
2017-02-04 17:43:50 +00:00
self.summary = data.attrib.get('summary')
2021-01-24 22:32:02 +00:00
self.tags = self.findItems(data, media.Tag)
2017-02-04 17:43:50 +00:00
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
2020-12-24 00:16:08 +00:00
self.titleSort = data.attrib.get('titleSort', self.title)
2017-02-04 17:43:50 +00:00
self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.userRating = utils.cast(float, data.attrib.get('userRating'))
2017-02-04 17:43:50 +00:00
self.year = utils.cast(int, data.attrib.get('year'))
2017-01-02 21:06:40 +00:00
def _prettyfilename(self):
""" Returns a filename for use in download. """
if self.parentTitle:
return f'{self.parentTitle} - {self.title}'
return self.title
2016-04-10 03:59:47 +00:00
def photoalbum(self):
2020-12-24 00:16:08 +00:00
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
return self.fetchItem(self.parentKey)
def section(self):
2020-12-24 00:16:08 +00:00
""" Returns the :class:`~plexapi.library.LibrarySection` the item belongs to. """
if hasattr(self, 'librarySectionID'):
return self._server.library.sectionByID(self.librarySectionID)
elif self.parentKey:
return self._server.library.sectionByID(self.photoalbum().librarySectionID)
else:
raise BadRequest("Unable to get section for photo, can't find librarySectionID")
2018-09-08 15:25:16 +00:00
2020-12-24 00:16:08 +00:00
@property
def locations(self):
""" This does not exist in plex xml response but is added to have a common
interface to get the locations of the photo.
2021-06-06 21:54:15 +00:00
Returns:
List<str> of file paths where the photo is found on disk.
2020-12-24 00:16:08 +00:00
"""
return [part.file for item in self.media for part in item.parts if part]
2018-09-08 15:25:16 +00:00
def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
""" Add current photo as sync item for specified device.
2020-11-23 03:06:30 +00:00
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
2018-09-08 15:25:16 +00:00
Parameters:
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
2020-11-23 03:06:30 +00:00
module :mod:`~plexapi.sync`.
client (:class:`~plexapi.myplex.MyPlexDevice`): sync destination, see
:func:`~plexapi.myplex.MyPlexAccount.sync`.
clientId (str): sync destination, see :func:`~plexapi.myplex.MyPlexAccount.sync`.
2018-09-08 15:25:16 +00:00
limit (int): maximum count of items to sync, unlimited if `None`.
2020-11-23 03:06:30 +00:00
title (str): descriptive title for the new :class:`~plexapi.sync.SyncItem`, if empty the value would be
2018-09-08 15:25:16 +00:00
generated from metadata of current photo.
Returns:
2020-11-23 03:06:30 +00:00
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
2018-09-08 15:25:16 +00:00
"""
from plexapi.sync import SyncItem, Policy, MediaSettings
myplex = self._server.myPlexAccount()
sync_item = SyncItem(self._server, None)
sync_item.title = title if title else self.title
sync_item.rootTitle = self.title
sync_item.contentType = self.listType
sync_item.metadataType = self.METADATA_TYPE
sync_item.machineIdentifier = self._server.machineIdentifier
Improvements in tests process (#297) * lets begin * skip plexpass tests if there is not plexpass on account * test new myplex attrubutes * bootstrap: proper photos organisation * fix rest of photos tests * fix myplex new attributes test * fix music bootstrap by setting agent to lastfm * fix sync tests * increase bootstrap timeout * remove timeout from .travis.yml * do not create playlist-style photoalbums in plex-bootstraptest.py * allow negative filtering in LibrarySection.search() * fix sync tests once again * use sendCrashReports in test_settings * fix test_settings * fix test_video * do not accept eula in bootstrap * fix PlexServer.isLatest() * add test against old version of PlexServer * fix MyPlexAccount.OutOut * add flag for one-time testing in Travis * fix test_library onDeck tests * fix more tests * use tqdm in plex-bootstraptest for media scanning progress * create sections one-by-one * update docs on AlertListener for timeline entries * fix plex-bootstraptest for server version 1.3.2 * display skip/xpass/xfail reasons * fix tests on 1.3 * wait for music to be fully processed in plex-bootstraptest * fix misplaced TEST_ACCOUNT_ONCE * fix test_myplex_users, not sure if in proper-way * add pytest-rerunfailures; mark test_myplex_optout as flaky * fix comment * Revert "add pytest-rerunfailures; mark test_myplex_optout as flaky" This reverts commit 580e4c95a758c92329d757eb2f3fc3bf44b26f09. * restart plex container on failure * add conftest.wait_until() and used where some retries are required * add more wait_until() usage in test_sync * fix managed user search * fix updating managed users in myplex * allow to add new servers to existent users * add new server to a shared user while bootstrapping * add some docs on testing process * perform few attemps when unable to get the claim token * unlock websocket-client in requirements_dev * fix docblock in tools/plex-teardowntest * do not hardcode mediapart size in test_video * remove cache:pip from travis * Revert "unlock websocket-client in requirements_dev" This reverts commit 0d536bd06dbdc4a4b869a1686f8cd008898859fe. * remove debug from server.py * improve webhook tests * fix type() check to isinstance() * remove excessive `else` branch due to Hellowlol advice * add `unknown` as allowed `myPlexMappingState` in test_server
2018-09-14 18:03:23 +00:00
section = self.section()
2018-09-08 15:25:16 +00:00
sync_item.location = f'library://{section.uuid}/item/{quote_plus(self.key)}'
2018-09-08 15:25:16 +00:00
sync_item.policy = Policy.create(limit)
sync_item.mediaSettings = MediaSettings.createPhoto(resolution)
2020-06-06 18:14:38 +00:00
return myplex.sync(sync_item, client=client, clientId=clientId)
2020-10-10 18:55:49 +00:00
2021-09-26 22:23:09 +00:00
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)
@utils.registerPlexObject
class PhotoSession(PlexSession, Photo):
""" Represents a single Photo session
loaded from :func:`~plexapi.server.PlexServer.sessions`.
"""
_SESSIONTYPE = True
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
Photo._loadData(self, data)
PlexSession._loadData(self, data)