CastagnaIT/plugin.video.netflix

View on GitHub
resources/lib/navigation/player.py

Summary

Maintainability
A
0 mins
Test Coverage
# -*- coding: utf-8 -*-
"""
    Copyright (C) 2017 Sebastian Golasch (plugin.video.netflix)
    Copyright (C) 2018 Caphm (original implementation module)
    Handle playback requests

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

import resources.lib.common as common
import resources.lib.kodi.infolabels as infolabels
import resources.lib.kodi.ui as ui
from resources.lib.common.kodi_wrappers import set_video_info_tag
from resources.lib.globals import G
from resources.lib.common.exceptions import InputStreamHelperError, ErrorMsgNoReport
from resources.lib.utils.logging import LOG, measure_exec_time_decorator

MANIFEST_PATH_FORMAT = common.IPC_ENDPOINT_MSL + '/get_manifest?videoid={}'
LICENSE_PATH_FORMAT = common.IPC_ENDPOINT_MSL + '/get_license?videoid={}'

INPUTSTREAM_SERVER_CERTIFICATE = (
    'Cr0CCAMSEOVEukALwQ8307Y2+LVP+0MYh/HPkwUijgIwggEKAoIBAQDm875btoWUbGqQD8eA'
    'GuBlGY+Pxo8YF1LQR+Ex0pDONMet8EHslcZRBKNQ/09RZFTP0vrYimyYiBmk9GG+S0wB3CRI'
    'TgweNE15cD33MQYyS3zpBd4z+sCJam2+jj1ZA4uijE2dxGC+gRBRnw9WoPyw7D8RuhGSJ95O'
    'Etzg3Ho+mEsxuE5xg9LM4+Zuro/9msz2bFgJUjQUVHo5j+k4qLWu4ObugFmc9DLIAohL58UR'
    '5k0XnvizulOHbMMxdzna9lwTw/4SALadEV/CZXBmswUtBgATDKNqjXwokohncpdsWSauH6vf'
    'S6FXwizQoZJ9TdjSGC60rUB2t+aYDm74cIuxAgMBAAE6EHRlc3QubmV0ZmxpeC5jb20SgAOE'
    '0y8yWw2Win6M2/bw7+aqVuQPwzS/YG5ySYvwCGQd0Dltr3hpik98WijUODUr6PxMn1ZYXOLo'
    '3eED6xYGM7Riza8XskRdCfF8xjj7L7/THPbixyn4mULsttSmWFhexzXnSeKqQHuoKmerqu0n'
    'u39iW3pcxDV/K7E6aaSr5ID0SCi7KRcL9BCUCz1g9c43sNj46BhMCWJSm0mx1XFDcoKZWhpj'
    '5FAgU4Q4e6f+S8eX39nf6D6SJRb4ap7Znzn7preIvmS93xWjm75I6UBVQGo6pn4qWNCgLYlG'
    'GCQCUm5tg566j+/g5jvYZkTJvbiZFwtjMW5njbSRwB3W4CrKoyxw4qsJNSaZRTKAvSjTKdqV'
    'DXV/U5HK7SaBA6iJ981/aforXbd2vZlRXO/2S+Maa2mHULzsD+S5l4/YGpSt7PnkCe25F+nA'
    'ovtl/ogZgjMeEdFyd/9YMYjOS4krYmwp3yJ7m9ZzYCQ6I8RQN4x/yLlHG5RH/+WNLNUs6JAZ'
    '0fFdCmw=')

PSSH_KID = 'AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQAAAAAAPSZ0kAAAAAAAAAAA==|AAAAAAPSZ0kAAAAAAAAAAA=='


@common.inject_video_id(path_offset=0, pathitems_arg='videoid', inject_full_pathitems=True)
def play_strm(videoid):
    _play(videoid, True)


@common.inject_video_id(path_offset=0, pathitems_arg='videoid', inject_full_pathitems=True)
def play(videoid):
    _play(videoid, False)


@measure_exec_time_decorator()
def _play(videoid, is_played_from_strm=False):
    """Play an episode or movie as specified by the path"""
    is_upnext_enabled = G.ADDON.getSettingBool('UpNextNotifier_enabled')
    LOG.info('Playing {}{}{}',
             videoid,
             ' [STRM file]' if is_played_from_strm else '',
             ' [external call]' if G.IS_ADDON_EXTERNAL_CALL else '')

    # Profile switch when playing from a STRM file (library)
    if is_played_from_strm and not _profile_switch():
        xbmcplugin.endOfDirectory(G.PLUGIN_HANDLE, succeeded=False)
        return

    # Generate the xbmcgui.ListItem to be played
    list_item = get_inputstream_listitem(videoid)

    # STRM file resume workaround (Kodi library)
    resume_position = _strm_resume_workaroud(is_played_from_strm, videoid)
    if resume_position == '':
        xbmcplugin.setResolvedUrl(handle=G.PLUGIN_HANDLE, succeeded=False, listitem=list_item)
        return

    # When a video is played from Kodi library or Up Next add-on is needed set infoLabels and art info to list_item
    if is_played_from_strm or is_upnext_enabled or G.IS_ADDON_EXTERNAL_CALL:
        info, arts = common.make_call('get_videoid_info', videoid)
        if G.IS_OLD_KODI_MODULES:
            list_item.setInfo('video', info)
        else:
            set_video_info_tag(info, list_item.getVideoInfoTag())
        list_item.setArt(arts)

    # Start and initialize the action controller (see action_controller.py)
    LOG.debug('Sending initialization signal')
    # Do not use send_signal as threaded slow devices are not powerful to send in faster way and arrive late to service
    common.send_signal(common.Signals.PLAYBACK_INITIATED, {
        'videoid': videoid,
        'is_played_from_strm': is_played_from_strm,
        'resume_position': resume_position})
    xbmcplugin.setResolvedUrl(handle=G.PLUGIN_HANDLE, succeeded=True, listitem=list_item)


def get_inputstream_listitem(videoid):
    """Return a listitem that has all inputstream relevant properties set for playback of the given video_id"""
    service_url = f'http://127.0.0.1:{G.LOCAL_DB.get_value("nf_server_service_port")}'
    manifest_path = MANIFEST_PATH_FORMAT.format(videoid.value)
    list_item = xbmcgui.ListItem(path=service_url + manifest_path, offscreen=True)
    list_item.setContentLookup(False)
    list_item.setMimeType('application/xml+dash')
    list_item.setProperty('IsPlayable', 'true')
    # Allows the add-on to always have play callbacks also when using the playlist (Kodi versions >= 20)
    list_item.setProperty('ForceResolvePlugin', 'true')
    try:
        import inputstreamhelper
        is_helper = inputstreamhelper.Helper('mpd', drm='widevine')
        inputstream_ready = is_helper.check_inputstream()
    except Exception as exc:  # pylint: disable=broad-except
        # Captures all types of ISH internal errors
        import traceback
        LOG.error(traceback.format_exc())
        raise InputStreamHelperError(str(exc)) from exc
    if not inputstream_ready:
        raise ErrorMsgNoReport(common.get_local_string(30046))
    list_item.setProperty(
        key='inputstream',
        value='inputstream.adaptive')
    list_item.setProperty(
        key='inputstream.adaptive.stream_headers',
        value=f'user-agent={common.get_user_agent()}')
    list_item.setProperty(
        key='inputstream.adaptive.license_type',
        value='com.widevine.alpha')
    list_item.setProperty(
        key='inputstream.adaptive.manifest_type',
        value='mpd')
    list_item.setProperty(
        key='inputstream.adaptive.license_key',
        value=service_url + LICENSE_PATH_FORMAT.format(videoid.value) + '||b{SSM}!b{SID}|')
    list_item.setProperty(
        key='inputstream.adaptive.server_certificate',
        value=INPUTSTREAM_SERVER_CERTIFICATE)
    # Set PSSH/KID to pre-initialize the DRM to get challenge/session ID data in the ISA manifest proxy callback
    if common.get_system_platform() != 'android':
        list_item.setProperty(
            key='inputstream.adaptive.pre_init_data',
            value=PSSH_KID)
    # Override InputStreamAdaptive "Stream selection type" setting (Kodi v20 and above, this is ignored on old versions)
    stream_sel_type = G.ADDON.getSettingString('isa_streamselection_override')
    if stream_sel_type != 'disabled':
        list_item.setProperty(
            key='inputstream.adaptive.stream_selection_type',
            value=G.ADDON.getSettingString('isa_streamselection_override'))
    return list_item


def _profile_switch():
    """Profile switch to play from the library"""
    # This is needed to play videos with the appropriate Netflix profile to avoid problems like:
    #   Missing audio/subtitle languages; Missing metadata; Wrong age restrictions;
    #   Video content not available; Sync with netflix watched status to wrong profile
    # Of course if the user still selects the wrong profile the problems remains,
    # but now will be caused only by the user for inappropriate use.
    library_playback_profile_guid = G.LOCAL_DB.get_value('library_playback_profile_guid')
    if library_playback_profile_guid:
        selected_guid = library_playback_profile_guid
    else:
        selected_guid = ui.show_profiles_dialog(title_prefix=common.get_local_string(15213),
                                                preselect_guid=G.LOCAL_DB.get_active_profile_guid())
    if not selected_guid:
        return False
    # Perform the profile switch
    # The profile switch is done to NFSession, the MSL part will be switched automatically
    from resources.lib.navigation.directory_utils import activate_profile
    if not activate_profile(selected_guid):
        return False
    return True


def _strm_resume_workaroud(is_played_from_strm, videoid):
    """Workaround for resuming STRM files from library, for Kodi v19"""
    if G.KODI_VERSION >= '20':
        return None
    if not is_played_from_strm or not G.ADDON.getSettingBool('ResumeManager_enabled'):
        return None
    # The resume workaround will fail when:
    # - The metadata have a new episode, but the STRM is not exported yet
    # - User try to play STRM files copied from another/previous add-on installation (without import them)
    # - User try to play STRM files from a shared path (like SMB) of another device (without use shared db)
    resume_position = infolabels.get_resume_info_from_library(videoid).get('position')
    if resume_position:
        index_selected = (ui.ask_for_resume(resume_position)
                          if G.ADDON.getSettingBool('ResumeManager_dialog') else None)
        if index_selected == -1:
            # Cancel playback
            return ''
        if index_selected == 1:
            resume_position = None
    return resume_position