Merge branch 'main' into pr/tsia/76

This commit is contained in:
dmunozv04
2023-11-29 09:54:45 +01:00
12 changed files with 167 additions and 73 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
# Ignore files
.dockerignore
.gitignore
.deepsource.toml
.github
Dockerfile
docker-compose.yml
README.md

View File

@@ -22,15 +22,16 @@ Open an issue/pull request if you have tested a device that isn't listed here.
| Chromecast | ❔ | | Chromecast | ❔ |
| Google TV | ✅ | | Google TV | ✅ |
| Roku | ✅ | | Roku | ✅ |
| Fire TV | | | Fire TV | |
| CCwGTV | ✅ | | CCwGTV | ✅ |
| Nintendo Switch | ✅ | | Nintendo Switch | ✅ |
| Xbox One/Series | | | Xbox One/Series | |
| Playstation 4/5 | ✅ | | Playstation 4/5 | ✅ |
## Usage ## Usage
Run iSponsorBlockTV on a computer that has network access. Run iSponsorBlockTV on a computer that has network access.
Auto discovery will require the computer to be on the same network as the device during setup. Auto discovery will require the computer to be on the same network as the device during setup.
The device can also be manually added to iSponsorBlockTV with a YouTube TV code. This code can be found in the settings page of your YouTube application.
It connects to the device, watches its activity and skips any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API. It connects to the device, watches its activity and skips any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API.
It can also skip/mute YouTube ads. It can also skip/mute YouTube ads.

View File

@@ -1,8 +1,7 @@
from cache import AsyncTTL, AsyncLRU from cache import AsyncLRU
from .conditional_ttl_cache import AsyncConditionalTTL from .conditional_ttl_cache import AsyncConditionalTTL
from . import constants, dial_client from . import constants, dial_client
from hashlib import sha256 from hashlib import sha256
from asyncio import create_task
from aiohttp import ClientSession from aiohttp import ClientSession
import html import html
@@ -39,17 +38,17 @@ class ApiHelper:
return return
for i in data["items"]: for i in data["items"]:
if (i["id"]["kind"] != "youtube#video"): if i["id"]["kind"] != "youtube#video":
continue continue
title_api = html.unescape(i["snippet"]["title"]) title_api = html.unescape(i["snippet"]["title"])
artist_api = html.unescape(i["snippet"]["channelTitle"]) artist_api = html.unescape(i["snippet"]["channelTitle"])
if title_api == title and artist_api == artist: if title_api == title and artist_api == artist:
return (i["id"]["videoId"], i["snippet"]["channelId"]) return i["id"]["videoId"], i["snippet"]["channelId"]
return return
@AsyncLRU(maxsize=100) @AsyncLRU(maxsize=100)
async def is_whitelisted(self, vid_id): async def is_whitelisted(self, vid_id):
if (self.apikey and self.channel_whitelist): if self.apikey and self.channel_whitelist:
channel_id = await self.__get_channel_id(vid_id) channel_id = await self.__get_channel_id(vid_id)
# check if channel id is in whitelist # check if channel id is in whitelist
for i in self.channel_whitelist: for i in self.channel_whitelist:
@@ -66,7 +65,7 @@ class ApiHelper:
if "error" in data: if "error" in data:
return return
data = data["items"][0] data = data["items"][0]
if (data["kind"] != "youtube#video"): if data["kind"] != "youtube#video":
return return
return data["snippet"]["channelId"] return data["snippet"]["channelId"]
@@ -113,11 +112,21 @@ class ApiHelper:
headers = {"Accept": "application/json"} headers = {"Accept": "application/json"}
url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed
async with self.web_session.get(url, headers=headers, params=params) as response: async with self.web_session.get(url, headers=headers, params=params) as response:
response = await response.json() response_json = await response.json()
for i in response: if response.status != 200:
response_text = await response.text()
print(
f"Error getting segments for video {vid_id}, hashed as {vid_id_hashed}. "
f"Code: {response.status} - {response_text}")
return ([], True)
for i in response_json:
if str(i["videoID"]) == str(vid_id): if str(i["videoID"]) == str(vid_id):
response = i response_json = i
break break
return self.process_segments(response_json)
@staticmethod
def process_segments(response):
segments = [] segments = []
ignore_ttl = True ignore_ttl = True
try: try:
@@ -146,7 +155,8 @@ class ApiHelper:
return (segments, ignore_ttl) return (segments, ignore_ttl)
async def mark_viewed_segments(self, UUID): async def mark_viewed_segments(self, UUID):
"""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)""" """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)"""
if self.skip_count_tracking: if self.skip_count_tracking:
for i in UUID: for i in UUID:
url = constants.SponsorBlock_api + "viewedVideoSponsorTime/" url = constants.SponsorBlock_api + "viewedVideoSponsorTime/"

