Skip to content

Commit

Permalink
Add support for DRTV app as a custom controller
Browse files Browse the repository at this point in the history
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
mchro committed Jul 4, 2022
1 parent e32fa90 commit 6ad737d
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 0 deletions.
96 changes: 96 additions & 0 deletions examples/drplayer_example.py
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()


1 change: 1 addition & 0 deletions pychromecast/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
APP_BUBBLEUPNP = "3927FA74"
APP_BBCSOUNDS = "03977A48"
APP_BBCIPLAYER = "5E81F6DB"
APP_DRTV = "59047AFC"


def get_possible_app_ids():
Expand Down
118 changes: 118 additions & 0 deletions pychromecast/controllers/drtv.py
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()

3 changes: 3 additions & 0 deletions pychromecast/quick_play.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand Down

0 comments on commit 6ad737d

Please sign in to comment.