mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 14:14:19 +00:00
Merge pull request #741 from JonnyWong16/feature/tag_items
Add ability to retrieve a list of items and collection object from media tags
This commit is contained in:
commit
8c2b3ce063
9 changed files with 258 additions and 125 deletions
|
@ -1698,6 +1698,12 @@ class HubMediaTag(PlexObject):
|
|||
self.tagValue = utils.cast(int, data.attrib.get('tagValue'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
def items(self, *args, **kwargs):
|
||||
""" Return the list of items within this tag. """
|
||||
if not self.key:
|
||||
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
|
||||
return self.fetchItems(self.key)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(HubMediaTag):
|
||||
|
|
232
plexapi/media.py
232
plexapi/media.py
|
@ -648,59 +648,43 @@ class MediaTag(PlexObject):
|
|||
the construct used for things such as Country, Director, Genre, etc.
|
||||
|
||||
Attributes:
|
||||
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
||||
filter (str): The library filter for the tag.
|
||||
id (id): Tag ID (This seems meaningless except to use it as a unique id).
|
||||
role (str): Unknown
|
||||
key (str): API URL (/library/section/<librarySectionID>/all?<filter>).
|
||||
role (str): The name of the character role for :class:`~plexapi.media.Role` only.
|
||||
tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of
|
||||
person for Directors and Roles (ex: Animation, Stephen Graham, etc).
|
||||
<Hub_Search_Attributes>: Attributes only applicable in search results from
|
||||
PlexServer :func:`~plexapi.server.PlexServer.search`. They provide details of which
|
||||
library section the tag was found as well as the url to dig deeper into the results.
|
||||
|
||||
* key (str): API URL to dig deeper into this tag (ex: /library/sections/1/all?actor=9081).
|
||||
* librarySectionID (int): Section ID this tag was generated from.
|
||||
* librarySectionTitle (str): Library section title this tag was found.
|
||||
* librarySectionType (str): Media type of the library section this tag was found.
|
||||
* tagType (int): Tag type ID.
|
||||
* thumb (str): URL to thumbnail image.
|
||||
thumb (str): URL to thumbnail image for :class:`~plexapi.media.Role` only.
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
self.key = data.attrib.get('key')
|
||||
self.role = data.attrib.get('role')
|
||||
self.tag = data.attrib.get('tag')
|
||||
# additional attributes only from hub search
|
||||
self.key = data.attrib.get('key')
|
||||
self.librarySectionID = cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.librarySectionType = data.attrib.get('librarySectionType')
|
||||
self.tagType = cast(int, data.attrib.get('tagType'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
def items(self, *args, **kwargs):
|
||||
""" Return the list of items within this tag. This function is only applicable
|
||||
in search results from PlexServer :func:`~plexapi.server.PlexServer.search`.
|
||||
"""
|
||||
parent = self._parent()
|
||||
self._librarySectionID = utils.cast(int, parent._data.attrib.get('librarySectionID'))
|
||||
self._librarySectionKey = parent._data.attrib.get('librarySectionKey')
|
||||
self._librarySectionTitle = parent._data.attrib.get('librarySectionTitle')
|
||||
self._parentType = parent.TYPE
|
||||
|
||||
if self._librarySectionKey and self.filter:
|
||||
self.key = '%s/all?%s&type=%s' % (
|
||||
self._librarySectionKey, self.filter, utils.searchType(self._parentType))
|
||||
|
||||
def items(self):
|
||||
""" Return the list of items within this tag. """
|
||||
if not self.key:
|
||||
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
|
||||
raise BadRequest('Key is not defined for this tag: %s. '
|
||||
'Reload the parent object.' % self.tag)
|
||||
return self.fetchItems(self.key)
|
||||
|
||||
|
||||
class GuidTag(PlexObject):
|
||||
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
||||
|
||||
Attributes:
|
||||
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB).
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = data.attrib.get('id')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Collection(MediaTag):
|
||||
""" Represents a single Collection media tag.
|
||||
|
@ -712,36 +696,11 @@ class Collection(MediaTag):
|
|||
TAG = 'Collection'
|
||||
FILTER = 'collection'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Label(MediaTag):
|
||||
""" Represents a single Label media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Label'
|
||||
FILTER (str): 'label'
|
||||
"""
|
||||
TAG = 'Label'
|
||||
FILTER = 'label'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(MediaTag):
|
||||
""" Represents a single Tag media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Tag'
|
||||
FILTER (str): 'tag'
|
||||
"""
|
||||
TAG = 'Tag'
|
||||
FILTER = 'tag'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.id = cast(int, data.attrib.get('id', 0))
|
||||
self.filter = data.attrib.get('filter')
|
||||
self.tag = data.attrib.get('tag')
|
||||
self.title = self.tag
|
||||
def collection(self):
|
||||
""" Return the :class:`~plexapi.collection.Collection` object for this collection tag.
|
||||
"""
|
||||
key = '%s/collections' % self._librarySectionKey
|
||||
return self.fetchItem(key, etag='Directory', index=self.id)
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -781,13 +740,15 @@ class Genre(MediaTag):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Guid(GuidTag):
|
||||
""" Represents a single Guid media tag.
|
||||
class Label(MediaTag):
|
||||
""" Represents a single Label media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Guid'
|
||||
TAG (str): 'Label'
|
||||
FILTER (str): 'label'
|
||||
"""
|
||||
TAG = "Guid"
|
||||
TAG = 'Label'
|
||||
FILTER = 'label'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -802,6 +763,42 @@ class Mood(MediaTag):
|
|||
FILTER = 'mood'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Producer(MediaTag):
|
||||
""" Represents a single Producer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Producer'
|
||||
FILTER (str): 'producer'
|
||||
"""
|
||||
TAG = 'Producer'
|
||||
FILTER = 'producer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Role(MediaTag):
|
||||
""" Represents a single Role (actor/actress) media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Role'
|
||||
FILTER (str): 'role'
|
||||
"""
|
||||
TAG = 'Role'
|
||||
FILTER = 'role'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Similar(MediaTag):
|
||||
""" Represents a single Similar media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Similar'
|
||||
FILTER (str): 'similar'
|
||||
"""
|
||||
TAG = 'Similar'
|
||||
FILTER = 'similar'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Style(MediaTag):
|
||||
""" Represents a single Style media tag.
|
||||
|
@ -814,6 +811,53 @@ class Style(MediaTag):
|
|||
FILTER = 'style'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Tag(MediaTag):
|
||||
""" Represents a single Tag media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Tag'
|
||||
FILTER (str): 'tag'
|
||||
"""
|
||||
TAG = 'Tag'
|
||||
FILTER = 'tag'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Writer(MediaTag):
|
||||
""" Represents a single Writer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Writer'
|
||||
FILTER (str): 'writer'
|
||||
"""
|
||||
TAG = 'Writer'
|
||||
FILTER = 'writer'
|
||||
|
||||
|
||||
class GuidTag(PlexObject):
|
||||
""" Base class for guid tags used only for Guids, as they contain only a string identifier
|
||||
|
||||
Attributes:
|
||||
id (id): The guid for external metadata sources (e.g. IMDB, TMDB, TVDB).
|
||||
"""
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
self.id = data.attrib.get('id')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Guid(GuidTag):
|
||||
""" Represents a single Guid media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Guid'
|
||||
"""
|
||||
TAG = 'Guid'
|
||||
|
||||
|
||||
class BaseImage(PlexObject):
|
||||
""" Base class for all Art, Banner, and Poster objects.
|
||||
|
||||
|
@ -856,54 +900,6 @@ class Poster(BaseImage):
|
|||
""" Represents a single Poster object. """
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Producer(MediaTag):
|
||||
""" Represents a single Producer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Producer'
|
||||
FILTER (str): 'producer'
|
||||
"""
|
||||
TAG = 'Producer'
|
||||
FILTER = 'producer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Role(MediaTag):
|
||||
""" Represents a single Role (actor/actress) media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Role'
|
||||
FILTER (str): 'role'
|
||||
"""
|
||||
TAG = 'Role'
|
||||
FILTER = 'role'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Similar(MediaTag):
|
||||
""" Represents a single Similar media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Similar'
|
||||
FILTER (str): 'similar'
|
||||
"""
|
||||
TAG = 'Similar'
|
||||
FILTER = 'similar'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Writer(MediaTag):
|
||||
""" Represents a single Writer media tag.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Writer'
|
||||
FILTER (str): 'writer'
|
||||
"""
|
||||
TAG = 'Writer'
|
||||
FILTER = 'writer'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Chapter(PlexObject):
|
||||
""" Represents a single Writer media tag.
|
||||
|
|
|
@ -234,11 +234,11 @@ def movie(movies):
|
|||
@pytest.fixture()
|
||||
def collection(movies):
|
||||
try:
|
||||
return movies.collections(title="marvel")[0]
|
||||
return movies.collections(title="Marvel")[0]
|
||||
except IndexError:
|
||||
movie = movies.get("Elephants Dream")
|
||||
movie.addCollection("marvel")
|
||||
return movies.collections(title="marvel")[0]
|
||||
movie.addCollection("Marvel")
|
||||
return movies.collections(title="Marvel")[0]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import conftest as utils
|
||||
from . import test_mixins
|
||||
from . import test_media, test_mixins
|
||||
|
||||
|
||||
def test_audio_Artist_attr(artist):
|
||||
|
@ -87,6 +87,16 @@ def test_audio_Artist_mixins_tags(artist):
|
|||
test_mixins.edit_style(artist)
|
||||
|
||||
|
||||
def test_audio_Artist_media_tags(artist):
|
||||
artist.reload()
|
||||
test_media.tag_collection(artist)
|
||||
test_media.tag_country(artist)
|
||||
test_media.tag_genre(artist)
|
||||
test_media.tag_mood(artist)
|
||||
test_media.tag_similar(artist)
|
||||
test_media.tag_style(artist)
|
||||
|
||||
|
||||
def test_audio_Album_attrs(album):
|
||||
assert utils.is_datetime(album.addedAt)
|
||||
if album.art:
|
||||
|
@ -165,6 +175,15 @@ def test_audio_Album_mixins_tags(album):
|
|||
test_mixins.edit_style(album)
|
||||
|
||||
|
||||
def test_audio_Album_media_tags(album):
|
||||
album.reload()
|
||||
test_media.tag_collection(album)
|
||||
test_media.tag_genre(album)
|
||||
test_media.tag_label(album)
|
||||
test_media.tag_mood(album)
|
||||
test_media.tag_style(album)
|
||||
|
||||
|
||||
def test_audio_Track_attrs(album):
|
||||
track = album.get("As Colourful As Ever").reload()
|
||||
assert utils.is_datetime(track.addedAt)
|
||||
|
@ -294,6 +313,12 @@ def test_audio_Track_mixins_tags(track):
|
|||
test_mixins.edit_mood(track)
|
||||
|
||||
|
||||
def test_audio_Track_media_tags(track):
|
||||
track.reload()
|
||||
test_media.tag_collection(track)
|
||||
test_media.tag_mood(track)
|
||||
|
||||
|
||||
def test_audio_Audio_section(artist, album, track):
|
||||
assert artist.section()
|
||||
assert album.section()
|
||||
|
|
|
@ -33,7 +33,7 @@ def test_Collection_attrs(collection):
|
|||
assert collection.summary == ""
|
||||
assert collection.thumb.startswith("/library/collections/%s/composite" % collection.ratingKey)
|
||||
assert collection.thumbBlurHash is None
|
||||
assert collection.title == "marvel"
|
||||
assert collection.title == "Marvel"
|
||||
assert collection.titleSort == collection.title
|
||||
assert collection.type == "collection"
|
||||
assert utils.is_datetime(collection.updatedAt)
|
||||
|
|
54
tests/test_media.py
Normal file
54
tests/test_media.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
def _test_media_tag(obj, attr):
|
||||
tags = getattr(obj, attr)
|
||||
if tags:
|
||||
assert obj in tags[0].items()
|
||||
|
||||
|
||||
def tag_collection(obj):
|
||||
_test_media_tag(obj, "collections")
|
||||
|
||||
|
||||
def tag_country(obj):
|
||||
_test_media_tag(obj, "countries")
|
||||
|
||||
|
||||
def tag_director(obj):
|
||||
_test_media_tag(obj, "directors")
|
||||
|
||||
|
||||
def tag_genre(obj):
|
||||
_test_media_tag(obj, "genres")
|
||||
|
||||
|
||||
def tag_label(obj):
|
||||
_test_media_tag(obj, "labels")
|
||||
|
||||
|
||||
def tag_mood(obj):
|
||||
_test_media_tag(obj, "moods")
|
||||
|
||||
|
||||
def tag_producer(obj):
|
||||
_test_media_tag(obj, "producers")
|
||||
|
||||
|
||||
def tag_role(obj):
|
||||
_test_media_tag(obj, "roles")
|
||||
|
||||
|
||||
def tag_similar(obj):
|
||||
_test_media_tag(obj, "similar")
|
||||
|
||||
|
||||
def tag_style(obj):
|
||||
_test_media_tag(obj, "styles")
|
||||
|
||||
|
||||
def tag_tag(obj):
|
||||
_test_media_tag(obj, "tags")
|
||||
|
||||
|
||||
def tag_writer(obj):
|
||||
_test_media_tag(obj, "writers")
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import test_mixins
|
||||
from . import test_media, test_mixins
|
||||
|
||||
|
||||
def test_photo_Photoalbum(photoalbum):
|
||||
|
@ -20,3 +20,8 @@ def test_photo_Photoalbum_mixins_images(photoalbum):
|
|||
|
||||
def test_photo_Photo_mixins_tags(photo):
|
||||
test_mixins.edit_tag(photo)
|
||||
|
||||
|
||||
def test_photo_Photo_media_tags(photo):
|
||||
photo.reload()
|
||||
test_media.tag_tag(photo)
|
||||
|
|
|
@ -135,6 +135,7 @@ def test_server_search(plex, movie):
|
|||
assert hub_tag.tagType == 1
|
||||
assert hub_tag.tagValue is None
|
||||
assert hub_tag.thumb is None
|
||||
assert movie in hub_tag.items()
|
||||
# Test director search
|
||||
director = movie.directors[0]
|
||||
assert plex.search(director.tag, mediatype="director")
|
||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
from . import conftest as utils
|
||||
from . import test_mixins
|
||||
from . import test_media, test_mixins
|
||||
|
||||
|
||||
def test_video_Movie(movies, movie):
|
||||
|
@ -59,6 +59,30 @@ def test_video_Movie_mixins_tags(movie):
|
|||
test_mixins.edit_writer(movie)
|
||||
|
||||
|
||||
def test_video_Movie_media_tags(movie):
|
||||
movie.reload()
|
||||
test_media.tag_collection(movie)
|
||||
test_media.tag_country(movie)
|
||||
test_media.tag_director(movie)
|
||||
test_media.tag_genre(movie)
|
||||
test_media.tag_label(movie)
|
||||
test_media.tag_producer(movie)
|
||||
test_media.tag_role(movie)
|
||||
test_media.tag_similar(movie)
|
||||
test_media.tag_writer(movie)
|
||||
|
||||
|
||||
def test_video_Movie_media_tags_Exception(movie):
|
||||
with pytest.raises(BadRequest):
|
||||
movie.genres[0].items()
|
||||
|
||||
|
||||
def test_video_Movie_media_tags_collection(movie, collection):
|
||||
movie.reload()
|
||||
collection_tag = next(c for c in movie.collections if c.tag == "Marvel")
|
||||
assert collection == collection_tag.collection()
|
||||
|
||||
|
||||
def test_video_Movie_getStreamURL(movie, account):
|
||||
key = movie.ratingKey
|
||||
assert movie.getStreamURL() == (
|
||||
|
@ -741,6 +765,15 @@ def test_video_Show_mixins_tags(show):
|
|||
test_mixins.edit_label(show)
|
||||
|
||||
|
||||
def test_video_Show_media_tags(show):
|
||||
show.reload()
|
||||
test_media.tag_collection(show)
|
||||
test_media.tag_genre(show)
|
||||
test_media.tag_label(show)
|
||||
test_media.tag_role(show)
|
||||
test_media.tag_similar(show)
|
||||
|
||||
|
||||
def test_video_Episode(show):
|
||||
episode = show.episode("Winter Is Coming")
|
||||
assert episode == show.episode(season=1, episode=1)
|
||||
|
@ -900,6 +933,13 @@ def test_video_Episode_mixins_tags(episode):
|
|||
test_mixins.edit_writer(episode)
|
||||
|
||||
|
||||
def test_video_Episode_media_tags(episode):
|
||||
episode.reload()
|
||||
test_media.tag_collection(episode)
|
||||
test_media.tag_director(episode)
|
||||
test_media.tag_writer(episode)
|
||||
|
||||
|
||||
def test_video_Season(show):
|
||||
seasons = show.seasons()
|
||||
assert len(seasons) == 2
|
||||
|
@ -998,6 +1038,12 @@ def test_video_Season_mixins_tags(show):
|
|||
test_mixins.edit_collection(season)
|
||||
|
||||
|
||||
def test_video_Season_media_tags(show):
|
||||
season = show.season(season=1)
|
||||
season.reload()
|
||||
test_media.tag_collection(season)
|
||||
|
||||
|
||||
def test_that_reload_return_the_same_object(plex):
|
||||
# we want to check this that all the urls are correct
|
||||
movie_library_search = plex.library.section("Movies").search("Elephants Dream")[0]
|
||||
|
|
Loading…
Reference in a new issue