Allow playing drm protected content (#47)
* Allow playing drm protected content
This commit is contained in:
parent
d8824d32be
commit
f54622f930
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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__
|
||||
|
@ -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.
|
||||
|
@ -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'))
|
||||
|
@ -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__':
|
||||
|
Loading…
Reference in New Issue
Block a user