View File

@@ -1,14 +1,13 @@
import json
import asyncio import asyncio
import sys
import aiohttp import aiohttp
from . import api_helpers, ytlounge from . import api_helpers, ytlounge
async def pair_device(loop):
async def pair_device():
try: try:
lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV") lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV")
pairing_code = input("Enter pairing code (found in Settings - Link with TV code): ") pairing_code = input("Enter pairing code (found in Settings - Link with TV code): ")
pairing_code = int(pairing_code.replace("-", "").replace(" ","")) # remove dashes and spaces pairing_code = int(pairing_code.replace("-", "").replace(" ", "")) # remove dashes and spaces
print("Pairing...") print("Pairing...")
paired = await lounge_controller.pair(pairing_code) paired = await lounge_controller.pair(pairing_code)
if not paired: if not paired:
@@ -23,7 +22,8 @@ async def pair_device(loop):
except Exception as e: except Exception as e:
print(f"Failed to pair device: {e}") print(f"Failed to pair device: {e}")
return return
def main(config, debug: bool) -> None: def main(config, debug: bool) -> None:
print("Welcome to the iSponsorBlockTV cli setup wizard") print("Welcome to the iSponsorBlockTV cli setup wizard")
loop = asyncio.get_event_loop_policy().get_event_loop() loop = asyncio.get_event_loop_policy().get_event_loop()
@@ -31,12 +31,14 @@ def main(config, debug: bool) -> None:
loop.set_debug(True) loop.set_debug(True)
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
if hasattr(config, "atvs"): if hasattr(config, "atvs"):
print("The atvs config option is deprecated and has stopped working. Please read this for more information on how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2") print(
"The atvs config option is deprecated and has stopped working. Please read this for more information on "
"how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2")
if input("Do you want to remove the legacy 'atvs' entry (the app won't start with it present)? (y/n) ") == "y": if input("Do you want to remove the legacy 'atvs' entry (the app won't start with it present)? (y/n) ") == "y":
del config["atvs"] del config["atvs"]
devices = config.devices devices = config.devices
while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n":
task = loop.create_task(pair_device(loop)) task = loop.create_task(pair_device())
loop.run_until_complete(task) loop.run_until_complete(task)
device = task.result() device = task.result()
if device: if device:
@@ -56,41 +58,46 @@ def main(config, debug: bool) -> None:
apikey = input("Enter your API key: ") apikey = input("Enter your API key: ")
config["apikey"] = apikey config["apikey"] = apikey
config.apikey = apikey config.apikey = apikey
skip_categories = config.skip_categories skip_categories = config.skip_categories
if skip_categories: if skip_categories:
if input("Skip categories already specified. Change them? (y/n) ") == "y": if input("Skip categories already specified. Change them? (y/n) ") == "y":
categories = input( categories = input(
"Enter skip categories (space or comma sepparated) Options: [sponsor selfpromo exclusive_access interaction poi_highlight intro outro preview filler music_offtopic]:\n" "Enter skip categories (space or comma sepparated) Options: [sponsor selfpromo exclusive_access "
"interaction poi_highlight intro outro preview filler music_offtopic]:\n"
) )
skip_categories = categories.replace(",", " ").split(" ") skip_categories = categories.replace(",", " ").split(" ")
skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings
else: else:
categories = input( categories = input(
"Enter skip categories (space or comma sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n" "Enter skip categories (space or comma sepparated) Options: [sponsor, selfpromo, exclusive_access, "
"interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n"
) )
skip_categories = categories.replace(",", " ").split(" ") skip_categories = categories.replace(",", " ").split(" ")
skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings skip_categories = [x for x in skip_categories if x != ''] # Remove empty strings
config.skip_categories = skip_categories config.skip_categories = skip_categories
channel_whitelist = config.channel_whitelist channel_whitelist = config.channel_whitelist
if input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") == "y": if input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") == "y":
if(not apikey): if not apikey:
print("WARNING: You need to specify an API key to use this function, otherwise the program will fail to start.\nYou can add one by re-running this setup wizard.") print(
"WARNING: You need to specify an API key to use this function, otherwise the program will fail to "
"start.\nYou can add one by re-running this setup wizard.")
web_session = aiohttp.ClientSession() web_session = aiohttp.ClientSession()
api_helper = api_helpers.ApiHelper(config, web_session)
while True: while True:
channel_info = {} channel_info = {}
channel = input("Enter a channel name or \"/exit\" to exit: ") channel = input("Enter a channel name or \"/exit\" to exit: ")
if channel == "/exit": if channel == "/exit":
break break
task = loop.create_task(api_helpers.search_channels(channel, apikey, web_session)) task = loop.create_task(api_helper.search_channels(channel, apikey, web_session))
loop.run_until_complete(task) loop.run_until_complete(task)
results = task.result() results = task.result()
if len(results) == 0: if len(results) == 0:
print("No channels found") print("No channels found")
continue continue
for i in range(len(results)): for i in range(len(results)):
print(f"{i}: {results[i][1]} - Subs: {results[i][2]}") print(f"{i}: {results[i][1]} - Subs: {results[i][2]}")
print("5: Enter a custom channel ID") print("5: Enter a custom channel ID")
@@ -115,9 +122,10 @@ def main(config, debug: bool) -> None:
channel_whitelist.append(channel_info) channel_whitelist.append(channel_info)
# Close web session asynchronously # Close web session asynchronously
loop.run_until_complete(web_session.close()) loop.run_until_complete(web_session.close())
config.channel_whitelist = channel_whitelist config.channel_whitelist = channel_whitelist
config.skip_count_tracking = not input("Do you want to report skipped segments to sponsorblock. Only the segment UUID will be sent? (y/n) ") == "n" config.skip_count_tracking = not input(
"Do you want to report skipped segments to sponsorblock. Only the segment UUID will be sent? (y/n) ") == "n"
print("Config finished") print("Config finished")
config.save() config.save()

