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'
|
'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)
|
||||||
|
@ -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)
|
||||||
|
@ -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__
|
||||||
|
@ -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.
|
||||||
|
@ -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'))
|
||||||
|
@ -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__':
|
||||||
|
Loading…
Reference in New Issue
Block a user