Compare commits

..

1 Commits

Author SHA1 Message Date
dmunozv04
93f4f0f752 Connect without playlist 2025-04-15 14:03:02 +02:00
9 changed files with 77 additions and 126 deletions

View File

@@ -19,7 +19,7 @@ repos:
- id: mixed-line-ending # replaces or checks mixed line ending
- id: trailing-whitespace # checks for trailing whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.9
rev: v0.11.4
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]

View File

@@ -12,7 +12,6 @@
"skip_count_tracking": true,
"mute_ads": true,
"skip_ads": true,
"minimum_skip_length": 1,
"auto_play": true,
"join_name": "iSponsorBlockTV",
"apikey": "",

View File

@@ -1,6 +1,6 @@
[project]
name = "iSponsorBlockTV"
version = "2.5.1"
version = "2.4.0"
authors = [
{"name" = "dmunozv04"}
]

View File

@@ -1,4 +1,4 @@
aiohttp==3.11.18
aiohttp==3.11.16
appdirs==1.4.4
async-cache==1.1.1
pyytlounge==2.3.0

View File

@@ -27,7 +27,6 @@ class ApiHelper:
self.skip_count_tracking = config.skip_count_tracking
self.web_session = web_session
self.num_devices = len(config.devices)
self.minimum_skip_length = config.minimum_skip_length
# Not used anymore, maybe it can stay here a little longer
@AsyncLRU(maxsize=10)
@@ -140,10 +139,10 @@ class ApiHelper:
if str(i["videoID"]) == str(vid_id):
response_json = i
break
return self.process_segments(response_json, self.minimum_skip_length)
return self.process_segments(response_json)
@staticmethod
def process_segments(response, minimum_skip_length):
def process_segments(response):
segments = []
ignore_ttl = True
try:
@@ -185,9 +184,7 @@ class ApiHelper:
segment_dict["start"] = segment_before_start
segment_dict["UUID"].extend(segment_before_UUID)
segments.pop()
# Only add segments greater than minimum skip length
if segment_dict["end"] - segment_dict["start"] > minimum_skip_length:
segments.append(segment_dict)
segments.append(segment_dict)
except BaseException:
pass
return segments, ignore_ttl

View File