View File

@@ -17,3 +17,5 @@ skip_categories = (
('Preview', 'preview'), ('Preview', 'preview'),
('Filler', 'filler'), ('Filler', 'filler'),
) )
youtube_client_blacklist = ["TVHTML5_FOR_KIDS"]

View File

@@ -1,24 +1,19 @@
"""Send out a M-SEARCH request and listening for responses.""" """Send out a M-SEARCH request and listening for responses."""
import asyncio import asyncio
import socket import socket
import aiohttp
import ssdp import ssdp
from ssdp import network from ssdp import network
import xmltodict import xmltodict
''' '''Redistribution and use of the DIAL DIscovery And Launch protocol specification (the “DIAL Specification”),
Redistribution and use of the DIAL DIscovery And Launch protocol specification (the “DIAL Specification”), with or without modification, with or without modification, are permitted provided that the following conditions are met: ● Redistributions of the
are permitted provided that the following conditions are met: DIAL Specification must retain the above copyright notice, this list of conditions and the following disclaimer. ●
Redistributions of the DIAL Specification must retain the above copyright notice, this list of conditions and the following Redistributions of implementations of the DIAL Specification in source code form must retain the above copyright
disclaimer. notice, this list of conditions and the following disclaimer. ● Redistributions of implementations of the DIAL
● Redistributions of implementations of the DIAL Specification in source code form must retain the above copyright notice, this Specification in binary form must include the above copyright notice. ● The DIAL mark, the NETFLIX mark and the names
list of conditions and the following disclaimer. of contributors to the DIAL Specification may not be used to endorse or promote specifications, software, products,
● Redistributions of implementations of the DIAL Specification in binary form must include the above copyright notice. or any other materials derived from the DIAL Specification without specific prior written permission. The DIAL mark
● The DIAL mark, the NETFLIX mark and the names of contributors to the DIAL Specification may not be used to endorse or is owned by Netflix and information on licensing the DIAL mark is available at www.dial-multiscreen.org.'''
promote specifications, software, products, or any other materials derived from the DIAL Specification without specific prior
written permission. The DIAL mark is owned by Netflix and information on licensing the DIAL mark is available at
www.dial-multiscreen.org.'''
''' '''
MIT License MIT License
@@ -44,6 +39,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''' SOFTWARE.'''
'''Modified code from https://github.com/codingjoe/ssdp/blob/main/ssdp/__main__.py''' '''Modified code from https://github.com/codingjoe/ssdp/blob/main/ssdp/__main__.py'''
def get_ip(): def get_ip():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0) s.settimeout(0)
@@ -69,6 +65,7 @@ class Handler(ssdp.aio.SSDP):
def __call__(self): def __call__(self):
return self return self
def response_received(self, response: ssdp.messages.SSDPResponse, addr): def response_received(self, response: ssdp.messages.SSDPResponse, addr):
headers = response.headers headers = response.headers
headers = {k.lower(): v for k, v in headers} headers = {k.lower(): v for k, v in headers}

