Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for DRTV app as a custom controller #646

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions examples/drplayer_example.py
Original file line number Diff line number Diff line change
@@ -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()
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
91 changes: 91 additions & 0 deletions pychromecast/controllers/drtv.py
Original file line number Diff line number Diff line change
@@ -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()
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