mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2026-01-03 15:19:10 +03:00
Merge branch 'main' into pr/tsia/76
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
# Ignore files
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.deepsource.toml
|
||||
.github
|
||||
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
README.md
|
||||
@@ -22,15 +22,16 @@ Open an issue/pull request if you have tested a device that isn't listed here.
|
||||
| Chromecast | ❔ |
|
||||
| Google TV | ✅ |
|
||||
| Roku | ✅ |
|
||||
| Fire TV | ❔ |
|
||||
| Fire TV | ✅ |
|
||||
| CCwGTV | ✅ |
|
||||
| Nintendo Switch | ✅ |
|
||||
| Xbox One/Series | ❔ |
|
||||
| Xbox One/Series | ✅ |
|
||||
| Playstation 4/5 | ✅ |
|
||||
|
||||
## Usage
|
||||
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.
|
||||
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 can also skip/mute YouTube ads.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from cache import AsyncTTL, AsyncLRU
|
||||
from cache import AsyncLRU
|
||||
from .conditional_ttl_cache import AsyncConditionalTTL
|
||||
from . import constants, dial_client
|
||||
from hashlib import sha256
|
||||
from asyncio import create_task
|
||||
from aiohttp import ClientSession
|
||||
import html
|
||||
|
||||
@@ -39,17 +38,17 @@ class ApiHelper:
|
||||
return
|
||||
|
||||
for i in data["items"]:
|
||||
if (i["id"]["kind"] != "youtube#video"):
|
||||
if i["id"]["kind"] != "youtube#video":
|
||||
continue
|
||||
title_api = html.unescape(i["snippet"]["title"])
|
||||
artist_api = html.unescape(i["snippet"]["channelTitle"])
|
||||
if title_api == title and artist_api == artist:
|
||||
return (i["id"]["videoId"], i["snippet"]["channelId"])
|
||||
return i["id"]["videoId"], i["snippet"]["channelId"]
|
||||
return
|
||||
|
||||
@AsyncLRU(maxsize=100)
|
||||
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)
|
||||
# check if channel id is in whitelist
|
||||
for i in self.channel_whitelist:
|
||||
@@ -66,7 +65,7 @@ class ApiHelper:
|
||||
if "error" in data:
|
||||
return
|
||||
data = data["items"][0]
|
||||
if (data["kind"] != "youtube#video"):
|
||||
if data["kind"] != "youtube#video":
|
||||
return
|
||||
return data["snippet"]["channelId"]
|
||||
|
||||
@@ -113,11 +112,21 @@ class ApiHelper:
|
||||
headers = {"Accept": "application/json"}
|
||||
url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed
|
||||
async with self.web_session.get(url, headers=headers, params=params) as response:
|
||||
response = await response.json()
|
||||
for i in response:
|
||||
response_json = await response.json()
|
||||
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):
|
||||
response = i
|
||||
response_json = i
|
||||
break
|
||||
return self.process_segments(response_json)
|
||||
|
||||
@staticmethod
|
||||
def process_segments(response):
|
||||
segments = []
|
||||
ignore_ttl = True
|
||||
try:
|
||||
@@ -146,7 +155,8 @@ class ApiHelper:
|
||||
return (segments, ignore_ttl)
|
||||
|
||||
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:
|
||||
for i in UUID:
|
||||
url = constants.SponsorBlock_api + "viewedVideoSponsorTime/"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import json
|
||||
import asyncio
|
||||
import sys
|
||||
import aiohttp
|
||||
from . import api_helpers, ytlounge
|
||||
|
||||
async def pair_device(loop):
|
||||
|
||||
async def pair_device():
|
||||
try:
|
||||
lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV")
|
||||
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...")
|
||||
paired = await lounge_controller.pair(pairing_code)
|
||||
if not paired:
|
||||
@@ -24,6 +23,7 @@ async def pair_device(loop):
|
||||
print(f"Failed to pair device: {e}")
|
||||
return
|
||||
|
||||
|
||||
def main(config, debug: bool) -> None:
|
||||
print("Welcome to the iSponsorBlockTV cli setup wizard")
|
||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
||||
@@ -31,12 +31,14 @@ def main(config, debug: bool) -> None:
|
||||
loop.set_debug(True)
|
||||
asyncio.set_event_loop(loop)
|
||||
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":
|
||||
del config["atvs"]
|
||||
devices = config.devices
|
||||
while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n":
|
||||
task = loop.create_task(pair_device(loop))
|
||||
task = loop.create_task(pair_device())
|
||||
loop.run_until_complete(task)
|
||||
device = task.result()
|
||||
if device:
|
||||
@@ -61,30 +63,35 @@ def main(config, debug: bool) -> None:
|
||||
if skip_categories:
|
||||
if input("Skip categories already specified. Change them? (y/n) ") == "y":
|
||||
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 = [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:
|
||||
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 = [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
|
||||
|
||||
channel_whitelist = config.channel_whitelist
|
||||
if input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") == "y":
|
||||
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.")
|
||||
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.")
|
||||
web_session = aiohttp.ClientSession()
|
||||
api_helper = api_helpers.ApiHelper(config, web_session)
|
||||
while True:
|
||||
channel_info = {}
|
||||
channel = input("Enter a channel name or \"/exit\" to exit: ")
|
||||
if channel == "/exit":
|
||||
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)
|
||||
results = task.result()
|
||||
if len(results) == 0:
|
||||
@@ -118,6 +125,7 @@ def main(config, debug: bool) -> None:
|
||||
|
||||
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")
|
||||
config.save()
|
||||
|
||||
@@ -17,3 +17,5 @@ skip_categories = (
|
||||
('Preview', 'preview'),
|
||||
('Filler', 'filler'),
|
||||
)
|
||||
|
||||
youtube_client_blacklist = ["TVHTML5_FOR_KIDS"]
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
"""Send out a M-SEARCH request and listening for responses."""
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
import aiohttp
|
||||
import ssdp
|
||||
from ssdp import network
|
||||
import xmltodict
|
||||
|
||||
'''
|
||||
Redistribution and use of the DIAL DIscovery And Launch protocol specification (the “DIAL Specification”), with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
● Redistributions of the DIAL Specification must retain the above copyright notice, this list of conditions and the following
|
||||
disclaimer.
|
||||
● Redistributions of implementations of the DIAL Specification in source code form must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
● Redistributions of implementations of the DIAL Specification in binary form must include the above copyright notice.
|
||||
● The DIAL mark, the NETFLIX mark and the names of contributors to the DIAL Specification may not be used to endorse or
|
||||
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.'''
|
||||
'''Redistribution and use of the DIAL DIscovery And Launch protocol specification (the “DIAL Specification”),
|
||||
with or without modification, are permitted provided that the following conditions are met: ● Redistributions of the
|
||||
DIAL Specification must retain the above copyright notice, this list of conditions and the following disclaimer. ●
|
||||
Redistributions of implementations of the DIAL Specification in source code form must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer. ● Redistributions of implementations of the DIAL
|
||||
Specification in binary form must include the above copyright notice. ● The DIAL mark, the NETFLIX mark and the names
|
||||
of contributors to the DIAL Specification may not be used to endorse or 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
|
||||
@@ -44,6 +39,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.'''
|
||||
'''Modified code from https://github.com/codingjoe/ssdp/blob/main/ssdp/__main__.py'''
|
||||
|
||||
|
||||
def get_ip():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(0)
|
||||
@@ -69,6 +65,7 @@ class Handler(ssdp.aio.SSDP):
|
||||
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
def response_received(self, response: ssdp.messages.SSDPResponse, addr):
|
||||
headers = response.headers
|
||||
headers = {k.lower(): v for k, v in headers}
|
||||
|
||||
@@ -43,7 +43,8 @@ class Config:
|
||||
def validate(self):
|
||||
if hasattr(self, "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",
|
||||
"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...")
|
||||
time.sleep(10)
|
||||
|
||||
@@ -3,7 +3,6 @@ import aiohttp
|
||||
import time
|
||||
import logging
|
||||
from . import api_helpers, ytlounge
|
||||
import traceback
|
||||
|
||||
|
||||
class DeviceListener:
|
||||
@@ -41,7 +40,6 @@ class DeviceListener:
|
||||
except:
|
||||
# traceback.print_exc()
|
||||
await asyncio.sleep(10)
|
||||
|
||||
while not self.cancelled:
|
||||
while not (await self.is_available()) and not self.cancelled:
|
||||
await asyncio.sleep(10)
|
||||
@@ -50,6 +48,7 @@ class DeviceListener:
|
||||
except:
|
||||
pass
|
||||
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)
|
||||
try:
|
||||
await lounge_controller.connect()
|
||||
@@ -57,10 +56,10 @@ class DeviceListener:
|
||||
pass
|
||||
print(f"Connected to device {lounge_controller.screen_name} ({self.name})")
|
||||
try:
|
||||
#print("Subscribing to lounge")
|
||||
# print("Subscribing to lounge")
|
||||
sub = await lounge_controller.subscribe_monitored(self)
|
||||
await sub
|
||||
#print("Subscription ended")
|
||||
await asyncio.sleep(10)
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -111,13 +110,12 @@ class DeviceListener:
|
||||
self.api_helper.mark_viewed_segments(UUID)
|
||||
) # Don't wait for this to finish
|
||||
|
||||
|
||||
# Stops the connection to the device
|
||||
async def cancel(self):
|
||||
self.cancelled = True
|
||||
try:
|
||||
self.task.cancel()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -143,7 +141,7 @@ def main(config, debug):
|
||||
tasks.append(loop.create_task(device.refresh_auth_loop()))
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt as e:
|
||||
except KeyboardInterrupt:
|
||||
print("Keyboard interrupt detected, cancelling tasks and exiting...")
|
||||
loop.run_until_complete(finish(devices))
|
||||
finally:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
import aiohttp
|
||||
# Textual imports (Textual is awesome!)
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
@@ -12,6 +13,7 @@ from textual.widgets import Button, Footer, Header, Static, Label, Input, Select
|
||||
RadioSet, RadioButton
|
||||
from textual.widgets.selection_list import Selection
|
||||
from textual_slider import Slider
|
||||
|
||||
# Local imports
|
||||
from . import api_helpers, ytlounge
|
||||
from .constants import skip_categories
|
||||
@@ -457,7 +459,7 @@ class DevicesManager(Vertical):
|
||||
@on(Button.Pressed, "#element-remove")
|
||||
def remove_channel(self, event: Button.Pressed):
|
||||
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()
|
||||
|
||||
@on(Button.Pressed, "#add-device")
|
||||
@@ -479,7 +481,7 @@ class ApiKeyManager(Vertical):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("YouTube Api Key", classes="title")
|
||||
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"):
|
||||
yield Input(placeholder="YouTube Api Key", id="api-key-input", password=True, value=self.config.apikey)
|
||||
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.",
|
||||
classes="subtitle", id="skip-count-tracking-subtitle")
|
||||
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",
|
||||
label="Enable skipping ads")
|
||||
yield Checkbox(value=self.config.mute_ads, id="mute-ads-switch",
|
||||
label="Enable muting ads")
|
||||
|
||||
@on(Checkbox.Changed, "#mute-ads-switch")
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import asyncio
|
||||
import json
|
||||
import aiohttp
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
try:
|
||||
self.subscribe_task.cancel()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Subscribe to the lounge and start the watchdog
|
||||
@@ -48,11 +54,10 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
pass
|
||||
finally:
|
||||
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":
|
||||
data = args[0]
|
||||
self.state.apply_state(data)
|
||||
self._update_state()
|
||||
# print(data)
|
||||
# Unmute when the video starts playing
|
||||
if self.mute_ads and data["state"] == "1":
|
||||
@@ -63,12 +68,12 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
self._update_state()
|
||||
# Unmute when the video starts playing
|
||||
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))
|
||||
elif self.mute_ads and event_type == "onAdStateChange":
|
||||
data = args[0]
|
||||
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))
|
||||
else: # Seen multiple other adStates, assuming they are all ads
|
||||
print("Ad has started, muting")
|
||||
@@ -78,10 +83,11 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
self.volume_state = args[0]
|
||||
pass
|
||||
# Gets segments for the next video before it starts playing
|
||||
elif event_type == "autoplayUpNext":
|
||||
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}")
|
||||
create_task(self.api_helper.get_segments(vid_id))
|
||||
# Comment "fix" since it doesn't seem to work
|
||||
# elif event_type == "autoplayUpNext":
|
||||
# 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}")
|
||||
# create_task(self.api_helper.get_segments(vid_id))
|
||||
|
||||
# #Used to know if an ad is skippable or not
|
||||
elif event_type == "adPlaying":
|
||||
@@ -97,8 +103,20 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
create_task(self.mute(False, override=True))
|
||||
elif self.mute_ads:
|
||||
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)
|
||||
async def set_volume(self, volume: int) -> None:
|
||||
@@ -117,3 +135,51 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
self.volume_state["muted"] = mute_str
|
||||
# YouTube wants the volume when unmuting, so we send it
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
aiohttp==3.8.6
|
||||
aiohttp==3.9.0
|
||||
argparse==1.4.0
|
||||
async-cache==1.1.1
|
||||
pyytlounge==1.6.2
|
||||
pyytlounge==1.6.3
|
||||
rich==13.6.0
|
||||
ssdp==1.3.0
|
||||
textual==0.40.0
|
||||
|
||||
Reference in New Issue
Block a user