View File

@@ -43,7 +43,8 @@ class Config:
def validate(self): def validate(self):
if hasattr(self, "atvs"): if hasattr(self, "atvs"):
print( print(
"The atvs config option is deprecated and has stopped working. Please read this for more information on how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2", "The atvs config option is deprecated and has stopped working. Please read this for more information "
"on how to upgrade to V2: \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2",
) )
print("Exiting in 10 seconds...") print("Exiting in 10 seconds...")
time.sleep(10) time.sleep(10)

View File

@@ -3,7 +3,6 @@ import aiohttp
import time import time
import logging import logging
from . import api_helpers, ytlounge from . import api_helpers, ytlounge
import traceback
class DeviceListener: class DeviceListener:
@@ -41,7 +40,6 @@ class DeviceListener:
except: except:
# traceback.print_exc() # 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:
await asyncio.sleep(10) await asyncio.sleep(10)
@@ -50,6 +48,7 @@ class DeviceListener:
except: except:
pass pass
while not lounge_controller.connected() and not self.cancelled: while not lounge_controller.connected() and not self.cancelled:
# Doesn't connect to the device if it's a kids profile (it's broken)
await asyncio.sleep(10) await asyncio.sleep(10)
try: try:
await lounge_controller.connect() await lounge_controller.connect()
@@ -57,10 +56,10 @@ class DeviceListener:
pass pass
print(f"Connected to device {lounge_controller.screen_name} ({self.name})") print(f"Connected to device {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)
await sub await sub
#print("Subscription ended") await asyncio.sleep(10)
except: except:
pass pass
@@ -111,13 +110,12 @@ class DeviceListener:
self.api_helper.mark_viewed_segments(UUID) self.api_helper.mark_viewed_segments(UUID)
) # Don't wait for this to finish ) # Don't wait for this to finish
# Stops the connection to the device # Stops the connection to the device
async def cancel(self): async def cancel(self):
self.cancelled = True self.cancelled = True
try: try:
self.task.cancel() self.task.cancel()
except Exception as e: except Exception:
pass pass
@@ -143,7 +141,7 @@ def main(config, debug):
tasks.append(loop.create_task(device.refresh_auth_loop())) tasks.append(loop.create_task(device.refresh_auth_loop()))
try: try:
loop.run_forever() loop.run_forever()
except KeyboardInterrupt as e: except KeyboardInterrupt:
print("Keyboard interrupt detected, cancelling tasks and exiting...") print("Keyboard interrupt detected, cancelling tasks and exiting...")
loop.run_until_complete(finish(devices)) loop.run_until_complete(finish(devices))
finally: finally:

View File

