Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions resources/lib/drm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
""" DRM UTILS """

from xml.etree.ElementTree import XML

from resources.lib.play import utils

class MissingModuleException(Exception):
""" Is thrown when a Python module is missing. """


def get_pssh_box(manifest_url):
""" Get PSSH Box.
:type manifest_url: str
:rtype str
"""
pssh_box = None
manifest_data = utils.get_url(manifest_url)
manifest = XML(manifest_data)
mpd_ns = {'mpd': 'urn:mpeg:dash:schema:mpd:2011'}
cenc_ns = {'cenc': 'urn:mpeg:cenc:2013'}
adaptionset = manifest.find('mpd:Period', mpd_ns).find('mpd:AdaptationSet', mpd_ns)
pssh_box = adaptionset.findall('mpd:ContentProtection', mpd_ns)[1].find('cenc:pssh', cenc_ns).text
return pssh_box


def get_license_keys(license_url, license_headers, pssh_box, device_path):
"""Get cenc license keys from Widevine CDM.
:type license_url: str
:type headers: str
:type pssh_box: str
:type device_path: str
:rtype dict
"""
try:
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
except ModuleNotFoundError as exc:
raise MissingModuleException(exc)

# Load device
device = Device.load(device_path)

# Load CDM
cdm = Cdm.from_device(device)

# Open cdm session
session_id = cdm.open()

# Get license challenge
challenge = cdm.get_license_challenge(session_id, PSSH(pssh_box))

# Request
wv_license = utils.post_url(license_url, headers=license_headers, data=challenge)

# parse license challenge
cdm.parse_license(session_id, wv_license)

# Get keys
license_keys = {}
for key in cdm.get_keys(session_id):
if key.type == 'CONTENT':
license_keys[key.kid.hex] = key.key.hex()

# close session, disposes of session data
cdm.close(session_id)

return license_keys
81 changes: 62 additions & 19 deletions resources/lib/kodiutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,15 @@
import os
import re

from html import unescape
from urllib.parse import quote, urlencode

import xbmc
import xbmcaddon
import xbmcgui
import xbmcplugin
import xbmcvfs

try: # Python 3
from html import unescape
except ImportError: # Python 2
from HTMLParser import HTMLParser

unescape = HTMLParser().unescape

ADDON = xbmcaddon.Addon()

