From 8deb614cdf04076ff4b3add7e0279c56080a430e Mon Sep 17 00:00:00 2001 From: boltgolt Date: Thu, 13 Feb 2025 21:51:59 +0100 Subject: [PATCH 1/3] Add setting to forcefully end the video after the normal play time --- config.json.template | 1 + src/iSponsorBlockTV/helpers.py | 1 + src/iSponsorBlockTV/main.py | 61 +++++++++++++++++++---------- src/iSponsorBlockTV/setup_wizard.py | 31 +++++++++++++++ 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/config.json.template b/config.json.template index e2d9e65..cf24f4f 100644 --- a/config.json.template +++ b/config.json.template @@ -13,6 +13,7 @@ "mute_ads": true, "skip_ads": true, "auto_play": true, + "force_end": false, "apikey": "", "channel_whitelist": [ {"id": "", diff --git a/src/iSponsorBlockTV/helpers.py b/src/iSponsorBlockTV/helpers.py index 6b4880e..0b956de 100644 --- a/src/iSponsorBlockTV/helpers.py +++ b/src/iSponsorBlockTV/helpers.py @@ -42,6 +42,7 @@ def __init__(self, data_dir): self.mute_ads = False self.skip_ads = False self.auto_play = True + self.force_end = False self.__load() def validate(self): diff --git a/src/iSponsorBlockTV/main.py b/src/iSponsorBlockTV/main.py index dd67b45..f5ca674 100644 --- a/src/iSponsorBlockTV/main.py +++ b/src/iSponsorBlockTV/main.py @@ -13,6 +13,7 @@ class DeviceListener: def __init__(self, api_helper, config, device, debug: bool, web_session): self.task: Optional[asyncio.Task] = None self.api_helper = api_helper + self.config = config self.offset = device.offset self.name = device.name self.cancelled = False @@ -93,36 +94,47 @@ async def __call__(self, state): # Processes the playback state change async def process_playstatus(self, state, time_start): + position = state.currentTime segments = [] + if state.videoId: segments = await self.api_helper.get_segments(state.videoId) + if state.state.value == 1: # Playing self.logger.info( f"Playing video {state.videoId} with {len(segments)} segments" ) + + next_segment = None + start_next_segment = None + if segments: # If there are segments - await self.time_to_segment(segments, state.currentTime, time_start) - - # Finds the next segment to skip to and skips to it - async def time_to_segment(self, segments, position, time_start): - start_next_segment = None - next_segment = None - for segment in segments: - if position < 2 and (segment["start"] <= position < segment["end"]): - next_segment = segment - start_next_segment = ( - position # different variable so segment doesn't change + for segment in segments: + if position < 2 and (segment["start"] <= position < segment["end"]): + next_segment = segment + start_next_segment = ( + position # different variable so segment doesn't change + ) + break + if segment["start"] > position: + next_segment = segment + start_next_segment = next_segment["start"] + break + + if start_next_segment: + time_to_next = ( + start_next_segment - position - (time.time() - time_start) - self.offset + ) + await self.skip(time_to_next, next_segment["end"], next_segment["UUID"]) + + # If there's no segment before the end of the video and the force_end is on + if not start_next_segment and self.config.force_end: + # Schedule a stop task + time_to_end = ( + state.duration - position - (time.time() - time_start) - self.offset ) - break - if segment["start"] > position: - next_segment = segment - start_next_segment = next_segment["start"] - break - if start_next_segment: - time_to_next = ( - start_next_segment - position - (time.time() - time_start) - self.offset - ) - await self.skip(time_to_next, next_segment["end"], next_segment["UUID"]) + if time_to_end > 2: + await self.stop(time_to_end) # Skips to the next segment (waits for the time to pass) async def skip(self, time_to, position, uuids): @@ -131,6 +143,13 @@ async def skip(self, time_to, position, uuids): await asyncio.create_task(self.api_helper.mark_viewed_segments(uuids)) await asyncio.create_task(self.lounge_controller.seek_to(position)) + # Force ends video after expected play time, skiping a video end ad + async def stop(self, time_to): + # Trigger a few frames earlier to avoid the video end event canceling the task + await asyncio.sleep(time_to - 0.1) + self.logger.info("Force stopping video, end of expected play time reached") + await asyncio.create_task(self.lounge_controller._command("stopVideo")) + async def cancel(self): self.cancelled = True await self.lounge_controller.disconnect() diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index de5cd21..cc2f02f 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -881,6 +881,34 @@ def compose(self) -> ComposeResult: def changed_skip(self, event: Checkbox.Changed): self.config.auto_play = event.checkbox.value +class ForceEndManager(Vertical): + """Manager for force_end, forcefully ends every video skiping post-video ads.""" + + def __init__(self, config, **kwargs) -> None: + super().__init__(**kwargs) + self.config = config + + def compose(self) -> ComposeResult: + yield Label("Force video end", classes="title") + yield Label( + ( + "If enabled, every video will be forcefully stopped once it has fully", + " played through. This skips any post-video ads that might play and" + " returns you to the YoutTube home screen." + ), + classes="subtitle", + id="force-end-subtitle", + ) + with Horizontal(id="force-end-container"): + yield Checkbox( + value=self.config.force_end, + id="force-end-switch", + label="Enable force end", + ) + + @on(Checkbox.Changed, "#force-end-switch") + def changed_skip(self, event: Checkbox.Changed): + self.config.force_end = event.checkbox.value class ISponsorBlockTVSetupMainScreen(Screen): """Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221""" @@ -921,6 +949,9 @@ def compose(self) -> ComposeResult: yield AutoPlayManager( config=self.config, id="autoplay-manager", classes="container" ) + yield ForceEndManager( + config=self.config, id="force-end-manager", classes="container" + ) def on_mount(self) -> None: if self.check_for_old_config_entries(): From c2e888390b5be6118a4dab4d4295e92c95304af1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:55:12 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/iSponsorBlockTV/main.py | 9 +++++++-- src/iSponsorBlockTV/setup_wizard.py | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/iSponsorBlockTV/main.py b/src/iSponsorBlockTV/main.py index f5ca674..c5a29fc 100644 --- a/src/iSponsorBlockTV/main.py +++ b/src/iSponsorBlockTV/main.py @@ -123,9 +123,14 @@ async def process_playstatus(self, state, time_start): if start_next_segment: time_to_next = ( - start_next_segment - position - (time.time() - time_start) - self.offset + start_next_segment + - position + - (time.time() - time_start) + - self.offset + ) + await self.skip( + time_to_next, next_segment["end"], next_segment["UUID"] ) - await self.skip(time_to_next, next_segment["end"], next_segment["UUID"]) # If there's no segment before the end of the video and the force_end is on if not start_next_segment and self.config.force_end: diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index cc2f02f..026bb08 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -881,6 +881,7 @@ def compose(self) -> ComposeResult: def changed_skip(self, event: Checkbox.Changed): self.config.auto_play = event.checkbox.value + class ForceEndManager(Vertical): """Manager for force_end, forcefully ends every video skiping post-video ads.""" @@ -894,7 +895,7 @@ def compose(self) -> ComposeResult: ( "If enabled, every video will be forcefully stopped once it has fully", " played through. This skips any post-video ads that might play and" - " returns you to the YoutTube home screen." + " returns you to the YoutTube home screen.", ), classes="subtitle", id="force-end-subtitle", @@ -910,6 +911,7 @@ def compose(self) -> ComposeResult: def changed_skip(self, event: Checkbox.Changed): self.config.force_end = event.checkbox.value + class ISponsorBlockTVSetupMainScreen(Screen): """Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221""" From 270caa4d8cc6147f4c785048318b7a9db54e4878 Mon Sep 17 00:00:00 2001 From: boltgolt Date: Fri, 14 Feb 2025 09:22:27 +0100 Subject: [PATCH 3/3] Add missing styling, clearify that you really don't want to have autoplay on too --- src/iSponsorBlockTV/setup-wizard-style.tcss | 6 ++++++ src/iSponsorBlockTV/setup_wizard.py | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/iSponsorBlockTV/setup-wizard-style.tcss b/src/iSponsorBlockTV/setup-wizard-style.tcss index cde381d..0f68fa2 100644 --- a/src/iSponsorBlockTV/setup-wizard-style.tcss +++ b/src/iSponsorBlockTV/setup-wizard-style.tcss @@ -383,3 +383,9 @@ MigrationScreen { padding: 1; height: auto; } + +/* Force end */ +#force-end-container{ + padding: 1; + height: auto; +} diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index 026bb08..1764e2c 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -893,9 +893,10 @@ def compose(self) -> ComposeResult: yield Label("Force video end", classes="title") yield Label( ( - "If enabled, every video will be forcefully stopped once it has fully", + "If enabled, every video will be forcefully stopped once it has fully" " played through. This skips any post-video ads that might play and" - " returns you to the YoutTube home screen.", + " returns you to the YoutTube home screen. Autoplay cannot be enabled" + " at the same time." ), classes="subtitle", id="force-end-subtitle", @@ -911,6 +912,9 @@ def compose(self) -> ComposeResult: def changed_skip(self, event: Checkbox.Changed): self.config.force_end = event.checkbox.value + if event.checkbox.value: + self.app.query_one("#autoplay-switch").value = False + class ISponsorBlockTVSetupMainScreen(Screen): """Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221"""