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'
]
STREAM_HLS = 'hls'
STREAM_DASH = 'mpd'
_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)
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"""
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)
if prop_dict:
play_item.setProperties(prop_dict)
if stream_dict:
play_item.addStreamInfo('video', stream_dict)
# Setup Inputstream Adaptive
if kodi_version_major() >= 19:
play_item.setProperty('inputstream', 'inputstream.adaptive')
else:
play_item.setProperty('inputstreamaddon', 'inputstream.adaptive')
play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
play_item.setMimeType('application/vnd.apple.mpegurl')
if stream_type == STREAM_HLS:
play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
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)
xbmcplugin.setResolvedUrl(routing.handle, True, listitem=play_item)

View File

@ -7,7 +7,7 @@ import logging
from resources.lib import kodiutils
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_awsidp import AuthenticationException, InvalidLoginException
from resources.lib.viervijfzes.content import ContentApi, GeoblockedException, UnavailableException
@ -51,7 +51,10 @@ class Player:
if episode.stream:
# We already have a resolved stream. Nice!
# 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)
if episode.uuid:
@ -61,7 +64,21 @@ class Player:
if resolved_stream:
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):
""" Play the requested item.
@ -69,7 +86,15 @@ class Player:
"""
# Lookup the stream
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
def _resolve_stream(uuid):
@ -107,3 +132,31 @@ class Player:
except UnavailableException:
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30712)) # The video is unavailable...
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,
'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
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__)
@ -164,6 +165,7 @@ class Category:
class ContentApi:
""" VIER/VIJF/ZES Content API"""
API_ENDPOINT = 'https://api.viervijfzes.be'
API2_ENDPOINT = 'https://api2.viervijfzes.be'
SITE_APIS = {
'vier': 'https://www.vier.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)
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):
""" Get a list of all categories of the specified channel.

View File

@ -9,7 +9,15 @@ import sys
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
if sys.version_info[0] == 2:
@ -17,7 +25,9 @@ if sys.version_info[0] == 2:
sys.setdefaultencoding("utf-8") # pylint: disable=no-member
# Set credentials based on environment data
if os.environ.get('ADDON_USERNAME') and os.environ.get('ADDON_PASSWORD'):
ADDON = xbmcaddon.Addon()
# Use the .env file with Pipenv to make this work nicely during development
ADDON = xbmcaddon.Addon()
if 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'))

View File

@ -9,6 +9,7 @@ import logging
import unittest
import resources.lib.kodiutils as kodiutils
from resources.lib.viervijfzes import ResolvedStream
from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import ContentApi, Program, Episode, Category, CACHE_PREVENT
@ -58,8 +59,14 @@ class TestApi(unittest.TestCase):
self.assertIsInstance(program, Program)
episode = program.episodes[0]
video = self._api.get_stream_by_uuid(episode.uuid)
self.assertTrue(video)
resolved_stream = self._api.get_stream_by_uuid(episode.uuid)
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__':