mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-07 20:36:44 +03:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ab7e73b52 | ||
|
|
d310e4c817 | ||
|
|
d21bb6320f | ||
|
|
dd42e20dc4 | ||
|
|
213ae97bf2 | ||
|
|
582b9bf725 | ||
|
|
ce95b6dbf0 | ||
|
|
80196b19aa | ||
|
|
4dd6aa1c4d | ||
|
|
1a5f29fe2a | ||
|
|
13fe1f69ae | ||
|
|
3b1ce5297f | ||
|
|
e689a713ef | ||
|
|
e6b1e14d80 | ||
|
|
4934eff8b7 | ||
|
|
674a13e43a | ||
|
|
152ba104a6 | ||
|
|
7e954478f2 | ||
|
|
8208a51176 | ||
|
|
ab6b67f88b | ||
|
|
9068b58bf6 |
2
.github/workflows/build_docker_images.yml
vendored
2
.github/workflows/build_docker_images.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64
|
||||
platforms: linux/amd64, linux/arm64, linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
26
Dockerfile
26
Dockerfile
@@ -1,5 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM python:3.11-alpine3.19 as compiler
|
||||
FROM python:3.11-alpine3.19 as BASE
|
||||
|
||||
FROM base as compiler
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -8,18 +10,26 @@ COPY src .
|
||||
RUN python3 -m compileall -b -f . && \
|
||||
find . -name "*.py" -type f -delete
|
||||
|
||||
FROM python:3.11-alpine3.19
|
||||
FROM base as DEP_INSTALLER
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN apk add --no-cache gcc musl-dev && \
|
||||
pip install --upgrade pip wheel && \
|
||||
pip install --user -r requirements.txt && \
|
||||
pip uninstall -y pip wheel && \
|
||||
apk del gcc musl-dev && \
|
||||
python3 -m compileall -b -f /root/.local/lib/python3.11/site-packages && \
|
||||
find /root/.local/lib/python3.11/site-packages -name "*.py" -type f -delete && \
|
||||
find /root/.local/lib/python3.11/ -name "__pycache__" -type d -exec rm -rf {} +
|
||||
|
||||
FROM base
|
||||
|
||||
ENV PIP_NO_CACHE_DIR=off iSPBTV_docker=True iSPBTV_data_dir=data TERM=xterm-256color COLORTERM=truecolor
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --upgrade pip wheel && \
|
||||
pip install -r requirements.txt && \
|
||||
pip uninstall -y pip wheel && \
|
||||
python3 -m compileall -b -f /usr/local/lib/python3.11/site-packages && \
|
||||
find /usr/local/lib/python3.11/site-packages -name "*.py" -type f -delete && \
|
||||
find /usr/local/lib/python3.11/ -name "__pycache__" -type d -exec rm -rf {} +
|
||||
COPY --from=dep_installer /root/.local /root/.local
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "iSponsorBlockTV"
|
||||
version = "2.0.5"
|
||||
version = "2.0.7"
|
||||
authors = [
|
||||
{"name" = "dmunozv04"}
|
||||
]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
aiohttp==3.9.0
|
||||
aiohttp==3.9.5
|
||||
appdirs==1.4.4
|
||||
argparse==1.4.0
|
||||
async-cache==1.1.1
|
||||
pyytlounge==1.6.3
|
||||
rich==13.6.0
|
||||
pyytlounge==2.0.0
|
||||
rich==13.7.1
|
||||
ssdp==1.3.0
|
||||
textual==0.40.0
|
||||
textual==0.58.0
|
||||
textual-slider==0.1.1
|
||||
xmltodict==0.13.0
|
||||
|
||||
@@ -153,7 +153,24 @@ class ApiHelper:
|
||||
segments = []
|
||||
ignore_ttl = True
|
||||
try:
|
||||
for i in response["segments"]:
|
||||
response_segments = response["segments"]
|
||||
# sort by end
|
||||
response_segments.sort(key=lambda x: x["segment"][1])
|
||||
# extend ends of overlapping segments to make one big segment
|
||||
for i in response_segments:
|
||||
for j in response_segments:
|
||||
if j["segment"][0] <= i["segment"][1] <= j["segment"][1]:
|
||||
i["segment"][1] = j["segment"][1]
|
||||
|
||||
# sort by start
|
||||
response_segments.sort(key=lambda x: x["segment"][0])
|
||||
# extend starts of overlapping segments to make one big segment
|
||||
for i in reversed(response_segments):
|
||||
for j in reversed(response_segments):
|
||||
if j["segment"][0] <= i["segment"][0] <= j["segment"][1]:
|
||||
i["segment"][0] = j["segment"][0]
|
||||
|
||||
for i in response_segments:
|
||||
ignore_ttl = (
|
||||
ignore_ttl and i["locked"] == 1
|
||||
) # If all segments are locked, ignore ttl
|
||||
|
||||
@@ -5,9 +5,11 @@ import aiohttp
|
||||
from . import api_helpers, ytlounge
|
||||
|
||||
|
||||
async def pair_device():
|
||||
async def pair_device(web_session):
|
||||
try:
|
||||
lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV")
|
||||
lounge_controller = ytlounge.YtLoungeApi(
|
||||
"iSponsorBlockTV", web_session=web_session
|
||||
)
|
||||
pairing_code = input(
|
||||
"Enter pairing code (found in Settings - Link with TV code): "
|
||||
)
|
||||
@@ -33,6 +35,7 @@ async def pair_device():
|
||||
def main(config, debug: bool) -> None:
|
||||
print("Welcome to the iSponsorBlockTV cli setup wizard")
|
||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
||||
web_session = aiohttp.ClientSession()
|
||||
if debug:
|
||||
loop.set_debug(True)
|
||||
asyncio.set_event_loop(loop)
|
||||
@@ -52,7 +55,7 @@ def main(config, debug: bool) -> None:
|
||||
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())
|
||||
task = loop.create_task(pair_device(web_session))
|
||||
loop.run_until_complete(task)
|
||||
device = task.result()
|
||||
if device:
|
||||
@@ -112,7 +115,6 @@ def main(config, debug: bool) -> None:
|
||||
" 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 = {}
|
||||
@@ -152,7 +154,6 @@ def main(config, debug: bool) -> None:
|
||||
channel_info["name"] = results[int(choice)][1]
|
||||
channel_whitelist.append(channel_info)
|
||||
# Close web session asynchronously
|
||||
loop.run_until_complete(web_session.close())
|
||||
|
||||
config.channel_whitelist = channel_whitelist
|
||||
|
||||
@@ -165,3 +166,4 @@ def main(config, debug: bool) -> None:
|
||||
)
|
||||
print("Config finished")
|
||||
config.save()
|
||||
loop.run_until_complete(web_session.close())
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import logging
|
||||
|
||||
from rich.logging import RichHandler
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
"""
|
||||
Copyright (c) 2020 Will McGugan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Modified code from rich (https://github.com/textualize/rich)
|
||||
"""
|
||||
|
||||
|
||||
class LogHandler(RichHandler):
|
||||
def __init__(self, device_name, log_name_len, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.filter_strings = []
|
||||
self._log_render = LogRender(
|
||||
device_name=device_name,
|
||||
log_name_len=log_name_len,
|
||||
show_time=True,
|
||||
show_level=True,
|
||||
show_path=False,
|
||||
time_format="[%x %X]",
|
||||
omit_repeated_times=True,
|
||||
level_width=None,
|
||||
)
|
||||
|
||||
def add_filter_string(self, s):
|
||||
self.filter_strings.append(s)
|
||||
|
||||
def _filter(self, s):
|
||||
for i in self.filter_strings:
|
||||
s = s.replace(i, "REDACTED")
|
||||
return s
|
||||
|
||||
def format(self, record):
|
||||
original = super().format(record)
|
||||
return self._filter(original)
|
||||
|
||||
|
||||
class LogRender:
|
||||
def __init__(
|
||||
self,
|
||||
device_name,
|
||||
log_name_len,
|
||||
show_time=True,
|
||||
show_level=False,
|
||||
show_path=True,
|
||||
time_format="[%x %X]",
|
||||
omit_repeated_times=True,
|
||||
level_width=8,
|
||||
):
|
||||
self.filter_strings = []
|
||||
self.log_name_len = log_name_len
|
||||
self.device_name = device_name
|
||||
self.show_time = show_time
|
||||
self.show_level = show_level
|
||||
self.show_path = show_path
|
||||
self.time_format = time_format
|
||||
self.omit_repeated_times = omit_repeated_times
|
||||
self.level_width = level_width
|
||||
self._last_time = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
console,
|
||||
renderables,
|
||||
log_time,
|
||||
time_format=None,
|
||||
level="",
|
||||
path=None,
|
||||
line_no=None,
|
||||
link_path=None,
|
||||
):
|
||||
from rich.containers import Renderables
|
||||
from rich.table import Table
|
||||
|
||||
output = Table.grid(padding=(0, 1))
|
||||
output.expand = True
|
||||
if self.show_time:
|
||||
output.add_column(style="log.time")
|
||||
if self.show_level:
|
||||
output.add_column(style="log.level", width=self.level_width)
|
||||
output.add_column(
|
||||
width=self.log_name_len, style=Style(color="yellow"), overflow="fold"
|
||||
)
|
||||
output.add_column(ratio=1, style="log.message", overflow="fold")
|
||||
if self.show_path and path:
|
||||
output.add_column(style="log.path")
|
||||
row = []
|
||||
if self.show_time:
|
||||
log_time = log_time or console.get_datetime()
|
||||
time_format = time_format or self.time_format
|
||||
if callable(time_format):
|
||||
log_time_display = time_format(log_time)
|
||||
else:
|
||||
log_time_display = Text(log_time.strftime(time_format))
|
||||
if log_time_display == self._last_time and self.omit_repeated_times:
|
||||
row.append(Text(" " * len(log_time_display)))
|
||||
else:
|
||||
row.append(log_time_display)
|
||||
self._last_time = log_time_display
|
||||
if self.show_level:
|
||||
row.append(level)
|
||||
row.append(Text(self.device_name))
|
||||
row.append(Renderables(renderables))
|
||||
if self.show_path and path:
|
||||
path_text = Text()
|
||||
path_text.append(
|
||||
path, style=f"link file://{link_path}" if link_path else ""
|
||||
)
|
||||
if line_no:
|
||||
path_text.append(":")
|
||||
path_text.append(
|
||||
f"{line_no}",
|
||||
style=f"link file://{link_path}#{line_no}" if link_path else "",
|
||||
)
|
||||
row.append(path_text)
|
||||
|
||||
output.add_row(*row)
|
||||
return output
|
||||
|
||||
|
||||
class LogFormatter(logging.Formatter):
|
||||
def __init__(
|
||||
self, fmt=None, datefmt=None, style="%", validate=True, filter_strings=None
|
||||
):
|
||||
super().__init__(fmt, datefmt, style, validate)
|
||||
self.filter_strings = filter_strings or []
|
||||
|
||||
def add_filter_string(self, s):
|
||||
self.filter_strings.append(s)
|
||||
|
||||
def _filter(self, s):
|
||||
print(s)
|
||||
for i in self.filter_strings:
|
||||
s = s.replace(i, "REDACTED")
|
||||
return s
|
||||
|
||||
def format(self, record):
|
||||
original = logging.Formatter.format(self, record)
|
||||
return self._filter(original)
|
||||
@@ -5,29 +5,31 @@ from signal import SIGINT, SIGTERM, signal
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import rich
|
||||
|
||||
from . import api_helpers, logging_helpers, ytlounge
|
||||
from . import api_helpers, ytlounge
|
||||
|
||||
|
||||
class DeviceListener:
|
||||
def __init__(self, api_helper, config, device, log_name_len, debug: bool):
|
||||
def __init__(self, api_helper, config, device, debug: bool, web_session):
|
||||
self.task: Optional[asyncio.Task] = None
|
||||
self.api_helper = api_helper
|
||||
self.offset = device.offset
|
||||
self.name = device.name
|
||||
self.cancelled = False
|
||||
self.logger = logging.getLogger(f"iSponsorBlockTV-{device.screen_id}")
|
||||
self.web_session = web_session
|
||||
if debug:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
self.logger.setLevel(logging.INFO)
|
||||
rh = logging_helpers.LogHandler(device.name, log_name_len, level=logging.DEBUG)
|
||||
rh.add_filter_string(device.screen_id)
|
||||
self.logger.addHandler(rh)
|
||||
sh = logging.StreamHandler()
|
||||
sh.setFormatter(
|
||||
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
)
|
||||
self.logger.addHandler(sh)
|
||||
self.logger.info(f"Starting device")
|
||||
self.lounge_controller = ytlounge.YtLoungeApi(
|
||||
device.screen_id, config, api_helper, self.logger
|
||||
device.screen_id, config, api_helper, self.logger, self.web_session
|
||||
)
|
||||
|
||||
# Ensures that we have a valid auth token
|
||||
@@ -74,7 +76,7 @@ class DeviceListener:
|
||||
"Connected to device %s (%s)", lounge_controller.screen_name, self.name
|
||||
)
|
||||
try:
|
||||
# print("Subscribing to lounge")
|
||||
self.logger.info("Subscribing to lounge")
|
||||
sub = await lounge_controller.subscribe_monitored(self)
|
||||
await sub
|
||||
except:
|
||||
@@ -153,16 +155,16 @@ def main(config, debug):
|
||||
tcp_connector = aiohttp.TCPConnector(ttl_dns_cache=300)
|
||||
web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector)
|
||||
api_helper = api_helpers.ApiHelper(config, web_session)
|
||||
longest_name_len = len(list(sorted([i.name for i in config.devices]))[-1])
|
||||
for i in config.devices:
|
||||
device = DeviceListener(api_helper, config, i, longest_name_len, debug)
|
||||
device = DeviceListener(api_helper, config, i, debug, web_session)
|
||||
devices.append(device)
|
||||
tasks.append(loop.create_task(device.loop()))
|
||||
tasks.append(loop.create_task(device.refresh_auth_loop()))
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
signal(SIGTERM, lambda s, f: loop.stop())
|
||||
rich.reconfigure(color_system="standard")
|
||||
loop.run_forever()
|
||||
print("Cancelling tasks and exiting...")
|
||||
loop.run_until_complete(finish(devices))
|
||||
loop.run_until_complete(web_session.close())
|
||||
loop.run_until_complete(tcp_connector.close())
|
||||
loop.close()
|
||||
|
||||
@@ -102,7 +102,6 @@ class Device(Element):
|
||||
"""A device element."""
|
||||
|
||||
def process_values_from_data(self):
|
||||
print("HIIII")
|
||||
print(self.element_data)
|
||||
if "name" in self.element_data and self.element_data["name"]:
|
||||
self.element_name = self.element_data["name"]
|
||||
@@ -235,8 +234,8 @@ class AddDevice(ModalWithClickExit):
|
||||
def __init__(self, config, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.config = config
|
||||
web_session = aiohttp.ClientSession()
|
||||
self.api_helper = api_helpers.ApiHelper(config, web_session)
|
||||
self.web_session = aiohttp.ClientSession()
|
||||
self.api_helper = api_helpers.ApiHelper(config, self.web_session)
|
||||
self.devices_discovered_dial = []
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
@@ -337,7 +336,9 @@ class AddDevice(ModalWithClickExit):
|
||||
@on(Button.Pressed, "#add-device-pin-add-button")
|
||||
async def handle_add_device_pin(self) -> None:
|
||||
self.query_one("#add-device-pin-add-button").disabled = True
|
||||
lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV")
|
||||
lounge_controller = ytlounge.YtLoungeApi(
|
||||
"iSponsorBlockTV", web_session=self.web_session
|
||||
)
|
||||
pairing_code = self.query_one("#pairing-code-input").value
|
||||
pairing_code = int(
|
||||
pairing_code.replace("-", "").replace(" ", "")
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import json
|
||||
|
||||
import pyytlounge
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from .constants import youtube_client_blacklist
|
||||
|
||||
@@ -9,8 +10,17 @@ create_task = asyncio.create_task
|
||||
|
||||
|
||||
class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
def __init__(self, screen_id, config=None, api_helper=None, logger=None):
|
||||
def __init__(
|
||||
self,
|
||||
screen_id,
|
||||
config=None,
|
||||
api_helper=None,
|
||||
logger=None,
|
||||
web_session: ClientSession = None,
|
||||
):
|
||||
super().__init__("iSponsorBlockTV", logger=logger)
|
||||
if web_session is not None:
|
||||
self.session = web_session # And use the one we passed
|
||||
self.auth.screen_id = screen_id
|
||||
self.auth.lounge_id_token = None
|
||||
self.api_helper = api_helper
|
||||
@@ -67,23 +77,23 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
data = args[0]
|
||||
# Unmute when the video starts playing
|
||||
if self.mute_ads and data.get("state", "0") == "1":
|
||||
# print("Ad has ended, unmuting")
|
||||
self.logger.info("Ad has ended, unmuting")
|
||||
create_task(self.mute(False, override=True))
|
||||
elif event_type == "onAdStateChange":
|
||||
data = args[0]
|
||||
if data["adState"] == "0": # Ad is not playing
|
||||
# print("Ad has ended, unmuting")
|
||||
self.logger.info("Ad has ended, unmuting")
|
||||
create_task(self.mute(False, override=True))
|
||||
elif (
|
||||
self.skip_ads and data["isSkipEnabled"] == "true"
|
||||
): # YouTube uses strings for booleans
|
||||
self._logger.info("Ad can be skipped, skipping")
|
||||
self.logger.info("Ad can be skipped, skipping")
|
||||
create_task(self.skip_ad())
|
||||
create_task(self.mute(False, override=True))
|
||||
elif (
|
||||
self.mute_ads
|
||||
): # Seen multiple other adStates, assuming they are all ads
|
||||
self._logger.info("Ad has started, muting")
|
||||
self.logger.info("Ad has started, muting")
|
||||
create_task(self.mute(True, override=True))
|
||||
# Manages volume, useful since YouTube wants to know the volume when unmuting (even if they already have it)
|
||||
elif event_type == "onVolumeChanged":
|
||||
@@ -94,7 +104,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
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}")
|
||||
self.logger.info(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
|
||||
@@ -102,18 +112,18 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
data = args[0]
|
||||
# Gets segments for the next video (after the ad) before it starts playing
|
||||
if vid_id := data["contentVideoId"]:
|
||||
self._logger.info(f"Getting segments for next video: {vid_id}")
|
||||
self.logger.info(f"Getting segments for next video: {vid_id}")
|
||||
create_task(self.api_helper.get_segments(vid_id))
|
||||
elif (
|
||||
self.skip_ads and data["isSkipEnabled"] == "true"
|
||||
): # YouTube uses strings for booleans
|
||||
self._logger.info("Ad can be skipped, skipping")
|
||||
self.logger.info("Ad can be skipped, skipping")
|
||||
create_task(self.skip_ad())
|
||||
create_task(self.mute(False, override=True))
|
||||
elif (
|
||||
self.mute_ads
|
||||
): # Seen multiple other adStates, assuming they are all ads
|
||||
self._logger.info("Ad has started, muting")
|
||||
self.logger.info("Ad has started, muting")
|
||||
create_task(self.mute(True, override=True))
|
||||
|
||||
elif event_type == "loungeStatus":
|
||||
|
||||
Reference in New Issue
Block a user