SORT_METHODS = {
Expand Down Expand Up @@ -231,11 +227,11 @@ def show_listing(title_items, category=None, sort=None, content=None, cache=True
xbmcplugin.endOfDirectory(routing.handle, succeeded, cacheToDisc=cache)


def play(stream, stream_type=STREAM_HLS, license_key=None, title=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None):
def play(stream, title=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None):
"""Play the given stream"""
from resources.lib.addon import routing

play_item = xbmcgui.ListItem(label=title, path=stream)
play_item = xbmcgui.ListItem(label=title, path=stream.url)
if art_dict:
play_item.setArt(art_dict)
if info_dict:
Expand All @@ -251,25 +247,43 @@ def play(stream, stream_type=STREAM_HLS, license_key=None, title=None, art_dict=
else:
play_item.setProperty('inputstreamaddon', 'inputstream.adaptive')

if stream_type == STREAM_HLS:
if stream.stream_type == STREAM_HLS:
play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
play_item.setMimeType('application/vnd.apple.mpegurl')

elif stream_type == STREAM_DASH:
elif stream.stream_type == STREAM_DASH:
from json import dumps
play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
play_item.setMimeType('application/dash+xml')
drm_cfg = {}
import inputstreamhelper
if license_key is not None:
if stream.license_keys:
# Clearkey
if license_key.startswith('org.w3.clearkey'):
is_helper = inputstreamhelper.Helper('mpd')
if is_helper.check_inputstream():
play_item.setProperty('inputstream.adaptive.drm_legacy', license_key)
if kodi_version_major() > 21:
drm_cfg['org.w3.clearkey'] = {
'license': {
'keyids': stream.license_keys
}
}
play_item.setProperty('inputstream.adaptive.drm', dumps(drm_cfg))
else:
# DRM protected MPEG-DASH
is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
if is_helper.check_inputstream():
clearkey = generate_ia_license_key(license_keys=stream.license_keys)
play_item.setProperty('inputstream.adaptive.drm_legacy', clearkey)
elif stream.license_headers:
# Widevine
is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
if is_helper.check_inputstream():
if kodi_version_major() > 21:
drm_cfg['com.widevine.alpha'] = {
'license': {
'server_url': stream.license_url,
'req_headers': urlencode(stream.license_headers)
}
}
play_item.setProperty('inputstream.adaptive.drm', dumps(drm_cfg))
else:
play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
license_key = generate_ia_license_key(stream.license_url, license_headers=stream.license_headers)
play_item.setProperty('inputstream.adaptive.license_key', license_key)
else:
# Unprotected MPEG-DASH
Expand All @@ -281,6 +295,35 @@ def play(stream, stream_type=STREAM_HLS, license_key=None, title=None, art_dict=
xbmcplugin.setResolvedUrl(routing.handle, True, listitem=play_item)


def generate_ia_license_key(license_url='', license_headers='', license_keys='', postdata_type='R', postdata_value='', response_type=''):
"""Generates an InputStream Adaptive license_key
:type license_url: str
:type license_headers: str
:type license_keys: str
:type postdata_type: str
:type postdata_value: str
:type response_type: str
:type device_path: str
:type manifest_url: str
:rtype str
"""

if license_keys:
return f'org.w3.clearkey|{",".join(f"{k}:{v}" for k, v in license_keys.items())}'

if license_headers:
license_headers = urlencode(license_headers)

if postdata_type in ('A', 'R', 'B'):
postdata_value = postdata_type + '{SSM}'
elif postdata_type == 'D':
if 'D{SSM}' not in postdata_value:
raise ValueError('Missing D{SSM} placeholder')
postdata_value = quote(postdata_value)

return f'{license_url}|{license_headers}|{postdata_value}|{response_type}'


def get_search_string(heading='', message=''):
"""Ask the user for a search string"""
search_string = None
Expand Down
6 changes: 4 additions & 2 deletions resources/lib/modules/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from resources.lib import kodiutils
from resources.lib.play.auth import AuthApi
from resources.lib.play.aws.cognito_idp import AuthenticationException, InvalidLoginException
from resources.lib.play.content import ApiException, ContentApi, GeoblockedException, MissingModuleException, UnavailableException
from resources.lib.play.content import ContentApi
from resources.lib.play.exceptions import ApiException, GeoblockedException, UnavailableException
from resources.lib.drm import MissingModuleException

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -34,7 +36,7 @@ def play(self, uuid, content_type):
# Lookup the stream
resolved_stream = self._resolve_stream(uuid, content_type)
if resolved_stream:
kodiutils.play(resolved_stream.url, resolved_stream.stream_type, resolved_stream.license_key)
kodiutils.play(resolved_stream)
return

@staticmethod
Expand Down
10 changes: 7 additions & 3 deletions resources/lib/play/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@
class ResolvedStream:
""" Defines a stream that we can play"""

def __init__(self, uuid=None, url=None, stream_type=None, license_key=None):
def __init__(self, uuid=None, url=None, stream_type=None, license_url=None, license_headers=None, license_keys=None):
"""
:type uuid: str
:type url: str
:type stream_type: str
:type license_key: str
:type license_url: str
:type license_headers: str
:type license_keys: dict
"""
self.uuid = uuid
self.url = url
self.stream_type = stream_type
self.license_key = license_key
self.license_url = license_url
self.license_headers = license_headers
self.license_keys = license_keys

def __repr__(self):
return "%r" % self.__dict__
Loading