From 6ad737d94cb17b9ffecb6aa67237efaa8b6ddde8 Mon Sep 17 00:00:00 2001 From: "Mads Chr. Olesen" Date: Sun, 3 Jul 2022 22:19:15 +0200 Subject: [PATCH 1/3] 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. --- examples/drplayer_example.py | 96 +++++++++++++++++++++++++ pychromecast/config.py | 1 + pychromecast/controllers/drtv.py | 118 +++++++++++++++++++++++++++++++ pychromecast/quick_play.py | 3 + 4 files changed, 218 insertions(+) create mode 100644 examples/drplayer_example.py create mode 100644 pychromecast/controllers/drtv.py diff --git a/examples/drplayer_example.py b/examples/drplayer_example.py new file mode 100644 index 000000000..3a81180f9 --- /dev/null +++ b/examples/drplayer_example.py @@ -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() + + diff --git a/pychromecast/config.py b/pychromecast/config.py index 143a27d25..80cb379a2 100644 --- a/pychromecast/config.py +++ b/pychromecast/config.py @@ -17,6 +17,7 @@ APP_BUBBLEUPNP = "3927FA74" APP_BBCSOUNDS = "03977A48" APP_BBCIPLAYER = "5E81F6DB" +APP_DRTV = "59047AFC" def get_possible_app_ids(): diff --git a/pychromecast/controllers/drtv.py b/pychromecast/controllers/drtv.py new file mode 100644 index 000000000..da3a907e1 --- /dev/null +++ b/pychromecast/controllers/drtv.py @@ -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() + diff --git a/pychromecast/quick_play.py b/pychromecast/quick_play.py index df63721cb..93f71b03b 100644 --- a/pychromecast/quick_play.py +++ b/pychromecast/quick_play.py @@ -3,6 +3,7 @@ from .controllers.bbciplayer import BbcIplayerController from .controllers.bbcsounds import BbcSoundsController from .controllers.bubbleupnp import BubbleUPNPController +from .controllers.drtv import DRTVController from .controllers.homeassistant_media import HomeAssistantMediaController from .controllers.media import DefaultMediaReceiverController from .controllers.supla import SuplaController @@ -61,6 +62,8 @@ def quick_play(cast, app_name, data): controller = BubbleUPNPController() elif app_name == "default_media_receiver": controller = DefaultMediaReceiverController() + elif app_name == "drtv": + controller = DRTVController() elif app_name == "homeassistant_media": controller = HomeAssistantMediaController() elif app_name == "supla": From c3dab029144c451e7013344ff66065ceae125485 Mon Sep 17 00:00:00 2001 From: "Mads Chr. Olesen" Date: Mon, 11 Jul 2022 20:53:52 +0200 Subject: [PATCH 2/3] DRTV: make tokens required in the controller; fix lint warnings --- examples/drplayer_example.py | 30 +++++++++++++++++++--- pychromecast/controllers/drtv.py | 43 ++++++-------------------------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/examples/drplayer_example.py b/examples/drplayer_example.py index 3a81180f9..4381af642 100644 --- a/examples/drplayer_example.py +++ b/examples/drplayer_example.py @@ -7,8 +7,6 @@ import logging import sys from time import sleep -import json -import threading import zeroconf import pychromecast @@ -79,6 +77,32 @@ cast.wait() print(f'Found chromecast with name "{args.cast}", attempting to play "{args.media_id}"') +if not args.dr_tokens: + print("Trying to automatically retrieve a token from the webplayer. Requires Selenium with Chrome support.") + 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(20): + script_get_token = """return localStorage['session.tokens']""" + result = driver.execute_script(script_get_token) + if result: + args.dr_tokens = result + break + sleep(1) + + if not args.dr_tokens: + raise Exception("Failed in retrieving DR token automatically") + finally: + driver.quit() + app_name = "drtv" app_data = { "media_id": args.media_id, @@ -92,5 +116,3 @@ sleep(10) browser.stop_discovery() - - diff --git a/pychromecast/controllers/drtv.py b/pychromecast/controllers/drtv.py index da3a907e1..f0580cab0 100644 --- a/pychromecast/controllers/drtv.py +++ b/pychromecast/controllers/drtv.py @@ -1,6 +1,5 @@ """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 @@ -10,6 +9,7 @@ APP_NAMESPACE = "urn:x-cast:com.google.cast.media" + class DRTVController(BaseMediaPlayer): """Controller to interact with DRTV app.""" @@ -63,43 +63,17 @@ def play_drtv( # pylint: disable=too-many-locals "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() + def quick_play(self, media_id, dr_tokens, **kwargs): + """ + Quick Play + Parameters: + media_id: the id of the media to play, e.g. 20875 + dr_session_tokens: JWT tokens to allow access to the content + """ play_media_done_event = threading.Event() def play_media_done(_): @@ -115,4 +89,3 @@ def play_media_done(_): play_media_done_event.wait(30) if not play_media_done_event.is_set(): raise PyChromecastError() - From f2f3712040838d4ba2f0448638d4c9cca7fc53a2 Mon Sep 17 00:00:00 2001 From: "Mads Chr. Olesen" Date: Tue, 12 Jul 2022 13:52:52 +0200 Subject: [PATCH 3/3] DRTV: Reference the Python Selenium package --- examples/drplayer_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/drplayer_example.py b/examples/drplayer_example.py index 4381af642..223a0dcfa 100644 --- a/examples/drplayer_example.py +++ b/examples/drplayer_example.py @@ -78,7 +78,7 @@ print(f'Found chromecast with name "{args.cast}", attempting to play "{args.media_id}"') if not args.dr_tokens: - print("Trying to automatically retrieve a token from the webplayer. Requires Selenium with Chrome support.") + print("Trying to automatically retrieve a token from the webplayer. Requires Selenium with Chrome support. See https://pypi.org/project/selenium/") from selenium import webdriver from selenium.webdriver.chrome.options import Options