From db647362c6414176915f98f39a40f9e9587385a4 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sun, 5 Nov 2023 17:36:39 +0100 Subject: [PATCH] Handle YouTube Kids (TVHTML5_FOR_KIDS) Should fix #84 --- iSponsorBlockTV/constants.py | 2 + iSponsorBlockTV/main.py | 9 ++--- iSponsorBlockTV/ytlounge.py | 78 +++++++++++++++++++++++++++++++----- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/iSponsorBlockTV/constants.py b/iSponsorBlockTV/constants.py index 1f3f75f..29117db 100644 --- a/iSponsorBlockTV/constants.py +++ b/iSponsorBlockTV/constants.py @@ -17,3 +17,5 @@ skip_categories = ( ('Preview', 'preview'), ('Filler', 'filler'), ) + +youtube_client_blacklist = ["TVHTML5_FOR_KIDS"] diff --git a/iSponsorBlockTV/main.py b/iSponsorBlockTV/main.py index eb9fd29..98c65ac 100644 --- a/iSponsorBlockTV/main.py +++ b/iSponsorBlockTV/main.py @@ -3,6 +3,7 @@ import aiohttp import time import logging from . import api_helpers, ytlounge +from .constants import youtube_client_blacklist import traceback @@ -40,7 +41,6 @@ class DeviceListener: except: # traceback.print_exc() await asyncio.sleep(10) - while not self.cancelled: while not (await self.is_available()) and not self.cancelled: await asyncio.sleep(10) @@ -49,17 +49,17 @@ class DeviceListener: except: pass while not lounge_controller.connected() and not self.cancelled: + # Doesn't connect to the device if it's a kids profile (it's broken) await asyncio.sleep(10) try: await lounge_controller.connect() except: pass - print(f"Connected to device {lounge_controller.screen_name}") try: - #print("Subscribing to lounge") + # print("Subscribing to lounge") sub = await lounge_controller.subscribe_monitored(self) await sub - #print("Subscription ended") + await asyncio.sleep(10) except: pass @@ -110,7 +110,6 @@ class DeviceListener: self.api_helper.mark_viewed_segments(UUID) ) # Don't wait for this to finish - # Stops the connection to the device async def cancel(self): self.cancelled = True diff --git a/iSponsorBlockTV/ytlounge.py b/iSponsorBlockTV/ytlounge.py index fa6e436..5ba9596 100644 --- a/iSponsorBlockTV/ytlounge.py +++ b/iSponsorBlockTV/ytlounge.py @@ -1,7 +1,13 @@ import asyncio +import json import aiohttp import pyytlounge +from .constants import youtube_client_blacklist + +# Temporary imports +from pyytlounge.api import api_base +from pyytlounge.wrapper import NotLinkedException, desync create_task = asyncio.create_task @@ -40,7 +46,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): # Process a lounge subscription event def _process_event(self, event_id: int, event_type: str, args): - # print(f"YtLoungeApi.__process_event({event_id}, {event_type}, {args})") + print(f"YtLoungeApi.__process_event({event_id}, {event_type}, {args})") # (Re)start the watchdog try: self.subscribe_task_watchdog.cancel() @@ -51,8 +57,6 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): # A bunch of events useful to detect ads playing, and the next video before it starts playing (that way we can get the segments) if event_type == "onStateChange": data = args[0] - self.state.apply_state(data) - self._update_state() # print(data) # Unmute when the video starts playing if self.mute_ads and data["state"] == "1": @@ -63,12 +67,12 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self._update_state() # Unmute when the video starts playing if self.mute_ads and data.get("state", "0") == "1": - #print("Ad has ended, unmuting") + # print("Ad has ended, unmuting") create_task(self.mute(False, override=True)) elif self.mute_ads and event_type == "onAdStateChange": data = args[0] if data["adState"] == '0': # Ad is not playing - #print("Ad has ended, unmuting") + # print("Ad has ended, unmuting") create_task(self.mute(False, override=True)) else: # Seen multiple other adStates, assuming they are all ads print("Ad has started, muting") @@ -98,11 +102,20 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): create_task(self.mute(False, override=True)) elif self.mute_ads: create_task(self.mute(True, override=True)) - elif event_type == "onAutoplayModeChanged": + + elif event_type == "loungeStatus": data = args[0] - create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED")) - else: - super()._process_event(event_id, event_type, args) + devices = json.loads(data["devices"]) + for device in devices: + if device["type"] == "LOUNGE_SCREEN": + device_info = json.loads(device.get("deviceInfo", "")) + if device_info.get("clientName", "") in youtube_client_blacklist: + self._sid = None + self._gsession = None # Force disconnect + # elif event_type == "onAutoplayModeChanged": + # data = args[0] + # create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED")) + super()._process_event(event_id, event_type, args) # Set the volume to a specific value (0-100) async def set_volume(self, volume: int) -> None: @@ -123,4 +136,49 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): await super()._command("setVolume", {"volume": self.volume_state.get("volume", 100), "muted": mute_str}) async def set_auto_play_mode(self, enabled: bool): - await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"}) \ No newline at end of file + await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"}) + + # Here just temporarily, will be removed once the PR is merged on YTlounge + async def connect(self) -> bool: + """Attempt to connect using the previously set tokens""" + if not self.linked(): + raise NotLinkedException("Not linked") + + connect_body = { + "app": "web", + "mdx-version": "3", + "name": self.device_name, + "id": self.auth.screen_id, + "device": "REMOTE_CONTROL", + "capabilities": "que,dsdtr,atp", + "method": "setPlaylist", + "magnaKey": "cloudPairedDevice", + "ui": "false", + "deviceContext": "user_agent=dunno&window_width_points=&window_height_points=&os_name=android&ms=", + "theme": "cl", + "loungeIdToken": self.auth.lounge_id_token, + } + connect_url = ( + f"{api_base}/bc/bind?RID=1&VER=8&CVER=1&auth_failure_option=send_error" + ) + async with aiohttp.ClientSession() as session: + async with session.post(url=connect_url, data=connect_body) as resp: + try: + text = await resp.text() + if resp.status == 401: + self.auth.lounge_id_token = None + return False + + if resp.status != 200: + self._logger.warning( + "Unknown reply to connect %i %s", resp.status, resp.reason + ) + return False + lines = text.splitlines() + async for events in self._parse_event_chunks(desync(lines)): + self._process_events(events) + self._command_offset = 1 + return self.connected() + except Exception as ex: + self._logger.exception(ex, resp.status, resp.reason) + return False