mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-25 17:08:45 +03:00
Compare commits
33 Commits
connect_wi
...
v2.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a80c76fb6 | ||
|
|
a4f6462026 | ||
|
|
c4571ad90b | ||
|
|
c51c47b566 | ||
|
|
72ada4558e | ||
|
|
6219cfe0d0 | ||
|
|
b2dfe35698 | ||
|
|
af35982ac0 | ||
|
|
fb30d7e4cb | ||
|
|
f04f7560b2 | ||
|
|
d55fceda18 | ||
|
|
a6dacc1d84 | ||
|
|
a67e3eb860 | ||
|
|
a9d64af2ac | ||
|
|
fc8d1770cd | ||
|
|
82ce3e60e9 | ||
|
|
4417592b6b | ||
|
|
2dbeed99bc | ||
|
|
2124fff81b | ||
|
|
aabf5aa2bc | ||
|
|
068623bb03 | ||
|
|
b93f480848 | ||
|
|
e9fdc49480 | ||
|
|
4a55fe9539 | ||
|
|
328e70a175 | ||
|
|
7a1d8967ae | ||
|
|
33b0b6d224 | ||
|
|
e0c4322524 | ||
|
|
c360e2582e | ||
|
|
7b3e618628 | ||
|
|
886997beab | ||
|
|
ee786a53b9 | ||
|
|
0b785da448 |
@@ -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.4
|
||||
rev: v0.11.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"skip_count_tracking": true,
|
||||
"mute_ads": true,
|
||||
"skip_ads": true,
|
||||
"minimum_skip_length": 1,
|
||||
"auto_play": true,
|
||||
"join_name": "iSponsorBlockTV",
|
||||
"apikey": "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
aiohttp==3.11.16
|
||||
aiohttp==3.11.18
|
||||
appdirs==1.4.4
|
||||
async-cache==1.1.1
|
||||
pyytlounge==2.3.0
|
||||
|
||||
@@ -27,6 +27,7 @@ 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)
|
||||
@@ -139,10 +140,10 @@ class ApiHelper:
|
||||
if str(i["videoID"]) == str(vid_id):
|
||||
response_json = i
|
||||
break
|
||||
return self.process_segments(response_json)
|
||||
return self.process_segments(response_json, self.minimum_skip_length)
|
||||
|
||||
@staticmethod
|
||||
def process_segments(response):
|
||||
def process_segments(response, minimum_skip_length):
|
||||
segments = []
|
||||
ignore_ttl = True
|
||||
try:
|
||||
@@ -184,7 +185,9 @@ class ApiHelper:
|
||||
segment_dict["start"] = segment_before_start
|
||||
segment_dict["UUID"].extend(segment_before_UUID)
|
||||
segments.pop()
|
||||
segments.append(segment_dict)
|
||||
# Only add segments greater than minimum skip length
|
||||
if segment_dict["end"] - segment_dict["start"] > minimum_skip_length:
|
||||
segments.append(segment_dict)
|
||||
except BaseException:
|
||||
pass
|
||||
return segments, ignore_ttl
|
||||
|
||||
@@ -24,6 +24,10 @@ 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) "
|
||||
@@ -171,6 +175,21 @@ 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,6 +41,7 @@ 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,6 +692,43 @@ 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."""
|
||||
|
||||
@@ -873,6 +910,11 @@ 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"
|
||||
)
|
||||
|
||||
@@ -30,6 +30,8 @@ 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
|
||||
@@ -38,21 +40,58 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
|
||||
# Ensures that we still are subscribed to the lounge
|
||||
async def _watchdog(self):
|
||||
await asyncio.sleep(
|
||||
35
|
||||
) # YouTube sends at least a message every 30 seconds (no-op or any other)
|
||||
"""
|
||||
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()
|
||||
|
||||
try:
|
||||
self.subscribe_task.cancel()
|
||||
except BaseException:
|
||||
pass
|
||||
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
|
||||
|
||||
# Subscribe to the lounge and start the watchdog
|
||||
async def subscribe_monitored(self, callback):
|
||||
self.callback = callback
|
||||
try:
|
||||
|
||||
# Stop existing watchdog if running
|
||||
if self.subscribe_task_watchdog and not self.subscribe_task_watchdog.done():
|
||||
self.watchdog_running = False
|
||||
self.subscribe_task_watchdog.cancel()
|
||||
except BaseException:
|
||||
pass # No watchdog task
|
||||
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
|
||||
|
||||
self.subscribe_task = asyncio.create_task(super().subscribe(callback))
|
||||
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
|
||||
return self.subscribe_task
|
||||
@@ -61,13 +100,9 @@ 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})")
|
||||
# (Re)start the watchdog
|
||||
try:
|
||||
self.subscribe_task_watchdog.cancel()
|
||||
except BaseException:
|
||||
pass
|
||||
finally:
|
||||
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
|
||||
# Update last event time for the watchdog
|
||||
self.last_event_time = asyncio.get_event_loop().time()
|
||||
|
||||
# A bunch of events useful to detect ads playing,
|
||||
# and the next video before it starts playing
|
||||
# (that way we can get the segments)
|
||||
@@ -85,7 +120,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
create_task(self.mute(False, override=True))
|
||||
elif event_type == "onAdStateChange":
|
||||
data = args[0]
|
||||
if data["adState"] == "0": # Ad is not playing
|
||||
if data["adState"] == "0" and data["currentTime"] != "0": # Ad is not playing
|
||||
self.logger.info("Ad has ended, unmuting")
|
||||
create_task(self.mute(False, override=True))
|
||||
elif (
|
||||
@@ -114,7 +149,8 @@ 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))
|
||||
elif (
|
||||
|
||||
if (
|
||||
self.skip_ads and data["isSkipEnabled"] == "true"
|
||||
): # YouTube uses strings for booleans
|
||||
self.logger.info("Ad can be skipped, skipping")
|
||||
|
||||
Reference in New Issue
Block a user