@@ -24,10 +24,6 @@ SEARCH_CHANNEL_PROMPT = 'Enter a channel name or "/exit" to exit: '
SELECT_CHANNEL_PROMPT = "Select one option of the above [0-6]: "
ENTER_CHANNEL_ID_PROMPT = "Enter a channel ID: "
ENTER_CUSTOM_CHANNEL_NAME_PROMPT = "Enter the channel name: "
MINIMUM_SKIP_PROMPT = "Do you want to specify a minimum length of segment to skip? (y/N)"
MINIMUM_SKIP_SPECIFICATION_PROMPT = (
"Enter minimum length of segment to skip in seconds (enter 0 to disable):"
)
REPORT_SKIPPED_SEGMENTS_PROMPT = (
"Do you want to report skipped segments to sponsorblock. Only the segment"
" UUID will be sent? (Y/n) "
@@ -175,21 +171,6 @@ def main(config, debug: bool) -> None:
config.channel_whitelist = channel_whitelist
# Ask for minimum skip length. Confirm input is an integer
minimum_skip_length = config.minimum_skip_length
choice = get_yn_input(MINIMUM_SKIP_PROMPT)
if choice == "y":
while True:
try:
minimum_skip_length = int(input(MINIMUM_SKIP_SPECIFICATION_PROMPT))
break
except ValueError:
print("You entered a non integer value, try again.")
continue
config.minimum_skip_length = minimum_skip_length
choice = get_yn_input(REPORT_SKIPPED_SEGMENTS_PROMPT)
config.skip_count_tracking = choice != "n"

View File

@@ -41,7 +41,6 @@ class Config:
self.skip_count_tracking = True
self.mute_ads = False
self.skip_ads = False
self.minimum_skip_length = 1
self.auto_play = True
self.join_name = "iSponsorBlockTV"
self.__load()

View File

@@ -692,43 +692,6 @@ class SkipCategoriesManager(Vertical):
self.config.skip_categories = event.selection_list.selected
class MinimumSkipLengthManager(Vertical):
"""Manager for minimum skip length setting."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Minimum Skip Length", classes="title")
yield Label(
(
"Specify the minimum length a segment must meet in order to skip "
"it (in seconds). Default is 1 second; entering 0 will skip all "
"segments."
),
classes="subtitle",
)
yield Input(
placeholder="Minimum skip length (0 to skip all)",
id="minimum-skip-length-input",
value=str(getattr(self.config, "minimum_skip_length", 1)),
validators=[
Function(
lambda user_input: user_input.isdigit(),
"Please enter a valid non-negative number",
)
],
)
@on(Input.Changed, "#minimum-skip-length-input")
def changed_minimum_skip_length(self, event: Input.Changed):
try:
self.config.minimum_skip_length = int(event.input.value)
except ValueError:
self.config.minimum_skip_length = 1
class SkipCountTrackingManager(Vertical):
"""Manager for skip count tracking, allows to enable/disable skip count tracking."""
@@ -910,11 +873,6 @@ class ISponsorBlockTVSetupMainScreen(Screen):
yield SkipCategoriesManager(
config=self.config, id="skip-categories-manager", classes="container"
)
yield MinimumSkipLengthManager(
config=self.config,
id="minimum-skip-length-manager",
classes="container",
)
yield SkipCountTrackingManager(
config=self.config, id="count-segments-manager", classes="container"
)

View File

@@ -7,6 +7,10 @@ from aiohttp import ClientSession
from .constants import youtube_client_blacklist
from pyytlounge.api import api_base
from pyytlounge.exceptions import NotLinkedException
from pyytlounge.util import as_aiter
create_task = asyncio.create_task
@@ -30,8 +34,6 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
self.logger = logger
self.shorts_disconnected = False
self.auto_play = True
self.watchdog_running = False
self.last_event_time = 0
if config:
self.mute_ads = config.mute_ads
self.skip_ads = config.skip_ads
@@ -40,58 +42,21 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
# Ensures that we still are subscribed to the lounge
async def _watchdog(self):
"""
Continuous watchdog that monitors for connection health.
If no events are received within the expected timeframe,
it cancels the current subscription.
"""
self.watchdog_running = True
self.last_event_time = asyncio.get_event_loop().time()
await asyncio.sleep(
35
) # YouTube sends at least a message every 30 seconds (no-op or any other)
try:
while self.watchdog_running:
await asyncio.sleep(10)
current_time = asyncio.get_event_loop().time()
time_since_last_event = current_time - self.last_event_time
# YouTube sends a message at least every 30 seconds
if time_since_last_event > 60:
self.logger.debug(
f"Watchdog triggered: No events for {time_since_last_event:.1f} seconds"
)
# Cancel current subscription
if self.subscribe_task and not self.subscribe_task.done():
self.subscribe_task.cancel()
await asyncio.sleep(1) # Give it time to cancel
except asyncio.CancelledError:
self.logger.debug("Watchdog task cancelled")
self.watchdog_running = False
except BaseException as e:
self.logger.error(f"Watchdog error: {e}")
self.watchdog_running = False
self.subscribe_task.cancel()
except BaseException:
pass
# Subscribe to the lounge and start the watchdog
async def subscribe_monitored(self, callback):
self.callback = callback
# Stop existing watchdog if running
if self.subscribe_task_watchdog and not self.subscribe_task_watchdog.done():
self.watchdog_running = False
try:
self.subscribe_task_watchdog.cancel()
try:
await self.subscribe_task_watchdog
except (asyncio.CancelledError, Exception):
pass
# Start new subscription
if self.subscribe_task and not self.subscribe_task.done():
self.subscribe_task.cancel()
try:
await self.subscribe_task
except (asyncio.CancelledError, Exception):
pass
except BaseException:
pass # No watchdog task
self.subscribe_task = asyncio.create_task(super().subscribe(callback))
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
return self.subscribe_task
@@ -100,9 +65,13 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
# skipcq: PY-R1000
def _process_event(self, event_type: str, args: List[Any]):
self.logger.debug(f"process_event({event_type}, {args})")
# Update last event time for the watchdog
self.last_event_time = asyncio.get_event_loop().time()
# (Re)start the watchdog
try:
self.subscribe_task_watchdog.cancel()
except BaseException:
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)
@@ -120,7 +89,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
create_task(self.mute(False, override=True))
elif event_type == "onAdStateChange":
data = args[0]
if data["adState"] == "0" and data["currentTime"] != "0": # Ad is not playing
if data["adState"] == "0": # Ad is not playing
self.logger.info("Ad has ended, unmuting")
create_task(self.mute(False, override=True))
elif (
@@ -149,8 +118,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
if vid_id := data["contentVideoId"]:
self.logger.info(f"Getting segments for next video: {vid_id}")
create_task(self.api_helper.get_segments(vid_id))
if (
elif (
self.skip_ads and data["isSkipEnabled"] == "true"
): # YouTube uses strings for booleans
self.logger.info("Ad can be skipped, skipping")
@@ -236,3 +204,52 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
if self.conn is not None:
await self.conn.close()
self.session = web_session
# TODO: Open a PR upstream to specify the connect_body.method
# if this works
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,vsp",
"method": "getNowPlaying",
"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 self.session.post(url=connect_url, data=connect_body) as resp:
try:
text = await resp.text()
if resp.status == 401:
self._lounge_token_expired()
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(as_aiter(lines)):
self._process_events(events)
self._command_offset = 1
return self.connected()
except:
self._logger.exception(
"Handle connect failed, status %s reason %s",
resp.status,
resp.reason,
)
raise