Allow playing drm protected content (#47)

* Allow playing drm protected content
This commit is contained in:
Michaël Arnauts 2020-11-04 12:48:33 +01:00 committed by GitHub
parent d8824d32be
commit f54622f930
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 14 deletions

View File

@ -26,6 +26,9 @@ DEFAULT_SORT_METHODS = [
'unsorted', 'title' 'unsorted', 'title'
] ]
STREAM_HLS = 'hls'
STREAM_DASH = 'mpd'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -190,7 +193,7 @@ def show_listing(title_items, category=None, sort=None, content=None, cache=True
xbmcplugin.endOfDirectory(routing.handle, succeeded, cacheToDisc=cache) xbmcplugin.endOfDirectory(routing.handle, succeeded, cacheToDisc=cache)
def play(stream, title=None, art_dict=None, info_dict=None, prop_dict=None): def play(stream, stream_type=STREAM_HLS, license_key=None, title=None, art_dict=None, info_dict=None, prop_dict=None, stream_dict=None):
"""Play the given stream""" """Play the given stream"""
from resources.lib.addon import routing from resources.lib.addon import routing
@ -201,14 +204,26 @@ def play(stream, title=None, art_dict=None, info_dict=None, prop_dict=None):
play_item.setInfo(type='video', infoLabels=info_dict) play_item.setInfo(type='video', infoLabels=info_dict)
if prop_dict: if prop_dict:
play_item.setProperties(prop_dict) play_item.setProperties(prop_dict)
if stream_dict:
play_item.addStreamInfo('video', stream_dict)
# Setup Inputstream Adaptive # Setup Inputstream Adaptive
if kodi_version_major() >= 19: if kodi_version_major() >= 19:
play_item.setProperty('inputstream', 'inputstream.adaptive') play_item.setProperty('inputstream', 'inputstream.adaptive')
else: else:
play_item.setProperty('inputstreamaddon', 'inputstream.adaptive') play_item.setProperty('inputstreamaddon', 'inputstream.adaptive')
if stream_type == STREAM_HLS:
play_item.setProperty('inputstream.adaptive.manifest_type', 'hls') play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
play_item.setMimeType('application/vnd.apple.mpegurl') play_item.setMimeType('application/vnd.apple.mpegurl')
elif stream_type == STREAM_DASH:
play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
play_item.setMimeType('application/dash+xml')
if license_key:
play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
play_item.setProperty('inputstream.adaptive.license_key', license_key)
play_item.setContentLookup(False) play_item.setContentLookup(False)
xbmcplugin.setResolvedUrl(routing.handle, True, listitem=play_item) xbmcplugin.setResolvedUrl(routing.handle, True, listitem=play_item)

View File

@ -7,7 +7,7 @@ import logging
from resources.lib import kodiutils from resources.lib import kodiutils
from resources.lib.modules.menu import Menu from resources.lib.modules.menu import Menu
from resources.lib.viervijfzes import CHANNELS from resources.lib.viervijfzes import CHANNELS, ResolvedStream
from resources.lib.viervijfzes.auth import AuthApi from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.auth_awsidp import AuthenticationException, InvalidLoginException from resources.lib.viervijfzes.auth_awsidp import AuthenticationException, InvalidLoginException
from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException
@ -51,7 +51,10 @@ class Player:
if episode.stream: if episode.stream:
# We already have a resolved stream. Nice! # We already have a resolved stream. Nice!
# We don't need credentials for these streams. # We don't need credentials for these streams.
resolved_stream = episode.stream resolved_stream = ResolvedStream(
uuid=episode.uuid,
url=episode.stream,
)
_LOGGER.debug('Already got a resolved stream: %s', resolved_stream) _LOGGER.debug('Already got a resolved stream: %s', resolved_stream)
if episode.uuid: if episode.uuid:
@ -61,7 +64,21 @@ class Player:
if resolved_stream: if resolved_stream:
titleitem = Menu.generate_titleitem(episode) titleitem = Menu.generate_titleitem(episode)
kodiutils.play(resolved_stream, info_dict=titleitem.info_dict, art_dict=titleitem.art_dict, prop_dict=titleitem.prop_dict) if resolved_stream.license_url:
# Generate license key
license_key = self.create_license_key(resolved_stream.license_url,
key_headers=dict(
customdata=resolved_stream.auth,
))
else:
license_key = None
kodiutils.play(resolved_stream.url,
resolved_stream.stream_type,
license_key,
info_dict=titleitem.info_dict,
art_dict=titleitem.art_dict,
prop_dict=titleitem.prop_dict)
def play(self, uuid): def play(self, uuid):
""" Play the requested item. """ Play the requested item.
@ -69,7 +86,15 @@ class Player:
""" """
# Lookup the stream # Lookup the stream
resolved_stream = self._resolve_stream(uuid) resolved_stream = self._resolve_stream(uuid)
kodiutils.play(resolved_stream) if resolved_stream.license_url:
# Generate license key
license_key = self.create_license_key(resolved_stream.license_url, key_headers=dict(
customdata=resolved_stream.auth,
))
else:
license_key = None
kodiutils.play(resolved_stream.url, resolved_stream.stream_type, license_key)
@staticmethod @staticmethod
def _resolve_stream(uuid): def _resolve_stream(uuid):
@ -107,3 +132,31 @@ class Player:
except UnavailableException: except UnavailableException:
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable... kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable...
return None return None
@staticmethod
def create_license_key(key_url, key_type='R', key_headers=None, key_value=None):
""" Create a license key string that we need for inputstream.adaptive.
:param str key_url:
:param str key_type:
:param dict[str, str] key_headers:
:param str key_value:
:rtype: str
"""
try: # Python 3
from urllib.parse import quote, urlencode
except ImportError: # Python 2
from urllib import quote, urlencode
header = ''
if key_headers:
header = urlencode(key_headers)
if key_type in ('A', 'R', 'B'):
key_value = key_type + '{SSM}'
elif key_type == 'D':
if 'D{SSM}' not in key_value:
raise ValueError('Missing D{SSM} placeholder')
key_value = quote(key_value)
return '%s|%s|%s|' % (key_url, header, key_value)

View File

@ -54,3 +54,24 @@ STREAM_DICT = {
'height': 544, 'height': 544,
'width': 960, 'width': 960,
} }
class ResolvedStream:
""" Defines a stream that we can play"""
def __init__(self, uuid=None, url=None, stream_type=None, license_url=None, auth=None):
"""
:type uuid: str
:type url: str
:type stream_type: str
:type license_url: str
:type auth: str
"""
self.uuid = uuid
self.url = url
self.stream_type = stream_type
self.license_url = license_url
self.auth = auth
def __repr__(self):
return "%r" % self.__dict__