@@ -1,6 +1,7 @@
import aiohttp
import asyncio import asyncio
import copy import copy
import aiohttp
# Textual imports (Textual is awesome!) # Textual imports (Textual is awesome!)
from textual import on from textual import on
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
@@ -12,6 +13,7 @@ from textual.widgets import Button, Footer, Header, Static, Label, Input, Select
RadioSet, RadioButton RadioSet, RadioButton
from textual.widgets.selection_list import Selection from textual.widgets.selection_list import Selection
from textual_slider import Slider from textual_slider import Slider
# Local imports # Local imports
from . import api_helpers, ytlounge from . import api_helpers, ytlounge
from .constants import skip_categories from .constants import skip_categories
@@ -457,7 +459,7 @@ class DevicesManager(Vertical):
@on(Button.Pressed, "#element-remove") @on(Button.Pressed, "#element-remove")
def remove_channel(self, event: Button.Pressed): def remove_channel(self, event: Button.Pressed):
channel_to_remove: Element = event.button.parent channel_to_remove: Element = event.button.parent
self.config.channel_whitelist.remove(channel_to_remove.element_data) self.config.devices.remove(channel_to_remove.element_data)
channel_to_remove.remove() channel_to_remove.remove()
@on(Button.Pressed, "#add-device") @on(Button.Pressed, "#add-device")
@@ -479,7 +481,7 @@ 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 Api Key from the [link=https://console.developers.google.com/apis/credentials]Google Cloud Console[/link]") "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")
@@ -557,9 +559,9 @@ class AdSkipMuteManager(Vertical):
"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.mute_ads, id="mute-ads-switch",
label="Enable skipping ads")
yield Checkbox(value=self.config.skip_ads, id="skip-ads-switch", yield Checkbox(value=self.config.skip_ads, id="skip-ads-switch",
label="Enable skipping ads")
yield Checkbox(value=self.config.mute_ads, id="mute-ads-switch",
label="Enable muting ads") label="Enable muting ads")
@on(Checkbox.Changed, "#mute-ads-switch") @on(Checkbox.Changed, "#mute-ads-switch")

View File

@@ -1,6 +1,12 @@
import asyncio import asyncio
import json
import aiohttp import aiohttp
import pyytlounge import pyytlounge
from .constants import youtube_client_blacklist
# Temporary imports
from pyytlounge.api import api_base
from pyytlounge.wrapper import NotLinkedException, desync
create_task = asyncio.create_task create_task = asyncio.create_task
@@ -24,7 +30,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
await asyncio.sleep(35) # YouTube sends at least a message every 30 seconds (no-op or any other) await asyncio.sleep(35) # YouTube sends at least a message every 30 seconds (no-op or any other)
try: try:
self.subscribe_task.cancel() self.subscribe_task.cancel()
except Exception as e: except Exception:
pass pass
# Subscribe to the lounge and start the watchdog # Subscribe to the lounge and start the watchdog
@@ -48,11 +54,10 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
pass pass
finally: finally:
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog()) self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
# A bunch of events useful to detect ads playing, and the next video before it starts playing (that way we can get the segments) # A bunch of events useful to detect ads playing, and the next video before it starts playing (that way we
# can get the segments)
if event_type == "onStateChange": if event_type == "onStateChange":
data = args[0] data = args[0]
self.state.apply_state(data)
self._update_state()
# print(data) # print(data)
# Unmute when the video starts playing # Unmute when the video starts playing
if self.mute_ads and data["state"] == "1": if self.mute_ads and data["state"] == "1":
@@ -63,12 +68,12 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
self._update_state() self._update_state()
# Unmute when the video starts playing # Unmute when the video starts playing
if self.mute_ads and data.get("state", "0") == "1": if self.mute_ads and data.get("state", "0") == "1":
#print("Ad has ended, unmuting") # print("Ad has ended, unmuting")
create_task(self.mute(False, override=True)) create_task(self.mute(False, override=True))
elif self.mute_ads and event_type == "onAdStateChange": elif self.mute_ads and event_type == "onAdStateChange":
data = args[0] data = args[0]
if data["adState"] == '0': # Ad is not playing if data["adState"] == '0': # Ad is not playing
#print("Ad has ended, unmuting") # print("Ad has ended, unmuting")
create_task(self.mute(False, override=True)) create_task(self.mute(False, override=True))
else: # Seen multiple other adStates, assuming they are all ads else: # Seen multiple other adStates, assuming they are all ads
print("Ad has started, muting") print("Ad has started, muting")
@@ -78,10 +83,11 @@ 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
elif event_type == "autoplayUpNext": # Comment "fix" since it doesn't seem to work
if len(args) > 0 and (vid_id := args[0]["videoId"]): # if video id is not empty # elif event_type == "autoplayUpNext":
print(f"Getting segments for next video: {vid_id}") # if len(args) > 0 and (vid_id := args[0]["videoId"]): # if video id is not empty
create_task(self.api_helper.get_segments(vid_id)) # print(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 # #Used to know if an ad is skippable or not
elif event_type == "adPlaying": elif event_type == "adPlaying":
@@ -97,8 +103,20 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
create_task(self.mute(False, override=True)) create_task(self.mute(False, override=True))
elif self.mute_ads: elif self.mute_ads:
create_task(self.mute(True, override=True)) create_task(self.mute(True, override=True))
else:
super()._process_event(event_id, event_type, args) elif event_type == "loungeStatus":
data = args[0]
devices = json.loads(data["devices"])
for device in devices:
if device["type"] == "LOUNGE_SCREEN":
device_info = json.loads(device.get("deviceInfo", ""))
if device_info.get("clientName", "") in youtube_client_blacklist:
self._sid = None
self._gsession = None # Force disconnect
# elif event_type == "onAutoplayModeChanged":
# data = args[0]
# create_task(self.set_auto_play_mode(data["autoplayMode"] == "ENABLED"))
super()._process_event(event_id, event_type, args)
# Set the volume to a specific value (0-100) # Set the volume to a specific value (0-100)
async def set_volume(self, volume: int) -> None: async def set_volume(self, volume: int) -> None:
@@ -117,3 +135,51 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
self.volume_state["muted"] = mute_str self.volume_state["muted"] = mute_str
# YouTube wants the volume when unmuting, so we send it # YouTube wants the volume when unmuting, so we send it
await super()._command("setVolume", {"volume": self.volume_state.get("volume", 100), "muted": mute_str}) await super()._command("setVolume", {"volume": self.volume_state.get("volume", 100), "muted": mute_str})
async def set_auto_play_mode(self, enabled: bool):
await super()._command("setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"})
# Here just temporarily, will be removed once the PR is merged on YTlounge
async def connect(self) -> bool:
"""Attempt to connect using the previously set tokens"""
if not self.linked():
raise NotLinkedException("Not linked")
connect_body = {
"app": "web",
"mdx-version": "3",
"name": self.device_name,
"id": self.auth.screen_id,
"device": "REMOTE_CONTROL",
"capabilities": "que,dsdtr,atp",
"method": "setPlaylist",
"magnaKey": "cloudPairedDevice",
"ui": "false",
"deviceContext": "user_agent=dunno&window_width_points=&window_height_points=&os_name=android&ms=",
"theme": "cl",
"loungeIdToken": self.auth.lounge_id_token,
}
connect_url = (
f"{api_base}/bc/bind?RID=1&VER=8&CVER=1&auth_failure_option=send_error"
)
async with aiohttp.ClientSession() as session:
async with session.post(url=connect_url, data=connect_body) as resp:
try:
text = await resp.text()
if resp.status == 401:
self.auth.lounge_id_token = None
return False
if resp.status != 200:
self._logger.warning(
"Unknown reply to connect %i %s", resp.status, resp.reason
)
return False
lines = text.splitlines()
async for events in self._parse_event_chunks(desync(lines)):
self._process_events(events)
self._command_offset = 1
return self.connected()
except Exception as ex:
self._logger.exception(ex, resp.status, resp.reason)
return False

View File

@@ -2,4 +2,4 @@ from iSponsorBlockTV import setup_wizard
from iSponsorBlockTV.helpers import Config from iSponsorBlockTV.helpers import Config
config = Config("data/config.json") config = Config("data/config.json")
setup_wizard.main(config) setup_wizard.main(config)

View File

@@ -1,7 +1,7 @@
aiohttp==3.8.6 aiohttp==3.9.0
argparse==1.4.0 argparse==1.4.0
async-cache==1.1.1 async-cache==1.1.1
pyytlounge==1.6.2 pyytlounge==1.6.3
rich==13.6.0 rich==13.6.0
ssdp==1.3.0 ssdp==1.3.0
textual==0.40.0 textual==0.40.0