diff --git a/.gitignore b/.gitignore index bee8a64..28424e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ __pycache__ +venv/ +plugin.video.stash.zip diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 4e3cc78..060c49f 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -64,3 +64,16 @@ msgstr "" msgctxt "#30012" msgid "Scene Tags" msgstr "" + + +msgctxt "#30013" +msgid "Server Settings" +msgstr "" + +msgctxt "#30014" +msgid "Playback Settings" +msgstr "" + +msgctxt "#30015" +msgid "Max Resolution" +msgstr "" \ No newline at end of file diff --git a/resources/lib/criterion_parser.py b/resources/lib/criterion_parser.py index d2a9992..8080ff7 100644 --- a/resources/lib/criterion_parser.py +++ b/resources/lib/criterion_parser.py @@ -1,4 +1,9 @@ import json +from resources.lib.utils.resolutions import Resolution_map + +""" elif "resolution" == criterion: + val = criterion["resolution"]["modifier"]["value"] + criterion["resolution"]["modifier"]["value"] = Resolution_map[val] """ def parse(criterions): @@ -8,9 +13,15 @@ def parse(criterions): if criterion in ('sceneIsMissing', 'imageIsMissing', 'performerIsMissing', 'galleryIsMissing', 'tagIsMissing', 'studioIsMissing', 'studioIsMissing'): filter['is_missing'] = criterion['value'] else: - is_timestamp_field = criterion in ('created_at', 'updated_at', 'scene_created_at', 'scene_updated_at') - value_transformer = (lambda v: v.replace(' ', 'T') if isinstance(v, str) else v) if is_timestamp_field else lambda v: v - filter[criterion] = parse_criterion(criterions[criterion], value_transformer) + if "resolution" == criterion: + val = criterions[criterion]["value"] + criterions[criterion]["value"] = Resolution_map[val] + is_timestamp_field = criterion in ( + 'created_at', 'updated_at', 'scene_created_at', 'scene_updated_at') + value_transformer = (lambda v: v.replace(' ', 'T') if isinstance( + v, str) else v) if is_timestamp_field else lambda v: v + filter[criterion] = parse_criterion( + criterions[criterion], value_transformer) return filter @@ -21,12 +32,14 @@ def parse_criterion(criterion, value_transformer): filter['modifier'] = criterion['modifier'] value = criterion.get('value', '') + if isinstance(value, dict) and not value.keys() - ['items', 'excluded', 'depth']: if value.get('items') is not None: filter['value'] = list(map(lambda v: v['id'], value['items'])) if value.get('excluded') is not None: - filter['excludes'] = list(map(lambda v: v['id'], value['excluded'])) + filter['excludes'] = list( + map(lambda v: v['id'], value['excluded'])) if value.get('depth') is not None: filter['depth'] = value['depth'] diff --git a/resources/lib/listing/listing.py b/resources/lib/listing/listing.py index 27cc192..1df52a6 100644 --- a/resources/lib/listing/listing.py +++ b/resources/lib/listing/listing.py @@ -22,7 +22,8 @@ def __init__(self, client: StashInterface, type: str, label: str, **kwargs): def list_items(self, params: dict): title = params['title'] if 'title' in params else self._label - criterion = json.loads(params['criterion']) if 'criterion' in params else {} + criterion = json.loads( + params['criterion']) if 'criterion' in params else {} sort_field = params['sort_field'] if 'sort_field' in params else None sort_dir = params['sort_dir'] if 'sort_dir' in params else 'asc' @@ -36,7 +37,8 @@ def list_items(self, params: dict): xbmcplugin.endOfDirectory(self.handle) def get_root_item(self, override_title: str = "") -> (xbmcgui.ListItem, str): - item = xbmcgui.ListItem(label=override_title if override_title != "" else self._label) + item = xbmcgui.ListItem( + label=override_title if override_title != "" else self._label) url = utils.get_url(list=self._type) return item, url @@ -48,7 +50,8 @@ def get_filters(self) -> [(xbmcgui.ListItem, str)]: items = [] default_filter = self._client.find_default_filter(self._filter_type) if default_filter is not None: - items.append(self._create_item_from_filter(default_filter, local.get_localized(30007))) + items.append(self._create_item_from_filter( + default_filter, local.get_localized(30007))) else: item = xbmcgui.ListItem(label=local.get_localized(30007)) url = utils.get_url(list=self._type) @@ -77,30 +80,39 @@ def _create_item(self, scene: dict, **kwargs): title = kwargs['title'] if 'title' in kwargs else scene['title'] screenshot = kwargs['screenshot'] if 'screenshot' in kwargs else scene['paths']['screenshot'] # * 2 because rating is 1 to 5 and Kodi uses 1 to 10 - rating = scene['rating'] * 2 if 'rating' in scene and scene['rating'] is not None else 0 - duration = int(scene['file']['duration']) + rating = scene['rating'] * \ + 2 if 'rating' in scene and scene['rating'] is not None else 0 + duration = int(scene['files'][0]['duration']) item = xbmcgui.ListItem(label=title) item.setInfo('video', {'title': title, 'mediatype': 'video', 'plot': scene['details'], 'cast': list(map(lambda p: p['name'], scene['performers'])), 'duration': duration, + 'playcount': scene.get("play_count", 0), 'studio': scene['studio']['name'] if scene['studio'] is not None else None, 'userrating': rating, 'premiered': scene['date'], 'tag': list(map(lambda t: t['name'], scene['tags'])), - 'dateadded': scene['created_at'] + 'dateadded': scene['created_at'], + 'lastplayed': scene["last_played_at"] }) - item.addStreamInfo('video', {'codec': scene['file']['video_codec'], - 'width': scene['file']['width'], - 'height': scene['file']['height'], + item.addStreamInfo('video', {'codec': scene['files'][0]['video_codec'], + 'width': scene['files'][0]['width'], + 'height': scene['files'][0]['height'], 'duration': duration}) - item.addStreamInfo('audio', {'codec': scene['file']['audio_codec']}) + item.addStreamInfo( + 'audio', {'codec': scene['files'][0]['audio_codec']}) screenshot = self._client.add_api_key(screenshot) - item.setArt({'thumb': screenshot, 'fanart': screenshot}) + art_dict = {'thumb': screenshot, 'fanart': screenshot} + if scene['studio']: + art_dict["banner"] = scene["studio"]["image_path"] + item.setArt(art_dict) + item.setProperty( + 'ResumeTime', str(0.0 if not scene["resume_time"] else float(scene["resume_time"]))) item.setProperty('IsPlayable', 'true') return item diff --git a/resources/lib/listing/scene_marker_listing.py b/resources/lib/listing/scene_marker_listing.py index 8bea8a6..fde1d29 100644 --- a/resources/lib/listing/scene_marker_listing.py +++ b/resources/lib/listing/scene_marker_listing.py @@ -8,13 +8,15 @@ class SceneMarkerListing(Listing): def __init__(self, client: StashInterface): - Listing.__init__(self, client, 'scene_markers', local.get_localized(30010), filter_type='SCENE_MARKERS') + Listing.__init__(self, client, 'scene_markers', local.get_localized( + 30010), filter_type='SCENE_MARKERS') def get_navigation(self) -> [NavigationItem]: return [ PerformerItem(self._client, 'scene_markers'), TagItem(self._client, 'scene_markers'), - TagItem(self._client, 'scene_markers', type='scene_tags', label=local.get_localized(30012)), + TagItem(self._client, 'scene_markers', type='scene_tags', + label=local.get_localized(30012)), ] def get_navigation_item(self, params: dict) -> Optional[NavigationItem]: @@ -30,19 +32,24 @@ def _create_items(self, criterion: dict, sort_field: str, sort_dir: int, params: if 'scene' in params: scene = self._client.find_scene(params['scene']) - self._set_title('{} {}'.format(local.get_localized(30011), scene['title'])) + self._set_title('{} {}'.format( + local.get_localized(30011), scene['title'])) markers = scene['scene_markers'] else: - (_, markers) = self._client.find_scene_markers(criterion, sort_field, sort_dir) + (_, markers) = self._client.find_scene_markers( + criterion, sort_field, sort_dir) items = [] for marker in markers: - title = '{} - {}'.format(marker['title'], marker['primary_tag']['name']) - item = self._create_item(marker['scene'], title=title, screenshot=marker['screenshot']) + title = '{} - {}'.format(marker['title'], + marker['primary_tag']['name']) + item = self._create_item( + marker['scene'], title=title, screenshot=marker['screenshot']) url = self._create_play_url(marker['scene']['id']) - duration = marker['scene']['file']['duration'] - item.setProperty('StartPercent', str(round(marker['seconds'] / duration * 100, 2))) + duration = marker['scene']['files'][0]['duration'] + item.setProperty('StartPercent', str( + round(marker['seconds'] / duration * 100, 2))) items.append((item, url)) diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 4c5f19d..c08fa49 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -18,12 +18,33 @@ api_key: str = '' client: Optional[StashInterface] = None +res_map = { + "Original": "", + "4k": "FOUR_K", + "1440p": "QUAD_HD", + "FHD": "FULL_HD", + "720p": "STANDARD_HD", + "SD": "STANDARD", + "240p": "LOW", +} + +res_height_map = { + "4k": 1920, + "1440p": 1440, + "FHD": 1080, + "720p": 720, + "SD": 480, + "240p": 240, +} + def run(): global api_key global client + global playback_res api_key = _ADDON.getSetting('api_key') client = StashInterface(_ADDON.getSetting('base_url'), api_key) + playback_res = res_map.get(_ADDON.getSetting('res'), "") router(sys.argv[2][1:]) @@ -80,16 +101,40 @@ def browse_for(params: dict): navigation.list_items() +def get_mp4_stream(sceneStreams: dict) -> str: + for stream in sceneStreams: + if stream["label"] == "DASH": + return stream + + def play(params: dict): scene = client.find_scene(params['play']) - item = xbmcgui.ListItem(path=scene['paths']['stream']) + item = xbmcgui.ListItem() + if playback_res != "" and scene["files"][0]["height"] > res_height_map[_ADDON.getSetting('res')]: + stream = get_mp4_stream(scene["sceneStreams"]) + url = stream["url"].replace("ORIGINAL", playback_res) + item.setMimeType('application/xml+dash') + item.setContentLookup(False) + + item.setProperty('inputstream', 'inputstream.adaptive') + item.setProperty('inputstream.adaptive.manifest_type', 'mpd') + item.setProperty('inputstream.adaptive.stream_headers', + f'apikey={api_key}') + item.setProperty('inputstream.adaptive.manifest_headers', + f'apikey={api_key}') + item.setPath(url) + else: + streamurl = scene['paths']['stream'] + item.setPath(streamurl) + xbmcplugin.setResolvedUrl(_HANDLE, True, listitem=item) def increment_o(params: dict): if 'scene' in params: o_count = client.scene_increment_o(params['scene']) - xbmc.executebuiltin('Notification(Stash, {} {})'.format(utils.local.get_localized(30009), o_count)) + xbmc.executebuiltin('Notification(Stash, {} {})'.format( + utils.local.get_localized(30009), o_count)) def router(param_string: str): diff --git a/resources/lib/stash_interface.py b/resources/lib/stash_interface.py index 12e855e..b1ef41d 100644 --- a/resources/lib/stash_interface.py +++ b/resources/lib/stash_interface.py @@ -19,7 +19,8 @@ def __init__(self, url, api_key): def add_api_key(self, url: str): if self._api_key: - url = "{}{}apikey={}".format(url, '&' if '?' in url else '?', urllib.parse.quote(self._api_key)) + url = "{}{}apikey={}".format( + url, '&' if '?' in url else '?', urllib.parse.quote(self._api_key)) return url @@ -34,7 +35,8 @@ def __call_graphql(self, query, variables=None): result = response.json() if result.get("errors", None): for error in result["errors"]: - raise Exception("GraphQL error: {}".format(error['message'])) + raise Exception( + "GraphQL error: {}".format(error['message'])) if result.get("data", None): return result.get("data") else: @@ -51,13 +53,16 @@ def find_scenes(self, scene_filter=None, sort_field='title', sort_dir='asc'): id title details - rating + rating100 date created_at + play_count + resume_time + last_played_at paths { screenshot } - file { + files { duration video_codec audio_codec @@ -66,6 +71,7 @@ def find_scenes(self, scene_filter=None, sort_field='title', sort_dir='asc'): } studio { name + image_path } performers { name @@ -101,14 +107,20 @@ def find_scene(self, id): id title details - rating + rating100 date created_at + sceneStreams { + url + mime_type + label + __typename + } paths { stream screenshot } - file { + files { duration video_codec audio_codec @@ -133,13 +145,13 @@ def find_scene(self, id): id title details - rating + rating100 date created_at paths { screenshot } - file { + files { duration video_codec audio_codec @@ -197,7 +209,7 @@ def find_performers(self, **kwargs): 'modifier': 'GREATER_THAN', 'value': 0, } - } + } } result = self.__call_graphql(query, variables) @@ -262,7 +274,7 @@ def find_studios(self): 'modifier': 'GREATER_THAN', 'value': 0, } - } + } } result = self.__call_graphql(query, variables) @@ -283,13 +295,13 @@ def find_scene_markers(self, markers_filter=None, sort_field='title', sort_dir=0 id title details - rating + rating100 date created_at paths { screenshot } - file { + files { duration video_codec audio_codec diff --git a/resources/lib/utils/resolutions.py b/resources/lib/utils/resolutions.py new file mode 100644 index 0000000..44da687 --- /dev/null +++ b/resources/lib/utils/resolutions.py @@ -0,0 +1,18 @@ + +# TODO: Replace strings with Enum +Resolution_map = { + "144p": "VERY_LOW", + "240p": "LOW", + "360p": "R360P", + "480p": "STANDARD", + "540p": "WEB_HD", + "720p": "STANDARD_HD", + "1080p": "FULL_HD", + "1440p": "QUAD_HD", + "4k": "FOUR_K", + "5k": "FIVE_K", + "6k": "SIX_K", + "7k": "SEVEN_K", + "8k": "EIGHT_K", + "Huge": "HUGE", +} diff --git a/resources/settings.xml b/resources/settings.xml index 2945e67..acb9a93 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,7 +1,11 @@ - + + + + +