mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-26 13:40:22 +00:00
Merge pull request #648 from JonnyWong16/feature/posters
Update poster and art and move to a mixin
This commit is contained in:
commit
c9b2c3628e
10 changed files with 160 additions and 146 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue