Merge pull request #649 from JonnyWong16/feature/edit_tags

Add more tag editing methods and move to a mixin
This commit is contained in:
Steffen Fredriksen 2021-02-07 16:58:56 +01:00 committed by GitHub
commit aca85d150c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 356 additions and 59 deletions

View file

@ -4,6 +4,7 @@ from urllib.parse import quote_plus
from plexapi import library, media, utils from plexapi import library, media, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
class Audio(PlexPartialObject): class Audio(PlexPartialObject):
@ -123,7 +124,7 @@ class Audio(PlexPartialObject):
@utils.registerPlexObject @utils.registerPlexObject
class Artist(Audio): class Artist(Audio, CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
""" Represents a single Artist. """ Represents a single Artist.
Attributes: Attributes:
@ -226,7 +227,7 @@ class Artist(Audio):
@utils.registerPlexObject @utils.registerPlexObject
class Album(Audio): class Album(Audio, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
""" Represents a single Album. """ Represents a single Album.
Attributes: Attributes:
@ -332,7 +333,7 @@ class Album(Audio):
@utils.registerPlexObject @utils.registerPlexObject
class Track(Audio, Playable): class Track(Audio, Playable, MoodMixin):
""" Represents a single Track. """ Represents a single Track.
Attributes: Attributes:

View file

@ -5,7 +5,7 @@ from urllib.parse import quote_plus, urlencode
from plexapi import log, utils from plexapi import log, utils
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
from plexapi.utils import tag_helper from plexapi.utils import tag_plural, tag_helper
DONT_RELOAD_FOR_KEYS = ['key', 'session'] DONT_RELOAD_FOR_KEYS = ['key', 'session']
OPERATORS = { OPERATORS = {
@ -462,40 +462,12 @@ class PlexPartialObject(PlexObject):
""" """
if not isinstance(items, list): if not isinstance(items, list):
items = [items] items = [items]
value = getattr(self, tag + 's') value = getattr(self, tag_plural(tag))
existing_cols = [t.tag for t in value if t and remove is False] existing_cols = [t.tag for t in value if t and remove is False]
d = tag_helper(tag, existing_cols + items, locked, remove) d = tag_helper(tag, existing_cols + items, locked, remove)
self.edit(**d) self.edit(**d)
self.refresh() self.refresh()
def addCollection(self, collections):
""" Add a collection(s).
Parameters:
collections (list): list of strings
"""
self._edit_tags('collection', collections)
def removeCollection(self, collections):
""" Remove a collection(s). """
self._edit_tags('collection', collections, remove=True)
def addLabel(self, labels):
""" Add a label(s). """
self._edit_tags('label', labels)
def removeLabel(self, labels):
""" Remove a label(s). """
self._edit_tags('label', labels, remove=True)
def addGenre(self, genres):
""" Add a genre(s). """
self._edit_tags('genre', genres)
def removeGenre(self, genres):
""" Remove a genre(s). """
self._edit_tags('genre', genres, remove=True)
def refresh(self): def refresh(self):
""" Refreshing a Library or individual item causes the metadata for the item to be """ Refreshing a Library or individual item causes the metadata for the item to be
refreshed, even if it already has metadata. You can think of refreshing as refreshed, even if it already has metadata. You can think of refreshing as

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 import X_PLEX_CONTAINER_SIZE, log, media, utils
from plexapi.base import OPERATORS, PlexObject, PlexPartialObject from plexapi.base import OPERATORS, PlexObject, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import LabelMixin
from plexapi.settings import Setting from plexapi.settings import Setting
from plexapi.utils import deprecated from plexapi.utils import deprecated
@ -1526,7 +1527,7 @@ class FirstCharacter(PlexObject):
@utils.registerPlexObject @utils.registerPlexObject
class Collections(PlexPartialObject): class Collections(PlexPartialObject, LabelMixin):
""" Represents a single Collection. """ Represents a single Collection.
Attributes: Attributes:

221
plexapi/mixins.py Normal file
View file

@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
class CollectionMixin(object):
""" Mixin for Plex objects that can have collections. """
def addCollection(self, collections):
""" Add a collection tag(s).
Parameters:
collections (list): List of strings.
"""
self._edit_tags('collection', collections)
def removeCollection(self, collections):
""" Remove a collection tag(s).
Parameters:
collections (list): List of strings.
"""
self._edit_tags('collection', collections, remove=True)
class CountryMixin(object):
""" Mixin for Plex objects that can have countries. """
def addCountry(self, countries):
""" Add a country tag(s).
Parameters:
countries (list): List of strings.
"""
self._edit_tags('country', countries)
def removeCountry(self, countries):
""" Remove a country tag(s).
Parameters:
countries (list): List of strings.
"""
self._edit_tags('country', countries, remove=True)
class DirectorMixin(object):
""" Mixin for Plex objects that can have directors. """
def addDirector(self, directors):
""" Add a director tag(s).
Parameters:
directors (list): List of strings.
"""
self._edit_tags('director', directors)
def removeDirector(self, directors):
""" Remove a director tag(s).
Parameters:
directors (list): List of strings.
"""
self._edit_tags('director', directors, remove=True)
class GenreMixin(object):
""" Mixin for Plex objects that can have genres. """
def addGenre(self, genres):
""" Add a genre tag(s).
Parameters:
genres (list): List of strings.
"""
self._edit_tags('genre', genres)
def removeGenre(self, genres):
""" Remove a genre tag(s).
Parameters:
genres (list): List of strings.
"""
self._edit_tags('genre', genres, remove=True)
class LabelMixin(object):
""" Mixin for Plex objects that can have labels. """
def addLabel(self, labels):
""" Add a label tag(s).
Parameters:
labels (list): List of strings.
"""
self._edit_tags('label', labels)
def removeLabel(self, labels):
""" Remove a label tag(s).
Parameters:
labels (list): List of strings.
"""
self._edit_tags('label', labels, remove=True)
class MoodMixin(object):
""" Mixin for Plex objects that can have moods. """
def addMood(self, moods):
""" Add a mood tag(s).
Parameters:
moods (list): List of strings.
"""
self._edit_tags('mood', moods)
def removeMood(self, moods):
""" Remove a mood tag(s).
Parameters:
moods (list): List of strings.
"""
self._edit_tags('mood', moods, remove=True)
class ProducerMixin(object):
""" Mixin for Plex objects that can have producers. """
def addProducer(self, producers):
""" Add a producer tag(s).
Parameters:
producers (list): List of strings.
"""
self._edit_tags('producer', producers)
def removeProducer(self, producers):
""" Remove a producer tag(s).
Parameters:
producers (list): List of strings.
"""
self._edit_tags('producer', producers, remove=True)
class SimilarArtistMixin(object):
""" Mixin for Plex objects that can have similar artists. """
def addSimilarArtist(self, artists):
""" Add a similar artist tag(s).
Parameters:
artists (list): List of strings.
"""
self._edit_tags('similar', artists)
def removeSimilarArtist(self, artists):
""" Remove a similar artist tag(s).
Parameters:
artists (list): List of strings.
"""
self._edit_tags('similar', artists, remove=True)
class StyleMixin(object):
""" Mixin for Plex objects that can have styles. """
def addStyle(self, styles):
""" Add a style tag(s).
Parameters:
styles (list): List of strings.
"""
self._edit_tags('style', styles)
def removeStyle(self, styles):
""" Remove a style tag(s).
Parameters:
styles (list): List of strings.
"""
self._edit_tags('style', styles, remove=True)
class TagMixin(object):
""" Mixin for Plex objects that can have tags. """
def addTag(self, tags):
""" Add a tag(s).
Parameters:
tags (list): List of strings.
"""
self._edit_tags('tag', tags)
def removeTag(self, tags):
""" Remove a tag(s).
Parameters:
tags (list): List of strings.
"""
self._edit_tags('tag', tags, remove=True)
class EditWriter(object):
""" Mixin for Plex objects that can have writers. """
def addWriter(self, writers):
""" Add a writer tag(s).
Parameters:
writers (list): List of strings.
"""
self._edit_tags('writer', writers)
def removeWriter(self, writers):
""" Remove a writer tag(s).
Parameters:
writers (list): List of strings.
"""
self._edit_tags('writer', writers, remove=True)

View file

@ -4,6 +4,7 @@ from urllib.parse import quote_plus
from plexapi import media, utils, video from plexapi import media, utils, video
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.mixins import TagMixin
@utils.registerPlexObject @utils.registerPlexObject
@ -136,7 +137,7 @@ class Photoalbum(PlexPartialObject):
@utils.registerPlexObject @utils.registerPlexObject
class Photo(PlexPartialObject, Playable): class Photo(PlexPartialObject, Playable, TagMixin):
""" Represents a single Photo. """ Represents a single Photo.
Attributes: Attributes:
@ -163,7 +164,7 @@ class Photo(PlexPartialObject, Playable):
parentTitle (str): Name of the photo album for the photo. parentTitle (str): Name of the photo album for the photo.
ratingKey (int): Unique key identifying the photo. ratingKey (int): Unique key identifying the photo.
summary (str): Summary of the photo. summary (str): Summary of the photo.
tag (List<:class:`~plexapi.media.Tag`>): List of tag objects. tags (List<:class:`~plexapi.media.Tag`>): List of tag objects.
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>). thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
title (str): Name of the photo. title (str): Name of the photo.
titleSort (str): Title to use when sorting (defaults to title). titleSort (str): Title to use when sorting (defaults to title).
@ -199,7 +200,7 @@ class Photo(PlexPartialObject, Playable):
self.parentTitle = data.attrib.get('parentTitle') self.parentTitle = data.attrib.get('parentTitle')
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.summary = data.attrib.get('summary') self.summary = data.attrib.get('summary')
self.tag = self.findItems(data, media.Tag) self.tags = self.findItems(data, media.Tag)
self.thumb = data.attrib.get('thumb') self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title') self.title = data.attrib.get('title')
self.titleSort = data.attrib.get('titleSort', self.title) self.titleSort = data.attrib.get('titleSort', self.title)

View file

@ -335,6 +335,15 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
return fullpath return fullpath
def tag_plural(tag):
if tag == 'country':
return 'countries'
elif tag == 'similar':
return 'similar'
else:
return tag + 's'
def tag_helper(tag, items, locked=True, remove=False): def tag_helper(tag, items, locked=True, remove=False):
""" Simple tag helper for editing a object. """ """ Simple tag helper for editing a object. """
if not isinstance(items, list): if not isinstance(items, list):

View file

@ -5,6 +5,7 @@ from urllib.parse import quote_plus, urlencode
from plexapi import library, media, settings, utils from plexapi import library, media, settings, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter
class Video(PlexPartialObject): class Video(PlexPartialObject):
@ -259,7 +260,7 @@ class Video(PlexPartialObject):
@utils.registerPlexObject @utils.registerPlexObject
class Movie(Playable, Video): class Movie(Playable, Video, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter):
""" Represents a single Movie. """ Represents a single Movie.
Attributes: Attributes:
@ -385,7 +386,7 @@ class Movie(Playable, Video):
@utils.registerPlexObject @utils.registerPlexObject
class Show(Video): class Show(Video, CollectionMixin, GenreMixin, LabelMixin):
""" Represents a single Show (including all seasons and episodes). """ Represents a single Show (including all seasons and episodes).
Attributes: Attributes:
@ -709,7 +710,7 @@ class Season(Video):
@utils.registerPlexObject @utils.registerPlexObject
class Episode(Playable, Video): class Episode(Playable, Video, DirectorMixin, EditWriter):
""" Represents a single Shows Episode. """ Represents a single Shows Episode.
Attributes: Attributes:

View file

@ -227,14 +227,14 @@ def movie(movies):
@pytest.fixture() @pytest.fixture()
def collection(plex): def collection(movies):
try: try:
return plex.library.section("Movies").collections()[0] return movies.collections()[0]
except IndexError: except IndexError:
movie = plex.library.section("Movies").get("Elephants Dream") movie = movies.get("Elephants Dream")
movie.addCollection(["marvel"]) movie.addCollection(["marvel"])
n = plex.library.section("Movies").reload() n = movies.reload()
return n.collections()[0] return n.collections()[0]

View file

@ -2,6 +2,7 @@
from datetime import datetime from datetime import datetime
from . import conftest as utils from . import conftest as utils
from . import test_mixins
def test_audio_Artist_attr(artist): def test_audio_Artist_attr(artist):
@ -63,6 +64,15 @@ def test_audio_Artist_albums(artist):
assert len(albums) == 1 and albums[0].title == "Layers" assert len(albums) == 1 and albums[0].title == "Layers"
def test_audio_Artist_mixins_tags(artist):
test_mixins.edit_collection(artist)
test_mixins.edit_country(artist)
test_mixins.edit_genre(artist)
test_mixins.edit_mood(artist)
test_mixins.edit_similar_artist(artist)
test_mixins.edit_style(artist)
def test_audio_Album_attrs(album): def test_audio_Album_attrs(album):
assert utils.is_datetime(album.addedAt) assert utils.is_datetime(album.addedAt)
assert isinstance(album.genres, list) assert isinstance(album.genres, list)
@ -211,6 +221,14 @@ def test_audio_Album_artist(album):
artist.title == "Broke For Free" artist.title == "Broke For Free"
def test_audio_Album_mixins_tags(album):
test_mixins.edit_collection(album)
test_mixins.edit_genre(album)
test_mixins.edit_label(album)
test_mixins.edit_mood(album)
test_mixins.edit_style(album)
def test_audio_Track_attrs(album): def test_audio_Track_attrs(album):
track = album.get("As Colourful As Ever").reload() track = album.get("As Colourful As Ever").reload()
assert utils.is_datetime(track.addedAt) assert utils.is_datetime(track.addedAt)
@ -328,6 +346,10 @@ def test_audio_Track_artist(album, artist):
assert tracks[0].artist() == artist assert tracks[0].artist() == artist
def test_audio_Track_mixins_tags(track):
test_mixins.edit_mood(track)
def test_audio_Audio_section(artist, album, track): def test_audio_Audio_section(artist, album, track):
assert artist.section() assert artist.section()
assert album.section() assert album.section()
@ -348,7 +370,3 @@ def test_audio_album_download(monkeydownload, album, tmpdir):
def test_audio_Artist_download(monkeydownload, artist, tmpdir): def test_audio_Artist_download(monkeydownload, artist, tmpdir):
f = artist.download(savepath=str(tmpdir)) f = artist.download(savepath=str(tmpdir))
assert len(f) == 1 assert len(f) == 1
def test_audio_Album_label(album, patched_http_call):
album.addLabel("YO")

View file

@ -4,6 +4,7 @@ import pytest
from plexapi.exceptions import NotFound from plexapi.exceptions import NotFound
from . import conftest as utils from . import conftest as utils
from . import test_mixins
def test_library_Library_section(plex): def test_library_Library_section(plex):
@ -323,6 +324,10 @@ def test_library_Collection_artUrl(collection):
assert collection.artUrl is None # Collections don't have default art assert collection.artUrl is None # Collections don't have default art
def test_library_Collection_mixins_tags(collection):
test_mixins.edit_label(collection)
def test_search_with_weird_a(plex): def test_search_with_weird_a(plex):
ep_title = "Coup de Grâce" ep_title = "Coup de Grâce"
result_root = plex.search(ep_title) result_root = plex.search(ep_title)
@ -366,6 +371,6 @@ def test_library_section_timeline(plex):
assert tl.mediaTagVersion > 1 assert tl.mediaTagVersion > 1
assert tl.thumb == "/:/resources/movie.png" assert tl.thumb == "/:/resources/movie.png"
assert tl.title1 == "Movies" assert tl.title1 == "Movies"
assert tl.updateQueueSize == 0 assert utils.is_int(tl.updateQueueSize, gte=0)
assert tl.viewGroup == "secondary" assert tl.viewGroup == "secondary"
assert tl.viewMode == 65592 assert tl.viewMode == 65592

58
tests/test_mixins.py Normal file
View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
TEST_MIXIN_TAG = "Test Tag"
def _test_mixins_tag(obj, attr, tag_method):
add_tag_method = getattr(obj, 'add' + tag_method)
remove_tag_method = getattr(obj, 'remove' + tag_method)
assert TEST_MIXIN_TAG not in [tag.tag for tag in getattr(obj, attr)]
add_tag_method(TEST_MIXIN_TAG)
obj.reload()
assert TEST_MIXIN_TAG in [tag.tag for tag in getattr(obj, attr)]
remove_tag_method(TEST_MIXIN_TAG)
obj.reload()
assert TEST_MIXIN_TAG not in [tag.tag for tag in getattr(obj, attr)]
def edit_collection(obj):
_test_mixins_tag(obj, 'collections', 'Collection')
def edit_country(obj):
_test_mixins_tag(obj, 'countries', 'Country')
def edit_director(obj):
_test_mixins_tag(obj, 'directors', 'Director')
def edit_genre(obj):
_test_mixins_tag(obj, 'genres', 'Genre')
def edit_label(obj):
_test_mixins_tag(obj, 'labels', 'Label')
def edit_mood(obj):
_test_mixins_tag(obj, 'moods', 'Mood')
def edit_producer(obj):
_test_mixins_tag(obj, 'producers', 'Producer')
def edit_similar_artist(obj):
_test_mixins_tag(obj, 'similar', 'SimilarArtist')
def edit_style(obj):
_test_mixins_tag(obj, 'styles', 'Style')
def edit_tag(obj):
_test_mixins_tag(obj, 'tags', 'Tag')
def edit_writer(obj):
_test_mixins_tag(obj, 'writers', 'Writer')

View file

@ -8,6 +8,7 @@ import pytest
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from . import conftest as utils from . import conftest as utils
from . import test_mixins
def test_video_Movie(movies, movie): def test_video_Movie(movies, movie):
@ -39,16 +40,14 @@ def test_video_Movie_merge(movie, patched_http_call):
movie.merge(1337) movie.merge(1337)
def test_video_Movie_addCollection(movie): def test_video_Movie_mixins_tags(movie):
labelname = "Random_label" test_mixins.edit_collection(movie)
org_collection = [tag.tag for tag in movie.collections if tag] test_mixins.edit_country(movie)
assert labelname not in org_collection test_mixins.edit_director(movie)
movie.addCollection(labelname) test_mixins.edit_genre(movie)
movie.reload() test_mixins.edit_label(movie)
assert labelname in [tag.tag for tag in movie.collections if tag] test_mixins.edit_producer(movie)
movie.removeCollection(labelname) test_mixins.edit_writer(movie)
movie.reload()
assert labelname not in [tag.tag for tag in movie.collections if tag]
def test_video_Movie_getStreamURL(movie, account): def test_video_Movie_getStreamURL(movie, account):
@ -723,6 +722,12 @@ def test_video_Show_section(show):
assert section.title == "TV Shows" assert section.title == "TV Shows"
def test_video_Show_mixins_tags(show):
test_mixins.edit_collection(show)
test_mixins.edit_genre(show)
test_mixins.edit_label(show)
def test_video_Episode(show): def test_video_Episode(show):
episode = show.episode("Winter Is Coming") episode = show.episode("Winter Is Coming")
assert episode == show.episode(season=1, episode=1) assert episode == show.episode(season=1, episode=1)
@ -813,6 +818,11 @@ def test_video_Episode_attrs(episode):
assert part.accessible assert part.accessible
def test_video_Episode_mixins_tags(episode):
test_mixins.edit_director(episode)
test_mixins.edit_writer(episode)
def test_video_Season(show): def test_video_Season(show):
seasons = show.seasons() seasons = show.seasons()
assert len(seasons) == 2 assert len(seasons) == 2