mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-25 08:58:29 +03:00
Compare commits
1 Commits
v2.5.1
...
connect_wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93f4f0f752 |
@@ -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 ]
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "iSponsorBlockTV"
|
||||
version = "2.5.1"
|
||||
version = "2.4.0"
|
||||
authors = [
|
||||
{"name" = "dmunozv04"}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user