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.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
class Audio(PlexPartialObject):
@ -123,7 +124,7 @@ class Audio(PlexPartialObject):
@utils.registerPlexObject
class Artist(Audio):
class Artist(Audio, CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
""" Represents a single Artist.
Attributes:
@ -226,7 +227,7 @@ class Artist(Audio):
@utils.registerPlexObject
class Album(Audio):
class Album(Audio, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
""" Represents a single Album.
Attributes:
@ -332,7 +333,7 @@ class Album(Audio):
@utils.registerPlexObject
class Track(Audio, Playable):
class Track(Audio, Playable, MoodMixin):
""" Represents a single Track.
Attributes:

View file

@ -5,7 +5,7 @@ from urllib.parse import quote_plus, urlencode
from plexapi import log, utils
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']
OPERATORS = {
@ -462,40 +462,12 @@ class PlexPartialObject(PlexObject):
"""
if not isinstance(items, list):
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]
d = tag_helper(tag, existing_cols + items, locked, remove)
self.edit(**d)
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):
""" 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

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 LabelMixin
from plexapi.settings import Setting
from plexapi.utils import deprecated
@ -1526,7 +1527,7 @@ class FirstCharacter(PlexObject):
@utils.registerPlexObject
class Collections(PlexPartialObject):
class Collections(PlexPartialObject, LabelMixin):
""" Represents a single Collection.
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.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest
from plexapi.mixins import TagMixin
@utils.registerPlexObject
@ -136,7 +137,7 @@ class Photoalbum(PlexPartialObject):
@utils.registerPlexObject
class Photo(PlexPartialObject, Playable):
class Photo(PlexPartialObject, Playable, TagMixin):
""" Represents a single Photo.
Attributes:
@ -163,7 +164,7 @@ class Photo(PlexPartialObject, Playable):
parentTitle (str): Name of the photo album for the photo.
ratingKey (int): Unique key identifying 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>).
title (str): Name of the photo.
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.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
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.title = data.attrib.get('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
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):
""" Simple tag helper for editing a object. """
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.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter
class Video(PlexPartialObject):
@ -259,7 +260,7 @@ class Video(PlexPartialObject):
@utils.registerPlexObject
class Movie(Playable, Video):
class Movie(Playable, Video, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter):
""" Represents a single Movie.
Attributes:
@ -385,7 +386,7 @@ class Movie(Playable, Video):
@utils.registerPlexObject
class Show(Video):
class Show(Video, CollectionMixin, GenreMixin, LabelMixin):
""" Represents a single Show (including all seasons and episodes).
Attributes:
@ -709,7 +710,7 @@ class Season(Video):
@utils.registerPlexObject
class Episode(Playable, Video):
class Episode(Playable, Video, DirectorMixin, EditWriter):
""" Represents a single Shows Episode.
Attributes:

View file

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

View file

@ -2,6 +2,7 @@
from datetime import datetime
from . import conftest as utils
from . import test_mixins
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"
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):
assert utils.is_datetime(album.addedAt)
assert isinstance(album.genres, list)
@ -211,6 +221,14 @@ def test_audio_Album_artist(album):
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):
track = album.get("As Colourful As Ever").reload()
assert utils.is_datetime(track.addedAt)
@ -328,6 +346,10 @@ def test_audio_Track_artist(album, 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):
assert artist.section()
assert album.section()
@ -348,7 +370,3 @@ def test_audio_album_download(monkeydownload, album, tmpdir):
def test_audio_Artist_download(monkeydownload, artist, tmpdir):
f = artist.download(savepath=str(tmpdir))
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 . import conftest as utils
from . import test_mixins
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
def test_library_Collection_mixins_tags(collection):
test_mixins.edit_label(collection)
def test_search_with_weird_a(plex):
ep_title = "Coup de Grâce"
result_root = plex.search(ep_title)
@ -366,6 +371,6 @@ def test_library_section_timeline(plex):
assert tl.mediaTagVersion > 1
assert tl.thumb == "/:/resources/movie.png"
assert tl.title1 == "Movies"
assert tl.updateQueueSize == 0
assert utils.is_int(tl.updateQueueSize, gte=0)
assert tl.viewGroup == "secondary"
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 . import conftest as utils
from . import test_mixins
def test_video_Movie(movies, movie):
@ -39,16 +40,14 @@ def test_video_Movie_merge(movie, patched_http_call):
movie.merge(1337)
def test_video_Movie_addCollection(movie):
labelname = "Random_label"
org_collection = [tag.tag for tag in movie.collections if tag]
assert labelname not in org_collection
movie.addCollection(labelname)
movie.reload()
assert labelname in [tag.tag for tag in movie.collections if tag]
movie.removeCollection(labelname)
movie.reload()
assert labelname not in [tag.tag for tag in movie.collections if tag]
def test_video_Movie_mixins_tags(movie):
test_mixins.edit_collection(movie)
test_mixins.edit_country(movie)
test_mixins.edit_director(movie)
test_mixins.edit_genre(movie)
test_mixins.edit_label(movie)
test_mixins.edit_producer(movie)
test_mixins.edit_writer(movie)
def test_video_Movie_getStreamURL(movie, account):
@ -723,6 +722,12 @@ def test_video_Show_section(show):
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):
episode = show.episode("Winter Is Coming")
assert episode == show.episode(season=1, episode=1)
@ -813,6 +818,11 @@ def test_video_Episode_attrs(episode):
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):
seasons = show.seasons()
assert len(seasons) == 2