From 9461d6516f255d6c8682949b9dc66b1b4f915fb6 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Tue, 17 Oct 2023 18:06:23 +0200 Subject: [PATCH 01/17] Clarification on API key requirement --- iSponsorBlockTV/setup_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iSponsorBlockTV/setup_wizard.py b/iSponsorBlockTV/setup_wizard.py index 75a35cf..28a485c 100644 --- a/iSponsorBlockTV/setup_wizard.py +++ b/iSponsorBlockTV/setup_wizard.py @@ -479,7 +479,7 @@ class ApiKeyManager(Vertical): def compose(self) -> ComposeResult: yield Label("YouTube Api Key", classes="title") yield Label( - "You can get a YouTube Api Key from the [link=https://console.developers.google.com/apis/credentials]Google Cloud Console[/link]") + "You can get a YouTube Data API v3 Key from the [link=https://console.developers.google.com/apis/credentials]Google Cloud Console[/link]. This key is only required if you're whitelisting channels.") with Grid(id="api-key-grid"): yield Input(placeholder="YouTube Api Key", id="api-key-input", password=True, value=self.config.apikey) yield Button("Show key", id="api-key-view") From 50b71d9f5c7f7522f33d751eec81f166a8b6f4b8 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:54:13 +0200 Subject: [PATCH 02/17] Attempts to respect the user's autoplay choice --- iSponsorBlockTV/ytlounge.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/iSponsorBlockTV/ytlounge.py b/iSponsorBlockTV/ytlounge.py index f0e3efe..7e54273 100644 --- a/iSponsorBlockTV/ytlounge.py +++ b/iSponsorBlockTV/ytlounge.py @@ -40,7 +40,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() @@ -97,6 +97,11 @@ 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": + data = args[0] + print("Setting it") + print(data) + create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED")) else: super()._process_event(event_id, event_type, args) @@ -117,3 +122,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self.volume_state["muted"] = mute_str # YouTube wants the volume when unmuting, so we send it await super()._command("setVolume", {"volume": self.volume_state.get("volume", 100), "muted": mute_str}) + + async def set_auto_play_mode(self, enabled: bool): + print(enabled) + await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"}) \ No newline at end of file From 064bdcd93cef8e227f5d37beffec00d26b62e08d Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:10:45 +0200 Subject: [PATCH 03/17] removed unnecesary prints --- iSponsorBlockTV/ytlounge.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/iSponsorBlockTV/ytlounge.py b/iSponsorBlockTV/ytlounge.py index 7e54273..4e22f2d 100644 --- a/iSponsorBlockTV/ytlounge.py +++ b/iSponsorBlockTV/ytlounge.py @@ -40,7 +40,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() @@ -99,8 +99,6 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): create_task(self.mute(True, override=True)) elif event_type == "onAutoplayModeChanged": data = args[0] - print("Setting it") - print(data) create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED")) else: super()._process_event(event_id, event_type, args) @@ -124,5 +122,4 @@ 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): - print(enabled) await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"}) \ No newline at end of file From 3bbabdb26e4d9cef89e78d8f6d839fcd79961c58 Mon Sep 17 00:00:00 2001 From: bertybuttface <110790513+bertybuttface@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:02:17 +0100 Subject: [PATCH 04/17] Confirm FireTV compatibility (thanks to #83) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0857a48..52e6d69 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Open an issue/pull request if you have tested a device that isn't listed here. | Chromecast | ❔ | | Google TV | ✅ | | Roku | ✅ | -| Fire TV | ❔ | +| Fire TV | ✅ | | CCwGTV | ✅ | | Nintendo Switch | ✅ | | Xbox One/Series | ❔ | From aa26e8fb04d4dedecc9f8782cf6fc93a70768c7a Mon Sep 17 00:00:00 2001 From: adripo <26493496+adripo@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:27:37 +0200 Subject: [PATCH 05/17] Create .dockerignore --- .dockerignore | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d332368 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# Ignore files +.dockerignore +.gitignore +.deepsource.toml +.github + +Dockerfile +docker-compose.yml +README.md From 262e3c606d6a99850248ebabf2cf9da108c72eab Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:49:16 +0100 Subject: [PATCH 06/17] Rollback attempt at fixing autoplay --- iSponsorBlockTV/ytlounge.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/iSponsorBlockTV/ytlounge.py b/iSponsorBlockTV/ytlounge.py index 4e22f2d..fa6e436 100644 --- a/iSponsorBlockTV/ytlounge.py +++ b/iSponsorBlockTV/ytlounge.py @@ -78,10 +78,11 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self.volume_state = args[0] pass # Gets segments for the next video before it starts playing - elif event_type == "autoplayUpNext": - if len(args) > 0 and (vid_id := args[0]["videoId"]): # if video id is not empty - print(f"Getting segments for next video: {vid_id}") - create_task(self.api_helper.get_segments(vid_id)) + # Comment "fix" since it doesn't seem to work + # elif event_type == "autoplayUpNext": + # if len(args) > 0 and (vid_id := args[0]["videoId"]): # if video id is not empty + # print(f"Getting segments for next video: {vid_id}") + # create_task(self.api_helper.get_segments(vid_id)) # #Used to know if an ad is skippable or not elif event_type == "adPlaying": From df118c967d2414d62210a1f122ac3268797b305f Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:50:39 +0100 Subject: [PATCH 07/17] Improve logging when an error occurs while getting segments --- iSponsorBlockTV/api_helpers.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/iSponsorBlockTV/api_helpers.py b/iSponsorBlockTV/api_helpers.py index c6f5dd2..4698c71 100644 --- a/iSponsorBlockTV/api_helpers.py +++ b/iSponsorBlockTV/api_helpers.py @@ -113,11 +113,17 @@ class ApiHelper: headers = {"Accept": "application/json"} url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed async with self.web_session.get(url, headers=headers, params=params) as response: - response = await response.json() - for i in response: + response_json = await response.json() + if(response.status != 200): + print(f"Error getting segments. Code: {response.status} - {response.text}") + return ([], True) + for i in response_json: if str(i["videoID"]) == str(vid_id): - response = i + response_json = i break + return self.process_segments(response) + + def process_segments(response): segments = [] ignore_ttl = True try: From 6e1ee572d5ddcdefb252a1604dcf7a30695ff60f Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:51:48 +0100 Subject: [PATCH 08/17] Print video id when logging the error --- iSponsorBlockTV/api_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iSponsorBlockTV/api_helpers.py b/iSponsorBlockTV/api_helpers.py index 4698c71..afa4096 100644 --- a/iSponsorBlockTV/api_helpers.py +++ b/iSponsorBlockTV/api_helpers.py @@ -115,7 +115,7 @@ class ApiHelper: async with self.web_session.get(url, headers=headers, params=params) as response: response_json = await response.json() if(response.status != 200): - print(f"Error getting segments. Code: {response.status} - {response.text}") + print(f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. Code: {response.status} - {response.text}") return ([], True) for i in response_json: if str(i["videoID"]) == str(vid_id): From 4f4d990544cd2b3475667764febc340de9dc78fe Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:57:39 +0100 Subject: [PATCH 09/17] Getting response is a method and not an attribute --- iSponsorBlockTV/api_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iSponsorBlockTV/api_helpers.py b/iSponsorBlockTV/api_helpers.py index afa4096..373b186 100644 --- a/iSponsorBlockTV/api_helpers.py +++ b/iSponsorBlockTV/api_helpers.py @@ -115,7 +115,7 @@ class ApiHelper: async with self.web_session.get(url, headers=headers, params=params) as response: response_json = await response.json() if(response.status != 200): - print(f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. Code: {response.status} - {response.text}") + print(f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. Code: {response.status} - {response.text()}") return ([], True) for i in response_json: if str(i["videoID"]) == str(vid_id): From 58ee703501bf24d030663e6ce21e221cf2c8100f Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sun, 5 Nov 2023 17:35:22 +0100 Subject: [PATCH 10/17] Fix getting segments (managed to break it last time) --- iSponsorBlockTV/api_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iSponsorBlockTV/api_helpers.py b/iSponsorBlockTV/api_helpers.py index 373b186..52a80b0 100644 --- a/iSponsorBlockTV/api_helpers.py +++ b/iSponsorBlockTV/api_helpers.py @@ -121,9 +121,9 @@ class ApiHelper: if str(i["videoID"]) == str(vid_id): response_json = i break - return self.process_segments(response) + return self.process_segments(response_json) - def process_segments(response): + def process_segments(self, response): segments = [] ignore_ttl = True try: 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 11/17] 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 From 138d8bd51cf2ed4e0285cafa90816590e74d87d4 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sun, 5 Nov 2023 17:38:10 +0100 Subject: [PATCH 12/17] Log errors when gettting segments properly. Also forgot to remove a print --- iSponsorBlockTV/api_helpers.py | 3 ++- iSponsorBlockTV/ytlounge.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/iSponsorBlockTV/api_helpers.py b/iSponsorBlockTV/api_helpers.py index 52a80b0..0d83a5d 100644 --- a/iSponsorBlockTV/api_helpers.py +++ b/iSponsorBlockTV/api_helpers.py @@ -115,7 +115,8 @@ class ApiHelper: async with self.web_session.get(url, headers=headers, params=params) as response: response_json = await response.json() if(response.status != 200): - print(f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. Code: {response.status} - {response.text()}") + response_text = await response.text() + print(f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. Code: {response.status} - {response_text}") return ([], True) for i in response_json: if str(i["videoID"]) == str(vid_id): diff --git a/iSponsorBlockTV/ytlounge.py b/iSponsorBlockTV/ytlounge.py index 5ba9596..e465f43 100644 --- a/iSponsorBlockTV/ytlounge.py +++ b/iSponsorBlockTV/ytlounge.py @@ -46,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() From 28b6ac32180d7ce8243ab261e927c79beff1c7b6 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sun, 5 Nov 2023 17:56:51 +0100 Subject: [PATCH 13/17] General fixes to make deepsource happier --- iSponsorBlockTV/api_helpers.py | 23 ++++++++++------- iSponsorBlockTV/config_setup.py | 46 +++++++++++++++++++-------------- iSponsorBlockTV/dial_client.py | 25 ++++++++---------- iSponsorBlockTV/helpers.py | 3 ++- iSponsorBlockTV/main.py | 6 ++--- iSponsorBlockTV/ytlounge.py | 11 ++++---- main_tui.py | 2 +- 7 files changed, 62 insertions(+), 54 deletions(-) diff --git a/iSponsorBlockTV/api_helpers.py b/iSponsorBlockTV/api_helpers.py index 0d83a5d..b462aa1 100644 --- a/iSponsorBlockTV/api_helpers.py +++ b/iSponsorBlockTV/api_helpers.py @@ -1,8 +1,7 @@ -from cache import AsyncTTL, AsyncLRU +from cache import AsyncLRU from .conditional_ttl_cache import AsyncConditionalTTL from . import constants, dial_client from hashlib import sha256 -from asyncio import create_task from aiohttp import ClientSession import html @@ -39,17 +38,17 @@ class ApiHelper: return for i in data["items"]: - if (i["id"]["kind"] != "youtube#video"): + if i["id"]["kind"] != "youtube#video": continue title_api = html.unescape(i["snippet"]["title"]) artist_api = html.unescape(i["snippet"]["channelTitle"]) if title_api == title and artist_api == artist: - return (i["id"]["videoId"], i["snippet"]["channelId"]) + return i["id"]["videoId"], i["snippet"]["channelId"] return @AsyncLRU(maxsize=100) async def is_whitelisted(self, vid_id): - if (self.apikey and self.channel_whitelist): + if self.apikey and self.channel_whitelist: channel_id = await self.__get_channel_id(vid_id) # check if channel id is in whitelist for i in self.channel_whitelist: @@ -66,7 +65,7 @@ class ApiHelper: if "error" in data: return data = data["items"][0] - if (data["kind"] != "youtube#video"): + if data["kind"] != "youtube#video": return return data["snippet"]["channelId"] @@ -114,9 +113,11 @@ class ApiHelper: url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed async with self.web_session.get(url, headers=headers, params=params) as response: response_json = await response.json() - if(response.status != 200): + if response.status != 200: response_text = await response.text() - print(f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. Code: {response.status} - {response_text}") + print( + f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. " + f"Code: {response.status} - {response_text}") return ([], True) for i in response_json: if str(i["videoID"]) == str(vid_id): @@ -124,7 +125,8 @@ class ApiHelper: break return self.process_segments(response_json) - def process_segments(self, response): + @staticmethod + def process_segments(response): segments = [] ignore_ttl = True try: @@ -153,7 +155,8 @@ class ApiHelper: return (segments, ignore_ttl) async def mark_viewed_segments(self, UUID): - """Marks the segments as viewed in the SponsorBlock API, if skip_count_tracking is enabled. Lets the contributor know that someone skipped the segment (thanks)""" + """Marks the segments as viewed in the SponsorBlock API, if skip_count_tracking is enabled. + Lets the contributor know that someone skipped the segment (thanks)""" if self.skip_count_tracking: for i in UUID: url = constants.SponsorBlock_api + "viewedVideoSponsorTime/" diff --git a/iSponsorBlockTV/config_setup.py b/iSponsorBlockTV/config_setup.py index f9ae79d..8a034af 100644 --- a/iSponsorBlockTV/config_setup.py +++ b/iSponsorBlockTV/config_setup.py @@ -1,14 +1,13 @@ -import json import asyncio -import sys import aiohttp from . import api_helpers, ytlounge -async def pair_device(loop): + +async def pair_device(): try: lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV") pairing_code = input("Enter pairing code (found in Settings - Link with TV code): ") - pairing_code = int(pairing_code.replace("-", "").replace(" ","")) # remove dashes and spaces + pairing_code = int(pairing_code.replace("-", "").replace(" ", "")) # remove dashes and spaces print("Pairing...") paired = await lounge_controller.pair(pairing_code) if not paired: @@ -23,7 +22,8 @@ async def pair_device(loop): except Exception as e: print(f"Failed to pair device: {e}") return - + + def main(config, debug: bool) -> None: print("Welcome to the iSponsorBlockTV cli setup wizard") loop = asyncio.get_event_loop_policy().get_event_loop() @@ -31,12 +31,14 @@ def main(config, debug: bool) -> None: loop.set_debug(True) asyncio.set_event_loop(loop) if hasattr(config, "atvs"): - print("The atvs config option is deprecated and has stopped working. Please read this for more information on how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2") + print( + "The atvs config option is deprecated and has stopped working. Please read this for more information on " + "how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2") if input("Do you want to remove the legacy 'atvs' entry (the app won't start with it present)? (y/n) ") == "y": del config["atvs"] devices = config.devices while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": - task = loop.create_task(pair_device(loop)) + task = loop.create_task(pair_device()) loop.run_until_complete(task) device = task.result() if device: @@ -56,41 +58,46 @@ def main(config, debug: bool) -> None: apikey = input("Enter your API key: ") config["apikey"] = apikey config.apikey = apikey - + skip_categories = config.skip_categories if skip_categories: if input("Skip categories already specified. Change them? (y/n) ") == "y": categories = input( - "Enter skip categories (space or comma sepparated) Options: [sponsor selfpromo exclusive_access interaction poi_highlight intro outro preview filler music_offtopic]:\n" + "Enter skip categories (space or comma sepparated) Options: [sponsor selfpromo exclusive_access " + "interaction poi_highlight intro outro preview filler music_offtopic]:\n" ) skip_categories = categories.replace(",", " ").split(" ") - skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings + skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings else: categories = input( - "Enter skip categories (space or comma sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n" + "Enter skip categories (space or comma sepparated) Options: [sponsor, selfpromo, exclusive_access, " + "interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n" ) skip_categories = categories.replace(",", " ").split(" ") - skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings + skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings config.skip_categories = skip_categories channel_whitelist = config.channel_whitelist if input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") == "y": - if(not apikey): - print("WARNING: You need to specify an API key to use this function, otherwise the program will fail to start.\nYou can add one by re-running this setup wizard.") + if not apikey: + print( + "WARNING: You need to specify an API key to use this function, otherwise the program will fail to " + "start.\nYou can add one by re-running this setup wizard.") web_session = aiohttp.ClientSession() + api_helper = api_helpers.ApiHelper(config, web_session) while True: channel_info = {} channel = input("Enter a channel name or \"/exit\" to exit: ") if channel == "/exit": break - task = loop.create_task(api_helpers.search_channels(channel, apikey, web_session)) + task = loop.create_task(api_helper.search_channels(channel, apikey, web_session)) loop.run_until_complete(task) results = task.result() if len(results) == 0: print("No channels found") continue - + for i in range(len(results)): print(f"{i}: {results[i][1]} - Subs: {results[i][2]}") print("5: Enter a custom channel ID") @@ -115,9 +122,10 @@ def main(config, debug: bool) -> None: channel_whitelist.append(channel_info) # Close web session asynchronously loop.run_until_complete(web_session.close()) - + config.channel_whitelist = channel_whitelist - - config.skip_count_tracking = not input("Do you want to report skipped segments to sponsorblock. Only the segment UUID will be sent? (y/n) ") == "n" + + config.skip_count_tracking = not input( + "Do you want to report skipped segments to sponsorblock. Only the segment UUID will be sent? (y/n) ") == "n" print("Config finished") config.save() diff --git a/iSponsorBlockTV/dial_client.py b/iSponsorBlockTV/dial_client.py index aa553cd..8c0bd74 100644 --- a/iSponsorBlockTV/dial_client.py +++ b/iSponsorBlockTV/dial_client.py @@ -1,24 +1,19 @@ """Send out a M-SEARCH request and listening for responses.""" import asyncio import socket - -import aiohttp import ssdp from ssdp import network import xmltodict -''' -Redistribution and use of the DIAL DIscovery And Launch protocol specification (the “DIAL Specification”), with or without modification, -are permitted provided that the following conditions are met: -● Redistributions of the DIAL Specification must retain the above copyright notice, this list of conditions and the following -disclaimer. -● Redistributions of implementations of the DIAL Specification in source code form must retain the above copyright notice, this -list of conditions and the following disclaimer. -● Redistributions of implementations of the DIAL Specification in binary form must include the above copyright notice. -● The DIAL mark, the NETFLIX mark and the names of contributors to the DIAL Specification may not be used to endorse or -promote specifications, software, products, or any other materials derived from the DIAL Specification without specific prior -written permission. The DIAL mark is owned by Netflix and information on licensing the DIAL mark is available at -www.dial-multiscreen.org.''' +'''Redistribution and use of the DIAL DIscovery And Launch protocol specification (the “DIAL Specification”), +with or without modification, are permitted provided that the following conditions are met: ● Redistributions of the +DIAL Specification must retain the above copyright notice, this list of conditions and the following disclaimer. ● +Redistributions of implementations of the DIAL Specification in source code form must retain the above copyright +notice, this list of conditions and the following disclaimer. ● Redistributions of implementations of the DIAL +Specification in binary form must include the above copyright notice. ● The DIAL mark, the NETFLIX mark and the names +of contributors to the DIAL Specification may not be used to endorse or promote specifications, software, products, +or any other materials derived from the DIAL Specification without specific prior written permission. The DIAL mark +is owned by Netflix and information on licensing the DIAL mark is available at www.dial-multiscreen.org.''' ''' MIT License @@ -44,6 +39,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''' '''Modified code from https://github.com/codingjoe/ssdp/blob/main/ssdp/__main__.py''' + def get_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(0) @@ -69,6 +65,7 @@ class Handler(ssdp.aio.SSDP): def __call__(self): return self + def response_received(self, response: ssdp.messages.SSDPResponse, addr): headers = response.headers headers = {k.lower(): v for k, v in headers} diff --git a/iSponsorBlockTV/helpers.py b/iSponsorBlockTV/helpers.py index 9649bea..481e45d 100644 --- a/iSponsorBlockTV/helpers.py +++ b/iSponsorBlockTV/helpers.py @@ -43,7 +43,8 @@ class Config: def validate(self): if hasattr(self, "atvs"): print( - "The atvs config option is deprecated and has stopped working. Please read this for more information on how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2", + "The atvs config option is deprecated and has stopped working. Please read this for more information " + "on how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2", ) print("Exiting in 10 seconds...") time.sleep(10) diff --git a/iSponsorBlockTV/main.py b/iSponsorBlockTV/main.py index 98c65ac..a059c26 100644 --- a/iSponsorBlockTV/main.py +++ b/iSponsorBlockTV/main.py @@ -3,8 +3,6 @@ import aiohttp import time import logging from . import api_helpers, ytlounge -from .constants import youtube_client_blacklist -import traceback class DeviceListener: @@ -115,7 +113,7 @@ class DeviceListener: self.cancelled = True try: self.task.cancel() - except Exception as e: + except Exception: pass @@ -141,7 +139,7 @@ def main(config, debug): tasks.append(loop.create_task(device.refresh_auth_loop())) try: loop.run_forever() - except KeyboardInterrupt as e: + except KeyboardInterrupt: print("Keyboard interrupt detected, cancelling tasks and exiting...") loop.run_until_complete(finish(devices)) finally: diff --git a/iSponsorBlockTV/ytlounge.py b/iSponsorBlockTV/ytlounge.py index e465f43..ebedbcc 100644 --- a/iSponsorBlockTV/ytlounge.py +++ b/iSponsorBlockTV/ytlounge.py @@ -4,10 +4,10 @@ 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 @@ -30,7 +30,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): await asyncio.sleep(35) # YouTube sends at least a message every 30 seconds (no-op or any other) try: self.subscribe_task.cancel() - except Exception as e: + except Exception: pass # Subscribe to the lounge and start the watchdog @@ -46,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() @@ -54,7 +54,8 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): pass finally: self.subscribe_task_watchdog = asyncio.create_task(self._watchdog()) - # A bunch of events useful to detect ads playing, and the next video before it starts playing (that way we can get the segments) + # 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] # print(data) @@ -111,7 +112,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): device_info = json.loads(device.get("deviceInfo", "")) if device_info.get("clientName", "") in youtube_client_blacklist: self._sid = None - self._gsession = None # Force disconnect + self._gsession = None # Force disconnect # elif event_type == "onAutoplayModeChanged": # data = args[0] # create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED")) diff --git a/main_tui.py b/main_tui.py index 4b7a9af..0b78dbe 100644 --- a/main_tui.py +++ b/main_tui.py @@ -2,4 +2,4 @@ from iSponsorBlockTV import setup_wizard from iSponsorBlockTV.helpers import Config config = Config("data/config.json") -setup_wizard.main(config) \ No newline at end of file +setup_wizard.main(config) From 81f5b6568d350acf5c496be6a6daacf0e65724b8 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 6 Nov 2023 12:51:45 +0200 Subject: [PATCH 14/17] Confirm Xbox Compatability. Clarify TV Code --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e6d69..218f78a 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,13 @@ Open an issue/pull request if you have tested a device that isn't listed here. | Fire TV | ✅ | | CCwGTV | ✅ | | Nintendo Switch | ✅ | -| Xbox One/Series | ❔ | +| Xbox One/Series | ✅ | | Playstation 4/5 | ✅ | ## Usage Run iSponsorBlockTV on a computer that has network access. Auto discovery will require the computer to be on the same network as the device during setup. +The device can also be manually added to iSponsorBlockTV with a YouTube TV code. This code can be found in the settings page of your YouTube application. It connects to the device, watches its activity and skips any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API. It can also skip/mute YouTube ads. From de8d03285f890d20eafb29df36c803f683a9cc86 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:36:26 +0100 Subject: [PATCH 15/17] Upgrade pyytlounge and fix #99 --- iSponsorBlockTV/setup_wizard.py | 6 ++++-- requirements.txt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/iSponsorBlockTV/setup_wizard.py b/iSponsorBlockTV/setup_wizard.py index 75a35cf..69b0de7 100644 --- a/iSponsorBlockTV/setup_wizard.py +++ b/iSponsorBlockTV/setup_wizard.py @@ -1,6 +1,7 @@ -import aiohttp import asyncio import copy + +import aiohttp # Textual imports (Textual is awesome!) from textual import on from textual.app import App, ComposeResult @@ -12,6 +13,7 @@ from textual.widgets import Button, Footer, Header, Static, Label, Input, Select RadioSet, RadioButton from textual.widgets.selection_list import Selection from textual_slider import Slider + # Local imports from . import api_helpers, ytlounge from .constants import skip_categories @@ -457,7 +459,7 @@ class DevicesManager(Vertical): @on(Button.Pressed, "#element-remove") def remove_channel(self, event: Button.Pressed): channel_to_remove: Element = event.button.parent - self.config.channel_whitelist.remove(channel_to_remove.element_data) + self.config.devices.remove(channel_to_remove.element_data) channel_to_remove.remove() @on(Button.Pressed, "#add-device") diff --git a/requirements.txt b/requirements.txt index 66d01a0..ed6a004 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ aiohttp==3.8.6 argparse==1.4.0 async-cache==1.1.1 -pyytlounge==1.6.2 +pyytlounge==1.6.3 rich==13.6.0 ssdp==1.3.0 textual==0.40.0 From 3d8fa562b40fa570bb8f581a7dd4bef7f5a65af1 Mon Sep 17 00:00:00 2001 From: kot0dama <89980752+kot0dama@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:08:14 +0900 Subject: [PATCH 16/17] Fix muting/skipping ads labels mixup in setup_wizard.py --- iSponsorBlockTV/setup_wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iSponsorBlockTV/setup_wizard.py b/iSponsorBlockTV/setup_wizard.py index 69b0de7..fb0f8cb 100644 --- a/iSponsorBlockTV/setup_wizard.py +++ b/iSponsorBlockTV/setup_wizard.py @@ -559,9 +559,9 @@ class AdSkipMuteManager(Vertical): "This feature allows you to automatically mute and/or skip native YouTube ads. Skipping ads only works if that ad shows the 'Skip Ad' button, if it doesn't then it will only be able to be muted.", classes="subtitle", id="skip-count-tracking-subtitle") with Horizontal(id="ad-skip-mute-container"): - yield Checkbox(value=self.config.mute_ads, id="mute-ads-switch", - label="Enable skipping ads") yield Checkbox(value=self.config.skip_ads, id="skip-ads-switch", + label="Enable skipping ads") + yield Checkbox(value=self.config.mute_ads, id="mute-ads-switch", label="Enable muting ads") @on(Checkbox.Changed, "#mute-ads-switch") From 4ab49ea61e84f6e3469156867c04f0eaf55371d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 00:51:31 +0000 Subject: [PATCH 17/17] Bump aiohttp from 3.8.6 to 3.9.0 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.6 to 3.9.0. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.6...v3.9.0) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed6a004..fad23ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.8.6 +aiohttp==3.9.0 argparse==1.4.0 async-cache==1.1.1 pyytlounge==1.6.3