resources/lib/common/videoid.py
# -*- coding: utf-8 -*-
"""
Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
Copyright (C) 2018 Caphm (original implementation module)
Universal representation of VideoIds
SPDX-License-Identifier: MIT
See LICENSES/MIT.md for more information.
"""
from functools import wraps
from .exceptions import InvalidVideoId, ErrorMsg
class VideoId:
"""Universal representation of a video id. Video IDs can be of multiple types:
- supplemental: a single identifier only for supplementalid, all other values must be None
- movie: a single identifier only for movieid, all other values must be None
- show: a single identifier only for tvshowid, all other values must be None
- season: identifiers for seasonid and tvshowid, all other values must be None
- episode: identifiers for episodeid, seasonid and tvshowid, all other values must be None
- unspecified: a single identifier only for videoid, all other values must be None"""
SUPPLEMENTAL = 'supplemental'
MOVIE = 'movie'
SHOW = 'show'
SEASON = 'season'
EPISODE = 'episode'
UNSPECIFIED = 'unspecified'
TV_TYPES = [SHOW, SEASON, EPISODE]
VALIDATION_MASKS = {
0b100000: UNSPECIFIED,
0b010000: SUPPLEMENTAL,
0b001000: MOVIE,
0b000001: SHOW,
0b000011: SEASON,
0b000111: EPISODE
}
def __init__(self, **kwargs):
self._id_values = _get_unicode_kwargs(kwargs)
# debug('VideoId validation values: {}'.format(self._id_values))
self._validate()
def _validate(self):
validation_mask = 0
# Example: ('39c9a88a-a56e-4c8a-921c-3c1f86c0ebb9_62682962X28X6548X1551537755876', None, None, None, None)
# This result in a VALIDATION_MASKS 'unspecified'. Because text data is on index 0, and others are None
for index, value in enumerate(self._id_values):
validation_mask |= (value is not None) << (5 - index)
try:
self._mediatype = VideoId.VALIDATION_MASKS[validation_mask]
except KeyError as exc:
raise InvalidVideoId from exc
@classmethod
def from_path(cls, pathitems):
"""Create a VideoId instance from pathitems"""
if pathitems[0] == VideoId.MOVIE:
return cls(movieid=pathitems[1])
if pathitems[0] == VideoId.SHOW:
return cls(tvshowid=_path_attr(pathitems, 1),
seasonid=_path_attr(pathitems, 3),
episodeid=_path_attr(pathitems, 5))
if pathitems[0] == VideoId.SUPPLEMENTAL:
return cls(supplementalid=pathitems[1])
return cls(videoid=pathitems[0])
@classmethod
def from_dict(cls, dict_items):
"""Create a VideoId instance from a dict items"""
mediatype = dict_items['mediatype']
if mediatype == VideoId.MOVIE:
return cls(movieid=dict_items['movieid'])
if mediatype in VideoId.TV_TYPES:
return cls(tvshowid=_path_attr_dict(dict_items, 'tvshowid'),
seasonid=_path_attr_dict(dict_items, 'seasonid'),
episodeid=_path_attr_dict(dict_items, 'episodeid'))
if mediatype == VideoId.SUPPLEMENTAL:
return cls(supplementalid=dict_items['supplementalid'])
raise InvalidVideoId
@classmethod
def from_videolist_item(cls, video):
"""Create a VideoId from a video item contained in a video list path response"""
mediatype = video['summary']['value']['type']
video_id = video['summary']['value']['id']
if mediatype == VideoId.MOVIE:
return cls(movieid=video_id)
if mediatype == VideoId.SHOW:
return cls(tvshowid=video_id)
if mediatype == VideoId.SUPPLEMENTAL:
return cls(supplementalid=video_id)
raise InvalidVideoId(
'Can only construct a VideoId from a show/movie/supplemental item')
@property
def value(self):
"""The value of this VideoId"""
return self._assigned_id_values()[0]
@property
def videoid(self):
"""The videoid value, if it exists"""
return self._id_values[0]
@property
def supplementalid(self):
"""The supplemental value, if it exists"""
return self._id_values[1]
@property
def movieid(self):
"""The movieid value, if it exists"""
return self._id_values[2]
@property
def episodeid(self):
"""The episodeid value, if it exists"""
return self._id_values[3]
@property
def seasonid(self):
"""The seasonid value, if it exists"""
return self._id_values[4]
@property
def tvshowid(self):
"""The tvshowid value, if it exists"""
return self._id_values[5]
@property
def mediatype(self):
"""The mediatype this VideoId instance represents.
Either movie, show, season, episode, supplemental or unspecified"""
return self._mediatype
def convert_old_videoid_type(self):
"""
If the data contained in to videoid comes from the previous version
of the videoid class (without supplementalid), then convert the class object
to the new type of class or else return same class
"""
if len(self._id_values) == 5:
videoid_dict = {
'mediatype': self._mediatype,
'movieid': self._id_values[1],
'episodeid': self._id_values[2],
'seasonid': self._id_values[3],
'tvshowid': self._id_values[4]
}
return VideoId.from_dict(videoid_dict)
return self
def to_string(self):
"""Generate a valid pathitems as string ('show'/tvshowid/...) from this instance"""
if self.videoid:
return self.videoid
if self.movieid:
return '/'.join([self.MOVIE, self.movieid])
if self.supplementalid:
return '/'.join([self.SUPPLEMENTAL, self.supplementalid])
pathitems = [self.SHOW, self.tvshowid]
if self.seasonid:
pathitems.extend([self.SEASON, self.seasonid])
if self.episodeid:
pathitems.extend([self.EPISODE, self.episodeid])
return '/'.join(pathitems)
def to_path(self):
"""Generate a valid pathitems list (['show', tvshowid, ...]) from
this instance"""
if self.videoid:
return [self.videoid]
if self.movieid:
return [self.MOVIE, self.movieid]
if self.supplementalid:
return [self.SUPPLEMENTAL, self.supplementalid]
pathitems = [self.SHOW, self.tvshowid]
if self.seasonid:
pathitems.extend([self.SEASON, self.seasonid])
if self.episodeid:
pathitems.extend([self.EPISODE, self.episodeid])
return pathitems
def to_list(self):
"""Generate a list representation that can be used with get_path"""
path = self._assigned_id_values()
if len(path) > 1:
path.reverse()
return path
def to_dict(self):
"""Return a dict containing the relevant properties of this
instance"""
result = {'mediatype': self.mediatype}
result.update({prop: getattr(self, prop)
for prop in ['videoid', 'supplementalid', 'movieid',
'tvshowid', 'seasonid', 'episodeid']
if getattr(self, prop, None) is not None})
return result
def derive_season(self, seasonid):
"""Return a new VideoId instance that represents the given season
of this show. Raises InvalidVideoId is this instance does not
represent a show."""
if self.mediatype != VideoId.SHOW:
raise InvalidVideoId(f'Cannot derive season VideoId from {self}')
return type(self)(tvshowid=self.tvshowid, seasonid=str(seasonid))
def derive_episode(self, episodeid):
"""Return a new VideoId instance that represents the given episode
of this season. Raises InvalidVideoId is this instance does not
represent a season."""
if self.mediatype != VideoId.SEASON:
raise InvalidVideoId(f'Cannot derive episode VideoId from {self}')
return type(self)(tvshowid=self.tvshowid, seasonid=self.seasonid,
episodeid=str(episodeid))
def derive_parent(self, videoid_type):
"""
Derive a parent VideoId, you can obtain:
[tvshow] from season, episode
[season] from episode
When it is not possible get a derived VideoId, it is returned the same VideoId instance.
:param videoid_type: The type of VideoId to be derived
:return: The parent VideoId of specified type, or when not match the same VideoId instance.
"""
if videoid_type == VideoId.SHOW:
if self.mediatype not in [VideoId.SEASON, VideoId.EPISODE]:
return self
return type(self)(tvshowid=self.tvshowid)
if videoid_type == VideoId.SEASON:
if self.mediatype != VideoId.SEASON:
return self
return type(self)(tvshowid=self.tvshowid,
seasonid=self.seasonid)
raise InvalidVideoId(f'VideoId type {videoid_type} not valid')
def _assigned_id_values(self):
"""Return a list of all id_values that are not None"""
return [id_value
for id_value in self._id_values
if id_value is not None]
def __str__(self):
return f'{self.mediatype}_{self.value}'
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
# pylint: disable=protected-access
if not isinstance(other, VideoId):
return False
return self._id_values == other._id_values
def __neq__(self, other):
return not self.__eq__(other)
def __repr__(self):
return f'VideoId object [{self}]'
def _get_unicode_kwargs(kwargs):
# Example of return value: (None, None, '70084801', None, None, None) this is a movieid
return tuple((str(kwargs[idpart])
if kwargs.get(idpart)
else None)
for idpart
in ['videoid', 'supplementalid', 'movieid',
'episodeid', 'seasonid', 'tvshowid'])
def _path_attr(pathitems, index):
return pathitems[index] if len(pathitems) > index else None
def _path_attr_dict(pathitems, key):
return pathitems[key] if key in pathitems else None
def inject_video_id(path_offset, pathitems_arg='pathitems',
inject_remaining_pathitems=False,
inject_full_pathitems=False):
"""Decorator that converts a pathitems argument into a VideoId
and injects this into the decorated function instead. Pathitems
that are to be converted into a video id must be passed into
the function via kwarg defined by pathitems_arg (default=pathitems)"""
# pylint: disable=missing-docstring
def injecting_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
_path_to_videoid(kwargs, pathitems_arg, path_offset,
inject_remaining_pathitems, inject_full_pathitems)
except KeyError as exc:
raise ErrorMsg(f'Pathitems must be passed as kwarg {pathitems_arg}') from exc
return func(*args, **kwargs)
return wrapper
return injecting_decorator
def _path_to_videoid(kwargs, pathitems_arg, path_offset,
inject_remaining_pathitems, inject_full_pathitems):
"""Parses a VideoId from the kwarg with name defined by pathitems_arg and
adds it to the kwargs dict.
If inject_remaining_pathitems is True, the pathitems representing the
VideoId are stripped from the end of the pathitems and the remaining
pathitems remain in kwargs. Otherwise, the pathitems will be removed
from the kwargs dict."""
kwargs['videoid'] = VideoId.from_path(kwargs[pathitems_arg][path_offset:])
if inject_remaining_pathitems or inject_full_pathitems:
if inject_remaining_pathitems:
kwargs[pathitems_arg] = kwargs[pathitems_arg][:path_offset]
else:
del kwargs[pathitems_arg]