Merge pull request #648 from JonnyWong16/feature/posters

Update poster and art and move to a mixin
This commit is contained in:
JonnyWong16 2021-02-14 14:30:43 -08:00 committed by GitHub
commit c9b2c3628e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 146 deletions

View file

@ -4,7 +4,7 @@ from urllib.parse import quote_plus
from plexapi import library, media, utils
from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
from plexapi.mixins import ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
@ -125,7 +125,7 @@ class Audio(PlexPartialObject):
@utils.registerPlexObject
class Artist(Audio, SplitMergeMixin, UnmatchMatchMixin,
class Artist(Audio, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
""" Represents a single Artist.
@ -229,7 +229,7 @@ class Artist(Audio, SplitMergeMixin, UnmatchMatchMixin,
@utils.registerPlexObject
class Album(Audio, UnmatchMatchMixin,
class Album(Audio, ArtMixin, PosterMixin, UnmatchMatchMixin,
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
""" Represents a single Album.

View file

@ -513,53 +513,6 @@ class PlexPartialObject(PlexObject):
"""
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
def posters(self):
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('%s/posters' % self.key)
def uploadPoster(self, url=None, filepath=None):
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '%s/posters?url=%s' % (self.key, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '%s/posters?' % self.key
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setPoster(self, poster):
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
poster.select()
def arts(self):
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('%s/arts' % self.key)
def uploadArt(self, url=None, filepath=None):
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/arts?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setArt(self, art):
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
art.select()
# The photo tag cant be built atm. TODO
# def arts(self):
# part = '%s/arts' % self.key
# return self.fetchItem(part)
# def poster(self):
# part = '%s/posters' % self.key
# return self.fetchItem(part, etag='Photo')
class Playable(object):
""" This is a general place to store functions specific to media that is Playable.

View file

@ -4,6 +4,7 @@ from urllib.parse import quote, quote_plus, unquote, urlencode
from plexapi import X_PLEX_CONTAINER_SIZE, log, media, utils
from plexapi.base import OPERATORS, PlexObject, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import ArtMixin, PosterMixin
from plexapi.mixins import LabelMixin
from plexapi.settings import Setting
from plexapi.utils import deprecated
@ -1527,7 +1528,7 @@ class FirstCharacter(PlexObject):
@utils.registerPlexObject
class Collections(PlexPartialObject, LabelMixin):
class Collections(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin):
""" Represents a single Collection.
Attributes:
@ -1684,44 +1685,6 @@ class Collections(PlexPartialObject, LabelMixin):
part = '/library/metadata/%s/prefs?collectionSort=%s' % (self.ratingKey, key)
return self._server.query(part, method=self._server._session.put)
def posters(self):
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey)
def uploadPoster(self, url=None, filepath=None):
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/posters?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setPoster(self, poster):
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
poster.select()
def arts(self):
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey)
def uploadArt(self, url=None, filepath=None):
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/arts?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setArt(self, art):
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
art.select()
# def edit(self, **kwargs):
# TODO

View file

@ -807,20 +807,25 @@ class Style(MediaTag):
FILTER = 'style'
@utils.registerPlexObject
class Poster(PlexObject):
""" Represents a Poster.
class BasePosterArt(PlexObject):
""" Base class for all Poster and Art objects.
Attributes:
TAG (str): 'Photo'
key (str): API URL (/library/metadata/<ratingkey>).
provider (str): The source of the poster or art.
ratingKey (str): Unique key identifying the poster or art.
selected (bool): True if the poster or art is currently selected.
thumb (str): The URL to retrieve the poster or art thumbnail.
"""
TAG = 'Photo'
def _loadData(self, data):
self._data = data
self.key = data.attrib.get('key')
self.provider = data.attrib.get('provider')
self.ratingKey = data.attrib.get('ratingKey')
self.selected = data.attrib.get('selected')
self.selected = cast(bool, data.attrib.get('selected'))
self.thumb = data.attrib.get('thumb')
def select(self):
@ -832,6 +837,14 @@ class Poster(PlexObject):
pass
class Poster(BasePosterArt):
""" Represents a single Poster object. """
class Art(BasePosterArt):
""" Represents a single Art object. """
@utils.registerPlexObject
class Producer(MediaTag):
""" Represents a single Producer media tag.

View file

@ -1,10 +1,72 @@
# -*- coding: utf-8 -*-
from urllib.parse import urlencode
from urllib.parse import quote_plus, urlencode
from plexapi import utils
from plexapi import media, utils
from plexapi.exceptions import NotFound
class ArtMixin(object):
""" Mixin for Plex objects that can have artwork."""
def arts(self):
""" Returns list of available :class:`~plexapi.media.Art` objects. """
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey, cls=media.Art)
def uploadArt(self, url=None, filepath=None):
""" Upload art from url or filepath and set it as the selected art.
Parameters:
url (str): The full URL to the image to upload.
filepath (str): The full file path the the image to upload.
"""
if url:
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/arts?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setArt(self, art):
""" Set the artwork for a Plex object.
Parameters:
art (:class:`~plexapi.media.Art`): The art object to select.
"""
art.select()
class PosterMixin(object):
""" Mixin for Plex objects that can have posters."""
def posters(self):
""" Returns list of available :class:`~plexapi.media.Poster` objects. """
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey, cls=media.Poster)
def uploadPoster(self, url=None, filepath=None):
""" Upload poster from url or filepath and set it as the selected poster.
Parameters:
url (str): The full URL to the image to upload.
filepath (str): The full file path the the image to upload.
"""
if url:
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/posters?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setPoster(self, poster):
""" Set the poster for a Plex object.
Parameters:
poster (:class:`~plexapi.media.Poster`): The poster object to select.
"""
poster.select()
class SplitMergeMixin(object):
""" Mixin for Plex objects that can be split and merged."""
@ -319,7 +381,7 @@ class TagMixin(object):
self._edit_tags('tag', tags, remove=True)
class EditWriter(object):
class WriterMixin(object):
""" Mixin for Plex objects that can have writers. """
def addWriter(self, writers):

View file

@ -5,12 +5,13 @@ from plexapi import utils
from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection
from plexapi.mixins import ArtMixin, PosterMixin
from plexapi.playqueue import PlayQueue
from plexapi.utils import cast, toDatetime
@utils.registerPlexObject
class Playlist(PlexPartialObject, Playable):
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
""" Represents a single Playlist.
Attributes:
@ -311,41 +312,3 @@ class Playlist(PlexPartialObject, Playable):
raise Unsupported('Unsupported playlist content')
return myplex.sync(sync_item, client=client, clientId=clientId)
def posters(self):
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey)
def uploadPoster(self, url=None, filepath=None):
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/posters?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setPoster(self, poster):
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
poster.select()
def arts(self):
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey)
def uploadArt(self, url=None, filepath=None):
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
if url:
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
self._server.query(key, method=self._server._session.post)
elif filepath:
key = '/library/metadata/%s/arts?' % self.ratingKey
data = open(filepath, 'rb').read()
self._server.query(key, method=self._server._session.post, data=data)
def setArt(self, art):
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
art.select()

