From aeb0bfbe62fad4b06c164f1b95581da7f35dce0b Mon Sep 17 00:00:00 2001 From: rg9400 <39887349+rg9400@users.noreply.github.com> Date: Mon, 31 Aug 2020 15:29:06 -0500 Subject: [PATCH] use external IDs from new agent if available (only for scrobbling) (#571) * use external IDs from new agent if available (only for scrobbling for now) * create taggings DB model (prep for sync) * create tags DB model (prep for sync) * (sync support) first attempt to make DB use external IDs, fallback to guid * some fixes (still broken for sync) * Sync now working for movies (TV groundwork still pending) * Finished sync preparations (hopefully) for Native TV agent * Update README to include min reqs for native agents --- README.md | 4 + .../Shared/plex/objects/library/extra/guid.py | 9 ++ .../plex/objects/library/metadata/episode.py | 10 ++ .../plex/objects/library/metadata/movie.py | 10 ++ .../plex/objects/library/metadata/season.py | 11 ++ .../plex/objects/library/metadata/show.py | 10 ++ .../Libraries/Shared/plex_database/library.py | 100 +++++++++++++++--- .../Shared/plex_database/models/__init__.py | 2 + .../Shared/plex_database/models/taggings.py | 15 +++ .../Shared/plex_database/models/tags.py | 13 +++ .../Shared/plex_metadata/agents/entries.py | 21 ++++ 11 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/extra/guid.py create mode 100644 Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/taggings.py create mode 100644 Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/tags.py diff --git a/README.md b/README.md index bfa55029b..c0bfe3715 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # Trakt.tv (for Plex) +Now compatible with the Native Plex Movie Agent (should also be compatible with the upcoming Native Plex TV Agent). If you use the new agents, you require at least PMS version 1.20.1.3213 or higher, and you have to do a metadata refresh on the entire library after upgrading to this version. This plugin should be fully backwards compatible with the earlier supported agents. + +I cannot support other issues with the plugin outside of the new agent support. + [![](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat-square)][license] [![](https://img.shields.io/requires/github/fuzeman/Plex-Trakt-Scrobbler.svg?style=flat-square)][requires.io] [![](https://img.shields.io/scrutinizer/build/g/fuzeman/Plex-Trakt-Scrobbler.svg?style=flat-square)][scrutinizer] [![](https://img.shields.io/scrutinizer/g/fuzeman/Plex-Trakt-Scrobbler.svg?style=flat-square)][scrutinizer] [![](https://img.shields.io/scrutinizer/coverage/g/fuzeman/Plex-Trakt-Scrobbler.svg?style=flat-square)][scrutinizer] [![](https://img.shields.io/gitter/room/trakt/Plex-Trakt-Scrobbler.svg?style=social)][gitter.im] diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/extra/guid.py b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/extra/guid.py new file mode 100644 index 000000000..46275bd47 --- /dev/null +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/extra/guid.py @@ -0,0 +1,9 @@ +from plex.objects.core.base import Descriptor, Property + + +class Guid(Descriptor): + id = Property(type=str) + + @classmethod + def from_node(cls, client, node): + return cls.construct(client, cls.helpers.find(node, 'Guid'), child=True) diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/episode.py b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/episode.py index b9817da32..bdc414db5 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/episode.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/episode.py @@ -1,4 +1,5 @@ from plex.objects.core.base import Property +from plex.objects.library.extra.guid import Guid from plex.objects.library.metadata.season import Season from plex.objects.library.metadata.show import Show from plex.objects.library.metadata.base import Metadata @@ -11,6 +12,15 @@ class Episode(Video, Metadata, PlaylistItemMixin, RateMixin, ScrobbleMixin): show = Property(resolver=lambda: Episode.construct_show) season = Property(resolver=lambda: Episode.construct_season) + guids = Property(resolver=lambda: Guid.from_node) + agent_guid = Property('guid') + + @property + def guid(self): + try: + return self.guids.id + except: + return self.agent_guid index = Property(type=int) absolute_index = Property('absoluteIndex', int) diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/movie.py b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/movie.py index 9a379e887..688efa6ed 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/movie.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/movie.py @@ -2,6 +2,7 @@ from plex.objects.library.extra.country import Country from plex.objects.library.extra.genre import Genre from plex.objects.library.extra.role import Role +from plex.objects.library.extra.guid import Guid from plex.objects.library.metadata.base import Metadata from plex.objects.library.video import Video from plex.objects.mixins.playlist_item import PlaylistItemMixin @@ -13,6 +14,15 @@ class Movie(Video, Metadata, PlaylistItemMixin, RateMixin, ScrobbleMixin): country = Property(resolver=lambda: Country.from_node) genres = Property(resolver=lambda: Genre.from_node) roles = Property(resolver=lambda: Role.from_node) + guids = Property(resolver=lambda: Guid.from_node) + agent_guid = Property('guid') + + @property + def guid(self): + try: + return self.guids.id + except: + return self.agent_guid def __repr__(self): return '' % (self.title, self.year) diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/season.py b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/season.py index 16e559da6..9d663fa7e 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/season.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/season.py @@ -1,4 +1,5 @@ from plex.objects.core.base import Property +from plex.objects.library.extra.guid import Guid from plex.objects.library.container import ChildrenContainer from plex.objects.library.metadata.show import Show from plex.objects.library.metadata.base import Metadata @@ -11,6 +12,16 @@ class Season(Directory, Metadata, RateMixin): index = Property(type=int) + guids = Property(resolver=lambda: Guid.from_node) + agent_guid = Property('guid') + + @property + def guid(self): + try: + return self.guids.id + except: + return self.agent_guid + banner = Property theme = Property diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/show.py b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/show.py index e06cd0e00..768a3bdfa 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/show.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex/objects/library/metadata/show.py @@ -1,4 +1,5 @@ from plex.objects.core.base import Property +from plex.objects.library.extra.guid import Guid from plex.objects.directory import Directory from plex.objects.library.container import LeavesContainer, ChildrenContainer from plex.objects.library.metadata.base import Metadata @@ -8,6 +9,15 @@ class Show(Directory, Metadata, RateMixin): index = Property(type=int) duration = Property(type=int) + guids = Property(resolver=lambda: Guid.from_node) + agent_guid = Property('guid') + + @property + def guid(self): + try: + return self.guids.id + except: + return self.agent_guid banner = Property theme = Property diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex_database/library.py b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/library.py index aaaa012aa..64ab648d2 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex_database/library.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/library.py @@ -280,9 +280,24 @@ def mapped(self, sections, fields=None, account=None, where=None, parse_guid=Fal if fields is None: fields = [] + # Build subquery + subq = (Taggings + .select( + Tags.tag_type, + Tags.tag, + Taggings.metadata_item + ) + .join(Tags, on=(Tags.id == Taggings.tag).alias('taggings')) + .where(Tags.tag_type == 314) + .order_by(Tags.id.asc()) + .switch(Taggings) + .alias('subq') + ) + fields = [ MetadataItem.id, MetadataItem.guid, + subq.c.tag, MediaPart.duration, MediaPart.file, @@ -304,6 +319,8 @@ def mapped(self, sections, fields=None, account=None, where=None, parse_guid=Fal # Build query query = (MetadataItem.select(*fields) + .join(subq, JOIN_LEFT_OUTER, on=(subq.c.metadata_item_id == MetadataItem.id).alias('tags')) + .switch(MetadataItem) .join(MediaItem, on=(MediaItem.metadata_item == MetadataItem.id).alias('media')) .join(MediaPart, on=(MediaPart.media_item == MediaItem.id).alias('part')) .switch(MetadataItem) @@ -318,7 +335,9 @@ def mapped(self, sections, fields=None, account=None, where=None, parse_guid=Fal query = self._join(query, models, account, [ MetadataItemSettings, MediaItem, - MediaPart + MediaPart, + Taggings, + Tags ]) # Apply `WHERE` filter @@ -331,13 +350,15 @@ def mapped(self, sections, fields=None, account=None, where=None, parse_guid=Fal def movies_iterator(): for row in self._tuple_iterator(query): - id, guid, movie = self._parse(fields, row, offset=2) + id, guid, tag, movie = self._parse(fields, row, offset=3) # Parse `guid` (if enabled, and not already parsed) if parse_guid: if id not in guids: - guids[id] = Guid.parse(guid, strict=True) - + if tag: + guids[id] = Guid.parse(tag, strict=True) + else: + guids[id] = Guid.parse(guid, strict=True) guid = guids[id] # Return item @@ -351,23 +372,66 @@ def __call__(self, sections, fields=None, account=None, where=None): # Set default `select()` fields if fields is None: fields = [] + + subq = (Taggings + .select( + Tags.tag_type, + Tags.tag, + Taggings.metadata_item + ) + .join(Tags, on=(Tags.id == Taggings.tag).alias('taggings')) + .where(Tags.tag_type == 314) + .order_by(Tags.id.asc()) + .switch(Taggings) + .alias('subq') + ) fields = [ MetadataItem.id, - MetadataItem.guid + MetadataItem.guid, + subq.c.tag ] + fields - # Build query - query = self.query( - sections, - fields=fields, - account=account, - where=where + query = (MetadataItem.select(*fields) + .join(subq, JOIN_LEFT_OUTER, on=(subq.c.metadata_item_id == MetadataItem.id).alias('tags')) + .switch(MetadataItem) + ) + + if account and type(account) is Account: + account = account.id + + # Map `Section` list to ids + section_ids = [id for (id, ) in sections] + + # Build `where()` query + if where is None: + where = [] + + where += [ + MetadataItem.library_section << section_ids, + MetadataItem.metadata_type == MetadataItemType.Show + ] + + # Join settings + query = self._join_settings(query, account, MetadataItem) + + # Join extra models + models = self._models(fields, account) + + query = self._join(query, models, account, [ + MetadataItemSettings, + Taggings, + Tags + ]) + + # Apply `WHERE` filter + query = query.where( + *where ) # Parse rows return [ - self._parse(fields, row, offset=2) + self._parse(fields, row, offset=3) for row in self._tuple_iterator(query) ] @@ -627,12 +691,16 @@ def mapped_shows(self, sections, fields=None, account=None): # Retrieve shows shows = Library.shows(sections, fields, account) + output = {} + for (id, guid, tag, show) in shows: + if id not in output: + if tag: + output[id] = (tag, show) + else: + output[id] = (guid, show) # Map shows by `id` - return dict([ - (id, (guid, show)) - for (id, guid, show) in shows - ]) + return output def mapped_seasons(self, sections, fields=None, account=None): # Parse `fields` diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/__init__.py b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/__init__.py index 8f5d95120..36a55b78e 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/__init__.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/__init__.py @@ -5,6 +5,8 @@ from plex_database.models.media_part import MediaPart from plex_database.models.metadata_item import MetadataItem, MetadataItemType from plex_database.models.metadata_item_settings import MetadataItemSettings +from plex_database.models.tags import Tags +from plex_database.models.taggings import Taggings # Model aliases Season = MetadataItem.alias() diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/taggings.py b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/taggings.py new file mode 100644 index 000000000..ffc451e3d --- /dev/null +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/taggings.py @@ -0,0 +1,15 @@ +from plex_database.core import db + +from peewee import * +from plex_database.models import MetadataItem +from plex_database.models import Tags + + +class Taggings(Model): + class Meta: + database = db + db_table = 'taggings' + + metadata_item = ForeignKeyField(MetadataItem, null=True, related_name='taggings') + tag = ForeignKeyField(Tags, null=True, related_name='taggings') + diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/tags.py b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/tags.py new file mode 100644 index 000000000..1f098aa7a --- /dev/null +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex_database/models/tags.py @@ -0,0 +1,13 @@ +from plex_database.core import db + +from peewee import * + + +class Tags(Model): + class Meta: + database = db + db_table = 'tags' + + tag = CharField(null=True) + tag_type = IntegerField(null=True) + diff --git a/Trakttv.bundle/Contents/Libraries/Shared/plex_metadata/agents/entries.py b/Trakttv.bundle/Contents/Libraries/Shared/plex_metadata/agents/entries.py index c0f7daa38..d417ed982 100644 --- a/Trakttv.bundle/Contents/Libraries/Shared/plex_metadata/agents/entries.py +++ b/Trakttv.bundle/Contents/Libraries/Shared/plex_metadata/agents/entries.py @@ -170,6 +170,27 @@ 'type': int }, + # + # Native Agents + # + + 'imdb': { + 'media': ['movie', 'show', 'season', 'episode'], + 'service': 'imdb' + }, + + 'tmdb': { + 'media': ['movie', 'show', 'season', 'episode'], + 'service': 'tmdb', + 'type': int + }, + + 'tvdb': { + 'media': ['show', 'season', 'episode'], + 'service': 'tvdb', + 'type': int + }, + # # Misc #