diff --git a/examples/drplayer_example.py b/examples/drplayer_example.py new file mode 100644 index 000000000..223a0dcfa --- /dev/null +++ b/examples/drplayer_example.py @@ -0,0 +1,118 @@ +""" +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 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}"') + +if not args.dr_tokens: + 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 + + 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, + "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..f0580cab0 --- /dev/null +++ b/pychromecast/controllers/drtv.py @@ -0,0 +1,91 @@ +"""Controller to interface with the DRTV app, from the Danish Broadcasting Corporation, dr.dk""" +import threading +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": "" + }, + } + self.send_message(msg, inc_session_id=True, callback_function=callback_function) + + # pylint: disable-next=arguments-differ + 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(_): + 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":