View File

@ -13,7 +13,8 @@ from datetime import datetime
import requests import requests
from six.moves.html_parser import HTMLParser # pylint: disable=wrong-import-order from six.moves.html_parser import HTMLParser # pylint: disable=wrong-import-order
from resources.lib.viervijfzes import CHANNELS from resources.lib.kodiutils import STREAM_DASH, STREAM_HLS
from resources.lib.viervijfzes import CHANNELS, ResolvedStream
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -164,6 +165,7 @@ class Category:
class ContentApi: class ContentApi:
""" VIER/VIJF/ZES Content API""" """ VIER/VIJF/ZES Content API"""
API_ENDPOINT = 'https://api.viervijfzes.be' API_ENDPOINT = 'https://api.viervijfzes.be'
API2_ENDPOINT = 'https://api2.viervijfzes.be'
SITE_APIS = { SITE_APIS = {
'vier': 'https://www.vier.be/api', 'vier': 'https://www.vier.be/api',
'vijf': 'https://www.vijf.be/api', 'vijf': 'https://www.vijf.be/api',
@ -340,7 +342,30 @@ class ContentApi:
""" """
response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True) response = self._get_url(self.API_ENDPOINT + '/content/%s' % uuid, authentication=True)
data = json.loads(response) data = json.loads(response)
return data['video']['S']
if 'videoDash' in data:
# DRM protected stream
# See https://docs.unified-streaming.com/documentation/drm/buydrm.html#setting-up-the-client
drm_key = data['drmKey']['S']
_LOGGER.debug('Fetching Authentication XML with drm_key %s', drm_key)
response_drm = self._get_url(self.API2_ENDPOINT + '/decode/%s' % drm_key, authentication=True)
data_drm = json.loads(response_drm)
return ResolvedStream(
uuid=uuid,
url=data['videoDash']['S'],
stream_type=STREAM_DASH,
license_url='https://wv-keyos.licensekeyserver.com/',
auth=data_drm.get('auth'),
)
# Normal HLS stream
return ResolvedStream(
uuid=uuid,
url=data['video']['S'],
stream_type=STREAM_HLS,
)
def get_categories(self, channel): def get_categories(self, channel):
""" Get a list of all categories of the specified channel. """ Get a list of all categories of the specified channel.

View File

@ -9,7 +9,15 @@ import sys
import xbmcaddon import xbmcaddon
logging.basicConfig(level=logging.INFO) try: # Python 3
from http.client import HTTPConnection
except ImportError: # Python 2
from httplib import HTTPConnection
logging.basicConfig(level=logging.DEBUG)
# Add logging to urllib
HTTPConnection.debuglevel = 1
# Make UTF-8 the default encoding in Python 2 # Make UTF-8 the default encoding in Python 2
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
@ -17,7 +25,9 @@ if sys.version_info[0] == 2:
sys.setdefaultencoding("utf-8") # pylint: disable=no-member sys.setdefaultencoding("utf-8") # pylint: disable=no-member
# Set credentials based on environment data # Set credentials based on environment data
if os.environ.get('ADDON_USERNAME') and os.environ.get('ADDON_PASSWORD'): # Use the .env file with Pipenv to make this work nicely during development
ADDON = xbmcaddon.Addon() ADDON = xbmcaddon.Addon()
if os.environ.get('ADDON_USERNAME'):
ADDON.setSetting('username', os.environ.get('ADDON_USERNAME')) ADDON.setSetting('username', os.environ.get('ADDON_USERNAME'))
if os.environ.get('ADDON_PASSWORD'):
ADDON.setSetting('password', os.environ.get('ADDON_PASSWORD')) ADDON.setSetting('password', os.environ.get('ADDON_PASSWORD'))

View File

@ -9,6 +9,7 @@ import logging
import unittest import unittest
import resources.lib.kodiutils as kodiutils import resources.lib.kodiutils as kodiutils
from resources.lib.viervijfzes import ResolvedStream
from resources.lib.viervijfzes.auth import AuthApi from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, Category, CACHE_PREVENT from resources.lib.viervijfzes.content import ContentApi, Program, Episode, Category, CACHE_PREVENT
@ -58,8 +59,14 @@ class TestApi(unittest.TestCase):
self.assertIsInstance(program, Program) self.assertIsInstance(program, Program)
episode = program.episodes[0] episode = program.episodes[0]
video = self._api.get_stream_by_uuid(episode.uuid) resolved_stream = self._api.get_stream_by_uuid(episode.uuid)
self.assertTrue(video) self.assertIsInstance(resolved_stream, ResolvedStream)
@unittest.skipUnless(kodiutils.get_setting('username') and kodiutils.get_setting('password'), 'Skipping since we have no credentials.')
def test_get_drm_stream(self):
# https://www.zestv.be/video/ncis-new-orleans/ncis-new-orleans-seizoen-3/ncis-new-orleans-s3-aflevering-8
resolved_stream = self._api.get_stream_by_uuid('5bd7211d-de78-490f-b40c-bacbee5919d2')
self.assertIsInstance(resolved_stream, ResolvedStream)
if __name__ == '__main__': if __name__ == '__main__':