CastagnaIT/plugin.video.netflix

View on GitHub
resources/lib/services/nfsession/session/path_requests.py

Summary

Maintainability
C
7 hrs
Test Coverage
# -*- coding: utf-8 -*-
"""
    Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
    Copyright (C) 2018 Caphm (original implementation module)
    Copyright (C) 2019 Stefano Gottardo - @CastagnaIT
    Manages the PATH requests

    SPDX-License-Identifier: MIT
    See LICENSES/MIT.md for more information.
"""
import json

import resources.lib.utils.api_paths as apipaths
import resources.lib.common as common
from resources.lib.globals import G
from resources.lib.services.nfsession.session.access import SessionAccess
from resources.lib.utils.logging import LOG, measure_exec_time_decorator


class SessionPathRequests(SessionAccess):
    """Manages the PATH requests"""

    @measure_exec_time_decorator(is_immediate=True)
    def path_request(self, paths):
        """Perform a path request against the Shakti API"""
        LOG.debug('Executing path request: {}', paths)
        custom_params = {}
        # Use separators with dumps because Netflix rejects spaces
        data = 'path=' + '&path='.join(json.dumps(path, separators=(',', ':')) for path in paths)
        response = self.post_safe(
            endpoint='shakti',
            params=custom_params,
            data=data)
        return response['jsonGraph']

    @measure_exec_time_decorator(is_immediate=True)
    def perpetual_path_request(self, paths, length_params, perpetual_range_start=None,
                               request_size=apipaths.PATH_REQUEST_SIZE_PAGINATED, no_limit_req=False):
        """
        Perform a perpetual path request against the Shakti API to retrieve a possibly large video list.
        :param paths: The paths that compose the request
        :param length_params: A list of two values, e.g. ['stdlist', [...]]:
                              1: A key of LENGTH_ATTRIBUTES that define where read the total number of objects
                              2: A list of keys used to get the list of objects in the JSON data of received response
        :param perpetual_range_start: defines the starting point of the range of objects to be requested
        :param request_size: defines the size of the range, the total number of objects that will be received
        :param no_limit_req: if True, the perpetual cycle of requests will be 'unlimited'
        :return: Union of all JSON raw data received
        """
        # When the requested video list's size is larger than 'request_size',
        # multiple path requests will be executed with forward shifting range selectors
        # and the results will be combined into one path response.
        response_type, length_args = length_params
        # context_name = length_args[0]
        response_length = apipaths.LENGTH_ATTRIBUTES[response_type]
        response_size = request_size + 1

        number_of_requests = 100 if no_limit_req else int(G.ADDON.getSettingInt('page_results') / 45)
        perpetual_range_start = int(perpetual_range_start) if perpetual_range_start else 0
        range_start = perpetual_range_start
        range_end = range_start + request_size
        merged_response = {}

        for n_req in range(number_of_requests):
            path_response = self.path_request(_set_range_selector(paths, range_start, range_end))
            if not path_response:
                break
            if not common.check_path_exists(length_args, path_response):
                # It may happen that the number of items to be received
                # is equal to the number of the response_size
                # so a second round will be performed, which will return an empty list
                break
            common.merge_dicts(path_response, merged_response)
            response_count = response_length(path_response, *length_args)
            if response_count < response_size:
                # There are no other elements to request
                break

            range_start += response_size
            if n_req == (number_of_requests - 1):
                merged_response['_perpetual_range_selector'] = {'next_start': range_start}
                LOG.debug('{} has other elements, added _perpetual_range_selector item', response_type)
            else:
                range_end = range_start + request_size

        if perpetual_range_start > 0:
            previous_start = perpetual_range_start - (response_size * number_of_requests)
            if '_perpetual_range_selector' in merged_response:
                merged_response['_perpetual_range_selector']['previous_start'] = previous_start
            else:
                merged_response['_perpetual_range_selector'] = {'previous_start': previous_start}
        return merged_response

    def perpetual_path_request_switch_profiles(self, paths, length_params, perpetual_range_start=None,
                                               request_size=apipaths.PATH_REQUEST_SIZE_STD, no_limit_req=False):
        """
        Perform a perpetual path request by activating a specified profile,
        Used exclusively to get My List of a profile other than the current one
        """
        # Profile chosen by the user for the synchronization from which to get My List videos
        mylist_profile_guid = G.SHARED_DB.get_value('sync_mylist_profile_guid',
                                                    G.LOCAL_DB.get_guid_owner_profile())
        # Current profile active
        current_profile_guid = G.LOCAL_DB.get_active_profile_guid()
        # Switch profile (only if necessary) in order to get My List videos
        self.external_func_activate_profile(mylist_profile_guid)  # pylint: disable=not-callable
        # Get the My List data
        path_response = self.perpetual_path_request(paths, length_params, perpetual_range_start,
                                                    request_size, no_limit_req)
        if mylist_profile_guid != current_profile_guid:
            # Reactive again the previous profile
            self.external_func_activate_profile(current_profile_guid)  # pylint: disable=not-callable
        return path_response

    @measure_exec_time_decorator(is_immediate=True)
    def callpath_request(self, callpaths, params=None, path_suffixs=None, path=None):
        """Perform a callPath request against the Shakti API"""
        LOG.debug('Executing callPath request: {} params: {} path_suffixs: {}',
                  callpaths, params, path_suffixs)
        custom_params = {
            'method': 'call',
            'withSize': 'true',
            'materialize': 'true',
        }
        # Use separators with dumps because Netflix rejects spaces
        # The data must be formatted correctly (numbers, strings, spaces) otherwise will raise HTTP error 401
        data = 'callPath=' + '&callPath='.join(
            json.dumps(callpath, separators=(',', ':')) for callpath in callpaths)
        if params:
            data += '&param=' + '&param='.join(json.dumps(param, separators=(',', ':')) for param in params)
        if path:
            data += '&path=' + json.dumps(path, separators=(',', ':'))
        if path_suffixs:
            data += '&pathSuffix=' + '&pathSuffix='.join(
                json.dumps(path_suffix, separators=(',', ':')) for path_suffix in path_suffixs)
        # LOG.debug('callPath request data: {}', data)
        response_data = self.post_safe(
            endpoint='shakti',
            params=custom_params,
            data=data)
        return response_data['jsonGraph']


def _set_range_selector(paths, range_start, range_end):
    """
    Replace the RANGE_PLACEHOLDER with an actual dict:
    {'from': range_start, 'to': range_end}
    """
    from copy import deepcopy
    # Make a deepcopy because we don't want to lose the original paths with the placeholder
    ranged_paths = deepcopy(paths)
    for path in ranged_paths:
        try:
            path[path.index(apipaths.RANGE_PLACEHOLDER)] = {'from': range_start, 'to': range_end}
        except ValueError:
            pass
    return ranged_paths