View file

@ -5,8 +5,8 @@ from urllib.parse import quote_plus, urlencode
from plexapi import library, media, settings, utils
from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter
from plexapi.mixins import ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
class Video(PlexPartialObject):
@ -261,8 +261,8 @@ class Video(PlexPartialObject):
@utils.registerPlexObject
class Movie(Playable, Video, SplitMergeMixin, UnmatchMatchMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter):
class Movie(Video, Playable, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin):
""" Represents a single Movie.
Attributes:
@ -388,7 +388,7 @@ class Movie(Playable, Video, SplitMergeMixin, UnmatchMatchMixin,
@utils.registerPlexObject
class Show(Video, SplitMergeMixin, UnmatchMatchMixin,
class Show(Video, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
CollectionMixin, GenreMixin, LabelMixin):
""" Represents a single Show (including all seasons and episodes).
@ -587,7 +587,7 @@ class Show(Video, SplitMergeMixin, UnmatchMatchMixin,
@utils.registerPlexObject
class Season(Video):
class Season(Video, ArtMixin, PosterMixin):
""" Represents a single Show Season (including all episodes).
Attributes:
@ -713,7 +713,8 @@ class Season(Video):
@utils.registerPlexObject
class Episode(Playable, Video, DirectorMixin, EditWriter):
class Episode(Video, Playable, ArtMixin, PosterMixin,
DirectorMixin, WriterMixin):
""" Represents a single Shows Episode.
Attributes:
@ -850,7 +851,7 @@ class Episode(Playable, Video, DirectorMixin, EditWriter):
@utils.registerPlexObject
class Clip(Playable, Video):
class Clip(Video, Playable):
"""Represents a single Clip.
Attributes:

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
import os
import time
from datetime import datetime
from functools import partial
from os import environ
import plexapi
import pytest
@ -66,6 +66,11 @@ TEST_ANONYMOUSLY = "anonymously"
ANON_PARAM = pytest.param(TEST_ANONYMOUSLY, marks=pytest.mark.anonymous)
AUTH_PARAM = pytest.param(TEST_AUTHENTICATED, marks=pytest.mark.authenticated)
BASE_DIR_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
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")
def pytest_addoption(parser):
parser.addoption(
@ -125,7 +130,7 @@ def account(sess):
@pytest.fixture(scope="session")
def account_once(account):
if environ.get("TEST_ACCOUNT_ONCE") not in ("1", "true") and environ.get("CI") == "true":
if os.environ.get("TEST_ACCOUNT_ONCE") not in ("1", "true") and os.environ.get("CI") == "true":
pytest.skip("Do not forget to test this by providing TEST_ACCOUNT_ONCE=1")
return account
@ -282,7 +287,7 @@ def subtitle():
@pytest.fixture()
def shared_username(account):
username = environ.get("SHARED_USERNAME", "PKKid")
username = os.environ.get("SHARED_USERNAME", "PKKid")
for user in account.users():
if user.title.lower() == username.lower():
return username

View file

@ -323,6 +323,16 @@ def test_library_Collection_thumbUrl(collection):
def test_library_Collection_artUrl(collection):
assert collection.artUrl is None # Collections don't have default art
def test_library_Collection_posters(collection):
posters = collection.posters()
assert posters
def test_library_Collection_art(collection):
arts = collection.arts()
assert not arts # Collection has no default art
def test_library_Collection_mixins_tags(collection):
test_mixins.edit_label(collection)

View file

@ -495,6 +495,50 @@ def test_video_Movie_match(movies):
assert len(results) == 0
def test_video_Movie_poster(movie):
posters = movie.posters()
poster = posters[0]
assert len(poster.key) >= 10
if not poster.ratingKey.startswith("media://"):
assert poster.provider
assert len(poster.ratingKey) >= 10
assert utils.is_bool(poster.selected)
assert len(poster.thumb) >= 10
# Select a different poster
movie.setPoster(posters[1])
posters = movie.posters()
assert posters[0].selected is False
assert posters[1].selected is True
# Test upload poster from file
movie.uploadPoster(filepath=utils.STUB_IMAGE_PATH)
posters = movie.posters()
file_poster = next(p for p in posters if p.ratingKey.startswith('upload://'))
assert file_poster.selected is True
movie.setPoster(posters[0]) # Reset to default poster
def test_video_Movie_art(movie):
arts = movie.arts()
art = arts[0]
assert len(art.key) >= 10
if not art.ratingKey.startswith("media://"):
assert art.provider
assert len(art.ratingKey) >= 10
assert utils.is_bool(art.selected)
assert len(art.thumb) >= 10
# Select a different art
movie.setArt(arts[1])
arts = movie.arts()
assert arts[0].selected is False
assert arts[1].selected is True
# Test upload poster from file
movie.uploadArt(filepath=utils.STUB_IMAGE_PATH)
arts = movie.arts()
file_art = next(a for a in arts if a.ratingKey.startswith('upload://'))
assert file_art.selected is True
movie.setArt(arts[0]) # Reset to default art
def test_video_Movie_hubs(movies):
movie = movies.get('Big Buck Bunny')
hubs = movie.hubs()