mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-08 21:06:43 +03:00
Clean code and fix #121
This commit is contained in:
3
.github/workflows/build_docker_images.yml
vendored
3
.github/workflows/build_docker_images.yml
vendored
@@ -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' || '' }}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
Reference in New Issue
Block a user