-
Notifications
You must be signed in to change notification settings - Fork 380
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for DRTV app as a custom controller
Notably, the app requires two JWTs ("session tokens") to allow playback. These can either be retrieved from a browser and given (expiry seem to not be critical), or alternatively retrieval of an anonymous token using Selenium will be attempted.
- Loading branch information
Showing
4 changed files
with
218 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
""" | ||
Example on how to use the DRTV Controller for the Danish Broadcasting Corporation, dr.dk | ||
""" | ||
# pylint: disable=invalid-name | ||
|
||
import argparse | ||
import logging | ||
import sys | ||
from time import sleep | ||
import json | ||
import threading | ||
|
||
import zeroconf | ||
import pychromecast | ||
from pychromecast import quick_play | ||
|
||
# Change to the name of your Chromecast | ||
CAST_NAME = "Stuen" | ||
|
||
# Media ID can be found in the URLs, e.g. "https://www.dr.dk/drtv/episode/fantus-og-maskinerne_-gravemaskine_278087" | ||
MEDIA_ID = "278087" | ||
IS_LIVE = False | ||
|
||
parser = argparse.ArgumentParser( | ||
description="Example on how to use the BBC iPlayer Controller to play an media stream." | ||
) | ||
parser.add_argument( | ||
"--cast", help='Name of cast device (default: "%(default)s")', default=CAST_NAME | ||
) | ||
parser.add_argument( | ||
"--known-host", | ||
help="Add known host (IP), can be used multiple times", | ||
action="append", | ||
) | ||
parser.add_argument("--show-debug", help="Enable debug log", action="store_true") | ||
parser.add_argument( | ||
"--show-zeroconf-debug", help="Enable zeroconf debug log", action="store_true" | ||
) | ||
parser.add_argument( | ||
"--media_id", help='MediaID (default: "%(default)s")', default=MEDIA_ID | ||
) | ||
parser.add_argument( | ||
"--no-autoplay", | ||
help="Disable autoplay", | ||
action="store_false", | ||
default=True, | ||
) | ||
parser.add_argument( | ||
"--dr_tokens", | ||
help='DR session tokens, from local storage in a browser: localStorage[\'session.tokens\']; token expiry does not seem to matter. If not given automatic retrieval of an anonymous token will be attempted.', | ||
default=None, | ||
) | ||
parser.add_argument( | ||
"--is_live", | ||
help="Show 'live' and no current/end timestamps on UI", | ||
action="store_true", | ||
default=IS_LIVE, | ||
) | ||
parser.add_argument( | ||
"--chainplay_countdown", help='seconds to countdown before the next media in the chain (typically next episode) is played. -1 to disable (default: %(default)s)', default=10 | ||
) | ||
args = parser.parse_args() | ||
|
||
if args.show_debug: | ||
logging.basicConfig(level=logging.DEBUG) | ||
if args.show_zeroconf_debug: | ||
print("Zeroconf version: " + zeroconf.__version__) | ||
logging.getLogger("zeroconf").setLevel(logging.DEBUG) | ||
|
||
chromecasts, browser = pychromecast.get_listed_chromecasts( | ||
friendly_names=[args.cast], known_hosts=args.known_host | ||
) | ||
if not chromecasts: | ||
print(f'No chromecast with name "{args.cast}" discovered') | ||
sys.exit(1) | ||
|
||
cast = chromecasts[0] | ||
# Start socket client's worker thread and wait for initial status update | ||
cast.wait() | ||
print(f'Found chromecast with name "{args.cast}", attempting to play "{args.media_id}"') | ||
|
||
app_name = "drtv" | ||
app_data = { | ||
"media_id": args.media_id, | ||
"is_live": args.is_live, | ||
"dr_tokens": args.dr_tokens, | ||
"autoplay": args.no_autoplay, | ||
"chainplay_countdown": args.chainplay_countdown, | ||
} | ||
quick_play.quick_play(cast, app_name, app_data) | ||
|
||
sleep(10) | ||
|
||
browser.stop_discovery() | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
"""Controller to interface with the DRTV app, from the Danish Broadcasting Corporation, dr.dk""" | ||
import threading | ||
import time | ||
import json | ||
|
||
from .media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE, MESSAGE_TYPE, TYPE_LOAD, BaseMediaPlayer | ||
from .. import __version__ | ||
from ..config import APP_DRTV | ||
from ..error import PyChromecastError | ||
|
||
APP_NAMESPACE = "urn:x-cast:com.google.cast.media" | ||
|
||
class DRTVController(BaseMediaPlayer): | ||
"""Controller to interact with DRTV app.""" | ||
|
||
def __init__(self): | ||
super().__init__(APP_DRTV) | ||
|
||
def play_drtv( # pylint: disable=too-many-locals | ||
self, | ||
media_id, | ||
dr_session_tokens, | ||
is_live=False, | ||
current_time=0, | ||
autoplay=True, | ||
chainplay_countdown=10, | ||
callback_function=None, | ||
): | ||
""" | ||
Play DRTV media. | ||
Parameters: | ||
media_id: the id of the media to play, e.g. 20875 | ||
dr_session_tokens: JWT tokens to allow access to the content | ||
chainplay_countdown: seconds to countdown before the next media in the chain (typically next episode) is played. -1 to disable | ||
""" | ||
stream_type = STREAM_TYPE_LIVE if is_live else STREAM_TYPE_BUFFERED | ||
|
||
session_tokens = json.loads(dr_session_tokens) | ||
account_token = next((t for t in session_tokens if t['type'] == 'UserAccount'), {}) | ||
profile_token = next((t for t in session_tokens if t['type'] == 'UserProfile'), {}) | ||
|
||
msg = { | ||
"media": { | ||
"contentId": media_id, | ||
"contentType": "video/hls", | ||
"streamType": stream_type, | ||
"metadata": {}, | ||
"customData": { | ||
"accessService": "StandardVideo" | ||
}, | ||
}, | ||
MESSAGE_TYPE: TYPE_LOAD, | ||
"currentTime": current_time, | ||
"autoplay": autoplay, | ||
"customData": { | ||
"accountToken": account_token, | ||
"chainPlayCountdown": chainplay_countdown, | ||
"profileToken": profile_token, | ||
"senderAppVersion": __version__, | ||
"senderDeviceType": "pyChromeCast", | ||
"showDebugOverlay": False, | ||
"userId": "" | ||
}, | ||
} | ||
|
||
print(msg) | ||
self.send_message(msg, inc_session_id=True, callback_function=callback_function) | ||
|
||
def _get_drtokens(self): | ||
"""Try to automatically retrieve a token from the webplayer. Requires Selenium with Chrome support.""" | ||
|
||
try: | ||
from selenium import webdriver | ||
from selenium.webdriver.chrome.options import Options | ||
|
||
options = Options() | ||
options.headless = True | ||
|
||
driver = webdriver.Chrome(options=options) | ||
try: | ||
url = 'http://dr.dk/tv/' | ||
driver.get(url) | ||
|
||
for _ in range(10): | ||
script_get_token = """return localStorage['session.tokens']""" | ||
result = driver.execute_script(script_get_token) | ||
if result: | ||
return result | ||
time.sleep(1) | ||
finally: | ||
driver.quit() | ||
except Exception as err: | ||
raise PyChromecastError("Failed in retrieving DR token automatically; Selenium installed with Chrome support?", err) | ||
return "" | ||
|
||
# pylint: disable-next=arguments-differ | ||
def quick_play(self, media_id=None, dr_tokens=None, **kwargs): | ||
"""Quick Play""" | ||
if not dr_tokens: | ||
dr_tokens = self._get_drtokens() | ||
|
||
play_media_done_event = threading.Event() | ||
|
||
def play_media_done(_): | ||
play_media_done_event.set() | ||
|
||
self.play_drtv( | ||
media_id, | ||
dr_tokens, | ||
callback_function=play_media_done, | ||
**kwargs | ||
) | ||
|
||
play_media_done_event.wait(30) | ||
if not play_media_done_event.is_set(): | ||
raise PyChromecastError() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters