Clean code and fix #121

This commit is contained in:
dmunozv04
2023-12-29 16:19:44 +01:00
parent 35652b6247
commit c3fd67df27
12 changed files with 151 additions and 98 deletions

View File

@@ -72,4 +72,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache cache-from: type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache
cache-to: type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache,mode=max # Only cache if it's not a pull request
cache-to: ${{ github.event_name != 'pull_request' && 'type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache,mode=max' || '' }}

View File

@@ -1,8 +1,9 @@
from . import helpers from . import helpers
def main(): def main():
helpers.app_start() helpers.app_start()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -6,11 +6,11 @@ from aiohttp import ClientSession
import html import html
def listToTuple(function): def list_to_tuple(function):
def wrapper(*args): def wrapper(*args):
args = [tuple(x) if type(x) == list else x for x in args] args = [tuple(x) if x is list else x for x in args]
result = function(*args) result = function(*args)
result = tuple(result) if type(result) == list else result result = tuple(result) if result is list else result
return result return result
return wrapper return wrapper
@@ -80,26 +80,26 @@ class ApiHelper:
return channels return channels
for i in data["items"]: for i in data["items"]:
# Get channel subcription number # Get channel subscription number
params = {"id": i["snippet"]["channelId"], "key": self.apikey, "part": "statistics"} params = {"id": i["snippet"]["channelId"], "key": self.apikey, "part": "statistics"}
url = constants.Youtube_api + "channels" url = constants.Youtube_api + "channels"
async with self.web_session.get(url, params=params) as resp: async with self.web_session.get(url, params=params) as resp:
channelData = await resp.json() channel_data = await resp.json()
if channelData["items"][0]["statistics"]["hiddenSubscriberCount"]: if channel_data["items"][0]["statistics"]["hiddenSubscriberCount"]:
subCount = "Hidden" sub_count = "Hidden"
else: else:
subCount = int(channelData["items"][0]["statistics"]["subscriberCount"]) sub_count = int(channel_data["items"][0]["statistics"]["subscriberCount"])
subCount = format(subCount, "_") sub_count = format(sub_count, "_")
channels.append((i["snippet"]["channelId"], i["snippet"]["channelTitle"], subCount)) channels.append((i["snippet"]["channelId"], i["snippet"]["channelTitle"], sub_count))
return channels return channels
@listToTuple # Convert list to tuple so it can be used as a key in the cache @list_to_tuple # Convert list to tuple so it can be used as a key in the cache
@AsyncConditionalTTL(time_to_live=300, maxsize=10) # 5 minutes for non-locked segments @AsyncConditionalTTL(time_to_live=300, maxsize=10) # 5 minutes for non-locked segments
async def get_segments(self, vid_id): async def get_segments(self, vid_id):
if await self.is_whitelisted(vid_id): if await self.is_whitelisted(vid_id):
return ([], True) # Return empty list and True to indicate that the cache should last forever return [], True # Return empty list and True to indicate that the cache should last forever
vid_id_hashed = sha256(vid_id.encode("utf-8")).hexdigest()[ vid_id_hashed = sha256(vid_id.encode("utf-8")).hexdigest()[
:4 :4
] # Hashes video id and gets the first 4 characters ] # Hashes video id and gets the first 4 characters
@@ -117,7 +117,7 @@ class ApiHelper:
print( print(
f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. " f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. "
f"Code: {response.status} - {response_text}") f"Code: {response.status} - {response_text}")
return ([], True) return [], True
for i in response_json: for i in response_json:
if str(i["videoID"]) == str(vid_id): if str(i["videoID"]) == str(vid_id):
response_json = i response_json = i
@@ -144,20 +144,20 @@ class ApiHelper:
segment_before_end = -10 segment_before_end = -10
if ( if (
segment_dict["start"] - segment_before_end < 1 segment_dict["start"] - segment_before_end < 1
): # Less than 1 second appart, combine them and skip them together ): # Less than 1 second apart, combine them and skip them together
segment_dict["start"] = segment_before_start segment_dict["start"] = segment_before_start
segment_dict["UUID"].extend(segment_before_UUID) segment_dict["UUID"].extend(segment_before_UUID)
segments.pop() segments.pop()
segments.append(segment_dict) segments.append(segment_dict)
except Exception: except Exception:
pass pass
return (segments, ignore_ttl) return segments, ignore_ttl
async def mark_viewed_segments(self, UUID): async def mark_viewed_segments(self, uuids):
"""Marks the segments as viewed in the SponsorBlock API, if skip_count_tracking is enabled. """Marks the segments as viewed in the SponsorBlock API, if skip_count_tracking is enabled.
Lets the contributor know that someone skipped the segment (thanks)""" Lets the contributor know that someone skipped the segment (thanks)"""
if self.skip_count_tracking: if self.skip_count_tracking:
for i in UUID: for i in uuids:
url = constants.SponsorBlock_api + "viewedVideoSponsorTime/" url = constants.SponsorBlock_api + "viewedVideoSponsorTime/"
params = {"UUID": i} params = {"UUID": i}
await self.web_session.post(url, params=params) await self.web_session.post(url, params=params)

View File

@@ -1,3 +1,7 @@
from cache.key import KEY
from cache.lru import LRU
import datetime
"""MIT License """MIT License
Copyright (c) 2020 Rajat Singh Copyright (c) 2020 Rajat Singh
@@ -21,10 +25,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.""" SOFTWARE."""
'''Modified code from https://github.com/iamsinghrajat/async-cache''' '''Modified code from https://github.com/iamsinghrajat/async-cache'''
from cache.key import KEY
from cache.lru import LRU
import datetime
class AsyncConditionalTTL: class AsyncConditionalTTL:
class _TTL(LRU): class _TTL(LRU):

View File

@@ -49,14 +49,12 @@ def main(config, debug: bool) -> None:
if apikey: if apikey:
if input("API key already specified. Change it? (y/n) ") == "y": if input("API key already specified. Change it? (y/n) ") == "y":
apikey = input("Enter your API key: ") apikey = input("Enter your API key: ")
config["apikey"] = apikey
else: else:
if input("API key only needed for the channel whitelist function. Add it? (y/n) ") == "y": if input("API key only needed for the channel whitelist function. Add it? (y/n) ") == "y":
print( print(
"Get youtube apikey here: https://developers.google.com/youtube/registering_an_application" "Get youtube apikey here: https://developers.google.com/youtube/registering_an_application"
) )
apikey = input("Enter your API key: ") apikey = input("Enter your API key: ")
config["apikey"] = apikey
config.apikey = apikey config.apikey = apikey
skip_categories = config.skip_categories skip_categories = config.skip_categories

View File

@@ -46,12 +46,12 @@ def get_ip():
try: try:
# doesn't even have to be reachable # doesn't even have to be reachable
s.connect(('10.254.254.254', 1)) s.connect(('10.254.254.254', 1))
IP = s.getsockname()[0] ip = s.getsockname()[0]
except Exception: except Exception:
IP = '127.0.0.1' ip = '127.0.0.1'
finally: finally:
s.close() s.close()
return IP return ip
class Handler(ssdp.aio.SSDP): class Handler(ssdp.aio.SSDP):

View File

@@ -36,7 +36,7 @@ class Config:
self.devices = [] self.devices = []
self.apikey = "" self.apikey = ""
self.skip_categories = [] self.skip_categories = [] # These are the categories on the config file
self.channel_whitelist = [] self.channel_whitelist = []
self.skip_count_tracking = True self.skip_count_tracking = True
self.mute_ads = False self.mute_ads = False
@@ -61,7 +61,7 @@ class Config:
if not self.apikey and self.channel_whitelist: if not self.apikey and self.channel_whitelist:
raise ValueError("No youtube API key found and channel whitelist is not empty") raise ValueError("No youtube API key found and channel whitelist is not empty")
if not self.skip_categories: if not self.skip_categories:
self.categories = ["sponsor"] self.skip_categories = ["sponsor"]
print("No categories found, using default: sponsor") print("No categories found, using default: sponsor")
def __load(self): def __load(self):
@@ -109,7 +109,7 @@ class Config:
def app_start(): def app_start():
#If env has a data dir use that, otherwise use the default # If env has a data dir use that, otherwise use the default
default_data_dir = os.getenv("iSPBTV_data_dir") or user_data_dir("iSponsorBlockTV", "dmunozv04") default_data_dir = os.getenv("iSPBTV_data_dir") or user_data_dir("iSponsorBlockTV", "dmunozv04")
parser = argparse.ArgumentParser(description="iSponsorblockTV") parser = argparse.ArgumentParser(description="iSponsorblockTV")
parser.add_argument("--data-dir", "-d", default=default_data_dir, help="data directory") parser.add_argument("--data-dir", "-d", default=default_data_dir, help="data directory")

View File

@@ -1,16 +1,40 @@
import logging import logging
from rich.logging import RichHandler from rich.logging import RichHandler
from rich._log_render import LogRender
from rich.text import Text from rich.text import Text
from rich.style import Style from rich.style import Style
'''
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): class LogHandler(RichHandler):
def __init__(self, device_name, log_name_len, *args, **kwargs): def __init__(self, device_name, log_name_len, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.filter_strings = [] self.filter_strings = []
self._log_render = LogRender( self._log_render = LogRender(
device_name = device_name, device_name=device_name,
log_name_len = log_name_len, log_name_len=log_name_len,
show_time=True, show_time=True,
show_level=True, show_level=True,
show_path=False, show_path=False,
@@ -20,34 +44,47 @@ class LogHandler(RichHandler):
def add_filter_string(self, s): def add_filter_string(self, s):
self.filter_strings.append(s) self.filter_strings.append(s)
def _filter(self, s): def _filter(self, s):
for i in self.filter_strings: for i in self.filter_strings:
s = s.replace(i, "REDACTED") s = s.replace(i, "REDACTED")
return s return s
def format(self, record): def format(self, record):
original = super().format(record) original = super().format(record)
return self._filter(original) return self._filter(original)
class LogRender(LogRender): class LogRender:
def __init__(self, device_name, log_name_len, *args, **kwargs): def __init__(self, device_name,
super().__init__(*args, **kwargs) 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.filter_strings = []
self.log_name_len = log_name_len self.log_name_len = log_name_len
self.device_name = device_name 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__( def __call__(
self, self,
console, console,
renderables, renderables,
log_time, log_time,
time_format = None, time_format=None,
level = "", level="",
path = None, path=None,
line_no = None, line_no=None,
link_path = None, link_path=None,
): ):
from rich.containers import Renderables from rich.containers import Renderables
from rich.table import Table from rich.table import Table
@@ -58,7 +95,7 @@ class LogRender(LogRender):
output.add_column(style="log.time") output.add_column(style="log.time")
if self.show_level: if self.show_level:
output.add_column(style="log.level", width=self.level_width) 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(width=self.log_name_len, style=Style(color="yellow"), overflow="fold")
output.add_column(ratio=1, style="log.message", overflow="fold") output.add_column(ratio=1, style="log.message", overflow="fold")
if self.show_path and path: if self.show_path and path:
output.add_column(style="log.path") output.add_column(style="log.path")
@@ -100,14 +137,16 @@ class LogFormatter(logging.Formatter):
def __init__(self, fmt=None, datefmt=None, style="%", validate=True, filter_strings=None): def __init__(self, fmt=None, datefmt=None, style="%", validate=True, filter_strings=None):
super().__init__(fmt, datefmt, style, validate) super().__init__(fmt, datefmt, style, validate)
self.filter_strings = filter_strings or [] self.filter_strings = filter_strings or []
def add_filter_string(self, s): def add_filter_string(self, s):
self.filter_strings.append(s) self.filter_strings.append(s)
def _filter(self, s): def _filter(self, s):
print(s) print(s)
for i in self.filter_strings: for i in self.filter_strings:
s = s.replace(i, "REDACTED") s = s.replace(i, "REDACTED")
return s return s
def format(self, record): def format(self, record):
original = logging.Formatter.format(self, record) original = logging.Formatter.format(self, record)
return self._filter(original) return self._filter(original)

View File

@@ -40,7 +40,8 @@ def main():
create_plist(correct_path) create_plist(correct_path)
run_setup(correct_path + "/config.json") run_setup(correct_path + "/config.json")
print( print(
"Launch daemon installed. Please restart the computer to enable it or use:\n launchctl load ~/Library/LaunchAgents/com.dmunozv04.iSponsorBlockTV.plist" 'Launch daemon installed. Please restart the computer to enable it or use:\n launchctl load '
'~/Library/LaunchAgents/com.dmunozv04.iSponsorBlockTV.plist'
) )
else: else:
if not os.path.exists(correct_path): if not os.path.exists(correct_path):
@@ -48,6 +49,6 @@ def main():
print( print(
"Please move the program to the correct path: " "Please move the program to the correct path: "
+ correct_path + correct_path
+ "opeing now on finder..." + "opening now on finder..."
) )
os.system("open -R " + correct_path) os.system("open -R " + correct_path)

View File

@@ -3,6 +3,7 @@ import aiohttp
import time import time
import logging import logging
import rich import rich
from typing import Optional
from signal import signal, SIGINT, SIGTERM from signal import signal, SIGINT, SIGTERM
from . import api_helpers, ytlounge, logging_helpers from . import api_helpers, ytlounge, logging_helpers
@@ -10,7 +11,7 @@ from . import api_helpers, ytlounge, logging_helpers
class DeviceListener: class DeviceListener:
def __init__(self, api_helper, config, device, log_name_len, debug: bool): def __init__(self, api_helper, config, device, log_name_len, debug: bool):
self.task: asyncio.Task = None self.task: Optional[asyncio.Task] = None
self.api_helper = api_helper self.api_helper = api_helper
self.offset = device.offset self.offset = device.offset
self.name = device.name self.name = device.name
@@ -52,7 +53,6 @@ class DeviceListener:
self.logger.debug("Refreshing auth") self.logger.debug("Refreshing auth")
await lounge_controller.refresh_auth() await lounge_controller.refresh_auth()
except: except:
# traceback.print_exc()
await asyncio.sleep(10) await asyncio.sleep(10)
while not self.cancelled: while not self.cancelled:
while not (await self.is_available()) and not self.cancelled: while not (await self.is_available()) and not self.cancelled:
@@ -68,7 +68,7 @@ class DeviceListener:
await lounge_controller.connect() await lounge_controller.connect()
except: except:
pass pass
self.logger.info(f"Connected to device {lounge_controller.screen_name} ({self.name})") self.logger.info("Connected to device %s (%s)", lounge_controller.screen_name, self.name)
try: try:
# print("Subscribing to lounge") # print("Subscribing to lounge")
sub = await lounge_controller.subscribe_monitored(self) sub = await lounge_controller.subscribe_monitored(self)
@@ -115,11 +115,11 @@ class DeviceListener:
await self.skip(time_to_next, next_segment["end"], next_segment["UUID"]) await self.skip(time_to_next, next_segment["end"], next_segment["UUID"])
# Skips to the next segment (waits for the time to pass) # Skips to the next segment (waits for the time to pass)
async def skip(self, time_to, position, UUID): async def skip(self, time_to, position, uuids):
await asyncio.sleep(time_to) await asyncio.sleep(time_to)
self.logger.info(f"Skipping segment: seeking to {position}") self.logger.info("Skipping segment: seeking to %s", position)
asyncio.create_task(self.api_helper.mark_viewed_segments(UUID)) await asyncio.create_task(self.api_helper.mark_viewed_segments(uuids))
asyncio.create_task(self.lounge_controller.seek_to(position)) await asyncio.create_task(self.lounge_controller.seek_to(position))
# Stops the connection to the device # Stops the connection to the device
async def cancel(self): async def cancel(self):

View File

@@ -80,7 +80,9 @@ class Device(Element):
if "name" in self.element_data and self.element_data["name"]: if "name" in self.element_data and self.element_data["name"]:
self.element_name = self.element_data["name"] self.element_name = self.element_data["name"]
else: else:
self.element_name = f"Unnamed device with id {self.element_data['screen_id'][:5]}...{self.element_data['screen_id'][-5:]}" self.element_name = (f"Unnamed device with id "
f"{self.element_data['screen_id'][:5]}..."
f"{self.element_data['screen_id'][-5:]}")
class Channel(Element): class Channel(Element):
@@ -112,7 +114,8 @@ class MigrationScreen(ModalWithClickExit):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Grid( yield Grid(
Label( Label(
"Welcome to the new configurator! You seem to have the legacy 'atvs' entry on your config file, do you want to remove it?\n(The app won't start with it present)", "Welcome to the new configurator! You seem to have the legacy 'atvs' entry on your config file, "
"do you want to remove it?\n(The app won't start with it present)",
id="question", classes="button-100"), id="question", classes="button-100"),
Button("Remove and save", variant="primary", id="migrate-remove-save", classes="button-100"), Button("Remove and save", variant="primary", id="migrate-remove-save", classes="button-100"),
Button("Don't remove", variant="error", id="migrate-no-change", classes="button-100"), Button("Don't remove", variant="error", id="migrate-no-change", classes="button-100"),
@@ -196,7 +199,9 @@ class AddDevice(ModalWithClickExit):
yield Label(id="add-device-info") yield Label(id="add-device-info")
with Container(id="add-device-dial-container"): with Container(id="add-device-dial-container"):
yield Label( yield Label(
"Make sure your device is on the same network as this computer\nIf it isn't showing up, try restarting the app.\nIf running in docker, make sure to use `--network=host`\nTo refresh the list, close and open the dialog again", "Make sure your device is on the same network as this computer\nIf it isn't showing up, "
"try restarting the app.\nIf running in docker, make sure to use `--network=host`\nTo refresh "
"the list, close and open the dialog again",
classes="subtitle") classes="subtitle")
yield SelectionList(("Searching for devices...", "", False), id="dial-devices-list", disabled=True) yield SelectionList(("Searching for devices...", "", False), id="dial-devices-list", disabled=True)
yield Button("Add selected devices", id="add-device-dial-add-button", variant="success", yield Button("Add selected devices", id="add-device-dial-add-button", variant="success",
@@ -223,7 +228,6 @@ class AddDevice(ModalWithClickExit):
@on(Button.Pressed, "#add-device-switch-buttons > *") @on(Button.Pressed, "#add-device-switch-buttons > *")
def handle_switch_buttons(self, event: Button.Pressed) -> None: def handle_switch_buttons(self, event: Button.Pressed) -> None:
button_ = event.button.id
self.query_one("#add-device-switcher").current = event.button.id.replace("-button", "-container") self.query_one("#add-device-switcher").current = event.button.id.replace("-button", "-container")
@on(Input.Changed, "#pairing-code-input") @on(Input.Changed, "#pairing-code-input")
@@ -304,7 +308,8 @@ class AddChannel(ModalWithClickExit):
classes="button-100") classes="button-100")
else: else:
yield Label( yield Label(
"[#ff0000]No api key set, cannot search for channels. You can add it the config section below", "[#ff0000]No api key set, cannot search for channels. You can add it the config section "
"below",
id="add-channel-search-no-key", classes="subtitle") id="add-channel-search-no-key", classes="subtitle")
with Vertical(id="add-channel-id-container"): with Vertical(id="add-channel-id-container"):
yield Input(placeholder="Enter channel ID (example: UCuAXFkgsw1L7xaCfnd5JJOw)", yield Input(placeholder="Enter channel ID (example: UCuAXFkgsw1L7xaCfnd5JJOw)",
@@ -406,9 +411,9 @@ class EditDevice(ModalWithClickExit):
yield Slider(name="Device offset", id="device-offset-slider", min=0, max=2000, step=100, value=offset) yield Slider(name="Device offset", id="device-offset-slider", min=0, max=2000, step=100, value=offset)
def on_slider_changed(self, event: Slider.Changed) -> None: def on_slider_changed(self, event: Slider.Changed) -> None:
input = self.query_one("#device-offset-input") offset_input = self.query_one("#device-offset-offset_input")
with input.prevent(Input.Changed): with offset_input.prevent(Input.Changed):
input.value = str(event.slider.value) offset_input.value = str(event.slider.value)
def on_input_changed(self, event: Input.Changed): def on_input_changed(self, event: Input.Changed):
if event.input.id == "device-offset-input": if event.input.id == "device-offset-input":
@@ -430,7 +435,8 @@ class EditDevice(ModalWithClickExit):
class DevicesManager(Vertical): class DevicesManager(Vertical):
"""Manager for devices, allows to add, edit and remove devices.""" """Manager for devices, allows adding, edit and removing devices."""
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
@@ -452,7 +458,8 @@ class DevicesManager(Vertical):
self.mount(device_widget) self.mount(device_widget)
device_widget.focus(scroll_visible=True) device_widget.focus(scroll_visible=True)
def edit_device(self, device_widget: Element) -> None: @staticmethod
def edit_device(device_widget: Element) -> None:
device_widget.process_values_from_data() device_widget.process_values_from_data()
device_widget.query_one("#element-name").label = device_widget.element_name device_widget.query_one("#element-name").label = device_widget.element_name
@@ -474,6 +481,7 @@ class DevicesManager(Vertical):
class ApiKeyManager(Vertical): class ApiKeyManager(Vertical):
"""Manager for the YouTube Api Key.""" """Manager for the YouTube Api Key."""
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
@@ -481,7 +489,9 @@ class ApiKeyManager(Vertical):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Label("YouTube Api Key", classes="title") yield Label("YouTube Api Key", classes="title")
yield Label( yield Label(
"You can get a YouTube Data API v3 Key from the [link=https://console.developers.google.com/apis/credentials]Google Cloud Console[/link]. This key is only required if you're whitelisting channels.") "You can get a YouTube Data API v3 Key from the ["
"link=https://console.developers.google.com/apis/credentials]Google Cloud Console[/link]. This key is "
"only required if you're whitelisting channels.")
with Grid(id="api-key-grid"): with Grid(id="api-key-grid"):
yield Input(placeholder="YouTube Api Key", id="api-key-input", password=True, value=self.config.apikey) yield Input(placeholder="YouTube Api Key", id="api-key-input", password=True, value=self.config.apikey)
yield Button("Show key", id="api-key-view") yield Button("Show key", id="api-key-view")
@@ -489,10 +499,6 @@ class ApiKeyManager(Vertical):
@on(Input.Changed, "#api-key-input") @on(Input.Changed, "#api-key-input")
def changed_api_key(self, event: Input.Changed): def changed_api_key(self, event: Input.Changed):
self.config.apikey = event.input.value self.config.apikey = event.input.value
# try: # ChannelWhitelist might not be mounted
# self.app.query_one("#warning-no-key").display = not self.config.apikey
# except:
# pass
@on(Button.Pressed, "#api-key-view") @on(Button.Pressed, "#api-key-view")
def pressed_api_key_view(self, event: Button.Pressed): def pressed_api_key_view(self, event: Button.Pressed):
@@ -505,7 +511,8 @@ class ApiKeyManager(Vertical):
class SkipCategoriesManager(Vertical): class SkipCategoriesManager(Vertical):
"""Manager for skip categories, allows to select which categories to skip.""" """Manager for skip categories, allows selecting which categories to skip."""
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
@@ -530,6 +537,7 @@ class SkipCategoriesManager(Vertical):
class SkipCountTrackingManager(Vertical): class SkipCountTrackingManager(Vertical):
"""Manager for skip count tracking, allows to enable/disable skip count tracking.""" """Manager for skip count tracking, allows to enable/disable skip count tracking."""
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
@@ -537,7 +545,10 @@ class SkipCountTrackingManager(Vertical):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Label("Skip count tracking", classes="title") yield Label("Skip count tracking", classes="title")
yield Label( yield Label(
"This feature tracks which segments you have skipped to let users know how much their submission has helped others and used as a metric along with upvotes to ensure that spam doesn't get into the database. The program sends a message to the sponsor block server each time you skip a segment. Hopefully most people don't change this setting so that the view numbers are accurate. :)", "This feature tracks which segments you have skipped to let users know how much their submission has "
"helped others and used as a metric along with upvotes to ensure that spam doesn't get into the database. "
"The program sends a message to the sponsor block server each time you skip a segment. Hopefully most "
"people don't change this setting so that the view numbers are accurate. :)",
classes="subtitle", id="skip-count-tracking-subtitle") classes="subtitle", id="skip-count-tracking-subtitle")
yield Checkbox(value=self.config.skip_count_tracking, id="skip-count-tracking-switch", yield Checkbox(value=self.config.skip_count_tracking, id="skip-count-tracking-switch",
label="Enable skip count tracking") label="Enable skip count tracking")
@@ -549,6 +560,7 @@ class SkipCountTrackingManager(Vertical):
class AdSkipMuteManager(Vertical): class AdSkipMuteManager(Vertical):
"""Manager for ad skip/mute, allows to enable/disable ad skip/mute.""" """Manager for ad skip/mute, allows to enable/disable ad skip/mute."""
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
@@ -556,7 +568,8 @@ class AdSkipMuteManager(Vertical):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Label("Skip/Mute ads", classes="title") yield Label("Skip/Mute ads", classes="title")
yield Label( yield Label(
"This feature allows you to automatically mute and/or skip native YouTube ads. Skipping ads only works if that ad shows the 'Skip Ad' button, if it doesn't then it will only be able to be muted.", "This feature allows you to automatically mute and/or skip native YouTube ads. Skipping ads only works if "
"that ad shows the 'Skip Ad' button, if it doesn't then it will only be able to be muted.",
classes="subtitle", id="skip-count-tracking-subtitle") classes="subtitle", id="skip-count-tracking-subtitle")
with Horizontal(id="ad-skip-mute-container"): with Horizontal(id="ad-skip-mute-container"):
yield Checkbox(value=self.config.skip_ads, id="skip-ads-switch", yield Checkbox(value=self.config.skip_ads, id="skip-ads-switch",
@@ -574,7 +587,8 @@ class AdSkipMuteManager(Vertical):
class ChannelWhitelistManager(Vertical): class ChannelWhitelistManager(Vertical):
"""Manager for channel whitelist, allows to add/remove channels from the whitelist.""" """Manager for channel whitelist, allows adding/removing channels from the whitelist."""
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
@@ -582,7 +596,8 @@ class ChannelWhitelistManager(Vertical):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Label("Channel Whitelist", classes="title") yield Label("Channel Whitelist", classes="title")
yield Label( yield Label(
"This feature allows to whitelist channels from being skipped. This feature is automatically disabled when no channels have been specified.", "This feature allows to whitelist channels from being skipped. This feature is automatically disabled "
"when no channels have been specified.",
classes="subtitle", id="channel-whitelist-subtitle") classes="subtitle", id="channel-whitelist-subtitle")
yield Label(":warning: [#FF0000]You need to set your YouTube Api Key in order to use this feature", yield Label(":warning: [#FF0000]You need to set your YouTube Api Key in order to use this feature",
id="warning-no-key") id="warning-no-key")
@@ -593,6 +608,7 @@ class ChannelWhitelistManager(Vertical):
def on_mount(self) -> None: def on_mount(self) -> None:
self.app.query_one("#warning-no-key").display = (not self.config.apikey) and bool(self.config.channel_whitelist) self.app.query_one("#warning-no-key").display = (not self.config.apikey) and bool(self.config.channel_whitelist)
def new_channel(self, channel: tuple) -> None: def new_channel(self, channel: tuple) -> None:
if channel: if channel:
channel_dict = { channel_dict = {
@@ -619,7 +635,7 @@ class ChannelWhitelistManager(Vertical):
self.app.push_screen(AddChannel(self.config), callback=self.new_channel) self.app.push_screen(AddChannel(self.config), callback=self.new_channel)
class iSponsorBlockTVSetupMainScreen(Screen): class ISponsorBlockTVSetupMainScreen(Screen):
"""Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221""" """Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221"""
TITLE = "iSponsorBlockTV" TITLE = "iSponsorBlockTV"
SUB_TITLE = "Setup Wizard" SUB_TITLE = "Setup Wizard"
@@ -668,15 +684,15 @@ class iSponsorBlockTVSetupMainScreen(Screen):
@on(Input.Changed, "#api-key-input") @on(Input.Changed, "#api-key-input")
def changed_api_key(self, event: Input.Changed): def changed_api_key(self, event: Input.Changed):
print("HIIII")
try: # ChannelWhitelist might not be mounted try: # ChannelWhitelist might not be mounted
# Show if no api key is set and at least one channel is in the whitelist # Show if no api key is set and at least one channel is in the whitelist
self.app.query_one("#warning-no-key").display = (not event.input.value) and self.config.channel_whitelist self.app.query_one("#warning-no-key").display = (not event.input.value) and self.config.channel_whitelist
except: except:
pass pass
class iSponsorBlockTVSetup(App):
CSS_PATH = "setup-wizard-style.tcss" # tcss is the recommended extension for textual css files class ISponsorBlockTVSetup(App):
CSS_PATH = "setup-wizard-style.tcss" # tcss is the recommended extension for textual css files
# Bindings for the whole app here, so they are available in all screens # Bindings for the whole app here, so they are available in all screens
BINDINGS = [ BINDINGS = [
("q,ctrl+c", "exit_modal", "Exit"), ("q,ctrl+c", "exit_modal", "Exit"),
@@ -686,7 +702,7 @@ class iSponsorBlockTVSetup(App):
def __init__(self, config, **kwargs) -> None: def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self.config = config self.config = config
self.main_screen = iSponsorBlockTVSetupMainScreen(config=self.config) self.main_screen = ISponsorBlockTVSetupMainScreen(config=self.config)
def on_mount(self) -> None: def on_mount(self) -> None:
self.push_screen(self.main_screen) self.push_screen(self.main_screen)
@@ -699,5 +715,5 @@ class iSponsorBlockTVSetup(App):
def main(config): def main(config):
app = iSponsorBlockTVSetup(config) app = ISponsorBlockTVSetup(config)
app.run() app.run()

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
import json import json
import aiohttp
import pyytlounge import pyytlounge
from .constants import youtube_client_blacklist from .constants import youtube_client_blacklist
create_task = asyncio.create_task create_task = asyncio.create_task
@@ -83,11 +83,10 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
self.volume_state = args[0] self.volume_state = args[0]
pass pass
# Gets segments for the next video before it starts playing # Gets segments for the next video before it starts playing
# Comment "fix" since it doesn't seem to work elif event_type == "autoplayUpNext":
# elif event_type == "autoplayUpNext": if len(args) > 0 and (vid_id := args[0]["videoId"]): # if video id is not empty
# 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}")
# print(f"Getting segments for next video: {vid_id}") create_task(self.api_helper.get_segments(vid_id))
# create_task(self.api_helper.get_segments(vid_id))
# #Used to know if an ad is skippable or not # #Used to know if an ad is skippable or not
elif event_type == "adPlaying": elif event_type == "adPlaying":
@@ -113,9 +112,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
if device_info.get("clientName", "") in youtube_client_blacklist: if device_info.get("clientName", "") in youtube_client_blacklist:
self._sid = None self._sid = None
self._gsession = None # Force disconnect self._gsession = None # Force disconnect
# elif event_type == "onAutoplayModeChanged":
# data = args[0]
# create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED"))
elif event_type == "onSubtitlesTrackChanged": elif event_type == "onSubtitlesTrackChanged":
if self.shorts_disconnected: if self.shorts_disconnected:
data = args[0] data = args[0]
@@ -124,7 +121,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
create_task(self.play_video(video_id_saved)) create_task(self.play_video(video_id_saved))
elif event_type == "loungeScreenDisconnected": elif event_type == "loungeScreenDisconnected":
data = args[0] data = args[0]
if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing? if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing?
self.shorts_disconnected = True self.shorts_disconnected = True
super()._process_event(event_id, event_type, args) super()._process_event(event_id, event_type, args)
@@ -151,4 +148,4 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"}) await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"})
async def play_video(self, video_id: str) -> bool: async def play_video(self, video_id: str) -> bool:
return await self._command("setPlaylist", {"videoId": video_id}) return await self._command("setPlaylist", {"videoId": video_id})